mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-03-22 23:30:14 +00:00
Compare commits
27 commits
251ed7733b
...
075b4c1772
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
075b4c1772 | ||
|
|
448560a0d2 | ||
|
|
38210ec186 | ||
|
|
f7a996ab26 | ||
|
|
2c32b72f95 | ||
|
|
800862c50a | ||
|
|
cebca9a882 | ||
|
|
1a4b21955c | ||
|
|
422ff99b2a | ||
|
|
f235b8641f | ||
|
|
9cd52c4dd7 | ||
|
|
c1baffadf0 | ||
|
|
013f6be162 | ||
|
|
5fa5020af5 | ||
|
|
5a10ae9df0 | ||
|
|
aa9dc128d8 | ||
|
|
76f54bbd2c | ||
|
|
5b195781ad | ||
|
|
7b5ead8bd9 | ||
|
|
e7c1000fc2 | ||
|
|
0b1ea464bb | ||
|
|
93cc092ee1 | ||
|
|
e0d7cba330 | ||
|
|
d7a0a9f675 | ||
|
|
a2f9ccc9b9 | ||
|
|
7e7ad325dc | ||
|
|
e0828cc35c |
24 changed files with 747 additions and 301 deletions
|
|
@ -25,6 +25,9 @@ void main() {
|
||||||
if (lum < 0.05) discard;
|
if (lum < 0.05) discard;
|
||||||
}
|
}
|
||||||
|
|
||||||
float edge = smoothstep(0.5, 0.4, length(p - 0.5));
|
// Soft circular falloff for point-sprite edges.
|
||||||
outColor = texColor * vColor * vec4(vec3(1.0), edge);
|
float edge = 1.0 - smoothstep(0.4, 0.5, length(p - 0.5));
|
||||||
|
float alpha = texColor.a * vColor.a * edge;
|
||||||
|
vec3 rgb = texColor.rgb * vColor.rgb * alpha;
|
||||||
|
outColor = vec4(rgb, alpha);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -44,19 +44,23 @@ void main() {
|
||||||
|
|
||||||
vec4 mapColor = texture(uComposite, mapUV);
|
vec4 mapColor = texture(uComposite, mapUV);
|
||||||
|
|
||||||
// Player arrow
|
// Single player direction indicator (center arrow) rendered in-shader.
|
||||||
float acs = cos(push.arrowRotation);
|
vec2 local = center; // [-0.5, 0.5] around minimap center
|
||||||
float asn = sin(push.arrowRotation);
|
float ac = cos(push.arrowRotation);
|
||||||
vec2 ac = center;
|
float as = sin(push.arrowRotation);
|
||||||
vec2 arrowPos = vec2(-(ac.x * acs - ac.y * asn), ac.x * asn + ac.y * acs);
|
// TexCoord Y grows downward on screen; use negative Y so 0-angle points North (up).
|
||||||
|
vec2 tip = vec2(0.0, -0.09);
|
||||||
vec2 tip = vec2(0.0, -0.04);
|
vec2 left = vec2(-0.045, 0.02);
|
||||||
vec2 left = vec2(-0.02, 0.02);
|
vec2 right = vec2( 0.045, 0.02);
|
||||||
vec2 right = vec2(0.02, 0.02);
|
mat2 rot = mat2(ac, -as, as, ac);
|
||||||
|
tip = rot * tip;
|
||||||
if (pointInTriangle(arrowPos, tip, left, right)) {
|
left = rot * left;
|
||||||
mapColor = vec4(1.0, 0.8, 0.0, 1.0);
|
right = rot * right;
|
||||||
|
if (pointInTriangle(local, tip, left, right)) {
|
||||||
|
mapColor.rgb = vec3(1.0, 0.86, 0.05);
|
||||||
}
|
}
|
||||||
|
float centerDot = smoothstep(0.016, 0.0, length(local));
|
||||||
|
mapColor.rgb = mix(mapColor.rgb, vec3(1.0), centerDot * 0.95);
|
||||||
|
|
||||||
// Dark border ring
|
// Dark border ring
|
||||||
float border = smoothstep(0.48, 0.5, dist);
|
float border = smoothstep(0.48, 0.5, dist);
|
||||||
|
|
|
||||||
|
|
@ -1978,7 +1978,7 @@ public:
|
||||||
const std::array<MailAttachSlot, 12>& getMailAttachments() const { return mailAttachments_; }
|
const std::array<MailAttachSlot, 12>& getMailAttachments() const { return mailAttachments_; }
|
||||||
int getMailAttachmentCount() const;
|
int getMailAttachmentCount() const;
|
||||||
void mailTakeMoney(uint32_t mailId);
|
void mailTakeMoney(uint32_t mailId);
|
||||||
void mailTakeItem(uint32_t mailId, uint32_t itemIndex);
|
void mailTakeItem(uint32_t mailId, uint32_t itemGuidLow);
|
||||||
void mailDelete(uint32_t mailId);
|
void mailDelete(uint32_t mailId);
|
||||||
void mailMarkAsRead(uint32_t mailId);
|
void mailMarkAsRead(uint32_t mailId);
|
||||||
void refreshMailList();
|
void refreshMailList();
|
||||||
|
|
@ -2534,6 +2534,7 @@ private:
|
||||||
std::unordered_set<uint32_t> pendingItemQueries_;
|
std::unordered_set<uint32_t> pendingItemQueries_;
|
||||||
std::array<uint64_t, 23> equipSlotGuids_{};
|
std::array<uint64_t, 23> equipSlotGuids_{};
|
||||||
std::array<uint64_t, 16> backpackSlotGuids_{};
|
std::array<uint64_t, 16> backpackSlotGuids_{};
|
||||||
|
std::array<uint64_t, 32> keyringSlotGuids_{};
|
||||||
// Container (bag) contents: containerGuid -> array of item GUIDs per slot
|
// Container (bag) contents: containerGuid -> array of item GUIDs per slot
|
||||||
struct ContainerInfo {
|
struct ContainerInfo {
|
||||||
uint32_t numSlots = 0;
|
uint32_t numSlots = 0;
|
||||||
|
|
@ -2829,6 +2830,7 @@ private:
|
||||||
struct LocalLootState {
|
struct LocalLootState {
|
||||||
LootResponseData data;
|
LootResponseData data;
|
||||||
bool moneyTaken = false;
|
bool moneyTaken = false;
|
||||||
|
bool itemAutoLootSent = false;
|
||||||
};
|
};
|
||||||
std::unordered_map<uint64_t, LocalLootState> localLootState_;
|
std::unordered_map<uint64_t, LocalLootState> localLootState_;
|
||||||
struct PendingLootRetry {
|
struct PendingLootRetry {
|
||||||
|
|
@ -3200,6 +3202,7 @@ private:
|
||||||
bool releasedSpirit_ = false;
|
bool releasedSpirit_ = false;
|
||||||
uint32_t corpseMapId_ = 0;
|
uint32_t corpseMapId_ = 0;
|
||||||
float corpseX_ = 0.0f, corpseY_ = 0.0f, corpseZ_ = 0.0f;
|
float corpseX_ = 0.0f, corpseY_ = 0.0f, corpseZ_ = 0.0f;
|
||||||
|
uint64_t corpseGuid_ = 0;
|
||||||
// Death Knight runes (class 6): slots 0-1=Blood, 2-3=Unholy, 4-5=Frost initially
|
// Death Knight runes (class 6): slots 0-1=Blood, 2-3=Unholy, 4-5=Frost initially
|
||||||
std::array<RuneSlot, 6> playerRunes_ = [] {
|
std::array<RuneSlot, 6> playerRunes_ = [] {
|
||||||
std::array<RuneSlot, 6> r{};
|
std::array<RuneSlot, 6> r{};
|
||||||
|
|
|
||||||
|
|
@ -69,6 +69,7 @@ struct ItemSlot {
|
||||||
class Inventory {
|
class Inventory {
|
||||||
public:
|
public:
|
||||||
static constexpr int BACKPACK_SLOTS = 16;
|
static constexpr int BACKPACK_SLOTS = 16;
|
||||||
|
static constexpr int KEYRING_SLOTS = 32;
|
||||||
static constexpr int NUM_EQUIP_SLOTS = 23;
|
static constexpr int NUM_EQUIP_SLOTS = 23;
|
||||||
static constexpr int NUM_BAG_SLOTS = 4;
|
static constexpr int NUM_BAG_SLOTS = 4;
|
||||||
static constexpr int MAX_BAG_SIZE = 36;
|
static constexpr int MAX_BAG_SIZE = 36;
|
||||||
|
|
@ -88,6 +89,12 @@ public:
|
||||||
bool setEquipSlot(EquipSlot slot, const ItemDef& item);
|
bool setEquipSlot(EquipSlot slot, const ItemDef& item);
|
||||||
bool clearEquipSlot(EquipSlot slot);
|
bool clearEquipSlot(EquipSlot slot);
|
||||||
|
|
||||||
|
// Keyring
|
||||||
|
const ItemSlot& getKeyringSlot(int index) const;
|
||||||
|
bool setKeyringSlot(int index, const ItemDef& item);
|
||||||
|
bool clearKeyringSlot(int index);
|
||||||
|
int getKeyringSize() const { return KEYRING_SLOTS; }
|
||||||
|
|
||||||
// Extra bags
|
// Extra bags
|
||||||
int getBagSize(int bagIndex) const;
|
int getBagSize(int bagIndex) const;
|
||||||
void setBagSize(int bagIndex, int size);
|
void setBagSize(int bagIndex, int size);
|
||||||
|
|
@ -123,6 +130,7 @@ public:
|
||||||
|
|
||||||
private:
|
private:
|
||||||
std::array<ItemSlot, BACKPACK_SLOTS> backpack{};
|
std::array<ItemSlot, BACKPACK_SLOTS> backpack{};
|
||||||
|
std::array<ItemSlot, KEYRING_SLOTS> keyring_{};
|
||||||
std::array<ItemSlot, NUM_EQUIP_SLOTS> equipment{};
|
std::array<ItemSlot, NUM_EQUIP_SLOTS> equipment{};
|
||||||
|
|
||||||
struct BagData {
|
struct BagData {
|
||||||
|
|
|
||||||
|
|
@ -266,8 +266,8 @@ public:
|
||||||
virtual bool parseMailList(network::Packet& packet, std::vector<MailMessage>& inbox);
|
virtual bool parseMailList(network::Packet& packet, std::vector<MailMessage>& inbox);
|
||||||
|
|
||||||
/** Build CMSG_MAIL_TAKE_ITEM */
|
/** Build CMSG_MAIL_TAKE_ITEM */
|
||||||
virtual network::Packet buildMailTakeItem(uint64_t mailboxGuid, uint32_t mailId, uint32_t itemSlot) {
|
virtual network::Packet buildMailTakeItem(uint64_t mailboxGuid, uint32_t mailId, uint32_t itemGuidLow) {
|
||||||
return MailTakeItemPacket::build(mailboxGuid, mailId, itemSlot);
|
return MailTakeItemPacket::build(mailboxGuid, mailId, itemGuidLow);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Build CMSG_MAIL_DELETE */
|
/** Build CMSG_MAIL_DELETE */
|
||||||
|
|
@ -404,7 +404,7 @@ public:
|
||||||
uint32_t money, uint32_t cod,
|
uint32_t money, uint32_t cod,
|
||||||
const std::vector<uint64_t>& itemGuids = {}) override;
|
const std::vector<uint64_t>& itemGuids = {}) override;
|
||||||
bool parseMailList(network::Packet& packet, std::vector<MailMessage>& inbox) override;
|
bool parseMailList(network::Packet& packet, std::vector<MailMessage>& inbox) override;
|
||||||
network::Packet buildMailTakeItem(uint64_t mailboxGuid, uint32_t mailId, uint32_t itemSlot) override;
|
network::Packet buildMailTakeItem(uint64_t mailboxGuid, uint32_t mailId, uint32_t itemGuidLow) override;
|
||||||
network::Packet buildMailDelete(uint64_t mailboxGuid, uint32_t mailId, uint32_t mailTemplateId) override;
|
network::Packet buildMailDelete(uint64_t mailboxGuid, uint32_t mailId, uint32_t mailTemplateId) override;
|
||||||
network::Packet buildItemQuery(uint32_t entry, uint64_t guid) override;
|
network::Packet buildItemQuery(uint32_t entry, uint64_t guid) override;
|
||||||
bool parseItemQueryResponse(network::Packet& packet, ItemQueryResponseData& data) override;
|
bool parseItemQueryResponse(network::Packet& packet, ItemQueryResponseData& data) override;
|
||||||
|
|
|
||||||
|
|
@ -57,6 +57,7 @@ enum class UF : uint16_t {
|
||||||
PLAYER_QUEST_LOG_START,
|
PLAYER_QUEST_LOG_START,
|
||||||
PLAYER_FIELD_INV_SLOT_HEAD,
|
PLAYER_FIELD_INV_SLOT_HEAD,
|
||||||
PLAYER_FIELD_PACK_SLOT_1,
|
PLAYER_FIELD_PACK_SLOT_1,
|
||||||
|
PLAYER_FIELD_KEYRING_SLOT_1,
|
||||||
PLAYER_FIELD_BANK_SLOT_1,
|
PLAYER_FIELD_BANK_SLOT_1,
|
||||||
PLAYER_FIELD_BANKBAG_SLOT_1,
|
PLAYER_FIELD_BANKBAG_SLOT_1,
|
||||||
PLAYER_SKILL_INFO_START,
|
PLAYER_SKILL_INFO_START,
|
||||||
|
|
|
||||||
|
|
@ -2441,6 +2441,12 @@ public:
|
||||||
static network::Packet build();
|
static network::Packet build();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** CMSG_RECLAIM_CORPSE packet builder */
|
||||||
|
class ReclaimCorpsePacket {
|
||||||
|
public:
|
||||||
|
static network::Packet build(uint64_t guid);
|
||||||
|
};
|
||||||
|
|
||||||
/** CMSG_SPIRIT_HEALER_ACTIVATE packet builder */
|
/** CMSG_SPIRIT_HEALER_ACTIVATE packet builder */
|
||||||
class SpiritHealerActivatePacket {
|
class SpiritHealerActivatePacket {
|
||||||
public:
|
public:
|
||||||
|
|
@ -2511,7 +2517,7 @@ public:
|
||||||
/** CMSG_MAIL_TAKE_ITEM packet builder */
|
/** CMSG_MAIL_TAKE_ITEM packet builder */
|
||||||
class MailTakeItemPacket {
|
class MailTakeItemPacket {
|
||||||
public:
|
public:
|
||||||
static network::Packet build(uint64_t mailboxGuid, uint32_t mailId, uint32_t itemIndex);
|
static network::Packet build(uint64_t mailboxGuid, uint32_t mailId, uint32_t itemGuidLow);
|
||||||
};
|
};
|
||||||
|
|
||||||
/** CMSG_MAIL_DELETE packet builder */
|
/** CMSG_MAIL_DELETE packet builder */
|
||||||
|
|
|
||||||
|
|
@ -161,6 +161,7 @@ public:
|
||||||
// Targeting support
|
// Targeting support
|
||||||
void setTargetPosition(const glm::vec3* pos);
|
void setTargetPosition(const glm::vec3* pos);
|
||||||
void setInCombat(bool combat) { inCombat_ = combat; }
|
void setInCombat(bool combat) { inCombat_ = combat; }
|
||||||
|
void resetCombatVisualState();
|
||||||
bool isMoving() const;
|
bool isMoving() const;
|
||||||
void triggerMeleeSwing();
|
void triggerMeleeSwing();
|
||||||
void setEquippedWeaponType(uint32_t inventoryType) { equippedWeaponInvType_ = inventoryType; meleeAnimId = 0; }
|
void setEquippedWeaponType(uint32_t inventoryType) { equippedWeaponInvType_ = inventoryType; meleeAnimId = 0; }
|
||||||
|
|
@ -340,6 +341,10 @@ private:
|
||||||
// Character animation state
|
// Character animation state
|
||||||
enum class CharAnimState { IDLE, WALK, RUN, JUMP_START, JUMP_MID, JUMP_END, SIT_DOWN, SITTING, EMOTE, SWIM_IDLE, SWIM, MELEE_SWING, MOUNT, CHARGE, COMBAT_IDLE };
|
enum class CharAnimState { IDLE, WALK, RUN, JUMP_START, JUMP_MID, JUMP_END, SIT_DOWN, SITTING, EMOTE, SWIM_IDLE, SWIM, MELEE_SWING, MOUNT, CHARGE, COMBAT_IDLE };
|
||||||
CharAnimState charAnimState = CharAnimState::IDLE;
|
CharAnimState charAnimState = CharAnimState::IDLE;
|
||||||
|
float locomotionStopGraceTimer_ = 0.0f;
|
||||||
|
bool locomotionWasSprinting_ = false;
|
||||||
|
uint32_t lastPlayerAnimRequest_ = UINT32_MAX;
|
||||||
|
bool lastPlayerAnimLoopRequest_ = true;
|
||||||
void updateCharacterAnimation();
|
void updateCharacterAnimation();
|
||||||
bool isFootstepAnimationState() const;
|
bool isFootstepAnimationState() const;
|
||||||
bool shouldTriggerFootstepEvent(uint32_t animationId, float animationTimeMs, float animationDurationMs);
|
bool shouldTriggerFootstepEvent(uint32_t animationId, float animationTimeMs, float animationDurationMs);
|
||||||
|
|
|
||||||
|
|
@ -621,6 +621,9 @@ private:
|
||||||
float resurrectFlashTimer_ = 0.0f;
|
float resurrectFlashTimer_ = 0.0f;
|
||||||
static constexpr float kResurrectFlashDuration = 3.0f;
|
static constexpr float kResurrectFlashDuration = 3.0f;
|
||||||
bool ghostStateCallbackSet_ = false;
|
bool ghostStateCallbackSet_ = false;
|
||||||
|
bool ghostOpacityStateKnown_ = false;
|
||||||
|
bool ghostOpacityLastState_ = false;
|
||||||
|
uint32_t ghostOpacityLastInstanceId_ = 0;
|
||||||
void renderResurrectFlash();
|
void renderResurrectFlash();
|
||||||
|
|
||||||
// Zone discovery text ("Entering: <ZoneName>")
|
// Zone discovery text ("Entering: <ZoneName>")
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,6 @@ public:
|
||||||
TOGGLE_WORLD_MAP,
|
TOGGLE_WORLD_MAP,
|
||||||
TOGGLE_NAMEPLATES,
|
TOGGLE_NAMEPLATES,
|
||||||
TOGGLE_RAID_FRAMES,
|
TOGGLE_RAID_FRAMES,
|
||||||
TOGGLE_QUEST_LOG,
|
|
||||||
TOGGLE_ACHIEVEMENTS,
|
TOGGLE_ACHIEVEMENTS,
|
||||||
ACTION_COUNT
|
ACTION_COUNT
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -391,8 +391,46 @@ void Application::run() {
|
||||||
}
|
}
|
||||||
|
|
||||||
auto lastTime = std::chrono::high_resolution_clock::now();
|
auto lastTime = std::chrono::high_resolution_clock::now();
|
||||||
|
std::atomic<bool> watchdogRunning{true};
|
||||||
|
std::atomic<int64_t> watchdogHeartbeatMs{
|
||||||
|
std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||||
|
std::chrono::steady_clock::now().time_since_epoch()).count()
|
||||||
|
};
|
||||||
|
std::thread watchdogThread([this, &watchdogRunning, &watchdogHeartbeatMs]() {
|
||||||
|
bool releasedForCurrentStall = false;
|
||||||
|
while (watchdogRunning.load(std::memory_order_acquire)) {
|
||||||
|
std::this_thread::sleep_for(std::chrono::milliseconds(250));
|
||||||
|
const int64_t nowMs = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||||
|
std::chrono::steady_clock::now().time_since_epoch()).count();
|
||||||
|
const int64_t lastBeatMs = watchdogHeartbeatMs.load(std::memory_order_acquire);
|
||||||
|
const int64_t stallMs = nowMs - lastBeatMs;
|
||||||
|
|
||||||
|
// Failsafe: if the main loop stalls while relative mouse mode is active,
|
||||||
|
// forcibly release grab so the user can move the cursor and close the app.
|
||||||
|
if (stallMs > 1500) {
|
||||||
|
if (!releasedForCurrentStall) {
|
||||||
|
SDL_SetRelativeMouseMode(SDL_FALSE);
|
||||||
|
SDL_ShowCursor(SDL_ENABLE);
|
||||||
|
if (window && window->getSDLWindow()) {
|
||||||
|
SDL_SetWindowGrab(window->getSDLWindow(), SDL_FALSE);
|
||||||
|
}
|
||||||
|
LOG_WARNING("Main-loop stall detected (", stallMs,
|
||||||
|
"ms) — force-released mouse capture failsafe");
|
||||||
|
releasedForCurrentStall = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
releasedForCurrentStall = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
while (running && !window->shouldClose()) {
|
while (running && !window->shouldClose()) {
|
||||||
|
watchdogHeartbeatMs.store(
|
||||||
|
std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||||
|
std::chrono::steady_clock::now().time_since_epoch()).count(),
|
||||||
|
std::memory_order_release);
|
||||||
|
|
||||||
// Calculate delta time
|
// Calculate delta time
|
||||||
auto currentTime = std::chrono::high_resolution_clock::now();
|
auto currentTime = std::chrono::high_resolution_clock::now();
|
||||||
std::chrono::duration<float> deltaTimeDuration = currentTime - lastTime;
|
std::chrono::duration<float> deltaTimeDuration = currentTime - lastTime;
|
||||||
|
|
@ -466,16 +504,6 @@ void Application::run() {
|
||||||
LOG_INFO("Shadows: ", enabled ? "ON" : "OFF");
|
LOG_INFO("Shadows: ", enabled ? "ON" : "OFF");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// F7: Test level-up effect (ignore key repeat)
|
|
||||||
else if (event.key.keysym.scancode == SDL_SCANCODE_F7 && event.key.repeat == 0) {
|
|
||||||
if (renderer) {
|
|
||||||
renderer->triggerLevelUpEffect(renderer->getCharacterPosition());
|
|
||||||
LOG_INFO("Triggered test level-up effect");
|
|
||||||
}
|
|
||||||
if (uiManager) {
|
|
||||||
uiManager->getGameScreen().triggerDing(99);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// F8: Debug WMO floor at current position
|
// F8: Debug WMO floor at current position
|
||||||
else if (event.key.keysym.scancode == SDL_SCANCODE_F8 && event.key.repeat == 0) {
|
else if (event.key.keysym.scancode == SDL_SCANCODE_F8 && event.key.repeat == 0) {
|
||||||
if (renderer && renderer->getWMORenderer()) {
|
if (renderer && renderer->getWMORenderer()) {
|
||||||
|
|
@ -529,6 +557,18 @@ void Application::run() {
|
||||||
window->setShouldClose(true);
|
window->setShouldClose(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} catch (...) {
|
||||||
|
watchdogRunning.store(false, std::memory_order_release);
|
||||||
|
if (watchdogThread.joinable()) {
|
||||||
|
watchdogThread.join();
|
||||||
|
}
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
|
||||||
|
watchdogRunning.store(false, std::memory_order_release);
|
||||||
|
if (watchdogThread.joinable()) {
|
||||||
|
watchdogThread.join();
|
||||||
|
}
|
||||||
|
|
||||||
LOG_INFO("Main loop ended");
|
LOG_INFO("Main loop ended");
|
||||||
}
|
}
|
||||||
|
|
@ -807,6 +847,7 @@ void Application::logoutToLogin() {
|
||||||
world.reset();
|
world.reset();
|
||||||
|
|
||||||
if (renderer) {
|
if (renderer) {
|
||||||
|
renderer->resetCombatVisualState();
|
||||||
// Remove old player model so it doesn't persist into next session
|
// Remove old player model so it doesn't persist into next session
|
||||||
if (auto* charRenderer = renderer->getCharacterRenderer()) {
|
if (auto* charRenderer = renderer->getCharacterRenderer()) {
|
||||||
charRenderer->removeInstance(1);
|
charRenderer->removeInstance(1);
|
||||||
|
|
@ -1074,6 +1115,15 @@ void Application::update(float deltaTime) {
|
||||||
gameHandler->isTaxiMountActive() ||
|
gameHandler->isTaxiMountActive() ||
|
||||||
gameHandler->isTaxiActivationPending());
|
gameHandler->isTaxiActivationPending());
|
||||||
bool onTransportNow = gameHandler && gameHandler->isOnTransport();
|
bool onTransportNow = gameHandler && gameHandler->isOnTransport();
|
||||||
|
// Clear stale client-side transport state when the tracked transport no longer exists.
|
||||||
|
if (onTransportNow && gameHandler->getTransportManager()) {
|
||||||
|
auto* currentTracked = gameHandler->getTransportManager()->getTransport(
|
||||||
|
gameHandler->getPlayerTransportGuid());
|
||||||
|
if (!currentTracked) {
|
||||||
|
gameHandler->clearPlayerTransport();
|
||||||
|
onTransportNow = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
// M2 transports (trams) use position-delta approach: player keeps normal
|
// M2 transports (trams) use position-delta approach: player keeps normal
|
||||||
// movement and the transport's frame-to-frame delta is applied on top.
|
// movement and the transport's frame-to-frame delta is applied on top.
|
||||||
// Only WMO transports (ships) use full external-driven mode.
|
// Only WMO transports (ships) use full external-driven mode.
|
||||||
|
|
@ -1309,23 +1359,29 @@ void Application::update(float deltaTime) {
|
||||||
} else {
|
} else {
|
||||||
glm::vec3 renderPos = renderer->getCharacterPosition();
|
glm::vec3 renderPos = renderer->getCharacterPosition();
|
||||||
|
|
||||||
// M2 transport riding: apply transport's frame-to-frame position delta
|
// M2 transport riding: resolve in canonical space and lock once per frame.
|
||||||
// so the player moves with the tram while retaining normal movement input.
|
// This avoids visible jitter from mixed render/canonical delta application.
|
||||||
if (isM2Transport && gameHandler->getTransportManager()) {
|
if (isM2Transport && gameHandler->getTransportManager()) {
|
||||||
auto* tr = gameHandler->getTransportManager()->getTransport(
|
auto* tr = gameHandler->getTransportManager()->getTransport(
|
||||||
gameHandler->getPlayerTransportGuid());
|
gameHandler->getPlayerTransportGuid());
|
||||||
if (tr) {
|
if (tr) {
|
||||||
static glm::vec3 lastTransportCanonical(0);
|
// Keep passenger locked to elevator vertical motion while grounded.
|
||||||
static uint64_t lastTransportGuid = 0;
|
// Without this, floor clamping can hold world-Z static unless the
|
||||||
if (lastTransportGuid == gameHandler->getPlayerTransportGuid()) {
|
// player is jumping, which makes lifts appear to not move vertically.
|
||||||
glm::vec3 deltaCanonical = tr->position - lastTransportCanonical;
|
glm::vec3 tentativeCanonical = core::coords::renderToCanonical(renderPos);
|
||||||
glm::vec3 deltaRender = core::coords::canonicalToRender(deltaCanonical)
|
glm::vec3 localOffset = gameHandler->getPlayerTransportOffset();
|
||||||
- core::coords::canonicalToRender(glm::vec3(0));
|
localOffset.x = tentativeCanonical.x - tr->position.x;
|
||||||
renderPos += deltaRender;
|
localOffset.y = tentativeCanonical.y - tr->position.y;
|
||||||
renderer->getCharacterPosition() = renderPos;
|
if (renderer->getCameraController() &&
|
||||||
|
!renderer->getCameraController()->isGrounded()) {
|
||||||
|
// While airborne (jump/fall), allow local Z offset to change.
|
||||||
|
localOffset.z = tentativeCanonical.z - tr->position.z;
|
||||||
}
|
}
|
||||||
lastTransportCanonical = tr->position;
|
gameHandler->setPlayerTransportOffset(localOffset);
|
||||||
lastTransportGuid = gameHandler->getPlayerTransportGuid();
|
|
||||||
|
glm::vec3 lockedCanonical = tr->position + localOffset;
|
||||||
|
renderPos = core::coords::canonicalToRender(lockedCanonical);
|
||||||
|
renderer->getCharacterPosition() = renderPos;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1362,21 +1418,45 @@ void Application::update(float deltaTime) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Client-side transport boarding detection (for M2 transports like trams
|
// Client-side transport boarding detection (for M2 transports like trams
|
||||||
// where the server doesn't send transport attachment data).
|
// and lifts where the server doesn't send transport attachment data).
|
||||||
// Use a generous AABB around each transport's current position.
|
// Thunder Bluff elevators use model origins that can be far from the deck
|
||||||
|
// the player stands on, so they need wider attachment bounds.
|
||||||
if (gameHandler->getTransportManager() && !gameHandler->isOnTransport()) {
|
if (gameHandler->getTransportManager() && !gameHandler->isOnTransport()) {
|
||||||
auto* tm = gameHandler->getTransportManager();
|
auto* tm = gameHandler->getTransportManager();
|
||||||
glm::vec3 playerCanonical = core::coords::renderToCanonical(renderPos);
|
glm::vec3 playerCanonical = core::coords::renderToCanonical(renderPos);
|
||||||
|
constexpr float kM2BoardHorizDistSq = 12.0f * 12.0f;
|
||||||
|
constexpr float kM2BoardVertDist = 15.0f;
|
||||||
|
constexpr float kTbLiftBoardHorizDistSq = 22.0f * 22.0f;
|
||||||
|
constexpr float kTbLiftBoardVertDist = 14.0f;
|
||||||
|
|
||||||
|
uint64_t bestGuid = 0;
|
||||||
|
float bestScore = 1e30f;
|
||||||
for (auto& [guid, transport] : tm->getTransports()) {
|
for (auto& [guid, transport] : tm->getTransports()) {
|
||||||
if (!transport.isM2) continue;
|
if (!transport.isM2) continue;
|
||||||
|
const bool isThunderBluffLift =
|
||||||
|
(transport.entry >= 20649u && transport.entry <= 20657u);
|
||||||
|
const float maxHorizDistSq = isThunderBluffLift
|
||||||
|
? kTbLiftBoardHorizDistSq
|
||||||
|
: kM2BoardHorizDistSq;
|
||||||
|
const float maxVertDist = isThunderBluffLift
|
||||||
|
? kTbLiftBoardVertDist
|
||||||
|
: kM2BoardVertDist;
|
||||||
glm::vec3 diff = playerCanonical - transport.position;
|
glm::vec3 diff = playerCanonical - transport.position;
|
||||||
float horizDistSq = diff.x * diff.x + diff.y * diff.y;
|
float horizDistSq = diff.x * diff.x + diff.y * diff.y;
|
||||||
float vertDist = std::abs(diff.z);
|
float vertDist = std::abs(diff.z);
|
||||||
if (horizDistSq < 144.0f && vertDist < 15.0f) {
|
if (horizDistSq < maxHorizDistSq && vertDist < maxVertDist) {
|
||||||
gameHandler->setPlayerOnTransport(guid, playerCanonical - transport.position);
|
float score = horizDistSq + vertDist * vertDist;
|
||||||
LOG_DEBUG("M2 transport boarding: guid=0x", std::hex, guid, std::dec);
|
if (score < bestScore) {
|
||||||
break;
|
bestScore = score;
|
||||||
|
bestGuid = guid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (bestGuid != 0) {
|
||||||
|
auto* tr = tm->getTransport(bestGuid);
|
||||||
|
if (tr) {
|
||||||
|
gameHandler->setPlayerOnTransport(bestGuid, playerCanonical - tr->position);
|
||||||
|
LOG_DEBUG("M2 transport boarding: guid=0x", std::hex, bestGuid, std::dec);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1389,7 +1469,19 @@ void Application::update(float deltaTime) {
|
||||||
glm::vec3 playerCanonical = core::coords::renderToCanonical(renderPos);
|
glm::vec3 playerCanonical = core::coords::renderToCanonical(renderPos);
|
||||||
glm::vec3 diff = playerCanonical - tr->position;
|
glm::vec3 diff = playerCanonical - tr->position;
|
||||||
float horizDistSq = diff.x * diff.x + diff.y * diff.y;
|
float horizDistSq = diff.x * diff.x + diff.y * diff.y;
|
||||||
if (horizDistSq > 225.0f) {
|
const bool isThunderBluffLift =
|
||||||
|
(tr->entry >= 20649u && tr->entry <= 20657u);
|
||||||
|
constexpr float kM2DisembarkHorizDistSq = 15.0f * 15.0f;
|
||||||
|
constexpr float kTbLiftDisembarkHorizDistSq = 28.0f * 28.0f;
|
||||||
|
constexpr float kM2DisembarkVertDist = 18.0f;
|
||||||
|
constexpr float kTbLiftDisembarkVertDist = 16.0f;
|
||||||
|
const float disembarkHorizDistSq = isThunderBluffLift
|
||||||
|
? kTbLiftDisembarkHorizDistSq
|
||||||
|
: kM2DisembarkHorizDistSq;
|
||||||
|
const float disembarkVertDist = isThunderBluffLift
|
||||||
|
? kTbLiftDisembarkVertDist
|
||||||
|
: kM2DisembarkVertDist;
|
||||||
|
if (horizDistSq > disembarkHorizDistSq || std::abs(diff.z) > disembarkVertDist) {
|
||||||
gameHandler->clearPlayerTransport();
|
gameHandler->clearPlayerTransport();
|
||||||
LOG_DEBUG("M2 transport disembark");
|
LOG_DEBUG("M2 transport disembark");
|
||||||
}
|
}
|
||||||
|
|
@ -1822,6 +1914,9 @@ void Application::setupUICallbacks() {
|
||||||
gameHandler->setWorldEntryCallback([this](uint32_t mapId, float x, float y, float z, bool isInitialEntry) {
|
gameHandler->setWorldEntryCallback([this](uint32_t mapId, float x, float y, float z, bool isInitialEntry) {
|
||||||
LOG_INFO("Online world entry: mapId=", mapId, " pos=(", x, ", ", y, ", ", z, ")"
|
LOG_INFO("Online world entry: mapId=", mapId, " pos=(", x, ", ", y, ", ", z, ")"
|
||||||
" initial=", isInitialEntry);
|
" initial=", isInitialEntry);
|
||||||
|
if (renderer) {
|
||||||
|
renderer->resetCombatVisualState();
|
||||||
|
}
|
||||||
|
|
||||||
// Reconnect to the same map: terrain stays loaded but all online entities are stale.
|
// Reconnect to the same map: terrain stays loaded but all online entities are stale.
|
||||||
// Despawn them properly so the server's fresh CREATE_OBJECTs will re-populate the world.
|
// Despawn them properly so the server's fresh CREATE_OBJECTs will re-populate the world.
|
||||||
|
|
@ -2832,6 +2927,12 @@ void Application::setupUICallbacks() {
|
||||||
}
|
}
|
||||||
|
|
||||||
transportManager->registerTransport(guid, wmoInstanceId, pathId, canonicalSpawnPos, entry);
|
transportManager->registerTransport(guid, wmoInstanceId, pathId, canonicalSpawnPos, entry);
|
||||||
|
// Keep type in sync with the spawned instance; needed for M2 lift boarding/motion.
|
||||||
|
if (!it->second.isWmo) {
|
||||||
|
if (auto* tr = transportManager->getTransport(guid)) {
|
||||||
|
tr->isM2 = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
pendingTransportMoves_[guid] = PendingTransportMove{x, y, z, orientation};
|
pendingTransportMoves_[guid] = PendingTransportMove{x, y, z, orientation};
|
||||||
LOG_DEBUG("Cannot auto-spawn transport 0x", std::hex, guid, std::dec,
|
LOG_DEBUG("Cannot auto-spawn transport 0x", std::hex, guid, std::dec,
|
||||||
|
|
@ -3011,9 +3112,11 @@ void Application::setupUICallbacks() {
|
||||||
if (charInstId == 0) return;
|
if (charInstId == 0) return;
|
||||||
// WoW stand state → M2 animation ID mapping
|
// WoW stand state → M2 animation ID mapping
|
||||||
// 0=Stand→0, 1-6=Sit variants→27 (SitGround), 7=Dead→1, 8=Kneel→72
|
// 0=Stand→0, 1-6=Sit variants→27 (SitGround), 7=Dead→1, 8=Kneel→72
|
||||||
|
// Do not force Stand(0) here: locomotion state machine already owns standing/running.
|
||||||
|
// Forcing Stand on packet timing causes visible run-cycle hitching while steering.
|
||||||
uint32_t animId = 0;
|
uint32_t animId = 0;
|
||||||
if (standState == 0) {
|
if (standState == 0) {
|
||||||
animId = 0; // Stand
|
return;
|
||||||
} else if (standState >= 1 && standState <= 6) {
|
} else if (standState >= 1 && standState <= 6) {
|
||||||
animId = 27; // SitGround (covers sit-chair too; correct visual differs by chair height)
|
animId = 27; // SitGround (covers sit-chair too; correct visual differs by chair height)
|
||||||
} else if (standState == 7) {
|
} else if (standState == 7) {
|
||||||
|
|
|
||||||
|
|
@ -309,6 +309,16 @@ bool isPlaceholderQuestTitle(const std::string& s) {
|
||||||
return s.rfind("Quest #", 0) == 0;
|
return s.rfind("Quest #", 0) == 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
float mergeCooldownSeconds(float current, float incoming) {
|
||||||
|
constexpr float kEpsilon = 0.05f;
|
||||||
|
if (incoming <= 0.0f) return 0.0f;
|
||||||
|
if (current <= 0.0f) return incoming;
|
||||||
|
// Cooldowns should normally tick down. If a duplicate/late packet reports a
|
||||||
|
// larger value, keep the local remaining time to avoid visible timer resets.
|
||||||
|
if (incoming > current + kEpsilon) return current;
|
||||||
|
return incoming;
|
||||||
|
}
|
||||||
|
|
||||||
bool looksLikeQuestDescriptionText(const std::string& s) {
|
bool looksLikeQuestDescriptionText(const std::string& s) {
|
||||||
int spaces = 0;
|
int spaces = 0;
|
||||||
int commas = 0;
|
int commas = 0;
|
||||||
|
|
@ -3208,7 +3218,14 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
||||||
uint32_t cdMs = packet.readUInt32();
|
uint32_t cdMs = packet.readUInt32();
|
||||||
float cdSec = cdMs / 1000.0f;
|
float cdSec = cdMs / 1000.0f;
|
||||||
if (cdSec > 0.0f) {
|
if (cdSec > 0.0f) {
|
||||||
if (spellId != 0) spellCooldowns[spellId] = cdSec;
|
if (spellId != 0) {
|
||||||
|
auto it = spellCooldowns.find(spellId);
|
||||||
|
if (it == spellCooldowns.end()) {
|
||||||
|
spellCooldowns[spellId] = cdSec;
|
||||||
|
} else {
|
||||||
|
it->second = mergeCooldownSeconds(it->second, cdSec);
|
||||||
|
}
|
||||||
|
}
|
||||||
// Resolve itemId from the GUID so item-type slots are also updated
|
// Resolve itemId from the GUID so item-type slots are also updated
|
||||||
uint32_t itemId = 0;
|
uint32_t itemId = 0;
|
||||||
auto iit = onlineItems_.find(itemGuid);
|
auto iit = onlineItems_.find(itemGuid);
|
||||||
|
|
@ -3217,8 +3234,14 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
||||||
bool match = (spellId != 0 && slot.type == ActionBarSlot::SPELL && slot.id == spellId)
|
bool match = (spellId != 0 && slot.type == ActionBarSlot::SPELL && slot.id == spellId)
|
||||||
|| (itemId != 0 && slot.type == ActionBarSlot::ITEM && slot.id == itemId);
|
|| (itemId != 0 && slot.type == ActionBarSlot::ITEM && slot.id == itemId);
|
||||||
if (match) {
|
if (match) {
|
||||||
|
float prevRemaining = slot.cooldownRemaining;
|
||||||
|
float merged = mergeCooldownSeconds(slot.cooldownRemaining, cdSec);
|
||||||
|
slot.cooldownRemaining = merged;
|
||||||
|
if (slot.cooldownTotal <= 0.0f || prevRemaining <= 0.0f) {
|
||||||
slot.cooldownTotal = cdSec;
|
slot.cooldownTotal = cdSec;
|
||||||
slot.cooldownRemaining = cdSec;
|
} else {
|
||||||
|
slot.cooldownTotal = std::max(slot.cooldownTotal, merged);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
LOG_DEBUG("SMSG_ITEM_COOLDOWN: itemGuid=0x", std::hex, itemGuid, std::dec,
|
LOG_DEBUG("SMSG_ITEM_COOLDOWN: itemGuid=0x", std::hex, itemGuid, std::dec,
|
||||||
|
|
@ -8447,6 +8470,7 @@ void GameHandler::selectCharacter(uint64_t characterGuid) {
|
||||||
pendingItemQueries_.clear();
|
pendingItemQueries_.clear();
|
||||||
equipSlotGuids_ = {};
|
equipSlotGuids_ = {};
|
||||||
backpackSlotGuids_ = {};
|
backpackSlotGuids_ = {};
|
||||||
|
keyringSlotGuids_ = {};
|
||||||
invSlotBase_ = -1;
|
invSlotBase_ = -1;
|
||||||
packSlotBase_ = -1;
|
packSlotBase_ = -1;
|
||||||
lastPlayerFields_.clear();
|
lastPlayerFields_.clear();
|
||||||
|
|
@ -8506,6 +8530,7 @@ void GameHandler::selectCharacter(uint64_t characterGuid) {
|
||||||
castTimeTotal = 0.0f;
|
castTimeTotal = 0.0f;
|
||||||
playerDead_ = false;
|
playerDead_ = false;
|
||||||
releasedSpirit_ = false;
|
releasedSpirit_ = false;
|
||||||
|
corpseGuid_ = 0;
|
||||||
targetGuid = 0;
|
targetGuid = 0;
|
||||||
focusGuid = 0;
|
focusGuid = 0;
|
||||||
lastTargetGuid = 0;
|
lastTargetGuid = 0;
|
||||||
|
|
@ -9847,8 +9872,17 @@ void GameHandler::sendMovement(Opcode opcode) {
|
||||||
sanitizeMovementForTaxi();
|
sanitizeMovementForTaxi();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add transport data if player is on a transport
|
bool includeTransportInWire = isOnTransport();
|
||||||
if (isOnTransport()) {
|
if (includeTransportInWire && transportManager_) {
|
||||||
|
if (auto* tr = transportManager_->getTransport(playerTransportGuid_); tr && tr->isM2) {
|
||||||
|
// Client-detected M2 elevators/trams are not always server-recognized transports.
|
||||||
|
// Sending ONTRANSPORT for these can trigger bad fall-state corrections server-side.
|
||||||
|
includeTransportInWire = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add transport data if player is on a server-recognized transport
|
||||||
|
if (includeTransportInWire) {
|
||||||
// Keep authoritative world position synchronized to parent transport transform
|
// Keep authoritative world position synchronized to parent transport transform
|
||||||
// so heartbeats/corrections don't drag the passenger through geometry.
|
// so heartbeats/corrections don't drag the passenger through geometry.
|
||||||
if (transportManager_) {
|
if (transportManager_) {
|
||||||
|
|
@ -9890,7 +9924,7 @@ void GameHandler::sendMovement(Opcode opcode) {
|
||||||
|
|
||||||
LOG_DEBUG("Sending movement packet: opcode=0x", std::hex,
|
LOG_DEBUG("Sending movement packet: opcode=0x", std::hex,
|
||||||
wireOpcode(opcode), std::dec,
|
wireOpcode(opcode), std::dec,
|
||||||
(isOnTransport() ? " ONTRANSPORT" : ""));
|
(includeTransportInWire ? " ONTRANSPORT" : ""));
|
||||||
|
|
||||||
// Convert canonical → server coordinates for the wire
|
// Convert canonical → server coordinates for the wire
|
||||||
MovementInfo wireInfo = movementInfo;
|
MovementInfo wireInfo = movementInfo;
|
||||||
|
|
@ -9903,7 +9937,7 @@ void GameHandler::sendMovement(Opcode opcode) {
|
||||||
wireInfo.orientation = core::coords::canonicalToServerYaw(wireInfo.orientation);
|
wireInfo.orientation = core::coords::canonicalToServerYaw(wireInfo.orientation);
|
||||||
|
|
||||||
// Also convert transport local position to server coordinates if on transport
|
// Also convert transport local position to server coordinates if on transport
|
||||||
if (isOnTransport()) {
|
if (includeTransportInWire) {
|
||||||
glm::vec3 serverTransportPos = core::coords::canonicalToServer(
|
glm::vec3 serverTransportPos = core::coords::canonicalToServer(
|
||||||
glm::vec3(wireInfo.transportX, wireInfo.transportY, wireInfo.transportZ));
|
glm::vec3(wireInfo.transportX, wireInfo.transportY, wireInfo.transportZ));
|
||||||
wireInfo.transportX = serverTransportPos.x;
|
wireInfo.transportX = serverTransportPos.x;
|
||||||
|
|
@ -9963,6 +9997,7 @@ void GameHandler::forceClearTaxiAndMovementState() {
|
||||||
resurrectRequestPending_ = false;
|
resurrectRequestPending_ = false;
|
||||||
playerDead_ = false;
|
playerDead_ = false;
|
||||||
releasedSpirit_ = false;
|
releasedSpirit_ = false;
|
||||||
|
corpseGuid_ = 0;
|
||||||
repopPending_ = false;
|
repopPending_ = false;
|
||||||
pendingSpiritHealerGuid_ = 0;
|
pendingSpiritHealerGuid_ = 0;
|
||||||
resurrectCasterGuid_ = 0;
|
resurrectCasterGuid_ = 0;
|
||||||
|
|
@ -10580,11 +10615,13 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
|
||||||
uint64_t ownerGuid = (static_cast<uint64_t>(ownerHigh) << 32) | ownerLow;
|
uint64_t ownerGuid = (static_cast<uint64_t>(ownerHigh) << 32) | ownerLow;
|
||||||
if (ownerGuid == playerGuid || ownerLow == static_cast<uint32_t>(playerGuid)) {
|
if (ownerGuid == playerGuid || ownerLow == static_cast<uint32_t>(playerGuid)) {
|
||||||
// Server coords from movement block
|
// Server coords from movement block
|
||||||
|
corpseGuid_ = block.guid;
|
||||||
corpseX_ = block.x;
|
corpseX_ = block.x;
|
||||||
corpseY_ = block.y;
|
corpseY_ = block.y;
|
||||||
corpseZ_ = block.z;
|
corpseZ_ = block.z;
|
||||||
corpseMapId_ = currentMapId_;
|
corpseMapId_ = currentMapId_;
|
||||||
LOG_INFO("Corpse object detected: server=(", block.x, ", ", block.y, ", ", block.z,
|
LOG_INFO("Corpse object detected: guid=0x", std::hex, corpseGuid_, std::dec,
|
||||||
|
" server=(", block.x, ", ", block.y, ", ", block.z,
|
||||||
") map=", corpseMapId_);
|
") map=", corpseMapId_);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -11136,6 +11173,7 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
|
||||||
repopPending_ = false;
|
repopPending_ = false;
|
||||||
resurrectPending_ = false;
|
resurrectPending_ = false;
|
||||||
corpseMapId_ = 0; // corpse reclaimed
|
corpseMapId_ = 0; // corpse reclaimed
|
||||||
|
corpseGuid_ = 0;
|
||||||
LOG_INFO("Player resurrected (PLAYER_FLAGS ghost cleared)");
|
LOG_INFO("Player resurrected (PLAYER_FLAGS ghost cleared)");
|
||||||
if (ghostStateCallback_) ghostStateCallback_(false);
|
if (ghostStateCallback_) ghostStateCallback_(false);
|
||||||
}
|
}
|
||||||
|
|
@ -12908,9 +12946,12 @@ bool GameHandler::canReclaimCorpse() const {
|
||||||
|
|
||||||
void GameHandler::reclaimCorpse() {
|
void GameHandler::reclaimCorpse() {
|
||||||
if (!canReclaimCorpse() || !socket) return;
|
if (!canReclaimCorpse() || !socket) return;
|
||||||
network::Packet packet(wireOpcode(Opcode::CMSG_RECLAIM_CORPSE));
|
// Reclaim expects the corpse object guid when known; fallback to player guid.
|
||||||
|
uint64_t reclaimGuid = (corpseGuid_ != 0) ? corpseGuid_ : playerGuid;
|
||||||
|
auto packet = ReclaimCorpsePacket::build(reclaimGuid);
|
||||||
socket->send(packet);
|
socket->send(packet);
|
||||||
LOG_INFO("Sent CMSG_RECLAIM_CORPSE");
|
LOG_INFO("Sent CMSG_RECLAIM_CORPSE for guid=0x", std::hex, reclaimGuid, std::dec,
|
||||||
|
(corpseGuid_ == 0 ? " (fallback player guid)" : ""));
|
||||||
}
|
}
|
||||||
|
|
||||||
void GameHandler::activateSpiritHealer(uint64_t npcGuid) {
|
void GameHandler::activateSpiritHealer(uint64_t npcGuid) {
|
||||||
|
|
@ -13589,6 +13630,21 @@ bool GameHandler::applyInventoryFields(const std::map<uint16_t, uint32_t>& field
|
||||||
bool slotsChanged = false;
|
bool slotsChanged = false;
|
||||||
int equipBase = (invSlotBase_ >= 0) ? invSlotBase_ : static_cast<int>(fieldIndex(UF::PLAYER_FIELD_INV_SLOT_HEAD));
|
int equipBase = (invSlotBase_ >= 0) ? invSlotBase_ : static_cast<int>(fieldIndex(UF::PLAYER_FIELD_INV_SLOT_HEAD));
|
||||||
int packBase = (packSlotBase_ >= 0) ? packSlotBase_ : static_cast<int>(fieldIndex(UF::PLAYER_FIELD_PACK_SLOT_1));
|
int packBase = (packSlotBase_ >= 0) ? packSlotBase_ : static_cast<int>(fieldIndex(UF::PLAYER_FIELD_PACK_SLOT_1));
|
||||||
|
int bankBase = static_cast<int>(fieldIndex(UF::PLAYER_FIELD_BANK_SLOT_1));
|
||||||
|
int bankBagBase = static_cast<int>(fieldIndex(UF::PLAYER_FIELD_BANKBAG_SLOT_1));
|
||||||
|
|
||||||
|
// Derive slot counts from field gap (Classic=24/6, TBC/WotLK=28/7).
|
||||||
|
if (bankBase != 0xFFFF && bankBagBase != 0xFFFF) {
|
||||||
|
effectiveBankSlots_ = std::min((bankBagBase - bankBase) / 2, 28);
|
||||||
|
effectiveBankBagSlots_ = (effectiveBankSlots_ <= 24) ? 6 : 7;
|
||||||
|
}
|
||||||
|
|
||||||
|
int keyringBase = static_cast<int>(fieldIndex(UF::PLAYER_FIELD_KEYRING_SLOT_1));
|
||||||
|
if (keyringBase == 0xFFFF && bankBagBase != 0xFFFF) {
|
||||||
|
// Layout fallback for profiles that don't define PLAYER_FIELD_KEYRING_SLOT_1.
|
||||||
|
// Bank bag slots are followed by 12 vendor buyback slots (24 fields), then keyring.
|
||||||
|
keyringBase = bankBagBase + (effectiveBankBagSlots_ * 2) + 24;
|
||||||
|
}
|
||||||
|
|
||||||
for (const auto& [key, val] : fields) {
|
for (const auto& [key, val] : fields) {
|
||||||
if (key >= equipBase && key <= equipBase + (game::Inventory::NUM_EQUIP_SLOTS * 2 - 1)) {
|
if (key >= equipBase && key <= equipBase + (game::Inventory::NUM_EQUIP_SLOTS * 2 - 1)) {
|
||||||
|
|
@ -13609,15 +13665,17 @@ bool GameHandler::applyInventoryFields(const std::map<uint16_t, uint32_t>& field
|
||||||
else guid = (guid & 0x00000000FFFFFFFFULL) | (uint64_t(val) << 32);
|
else guid = (guid & 0x00000000FFFFFFFFULL) | (uint64_t(val) << 32);
|
||||||
slotsChanged = true;
|
slotsChanged = true;
|
||||||
}
|
}
|
||||||
|
} else if (keyringBase != 0xFFFF &&
|
||||||
|
key >= keyringBase &&
|
||||||
|
key <= keyringBase + (game::Inventory::KEYRING_SLOTS * 2 - 1)) {
|
||||||
|
int slotIndex = (key - keyringBase) / 2;
|
||||||
|
bool isLow = ((key - keyringBase) % 2 == 0);
|
||||||
|
if (slotIndex < static_cast<int>(keyringSlotGuids_.size())) {
|
||||||
|
uint64_t& guid = keyringSlotGuids_[slotIndex];
|
||||||
|
if (isLow) guid = (guid & 0xFFFFFFFF00000000ULL) | val;
|
||||||
|
else guid = (guid & 0x00000000FFFFFFFFULL) | (uint64_t(val) << 32);
|
||||||
|
slotsChanged = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bank slots starting at PLAYER_FIELD_BANK_SLOT_1
|
|
||||||
int bankBase = static_cast<int>(fieldIndex(UF::PLAYER_FIELD_BANK_SLOT_1));
|
|
||||||
int bankBagBase = static_cast<int>(fieldIndex(UF::PLAYER_FIELD_BANKBAG_SLOT_1));
|
|
||||||
// Derive slot counts from field gap (Classic=24/6, TBC/WotLK=28/7)
|
|
||||||
if (bankBase != 0xFFFF && bankBagBase != 0xFFFF) {
|
|
||||||
effectiveBankSlots_ = std::min((bankBagBase - bankBase) / 2, 28);
|
|
||||||
effectiveBankBagSlots_ = (effectiveBankSlots_ <= 24) ? 6 : 7;
|
|
||||||
}
|
}
|
||||||
if (bankBase != 0xFFFF && key >= static_cast<uint16_t>(bankBase) &&
|
if (bankBase != 0xFFFF && key >= static_cast<uint16_t>(bankBase) &&
|
||||||
key <= static_cast<uint16_t>(bankBase) + (effectiveBankSlots_ * 2 - 1)) {
|
key <= static_cast<uint16_t>(bankBase) + (effectiveBankSlots_ * 2 - 1)) {
|
||||||
|
|
@ -13778,6 +13836,55 @@ void GameHandler::rebuildOnlineInventory() {
|
||||||
inventory.setBackpackSlot(i, def);
|
inventory.setBackpackSlot(i, def);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Keyring slots
|
||||||
|
for (int i = 0; i < game::Inventory::KEYRING_SLOTS; i++) {
|
||||||
|
uint64_t guid = keyringSlotGuids_[i];
|
||||||
|
if (guid == 0) continue;
|
||||||
|
|
||||||
|
auto itemIt = onlineItems_.find(guid);
|
||||||
|
if (itemIt == onlineItems_.end()) continue;
|
||||||
|
|
||||||
|
ItemDef def;
|
||||||
|
def.itemId = itemIt->second.entry;
|
||||||
|
def.stackCount = itemIt->second.stackCount;
|
||||||
|
def.curDurability = itemIt->second.curDurability;
|
||||||
|
def.maxDurability = itemIt->second.maxDurability;
|
||||||
|
def.maxStack = 1;
|
||||||
|
|
||||||
|
auto infoIt = itemInfoCache_.find(itemIt->second.entry);
|
||||||
|
if (infoIt != itemInfoCache_.end()) {
|
||||||
|
def.name = infoIt->second.name;
|
||||||
|
def.quality = static_cast<ItemQuality>(infoIt->second.quality);
|
||||||
|
def.inventoryType = infoIt->second.inventoryType;
|
||||||
|
def.maxStack = std::max(1, infoIt->second.maxStack);
|
||||||
|
def.displayInfoId = infoIt->second.displayInfoId;
|
||||||
|
def.subclassName = infoIt->second.subclassName;
|
||||||
|
def.damageMin = infoIt->second.damageMin;
|
||||||
|
def.damageMax = infoIt->second.damageMax;
|
||||||
|
def.delayMs = infoIt->second.delayMs;
|
||||||
|
def.armor = infoIt->second.armor;
|
||||||
|
def.stamina = infoIt->second.stamina;
|
||||||
|
def.strength = infoIt->second.strength;
|
||||||
|
def.agility = infoIt->second.agility;
|
||||||
|
def.intellect = infoIt->second.intellect;
|
||||||
|
def.spirit = infoIt->second.spirit;
|
||||||
|
def.sellPrice = infoIt->second.sellPrice;
|
||||||
|
def.itemLevel = infoIt->second.itemLevel;
|
||||||
|
def.requiredLevel = infoIt->second.requiredLevel;
|
||||||
|
def.bindType = infoIt->second.bindType;
|
||||||
|
def.description = infoIt->second.description;
|
||||||
|
def.startQuestId = infoIt->second.startQuestId;
|
||||||
|
def.extraStats.clear();
|
||||||
|
for (const auto& es : infoIt->second.extraStats)
|
||||||
|
def.extraStats.push_back({es.statType, es.statValue});
|
||||||
|
} else {
|
||||||
|
def.name = "Item " + std::to_string(def.itemId);
|
||||||
|
queryItemInfo(def.itemId, guid);
|
||||||
|
}
|
||||||
|
|
||||||
|
inventory.setKeyringSlot(i, def);
|
||||||
|
}
|
||||||
|
|
||||||
// Bag contents (BAG1-BAG4 are equip slots 19-22)
|
// Bag contents (BAG1-BAG4 are equip slots 19-22)
|
||||||
for (int bagIdx = 0; bagIdx < 4; bagIdx++) {
|
for (int bagIdx = 0; bagIdx < 4; bagIdx++) {
|
||||||
uint64_t bagGuid = equipSlotGuids_[19 + bagIdx];
|
uint64_t bagGuid = equipSlotGuids_[19 + bagIdx];
|
||||||
|
|
@ -14021,6 +14128,8 @@ void GameHandler::rebuildOnlineInventory() {
|
||||||
int c = 0; for (auto g : equipSlotGuids_) if (g) c++; return c;
|
int c = 0; for (auto g : equipSlotGuids_) if (g) c++; return c;
|
||||||
}(), " backpack=", [&](){
|
}(), " backpack=", [&](){
|
||||||
int c = 0; for (auto g : backpackSlotGuids_) if (g) c++; return c;
|
int c = 0; for (auto g : backpackSlotGuids_) if (g) c++; return c;
|
||||||
|
}(), " keyring=", [&](){
|
||||||
|
int c = 0; for (auto g : keyringSlotGuids_) if (g) c++; return c;
|
||||||
}());
|
}());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -16667,9 +16776,12 @@ void GameHandler::castSpell(uint32_t spellId, uint64_t targetGuid) {
|
||||||
socket->send(packet);
|
socket->send(packet);
|
||||||
LOG_INFO("Casting spell: ", spellId, " on 0x", std::hex, target, std::dec);
|
LOG_INFO("Casting spell: ", spellId, " on 0x", std::hex, target, std::dec);
|
||||||
|
|
||||||
// Optimistically start GCD immediately on cast — server will confirm or override
|
// Optimistically start GCD immediately on cast, but do not restart it while
|
||||||
|
// already active (prevents timeout animation reset on repeated key presses).
|
||||||
|
if (!isGCDActive()) {
|
||||||
gcdTotal_ = 1.5f;
|
gcdTotal_ = 1.5f;
|
||||||
gcdStartedAt_ = std::chrono::steady_clock::now();
|
gcdStartedAt_ = std::chrono::steady_clock::now();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void GameHandler::cancelCast() {
|
void GameHandler::cancelCast() {
|
||||||
|
|
@ -17184,13 +17296,24 @@ void GameHandler::handleSpellCooldown(network::Packet& packet) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
auto it = spellCooldowns.find(spellId);
|
||||||
|
if (it == spellCooldowns.end()) {
|
||||||
spellCooldowns[spellId] = seconds;
|
spellCooldowns[spellId] = seconds;
|
||||||
|
} else {
|
||||||
|
it->second = mergeCooldownSeconds(it->second, seconds);
|
||||||
|
}
|
||||||
for (auto& slot : actionBar) {
|
for (auto& slot : actionBar) {
|
||||||
bool match = (slot.type == ActionBarSlot::SPELL && slot.id == spellId)
|
bool match = (slot.type == ActionBarSlot::SPELL && slot.id == spellId)
|
||||||
|| (cdItemId != 0 && slot.type == ActionBarSlot::ITEM && slot.id == cdItemId);
|
|| (cdItemId != 0 && slot.type == ActionBarSlot::ITEM && slot.id == cdItemId);
|
||||||
if (match) {
|
if (match) {
|
||||||
|
float prevRemaining = slot.cooldownRemaining;
|
||||||
|
float merged = mergeCooldownSeconds(slot.cooldownRemaining, seconds);
|
||||||
|
slot.cooldownRemaining = merged;
|
||||||
|
if (slot.cooldownTotal <= 0.0f || prevRemaining <= 0.0f) {
|
||||||
slot.cooldownTotal = seconds;
|
slot.cooldownTotal = seconds;
|
||||||
slot.cooldownRemaining = seconds;
|
} else {
|
||||||
|
slot.cooldownTotal = std::max(slot.cooldownTotal, merged);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -18445,6 +18568,21 @@ void GameHandler::selectGossipOption(uint32_t optionId) {
|
||||||
LOG_INFO("Sent CMSG_BANKER_ACTIVATE for npc=0x", std::hex, currentGossip.npcGuid, std::dec);
|
LOG_INFO("Sent CMSG_BANKER_ACTIVATE for npc=0x", std::hex, currentGossip.npcGuid, std::dec);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Vendor / repair: some servers require an explicit CMSG_LIST_INVENTORY after gossip select.
|
||||||
|
const bool isVendor = (text == "GOSSIP_OPTION_VENDOR" ||
|
||||||
|
(textLower.find("browse") != std::string::npos &&
|
||||||
|
(textLower.find("goods") != std::string::npos || textLower.find("wares") != std::string::npos)));
|
||||||
|
const bool isArmorer = (text == "GOSSIP_OPTION_ARMORER" || textLower.find("repair") != std::string::npos);
|
||||||
|
if (isVendor || isArmorer) {
|
||||||
|
if (isArmorer) {
|
||||||
|
setVendorCanRepair(true);
|
||||||
|
}
|
||||||
|
auto pkt = ListInventoryPacket::build(currentGossip.npcGuid);
|
||||||
|
socket->send(pkt);
|
||||||
|
LOG_INFO("Sent CMSG_LIST_INVENTORY (gossip) to npc=0x", std::hex, currentGossip.npcGuid, std::dec,
|
||||||
|
" vendor=", (int)isVendor, " repair=", (int)isArmorer);
|
||||||
|
}
|
||||||
|
|
||||||
if (textLower.find("make this inn your home") != std::string::npos ||
|
if (textLower.find("make this inn your home") != std::string::npos ||
|
||||||
textLower.find("set your home") != std::string::npos) {
|
textLower.find("set your home") != std::string::npos) {
|
||||||
auto bindPkt = BinderActivatePacket::build(currentGossip.npcGuid);
|
auto bindPkt = BinderActivatePacket::build(currentGossip.npcGuid);
|
||||||
|
|
@ -19567,7 +19705,8 @@ void GameHandler::handleLootResponse(network::Packet& packet) {
|
||||||
std::remove_if(pendingGameObjectLootOpens_.begin(), pendingGameObjectLootOpens_.end(),
|
std::remove_if(pendingGameObjectLootOpens_.begin(), pendingGameObjectLootOpens_.end(),
|
||||||
[&](const PendingLootOpen& p) { return p.guid == currentLoot.lootGuid; }),
|
[&](const PendingLootOpen& p) { return p.guid == currentLoot.lootGuid; }),
|
||||||
pendingGameObjectLootOpens_.end());
|
pendingGameObjectLootOpens_.end());
|
||||||
localLootState_[currentLoot.lootGuid] = LocalLootState{currentLoot, false};
|
auto& localLoot = localLootState_[currentLoot.lootGuid];
|
||||||
|
localLoot.data = currentLoot;
|
||||||
|
|
||||||
// Query item info so loot window can show names instead of IDs
|
// Query item info so loot window can show names instead of IDs
|
||||||
for (const auto& item : currentLoot.items) {
|
for (const auto& item : currentLoot.items) {
|
||||||
|
|
@ -19592,11 +19731,12 @@ void GameHandler::handleLootResponse(network::Packet& packet) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auto-loot items when enabled
|
// Auto-loot items when enabled
|
||||||
if (autoLoot_ && state == WorldState::IN_WORLD && socket) {
|
if (autoLoot_ && state == WorldState::IN_WORLD && socket && !localLoot.itemAutoLootSent) {
|
||||||
for (const auto& item : currentLoot.items) {
|
for (const auto& item : currentLoot.items) {
|
||||||
auto pkt = AutostoreLootItemPacket::build(item.slotIndex);
|
auto pkt = AutostoreLootItemPacket::build(item.slotIndex);
|
||||||
socket->send(pkt);
|
socket->send(pkt);
|
||||||
}
|
}
|
||||||
|
localLoot.itemAutoLootSent = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -19805,13 +19945,14 @@ void GameHandler::handleListInventory(network::Packet& packet) {
|
||||||
bool savedCanRepair = currentVendorItems.canRepair; // preserve armorer flag set via gossip path
|
bool savedCanRepair = currentVendorItems.canRepair; // preserve armorer flag set via gossip path
|
||||||
if (!ListInventoryParser::parse(packet, currentVendorItems)) return;
|
if (!ListInventoryParser::parse(packet, currentVendorItems)) return;
|
||||||
|
|
||||||
// Check NPC_FLAG_REPAIR (0x40) on the vendor entity — this handles vendors that open
|
// Check NPC_FLAG_REPAIR (0x1000) on the vendor entity — this handles vendors that open
|
||||||
// directly without going through the gossip armorer option.
|
// directly without going through the gossip armorer option.
|
||||||
if (!savedCanRepair && currentVendorItems.vendorGuid != 0) {
|
if (!savedCanRepair && currentVendorItems.vendorGuid != 0) {
|
||||||
auto entity = entityManager.getEntity(currentVendorItems.vendorGuid);
|
auto entity = entityManager.getEntity(currentVendorItems.vendorGuid);
|
||||||
if (entity && entity->getType() == ObjectType::UNIT) {
|
if (entity && entity->getType() == ObjectType::UNIT) {
|
||||||
auto unit = std::static_pointer_cast<Unit>(entity);
|
auto unit = std::static_pointer_cast<Unit>(entity);
|
||||||
if (unit->getNpcFlags() & 0x40) { // NPC_FLAG_REPAIR
|
// MaNGOS/Trinity: UNIT_NPC_FLAG_REPAIR = 0x00001000.
|
||||||
|
if (unit->getNpcFlags() & 0x1000) {
|
||||||
savedCanRepair = true;
|
savedCanRepair = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -20402,6 +20543,7 @@ void GameHandler::handleNewWorld(network::Packet& packet) {
|
||||||
pendingSpiritHealerGuid_ = 0;
|
pendingSpiritHealerGuid_ = 0;
|
||||||
resurrectCasterGuid_ = 0;
|
resurrectCasterGuid_ = 0;
|
||||||
corpseMapId_ = 0;
|
corpseMapId_ = 0;
|
||||||
|
corpseGuid_ = 0;
|
||||||
hostileAttackers_.clear();
|
hostileAttackers_.clear();
|
||||||
stopAutoAttack();
|
stopAutoAttack();
|
||||||
tabCycleStale = true;
|
tabCycleStale = true;
|
||||||
|
|
@ -22120,9 +22262,9 @@ void GameHandler::mailTakeMoney(uint32_t mailId) {
|
||||||
socket->send(packet);
|
socket->send(packet);
|
||||||
}
|
}
|
||||||
|
|
||||||
void GameHandler::mailTakeItem(uint32_t mailId, uint32_t itemIndex) {
|
void GameHandler::mailTakeItem(uint32_t mailId, uint32_t itemGuidLow) {
|
||||||
if (state != WorldState::IN_WORLD || !socket || mailboxGuid_ == 0) return;
|
if (state != WorldState::IN_WORLD || !socket || mailboxGuid_ == 0) return;
|
||||||
auto packet = packetParsers_->buildMailTakeItem(mailboxGuid_, mailId, itemIndex);
|
auto packet = packetParsers_->buildMailTakeItem(mailboxGuid_, mailId, itemGuidLow);
|
||||||
socket->send(packet);
|
socket->send(packet);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -22873,7 +23015,7 @@ void GameHandler::declineTradeRequest() {
|
||||||
}
|
}
|
||||||
|
|
||||||
void GameHandler::acceptTrade() {
|
void GameHandler::acceptTrade() {
|
||||||
if (tradeStatus_ != TradeStatus::Open || !socket) return;
|
if (!isTradeOpen() || !socket) return;
|
||||||
tradeStatus_ = TradeStatus::Accepted;
|
tradeStatus_ = TradeStatus::Accepted;
|
||||||
socket->send(AcceptTradePacket::build());
|
socket->send(AcceptTradePacket::build());
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,23 @@ bool Inventory::clearEquipSlot(EquipSlot slot) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ItemSlot& Inventory::getKeyringSlot(int index) const {
|
||||||
|
if (index < 0 || index >= KEYRING_SLOTS) return EMPTY_SLOT;
|
||||||
|
return keyring_[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Inventory::setKeyringSlot(int index, const ItemDef& item) {
|
||||||
|
if (index < 0 || index >= KEYRING_SLOTS) return false;
|
||||||
|
keyring_[index].item = item;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Inventory::clearKeyringSlot(int index) {
|
||||||
|
if (index < 0 || index >= KEYRING_SLOTS) return false;
|
||||||
|
keyring_[index].item = ItemDef{};
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
int Inventory::getBagSize(int bagIndex) const {
|
int Inventory::getBagSize(int bagIndex) const {
|
||||||
if (bagIndex < 0 || bagIndex >= NUM_BAG_SLOTS) return 0;
|
if (bagIndex < 0 || bagIndex >= NUM_BAG_SLOTS) return 0;
|
||||||
return bags[bagIndex].size;
|
return bags[bagIndex].size;
|
||||||
|
|
|
||||||
|
|
@ -251,8 +251,10 @@ void TransportManager::updateTransportMovement(ActiveTransport& transport, float
|
||||||
if (path.durationMs == 0) {
|
if (path.durationMs == 0) {
|
||||||
// Just update transform (position already set)
|
// Just update transform (position already set)
|
||||||
updateTransformMatrices(transport);
|
updateTransformMatrices(transport);
|
||||||
if (wmoRenderer_) {
|
if (transport.isM2) {
|
||||||
wmoRenderer_->setInstanceTransform(transport.wmoInstanceId, transport.transform);
|
if (m2Renderer_) m2Renderer_->setInstanceTransform(transport.wmoInstanceId, transport.transform);
|
||||||
|
} else {
|
||||||
|
if (wmoRenderer_) wmoRenderer_->setInstanceTransform(transport.wmoInstanceId, transport.transform);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -287,8 +289,10 @@ void TransportManager::updateTransportMovement(ActiveTransport& transport, float
|
||||||
} else {
|
} else {
|
||||||
// Strict server-authoritative mode: do not guess movement between server snapshots.
|
// Strict server-authoritative mode: do not guess movement between server snapshots.
|
||||||
updateTransformMatrices(transport);
|
updateTransformMatrices(transport);
|
||||||
if (wmoRenderer_) {
|
if (transport.isM2) {
|
||||||
wmoRenderer_->setInstanceTransform(transport.wmoInstanceId, transport.transform);
|
if (m2Renderer_) m2Renderer_->setInstanceTransform(transport.wmoInstanceId, transport.transform);
|
||||||
|
} else {
|
||||||
|
if (wmoRenderer_) wmoRenderer_->setInstanceTransform(transport.wmoInstanceId, transport.transform);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -777,8 +781,10 @@ void TransportManager::updateServerTransport(uint64_t guid, const glm::vec3& pos
|
||||||
}
|
}
|
||||||
|
|
||||||
updateTransformMatrices(*transport);
|
updateTransformMatrices(*transport);
|
||||||
if (wmoRenderer_) {
|
if (transport->isM2) {
|
||||||
wmoRenderer_->setInstanceTransform(transport->wmoInstanceId, transport->transform);
|
if (m2Renderer_) m2Renderer_->setInstanceTransform(transport->wmoInstanceId, transport->transform);
|
||||||
|
} else {
|
||||||
|
if (wmoRenderer_) wmoRenderer_->setInstanceTransform(transport->wmoInstanceId, transport->transform);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,7 @@ static const UFNameEntry kUFNames[] = {
|
||||||
{"PLAYER_QUEST_LOG_START", UF::PLAYER_QUEST_LOG_START},
|
{"PLAYER_QUEST_LOG_START", UF::PLAYER_QUEST_LOG_START},
|
||||||
{"PLAYER_FIELD_INV_SLOT_HEAD", UF::PLAYER_FIELD_INV_SLOT_HEAD},
|
{"PLAYER_FIELD_INV_SLOT_HEAD", UF::PLAYER_FIELD_INV_SLOT_HEAD},
|
||||||
{"PLAYER_FIELD_PACK_SLOT_1", UF::PLAYER_FIELD_PACK_SLOT_1},
|
{"PLAYER_FIELD_PACK_SLOT_1", UF::PLAYER_FIELD_PACK_SLOT_1},
|
||||||
|
{"PLAYER_FIELD_KEYRING_SLOT_1", UF::PLAYER_FIELD_KEYRING_SLOT_1},
|
||||||
{"PLAYER_FIELD_BANK_SLOT_1", UF::PLAYER_FIELD_BANK_SLOT_1},
|
{"PLAYER_FIELD_BANK_SLOT_1", UF::PLAYER_FIELD_BANK_SLOT_1},
|
||||||
{"PLAYER_FIELD_BANKBAG_SLOT_1", UF::PLAYER_FIELD_BANKBAG_SLOT_1},
|
{"PLAYER_FIELD_BANKBAG_SLOT_1", UF::PLAYER_FIELD_BANKBAG_SLOT_1},
|
||||||
{"PLAYER_SKILL_INFO_START", UF::PLAYER_SKILL_INFO_START},
|
{"PLAYER_SKILL_INFO_START", UF::PLAYER_SKILL_INFO_START},
|
||||||
|
|
|
||||||
|
|
@ -1482,6 +1482,47 @@ bool MessageChatParser::parse(network::Packet& packet, MessageChatData& data) {
|
||||||
// Read unknown field
|
// Read unknown field
|
||||||
packet.readUInt32();
|
packet.readUInt32();
|
||||||
|
|
||||||
|
auto tryReadSizedCString = [&](std::string& out, uint32_t maxLen, size_t minTrailingBytes) -> bool {
|
||||||
|
size_t start = packet.getReadPos();
|
||||||
|
size_t remaining = packet.getSize() - start;
|
||||||
|
if (remaining < 4 + minTrailingBytes) return false;
|
||||||
|
|
||||||
|
uint32_t len = packet.readUInt32();
|
||||||
|
if (len < 2 || len > maxLen) {
|
||||||
|
packet.setReadPos(start);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if ((packet.getSize() - packet.getReadPos()) < (static_cast<size_t>(len) + minTrailingBytes)) {
|
||||||
|
packet.setReadPos(start);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string tmp;
|
||||||
|
tmp.resize(len);
|
||||||
|
for (uint32_t i = 0; i < len; ++i) {
|
||||||
|
tmp[i] = static_cast<char>(packet.readUInt8());
|
||||||
|
}
|
||||||
|
if (tmp.empty() || tmp.back() != '\0') {
|
||||||
|
packet.setReadPos(start);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
tmp.pop_back();
|
||||||
|
if (tmp.empty()) {
|
||||||
|
packet.setReadPos(start);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (char c : tmp) {
|
||||||
|
unsigned char uc = static_cast<unsigned char>(c);
|
||||||
|
if (uc < 32 || uc > 126) {
|
||||||
|
packet.setReadPos(start);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
out = std::move(tmp);
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
// Type-specific data
|
// Type-specific data
|
||||||
// WoW 3.3.5 SMSG_MESSAGECHAT format: after senderGuid+unk, most types
|
// WoW 3.3.5 SMSG_MESSAGECHAT format: after senderGuid+unk, most types
|
||||||
// have a receiverGuid (uint64). Some types have extra fields before it.
|
// have a receiverGuid (uint64). Some types have extra fields before it.
|
||||||
|
|
@ -1537,6 +1578,27 @@ bool MessageChatParser::parse(network::Packet& packet, MessageChatData& data) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case ChatType::WHISPER:
|
||||||
|
case ChatType::WHISPER_INFORM: {
|
||||||
|
// Some cores include an explicit sized sender/receiver name for whisper chat.
|
||||||
|
// Consume it when present so /r has a reliable last whisper sender.
|
||||||
|
if (data.type == ChatType::WHISPER) {
|
||||||
|
tryReadSizedCString(data.senderName, 128, 8 + 4 + 1);
|
||||||
|
} else {
|
||||||
|
tryReadSizedCString(data.receiverName, 128, 8 + 4 + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
data.receiverGuid = packet.readUInt64();
|
||||||
|
|
||||||
|
// Optional trailing whisper target/source name on some formats.
|
||||||
|
if (data.type == ChatType::WHISPER && data.receiverName.empty()) {
|
||||||
|
tryReadSizedCString(data.receiverName, 128, 4 + 1);
|
||||||
|
} else if (data.type == ChatType::WHISPER_INFORM && data.senderName.empty()) {
|
||||||
|
tryReadSizedCString(data.senderName, 128, 4 + 1);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
case ChatType::BG_SYSTEM_NEUTRAL:
|
case ChatType::BG_SYSTEM_NEUTRAL:
|
||||||
case ChatType::BG_SYSTEM_ALLIANCE:
|
case ChatType::BG_SYSTEM_ALLIANCE:
|
||||||
case ChatType::BG_SYSTEM_HORDE:
|
case ChatType::BG_SYSTEM_HORDE:
|
||||||
|
|
@ -4881,6 +4943,12 @@ network::Packet RepopRequestPacket::build() {
|
||||||
return packet;
|
return packet;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
network::Packet ReclaimCorpsePacket::build(uint64_t guid) {
|
||||||
|
network::Packet packet(wireOpcode(Opcode::CMSG_RECLAIM_CORPSE));
|
||||||
|
packet.writeUInt64(guid);
|
||||||
|
return packet;
|
||||||
|
}
|
||||||
|
|
||||||
network::Packet SpiritHealerActivatePacket::build(uint64_t npcGuid) {
|
network::Packet SpiritHealerActivatePacket::build(uint64_t npcGuid) {
|
||||||
network::Packet packet(wireOpcode(Opcode::CMSG_SPIRIT_HEALER_ACTIVATE));
|
network::Packet packet(wireOpcode(Opcode::CMSG_SPIRIT_HEALER_ACTIVATE));
|
||||||
packet.writeUInt64(npcGuid);
|
packet.writeUInt64(npcGuid);
|
||||||
|
|
@ -5001,11 +5069,12 @@ network::Packet MailTakeMoneyPacket::build(uint64_t mailboxGuid, uint32_t mailId
|
||||||
return packet;
|
return packet;
|
||||||
}
|
}
|
||||||
|
|
||||||
network::Packet MailTakeItemPacket::build(uint64_t mailboxGuid, uint32_t mailId, uint32_t itemIndex) {
|
network::Packet MailTakeItemPacket::build(uint64_t mailboxGuid, uint32_t mailId, uint32_t itemGuidLow) {
|
||||||
network::Packet packet(wireOpcode(Opcode::CMSG_MAIL_TAKE_ITEM));
|
network::Packet packet(wireOpcode(Opcode::CMSG_MAIL_TAKE_ITEM));
|
||||||
packet.writeUInt64(mailboxGuid);
|
packet.writeUInt64(mailboxGuid);
|
||||||
packet.writeUInt32(mailId);
|
packet.writeUInt32(mailId);
|
||||||
packet.writeUInt32(itemIndex);
|
// WotLK expects attachment item GUID low, not attachment slot index.
|
||||||
|
packet.writeUInt32(itemGuidLow);
|
||||||
return packet;
|
return packet;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -5072,10 +5141,9 @@ bool PacketParsers::parseMailList(network::Packet& packet, std::vector<MailMessa
|
||||||
msg.expirationTime = packet.readFloat();
|
msg.expirationTime = packet.readFloat();
|
||||||
msg.mailTemplateId = packet.readUInt32();
|
msg.mailTemplateId = packet.readUInt32();
|
||||||
msg.subject = packet.readString();
|
msg.subject = packet.readString();
|
||||||
|
// WotLK 3.3.5a always includes body text in SMSG_MAIL_LIST_RESULT.
|
||||||
if (msg.mailTemplateId == 0) {
|
// mailTemplateId != 0 still carries a (possibly empty) body string.
|
||||||
msg.body = packet.readString();
|
msg.body = packet.readString();
|
||||||
}
|
|
||||||
|
|
||||||
uint8_t attachCount = packet.readUInt8();
|
uint8_t attachCount = packet.readUInt8();
|
||||||
msg.attachments.reserve(attachCount);
|
msg.attachments.reserve(attachCount);
|
||||||
|
|
@ -5095,6 +5163,8 @@ bool PacketParsers::parseMailList(network::Packet& packet, std::vector<MailMessa
|
||||||
att.stackCount = packet.readUInt32();
|
att.stackCount = packet.readUInt32();
|
||||||
att.chargesOrDurability = packet.readUInt32();
|
att.chargesOrDurability = packet.readUInt32();
|
||||||
att.maxDurability = packet.readUInt32();
|
att.maxDurability = packet.readUInt32();
|
||||||
|
packet.readUInt32(); // durability/current durability
|
||||||
|
packet.readUInt8(); // unknown WotLK trailing byte per attachment
|
||||||
msg.attachments.push_back(att);
|
msg.attachments.push_back(att);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1623,10 +1623,6 @@ void CharacterRenderer::update(float deltaTime, const glm::vec3& cameraPos) {
|
||||||
if (t >= 1.0f) {
|
if (t >= 1.0f) {
|
||||||
inst.position = inst.moveEnd;
|
inst.position = inst.moveEnd;
|
||||||
inst.isMoving = false;
|
inst.isMoving = false;
|
||||||
// Return to idle when movement completes
|
|
||||||
if (inst.currentAnimationId == 4 || inst.currentAnimationId == 5) {
|
|
||||||
playAnimation(pair.first, 0, true);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
inst.position = glm::mix(inst.moveStart, inst.moveEnd, t);
|
inst.position = glm::mix(inst.moveStart, inst.moveEnd, t);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2654,8 +2654,12 @@ void M2Renderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, const
|
||||||
(batch.blendMode >= 3) ||
|
(batch.blendMode >= 3) ||
|
||||||
batch.colorKeyBlack ||
|
batch.colorKeyBlack ||
|
||||||
((batch.materialFlags & 0x01) != 0);
|
((batch.materialFlags & 0x01) != 0);
|
||||||
if ((batch.glowCardLike && lanternLikeModel) ||
|
const bool lanternGlowCardSkip =
|
||||||
(cardLikeSkipMesh && !lanternLikeModel)) {
|
lanternLikeModel &&
|
||||||
|
batch.lanternGlowHint &&
|
||||||
|
smallCardLikeBatch &&
|
||||||
|
cardLikeSkipMesh;
|
||||||
|
if (lanternGlowCardSkip || (cardLikeSkipMesh && !lanternLikeModel)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -2851,16 +2855,25 @@ void M2Renderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, const
|
||||||
|
|
||||||
// Skip glow sprites (handled after loop)
|
// Skip glow sprites (handled after loop)
|
||||||
const bool batchUnlit = (batch.materialFlags & 0x01) != 0;
|
const bool batchUnlit = (batch.materialFlags & 0x01) != 0;
|
||||||
|
const bool koboldFlameCard = batch.colorKeyBlack && model.isKoboldFlame;
|
||||||
|
const bool smallCardLikeBatch =
|
||||||
|
(batch.glowSize <= 1.35f) ||
|
||||||
|
(batch.lanternGlowHint && batch.glowSize <= 6.0f);
|
||||||
const bool shouldUseGlowSprite =
|
const bool shouldUseGlowSprite =
|
||||||
!batch.colorKeyBlack &&
|
!koboldFlameCard &&
|
||||||
(model.isElvenLike || model.isLanternLike) &&
|
(model.isElvenLike || model.isLanternLike) &&
|
||||||
!model.isSpellEffect &&
|
!model.isSpellEffect &&
|
||||||
(batch.glowSize <= 1.35f || (batch.lanternGlowHint && batch.glowSize <= 6.0f)) &&
|
smallCardLikeBatch &&
|
||||||
(batch.lanternGlowHint || (batch.blendMode >= 3) ||
|
(batch.lanternGlowHint || (batch.blendMode >= 3) ||
|
||||||
(batch.colorKeyBlack && batchUnlit && batch.blendMode >= 1));
|
(batch.colorKeyBlack && batchUnlit && batch.blendMode >= 1));
|
||||||
if (shouldUseGlowSprite) {
|
if (shouldUseGlowSprite) {
|
||||||
const bool cardLikeSkipMesh = (batch.blendMode >= 3) || batch.colorKeyBlack || batchUnlit;
|
const bool cardLikeSkipMesh = (batch.blendMode >= 3) || batch.colorKeyBlack || batchUnlit;
|
||||||
if ((batch.glowCardLike && model.isLanternLike) || (cardLikeSkipMesh && !model.isLanternLike))
|
const bool lanternGlowCardSkip =
|
||||||
|
model.isLanternLike &&
|
||||||
|
batch.lanternGlowHint &&
|
||||||
|
smallCardLikeBatch &&
|
||||||
|
cardLikeSkipMesh;
|
||||||
|
if (lanternGlowCardSkip || (cardLikeSkipMesh && !model.isLanternLike))
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1031,13 +1031,14 @@ void Renderer::beginFrame() {
|
||||||
|
|
||||||
// FXAA resource management — FXAA can coexist with FSR1 and FSR3.
|
// FXAA resource management — FXAA can coexist with FSR1 and FSR3.
|
||||||
// When both FXAA and FSR3 are enabled, FXAA runs as a post-FSR3 pass.
|
// When both FXAA and FSR3 are enabled, FXAA runs as a post-FSR3 pass.
|
||||||
// When both FXAA and FSR1 are enabled, FXAA takes priority (native res render).
|
// Do not force this pass for ghost mode; keep AA quality strictly user-controlled.
|
||||||
if (fxaa_.needsRecreate && fxaa_.sceneFramebuffer) {
|
const bool useFXAAPostPass = fxaa_.enabled;
|
||||||
|
if ((fxaa_.needsRecreate || !useFXAAPostPass) && fxaa_.sceneFramebuffer) {
|
||||||
destroyFXAAResources();
|
destroyFXAAResources();
|
||||||
fxaa_.needsRecreate = false;
|
fxaa_.needsRecreate = false;
|
||||||
if (!fxaa_.enabled) LOG_INFO("FXAA: disabled");
|
if (!useFXAAPostPass) LOG_INFO("FXAA: disabled");
|
||||||
}
|
}
|
||||||
if (fxaa_.enabled && !fxaa_.sceneFramebuffer) {
|
if (useFXAAPostPass && !fxaa_.sceneFramebuffer) {
|
||||||
if (!initFXAAResources()) {
|
if (!initFXAAResources()) {
|
||||||
LOG_ERROR("FXAA: initialization failed, disabling");
|
LOG_ERROR("FXAA: initialization failed, disabling");
|
||||||
fxaa_.enabled = false;
|
fxaa_.enabled = false;
|
||||||
|
|
@ -1060,9 +1061,8 @@ void Renderer::beginFrame() {
|
||||||
destroyFSR2Resources();
|
destroyFSR2Resources();
|
||||||
initFSR2Resources();
|
initFSR2Resources();
|
||||||
}
|
}
|
||||||
// Recreate FXAA resources for new swapchain dimensions
|
// Recreate FXAA resources for new swapchain dimensions.
|
||||||
// FXAA can coexist with FSR1 and FSR3 simultaneously.
|
if (useFXAAPostPass) {
|
||||||
if (fxaa_.enabled) {
|
|
||||||
destroyFXAAResources();
|
destroyFXAAResources();
|
||||||
initFXAAResources();
|
initFXAAResources();
|
||||||
}
|
}
|
||||||
|
|
@ -1152,7 +1152,7 @@ void Renderer::beginFrame() {
|
||||||
if (fsr2_.enabled && fsr2_.sceneFramebuffer) {
|
if (fsr2_.enabled && fsr2_.sceneFramebuffer) {
|
||||||
rpInfo.framebuffer = fsr2_.sceneFramebuffer;
|
rpInfo.framebuffer = fsr2_.sceneFramebuffer;
|
||||||
renderExtent = { fsr2_.internalWidth, fsr2_.internalHeight };
|
renderExtent = { fsr2_.internalWidth, fsr2_.internalHeight };
|
||||||
} else if (fxaa_.enabled && fxaa_.sceneFramebuffer) {
|
} else if (useFXAAPostPass && fxaa_.sceneFramebuffer) {
|
||||||
// FXAA takes priority over FSR1: renders at native res with AA post-process.
|
// FXAA takes priority over FSR1: renders at native res with AA post-process.
|
||||||
// When both FSR1 and FXAA are enabled, FXAA wins (native res, no downscale).
|
// When both FSR1 and FXAA are enabled, FXAA wins (native res, no downscale).
|
||||||
rpInfo.framebuffer = fxaa_.sceneFramebuffer;
|
rpInfo.framebuffer = fxaa_.sceneFramebuffer;
|
||||||
|
|
@ -1856,7 +1856,18 @@ void Renderer::updateCharacterAnimation() {
|
||||||
|
|
||||||
CharAnimState newState = charAnimState;
|
CharAnimState newState = charAnimState;
|
||||||
|
|
||||||
bool moving = cameraController->isMoving();
|
const bool rawMoving = cameraController->isMoving();
|
||||||
|
const bool rawSprinting = cameraController->isSprinting();
|
||||||
|
constexpr float kLocomotionStopGraceSec = 0.12f;
|
||||||
|
if (rawMoving) {
|
||||||
|
locomotionStopGraceTimer_ = kLocomotionStopGraceSec;
|
||||||
|
locomotionWasSprinting_ = rawSprinting;
|
||||||
|
} else {
|
||||||
|
locomotionStopGraceTimer_ = std::max(0.0f, locomotionStopGraceTimer_ - lastDeltaTime_);
|
||||||
|
}
|
||||||
|
// Debounce brief input/state dropouts (notably during both-mouse steering) so
|
||||||
|
// locomotion clips do not restart every few frames.
|
||||||
|
bool moving = rawMoving || locomotionStopGraceTimer_ > 0.0f;
|
||||||
bool movingForward = cameraController->isMovingForward();
|
bool movingForward = cameraController->isMovingForward();
|
||||||
bool movingBackward = cameraController->isMovingBackward();
|
bool movingBackward = cameraController->isMovingBackward();
|
||||||
bool autoRunning = cameraController->isAutoRunning();
|
bool autoRunning = cameraController->isAutoRunning();
|
||||||
|
|
@ -1869,7 +1880,7 @@ void Renderer::updateCharacterAnimation() {
|
||||||
bool anyStrafeRight = strafeRight && !strafeLeft && pureStrafe;
|
bool anyStrafeRight = strafeRight && !strafeLeft && pureStrafe;
|
||||||
bool grounded = cameraController->isGrounded();
|
bool grounded = cameraController->isGrounded();
|
||||||
bool jumping = cameraController->isJumping();
|
bool jumping = cameraController->isJumping();
|
||||||
bool sprinting = cameraController->isSprinting();
|
bool sprinting = rawSprinting || (!rawMoving && moving && locomotionWasSprinting_);
|
||||||
bool sitting = cameraController->isSitting();
|
bool sitting = cameraController->isSitting();
|
||||||
bool swim = cameraController->isSwimming();
|
bool swim = cameraController->isSwimming();
|
||||||
bool forceMelee = meleeSwingTimer > 0.0f && grounded && !swim;
|
bool forceMelee = meleeSwingTimer > 0.0f && grounded && !swim;
|
||||||
|
|
@ -2529,8 +2540,14 @@ void Renderer::updateCharacterAnimation() {
|
||||||
float currentAnimTimeMs = 0.0f;
|
float currentAnimTimeMs = 0.0f;
|
||||||
float currentAnimDurationMs = 0.0f;
|
float currentAnimDurationMs = 0.0f;
|
||||||
bool haveState = characterRenderer->getAnimationState(characterInstanceId, currentAnimId, currentAnimTimeMs, currentAnimDurationMs);
|
bool haveState = characterRenderer->getAnimationState(characterInstanceId, currentAnimId, currentAnimTimeMs, currentAnimDurationMs);
|
||||||
if (!haveState || currentAnimId != animId) {
|
// Some frames may transiently fail getAnimationState() while resources/instance state churn.
|
||||||
|
// Avoid reissuing the same clip on those frames, which restarts locomotion and causes hitches.
|
||||||
|
const bool requestChanged = (lastPlayerAnimRequest_ != animId) || (lastPlayerAnimLoopRequest_ != loop);
|
||||||
|
const bool shouldPlay = (haveState && currentAnimId != animId) || (!haveState && requestChanged);
|
||||||
|
if (shouldPlay) {
|
||||||
characterRenderer->playAnimation(characterInstanceId, animId, loop);
|
characterRenderer->playAnimation(characterInstanceId, animId, loop);
|
||||||
|
lastPlayerAnimRequest_ = animId;
|
||||||
|
lastPlayerAnimLoopRequest_ = loop;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2701,6 +2718,13 @@ void Renderer::setTargetPosition(const glm::vec3* pos) {
|
||||||
targetPosition = pos;
|
targetPosition = pos;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Renderer::resetCombatVisualState() {
|
||||||
|
inCombat_ = false;
|
||||||
|
targetPosition = nullptr;
|
||||||
|
meleeSwingTimer = 0.0f;
|
||||||
|
meleeSwingCooldown = 0.0f;
|
||||||
|
}
|
||||||
|
|
||||||
bool Renderer::isMoving() const {
|
bool Renderer::isMoving() const {
|
||||||
return cameraController && cameraController->isMoving();
|
return cameraController && cameraController->isMoving();
|
||||||
}
|
}
|
||||||
|
|
@ -5074,7 +5098,7 @@ void Renderer::renderFXAAPass() {
|
||||||
vkCmdBindDescriptorSets(currentCmd, VK_PIPELINE_BIND_POINT_GRAPHICS,
|
vkCmdBindDescriptorSets(currentCmd, VK_PIPELINE_BIND_POINT_GRAPHICS,
|
||||||
fxaa_.pipelineLayout, 0, 1, &fxaa_.descSet, 0, nullptr);
|
fxaa_.pipelineLayout, 0, 1, &fxaa_.descSet, 0, nullptr);
|
||||||
|
|
||||||
// Pass rcpFrame + sharpness + desaturate (vec4, 16 bytes).
|
// Pass rcpFrame + sharpness + effect flag (vec4, 16 bytes).
|
||||||
// When FSR2/FSR3 is active alongside FXAA, forward FSR2's sharpness so the
|
// When FSR2/FSR3 is active alongside FXAA, forward FSR2's sharpness so the
|
||||||
// post-FXAA unsharp-mask step restores the crispness that FXAA's blur removes.
|
// post-FXAA unsharp-mask step restores the crispness that FXAA's blur removes.
|
||||||
float sharpness = fsr2_.enabled ? fsr2_.sharpness : 0.0f;
|
float sharpness = fsr2_.enabled ? fsr2_.sharpness : 0.0f;
|
||||||
|
|
@ -5082,7 +5106,7 @@ void Renderer::renderFXAAPass() {
|
||||||
1.0f / static_cast<float>(ext.width),
|
1.0f / static_cast<float>(ext.width),
|
||||||
1.0f / static_cast<float>(ext.height),
|
1.0f / static_cast<float>(ext.height),
|
||||||
sharpness,
|
sharpness,
|
||||||
ghostMode_ ? 1.0f : 0.0f // desaturate: 1=ghost grayscale, 0=normal
|
0.0f
|
||||||
};
|
};
|
||||||
vkCmdPushConstants(currentCmd, fxaa_.pipelineLayout,
|
vkCmdPushConstants(currentCmd, fxaa_.pipelineLayout,
|
||||||
VK_SHADER_STAGE_FRAGMENT_BIT, 0, 16, pc);
|
VK_SHADER_STAGE_FRAGMENT_BIT, 0, 16, pc);
|
||||||
|
|
@ -5272,12 +5296,6 @@ void Renderer::renderWorld(game::World* world, game::GameHandler* gameHandler) {
|
||||||
renderOverlay(tint, cmd);
|
renderOverlay(tint, cmd);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Ghost mode desaturation overlay (non-FXAA path approximation).
|
|
||||||
// When FXAA is active the FXAA shader applies true per-pixel desaturation;
|
|
||||||
// otherwise a high-opacity gray overlay gives a similar washed-out effect.
|
|
||||||
if (ghostMode_ && overlayPipeline && !fxaa_.enabled) {
|
|
||||||
renderOverlay(glm::vec4(0.5f, 0.5f, 0.55f, 0.82f), cmd);
|
|
||||||
}
|
|
||||||
if (minimap && minimap->isEnabled() && camera && window) {
|
if (minimap && minimap->isEnabled() && camera && window) {
|
||||||
glm::vec3 minimapCenter = camera->getPosition();
|
glm::vec3 minimapCenter = camera->getPosition();
|
||||||
if (cameraController && cameraController->isThirdPerson())
|
if (cameraController && cameraController->isThirdPerson())
|
||||||
|
|
@ -5412,10 +5430,6 @@ void Renderer::renderWorld(game::World* world, game::GameHandler* gameHandler) {
|
||||||
renderOverlay(tint);
|
renderOverlay(tint);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Ghost mode desaturation overlay (non-FXAA path approximation).
|
|
||||||
if (ghostMode_ && overlayPipeline && !fxaa_.enabled) {
|
|
||||||
renderOverlay(glm::vec4(0.5f, 0.5f, 0.55f, 0.82f));
|
|
||||||
}
|
|
||||||
if (minimap && minimap->isEnabled() && camera && window) {
|
if (minimap && minimap->isEnabled() && camera && window) {
|
||||||
glm::vec3 minimapCenter = camera->getPosition();
|
glm::vec3 minimapCenter = camera->getPosition();
|
||||||
if (cameraController && cameraController->isThirdPerson())
|
if (cameraController && cameraController->isThirdPerson())
|
||||||
|
|
|
||||||
|
|
@ -396,9 +396,13 @@ bool TerrainRenderer::loadTerrain(const pipeline::TerrainMesh& mesh,
|
||||||
|
|
||||||
// Allocate and write material descriptor set
|
// Allocate and write material descriptor set
|
||||||
gpuChunk.materialSet = allocateMaterialSet();
|
gpuChunk.materialSet = allocateMaterialSet();
|
||||||
if (gpuChunk.materialSet) {
|
if (!gpuChunk.materialSet) {
|
||||||
writeMaterialDescriptors(gpuChunk.materialSet, gpuChunk);
|
// Pool exhaustion can happen transiently while tile churn is high.
|
||||||
|
// Drop this chunk instead of retaining non-renderable GPU resources.
|
||||||
|
destroyChunkGPU(gpuChunk);
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
writeMaterialDescriptors(gpuChunk.materialSet, gpuChunk);
|
||||||
|
|
||||||
chunks.push_back(std::move(gpuChunk));
|
chunks.push_back(std::move(gpuChunk));
|
||||||
}
|
}
|
||||||
|
|
@ -487,9 +491,12 @@ bool TerrainRenderer::loadTerrainIncremental(const pipeline::TerrainMesh& mesh,
|
||||||
}
|
}
|
||||||
|
|
||||||
gpuChunk.materialSet = allocateMaterialSet();
|
gpuChunk.materialSet = allocateMaterialSet();
|
||||||
if (gpuChunk.materialSet) {
|
if (!gpuChunk.materialSet) {
|
||||||
writeMaterialDescriptors(gpuChunk.materialSet, gpuChunk);
|
// Keep memory/work bounded under descriptor pool pressure.
|
||||||
|
destroyChunkGPU(gpuChunk);
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
writeMaterialDescriptors(gpuChunk.materialSet, gpuChunk);
|
||||||
|
|
||||||
chunks.push_back(std::move(gpuChunk));
|
chunks.push_back(std::move(gpuChunk));
|
||||||
uploaded++;
|
uploaded++;
|
||||||
|
|
@ -653,7 +660,11 @@ VkDescriptorSet TerrainRenderer::allocateMaterialSet() {
|
||||||
|
|
||||||
VkDescriptorSet set = VK_NULL_HANDLE;
|
VkDescriptorSet set = VK_NULL_HANDLE;
|
||||||
if (vkAllocateDescriptorSets(vkCtx->getDevice(), &allocInfo, &set) != VK_SUCCESS) {
|
if (vkAllocateDescriptorSets(vkCtx->getDevice(), &allocInfo, &set) != VK_SUCCESS) {
|
||||||
LOG_WARNING("TerrainRenderer: failed to allocate material descriptor set");
|
static uint64_t failCount = 0;
|
||||||
|
++failCount;
|
||||||
|
if (failCount <= 8 || (failCount % 256) == 0) {
|
||||||
|
LOG_WARNING("TerrainRenderer: failed to allocate material descriptor set (count=", failCount, ")");
|
||||||
|
}
|
||||||
return VK_NULL_HANDLE;
|
return VK_NULL_HANDLE;
|
||||||
}
|
}
|
||||||
return set;
|
return set;
|
||||||
|
|
|
||||||
|
|
@ -2054,6 +2054,9 @@ void WMORenderer::getVisibleGroupsViaPortals(const ModelData& model,
|
||||||
const Frustum& frustum,
|
const Frustum& frustum,
|
||||||
const glm::mat4& modelMatrix,
|
const glm::mat4& modelMatrix,
|
||||||
std::unordered_set<uint32_t>& outVisibleGroups) const {
|
std::unordered_set<uint32_t>& outVisibleGroups) const {
|
||||||
|
constexpr uint32_t WMO_GROUP_FLAG_OUTDOOR = 0x8;
|
||||||
|
constexpr uint32_t WMO_GROUP_FLAG_INDOOR = 0x2000;
|
||||||
|
|
||||||
// Find camera's containing group
|
// Find camera's containing group
|
||||||
int cameraGroup = findContainingGroup(model, cameraLocalPos);
|
int cameraGroup = findContainingGroup(model, cameraLocalPos);
|
||||||
|
|
||||||
|
|
@ -2067,6 +2070,21 @@ void WMORenderer::getVisibleGroupsViaPortals(const ModelData& model,
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Outdoor city WMOs (e.g. Stormwind) often have portal graphs that are valid for
|
||||||
|
// indoor visibility but too aggressive outdoors, causing direction-dependent popout.
|
||||||
|
// Only trust portal traversal when the camera is in an interior-only group.
|
||||||
|
if (cameraGroup < static_cast<int>(model.groups.size())) {
|
||||||
|
const uint32_t gFlags = model.groups[cameraGroup].groupFlags;
|
||||||
|
const bool isIndoor = (gFlags & WMO_GROUP_FLAG_INDOOR) != 0;
|
||||||
|
const bool isOutdoor = (gFlags & WMO_GROUP_FLAG_OUTDOOR) != 0;
|
||||||
|
if (!isIndoor || isOutdoor) {
|
||||||
|
for (size_t gi = 0; gi < model.groups.size(); gi++) {
|
||||||
|
outVisibleGroups.insert(static_cast<uint32_t>(gi));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// If the camera group has no portal refs, it's a dead-end group (utility/transition group).
|
// If the camera group has no portal refs, it's a dead-end group (utility/transition group).
|
||||||
// Fall back to showing all groups to avoid the rest of the WMO going invisible.
|
// Fall back to showing all groups to avoid the rest of the WMO going invisible.
|
||||||
if (cameraGroup < static_cast<int>(model.groupPortalRefs.size())) {
|
if (cameraGroup < static_cast<int>(model.groupPortalRefs.size())) {
|
||||||
|
|
|
||||||
|
|
@ -845,7 +845,23 @@ void GameScreen::render(game::GameHandler& gameHandler) {
|
||||||
// Update renderer face-target position and selection circle
|
// Update renderer face-target position and selection circle
|
||||||
auto* renderer = core::Application::getInstance().getRenderer();
|
auto* renderer = core::Application::getInstance().getRenderer();
|
||||||
if (renderer) {
|
if (renderer) {
|
||||||
renderer->setInCombat(gameHandler.isInCombat());
|
renderer->setInCombat(gameHandler.isInCombat() &&
|
||||||
|
!gameHandler.isPlayerDead() &&
|
||||||
|
!gameHandler.isPlayerGhost());
|
||||||
|
if (auto* cr = renderer->getCharacterRenderer()) {
|
||||||
|
uint32_t charInstId = renderer->getCharacterInstanceId();
|
||||||
|
if (charInstId != 0) {
|
||||||
|
const bool isGhost = gameHandler.isPlayerGhost();
|
||||||
|
if (!ghostOpacityStateKnown_ ||
|
||||||
|
ghostOpacityLastState_ != isGhost ||
|
||||||
|
ghostOpacityLastInstanceId_ != charInstId) {
|
||||||
|
cr->setInstanceOpacity(charInstId, isGhost ? 0.5f : 1.0f);
|
||||||
|
ghostOpacityStateKnown_ = true;
|
||||||
|
ghostOpacityLastState_ = isGhost;
|
||||||
|
ghostOpacityLastInstanceId_ = charInstId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
static glm::vec3 targetGLPos;
|
static glm::vec3 targetGLPos;
|
||||||
if (gameHandler.hasTarget()) {
|
if (gameHandler.hasTarget()) {
|
||||||
auto target = gameHandler.getTarget();
|
auto target = gameHandler.getTarget();
|
||||||
|
|
@ -2288,7 +2304,7 @@ void GameScreen::processTargetInput(game::GameHandler& gameHandler) {
|
||||||
refocusChatInput = true;
|
refocusChatInput = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const bool textFocus = chatInputActive || refocusChatInput || io.WantTextInput || io.WantCaptureKeyboard;
|
const bool textFocus = chatInputActive || refocusChatInput || io.WantTextInput;
|
||||||
|
|
||||||
// Tab targeting (when keyboard not captured by UI)
|
// Tab targeting (when keyboard not captured by UI)
|
||||||
if (!io.WantCaptureKeyboard) {
|
if (!io.WantCaptureKeyboard) {
|
||||||
|
|
@ -2319,21 +2335,17 @@ void GameScreen::processTargetInput(game::GameHandler& gameHandler) {
|
||||||
if (!textFocus) {
|
if (!textFocus) {
|
||||||
// Toggle character screen (C) and inventory/bags (I)
|
// Toggle character screen (C) and inventory/bags (I)
|
||||||
if (KeybindingManager::getInstance().isActionPressed(KeybindingManager::Action::TOGGLE_CHARACTER_SCREEN)) {
|
if (KeybindingManager::getInstance().isActionPressed(KeybindingManager::Action::TOGGLE_CHARACTER_SCREEN)) {
|
||||||
|
const bool wasOpen = inventoryScreen.isCharacterOpen();
|
||||||
inventoryScreen.toggleCharacter();
|
inventoryScreen.toggleCharacter();
|
||||||
|
if (!wasOpen && gameHandler.isConnected()) {
|
||||||
|
gameHandler.requestPlayedTime();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (KeybindingManager::getInstance().isActionPressed(KeybindingManager::Action::TOGGLE_INVENTORY)) {
|
if (KeybindingManager::getInstance().isActionPressed(KeybindingManager::Action::TOGGLE_INVENTORY)) {
|
||||||
inventoryScreen.toggle();
|
inventoryScreen.toggle();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (KeybindingManager::getInstance().isActionPressed(KeybindingManager::Action::TOGGLE_BAGS)) {
|
|
||||||
if (inventoryScreen.isSeparateBags()) {
|
|
||||||
inventoryScreen.openAllBags();
|
|
||||||
} else {
|
|
||||||
inventoryScreen.toggle();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (KeybindingManager::getInstance().isActionPressed(KeybindingManager::Action::TOGGLE_NAMEPLATES)) {
|
if (KeybindingManager::getInstance().isActionPressed(KeybindingManager::Action::TOGGLE_NAMEPLATES)) {
|
||||||
showNameplates_ = !showNameplates_;
|
showNameplates_ = !showNameplates_;
|
||||||
}
|
}
|
||||||
|
|
@ -2350,10 +2362,6 @@ void GameScreen::processTargetInput(game::GameHandler& gameHandler) {
|
||||||
showRaidFrames_ = !showRaidFrames_;
|
showRaidFrames_ = !showRaidFrames_;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (KeybindingManager::getInstance().isActionPressed(KeybindingManager::Action::TOGGLE_QUEST_LOG)) {
|
|
||||||
questLogScreen.toggle();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (KeybindingManager::getInstance().isActionPressed(KeybindingManager::Action::TOGGLE_ACHIEVEMENTS)) {
|
if (KeybindingManager::getInstance().isActionPressed(KeybindingManager::Action::TOGGLE_ACHIEVEMENTS)) {
|
||||||
showAchievementWindow_ = !showAchievementWindow_;
|
showAchievementWindow_ = !showAchievementWindow_;
|
||||||
}
|
}
|
||||||
|
|
@ -13602,6 +13610,13 @@ void GameScreen::renderTrainerWindow(game::GameHandler& gameHandler) {
|
||||||
|
|
||||||
bool open = true;
|
bool open = true;
|
||||||
if (ImGui::Begin("Trainer", &open)) {
|
if (ImGui::Begin("Trainer", &open)) {
|
||||||
|
// If user clicked window close, short-circuit before rendering large trainer tables.
|
||||||
|
if (!open) {
|
||||||
|
ImGui::End();
|
||||||
|
gameHandler.closeTrainer();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const auto& trainer = gameHandler.getTrainerSpells();
|
const auto& trainer = gameHandler.getTrainerSpells();
|
||||||
|
|
||||||
// NPC name
|
// NPC name
|
||||||
|
|
@ -15881,27 +15896,6 @@ void GameScreen::renderMinimapMarkers(game::GameHandler& gameHandler) {
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Player position marker — always drawn at minimap center with a directional arrow.
|
|
||||||
{
|
|
||||||
// The player is always at centerX, centerY on the minimap.
|
|
||||||
// Draw a yellow arrow pointing in the player's facing direction.
|
|
||||||
glm::vec3 fwd = camera->getForward();
|
|
||||||
float facing = std::atan2(fwd.y, -fwd.x); // clockwise bearing from North
|
|
||||||
float cosF = std::cos(facing - bearing);
|
|
||||||
float sinF = std::sin(facing - bearing);
|
|
||||||
float arrowLen = 8.0f;
|
|
||||||
float arrowW = 4.0f;
|
|
||||||
ImVec2 tip(centerX + sinF * arrowLen, centerY - cosF * arrowLen);
|
|
||||||
ImVec2 left(centerX - cosF * arrowW - sinF * arrowLen * 0.3f,
|
|
||||||
centerY - sinF * arrowW + cosF * arrowLen * 0.3f);
|
|
||||||
ImVec2 right(centerX + cosF * arrowW - sinF * arrowLen * 0.3f,
|
|
||||||
centerY + sinF * arrowW + cosF * arrowLen * 0.3f);
|
|
||||||
drawList->AddTriangleFilled(tip, left, right, IM_COL32(255, 220, 0, 255));
|
|
||||||
drawList->AddTriangle(tip, left, right, IM_COL32(0, 0, 0, 180), 1.0f);
|
|
||||||
// White dot at player center
|
|
||||||
drawList->AddCircleFilled(ImVec2(centerX, centerY), 2.5f, IM_COL32(255, 255, 255, 220));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build sets of entries that are incomplete objectives for tracked quests.
|
// Build sets of entries that are incomplete objectives for tracked quests.
|
||||||
// minimapQuestEntries: NPC creature entries (npcOrGoId > 0)
|
// minimapQuestEntries: NPC creature entries (npcOrGoId > 0)
|
||||||
// minimapQuestGoEntries: game object entries (npcOrGoId < 0, stored as abs value)
|
// minimapQuestGoEntries: game object entries (npcOrGoId < 0, stored as abs value)
|
||||||
|
|
@ -17737,7 +17731,7 @@ void GameScreen::renderMailWindow(game::GameHandler& gameHandler) {
|
||||||
}
|
}
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
if (ImGui::SmallButton("Take")) {
|
if (ImGui::SmallButton("Take")) {
|
||||||
gameHandler.mailTakeItem(mail.messageId, att.slot);
|
gameHandler.mailTakeItem(mail.messageId, att.itemGuidLow);
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui::PopID();
|
ImGui::PopID();
|
||||||
|
|
@ -17746,7 +17740,7 @@ void GameScreen::renderMailWindow(game::GameHandler& gameHandler) {
|
||||||
if (mail.attachments.size() > 1) {
|
if (mail.attachments.size() > 1) {
|
||||||
if (ImGui::SmallButton("Take All")) {
|
if (ImGui::SmallButton("Take All")) {
|
||||||
for (const auto& att2 : mail.attachments) {
|
for (const auto& att2 : mail.attachments) {
|
||||||
gameHandler.mailTakeItem(mail.messageId, att2.slot);
|
gameHandler.mailTakeItem(mail.messageId, att2.itemGuidLow);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -743,17 +743,6 @@ void InventoryScreen::render(game::Inventory& inventory, uint64_t moneyCopper) {
|
||||||
bool bToggled = bagsDown && !bKeyWasDown;
|
bool bToggled = bagsDown && !bKeyWasDown;
|
||||||
bKeyWasDown = bagsDown;
|
bKeyWasDown = bagsDown;
|
||||||
|
|
||||||
// Character screen toggle (C key, edge-triggered)
|
|
||||||
bool characterDown = KeybindingManager::getInstance().isActionPressed(
|
|
||||||
KeybindingManager::Action::TOGGLE_CHARACTER_SCREEN, false);
|
|
||||||
if (characterDown && !cKeyWasDown) {
|
|
||||||
characterOpen = !characterOpen;
|
|
||||||
if (characterOpen && gameHandler_) {
|
|
||||||
gameHandler_->requestPlayedTime();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
cKeyWasDown = characterDown;
|
|
||||||
|
|
||||||
bool wantsTextInput = ImGui::GetIO().WantTextInput;
|
bool wantsTextInput = ImGui::GetIO().WantTextInput;
|
||||||
|
|
||||||
if (separateBags_) {
|
if (separateBags_) {
|
||||||
|
|
@ -1028,7 +1017,11 @@ void InventoryScreen::renderBagWindow(const char* title, bool& isOpen,
|
||||||
|
|
||||||
int rows = (numSlots + columns - 1) / columns;
|
int rows = (numSlots + columns - 1) / columns;
|
||||||
float contentH = rows * (slotSize + 4.0f) + 10.0f;
|
float contentH = rows * (slotSize + 4.0f) + 10.0f;
|
||||||
if (bagIndex < 0) contentH += 25.0f; // money display for backpack
|
if (bagIndex < 0) {
|
||||||
|
int keyringRows = (inventory.getKeyringSize() + columns - 1) / columns;
|
||||||
|
contentH += 25.0f; // money display for backpack
|
||||||
|
contentH += 30.0f + keyringRows * (slotSize + 4.0f); // keyring header + slots
|
||||||
|
}
|
||||||
float gridW = columns * (slotSize + 4.0f) + 30.0f;
|
float gridW = columns * (slotSize + 4.0f) + 30.0f;
|
||||||
// Ensure window is wide enough for the title + close button
|
// Ensure window is wide enough for the title + close button
|
||||||
const char* displayTitle = title;
|
const char* displayTitle = title;
|
||||||
|
|
@ -1076,6 +1069,23 @@ void InventoryScreen::renderBagWindow(const char* title, bool& isOpen,
|
||||||
ImGui::PopID();
|
ImGui::PopID();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (bagIndex < 0) {
|
||||||
|
ImGui::Spacing();
|
||||||
|
ImGui::Separator();
|
||||||
|
ImGui::TextColored(ImVec4(0.8f, 0.8f, 0.0f, 1.0f), "Keyring");
|
||||||
|
for (int i = 0; i < inventory.getKeyringSize(); ++i) {
|
||||||
|
if (i % columns != 0) ImGui::SameLine();
|
||||||
|
const auto& slot = inventory.getKeyringSlot(i);
|
||||||
|
char id[32];
|
||||||
|
snprintf(id, sizeof(id), "##skr_%d", i);
|
||||||
|
ImGui::PushID(id);
|
||||||
|
// Keyring is display-only for now.
|
||||||
|
renderItemSlot(inventory, slot, slotSize, nullptr,
|
||||||
|
SlotKind::BACKPACK, -1, game::EquipSlot::NUM_SLOTS);
|
||||||
|
ImGui::PopID();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Money display at bottom of backpack
|
// Money display at bottom of backpack
|
||||||
if (bagIndex < 0 && moneyCopper > 0) {
|
if (bagIndex < 0 && moneyCopper > 0) {
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
|
|
@ -2031,6 +2041,30 @@ void InventoryScreen::renderBackpackPanel(game::Inventory& inventory, bool colla
|
||||||
ImGui::PopID();
|
ImGui::PopID();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool keyringHasAnyItems = false;
|
||||||
|
for (int i = 0; i < inventory.getKeyringSize(); ++i) {
|
||||||
|
if (!inventory.getKeyringSlot(i).empty()) {
|
||||||
|
keyringHasAnyItems = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!collapseEmptySections || keyringHasAnyItems) {
|
||||||
|
ImGui::Spacing();
|
||||||
|
ImGui::Separator();
|
||||||
|
ImGui::TextColored(ImVec4(0.8f, 0.8f, 0.0f, 1.0f), "Keyring");
|
||||||
|
for (int i = 0; i < inventory.getKeyringSize(); ++i) {
|
||||||
|
if (i % columns != 0) ImGui::SameLine();
|
||||||
|
const auto& slot = inventory.getKeyringSlot(i);
|
||||||
|
char sid[32];
|
||||||
|
snprintf(sid, sizeof(sid), "##keyring_%d", i);
|
||||||
|
ImGui::PushID(sid);
|
||||||
|
// Keyring is display-only for now.
|
||||||
|
renderItemSlot(inventory, slot, slotSize, nullptr,
|
||||||
|
SlotKind::BACKPACK, -1, game::EquipSlot::NUM_SLOTS);
|
||||||
|
ImGui::PopID();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void InventoryScreen::renderItemSlot(game::Inventory& inventory, const game::ItemSlot& slot,
|
void InventoryScreen::renderItemSlot(game::Inventory& inventory, const game::ItemSlot& slot,
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,6 @@ void KeybindingManager::initializeDefaults() {
|
||||||
bindings_[static_cast<int>(Action::TOGGLE_WORLD_MAP)] = ImGuiKey_M; // WoW standard: M opens world map
|
bindings_[static_cast<int>(Action::TOGGLE_WORLD_MAP)] = ImGuiKey_M; // WoW standard: M opens world map
|
||||||
bindings_[static_cast<int>(Action::TOGGLE_NAMEPLATES)] = ImGuiKey_V;
|
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_RAID_FRAMES)] = ImGuiKey_F; // Reassigned from R (now camera reset)
|
||||||
bindings_[static_cast<int>(Action::TOGGLE_QUEST_LOG)] = ImGuiKey_None; // Q conflicts with strafe-left; quest log accessible via TOGGLE_QUESTS (L)
|
|
||||||
bindings_[static_cast<int>(Action::TOGGLE_ACHIEVEMENTS)] = ImGuiKey_Y; // WoW standard key (Shift+Y in retail)
|
bindings_[static_cast<int>(Action::TOGGLE_ACHIEVEMENTS)] = ImGuiKey_Y; // WoW standard key (Shift+Y in retail)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -47,9 +46,7 @@ bool KeybindingManager::isActionPressed(Action action, bool repeat) {
|
||||||
|
|
||||||
// When typing in a text field (e.g. chat input), never treat A-Z or 0-9 as shortcuts.
|
// When typing in a text field (e.g. chat input), never treat A-Z or 0-9 as shortcuts.
|
||||||
const ImGuiIO& io = ImGui::GetIO();
|
const ImGuiIO& io = ImGui::GetIO();
|
||||||
// Note: WantTextInput may not be set until the text widget is processed later in the
|
if (io.WantTextInput) {
|
||||||
// frame, but WantCaptureKeyboard remains true while an ImGui widget is active.
|
|
||||||
if (io.WantTextInput || io.WantCaptureKeyboard) {
|
|
||||||
if ((key >= ImGuiKey_A && key <= ImGuiKey_Z) ||
|
if ((key >= ImGuiKey_A && key <= ImGuiKey_Z) ||
|
||||||
(key >= ImGuiKey_0 && key <= ImGuiKey_9)) {
|
(key >= ImGuiKey_0 && key <= ImGuiKey_9)) {
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -95,7 +92,6 @@ const char* KeybindingManager::getActionName(Action action) {
|
||||||
case Action::TOGGLE_WORLD_MAP: return "World Map";
|
case Action::TOGGLE_WORLD_MAP: return "World Map";
|
||||||
case Action::TOGGLE_NAMEPLATES: return "Nameplates";
|
case Action::TOGGLE_NAMEPLATES: return "Nameplates";
|
||||||
case Action::TOGGLE_RAID_FRAMES: return "Raid Frames";
|
case Action::TOGGLE_RAID_FRAMES: return "Raid Frames";
|
||||||
case Action::TOGGLE_QUEST_LOG: return "Quest Log";
|
|
||||||
case Action::TOGGLE_ACHIEVEMENTS: return "Achievements";
|
case Action::TOGGLE_ACHIEVEMENTS: return "Achievements";
|
||||||
case Action::ACTION_COUNT: break;
|
case Action::ACTION_COUNT: break;
|
||||||
}
|
}
|
||||||
|
|
@ -160,7 +156,7 @@ void KeybindingManager::loadFromConfigFile(const std::string& filePath) {
|
||||||
else if (action == "toggle_world_map") actionIdx = static_cast<int>(Action::TOGGLE_WORLD_MAP);
|
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_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_raid_frames") actionIdx = static_cast<int>(Action::TOGGLE_RAID_FRAMES);
|
||||||
else if (action == "toggle_quest_log") actionIdx = static_cast<int>(Action::TOGGLE_QUEST_LOG);
|
else if (action == "toggle_quest_log") actionIdx = static_cast<int>(Action::TOGGLE_QUESTS); // legacy alias
|
||||||
else if (action == "toggle_achievements") actionIdx = static_cast<int>(Action::TOGGLE_ACHIEVEMENTS);
|
else if (action == "toggle_achievements") actionIdx = static_cast<int>(Action::TOGGLE_ACHIEVEMENTS);
|
||||||
|
|
||||||
if (actionIdx < 0) continue;
|
if (actionIdx < 0) continue;
|
||||||
|
|
@ -257,7 +253,6 @@ void KeybindingManager::saveToConfigFile(const std::string& filePath) const {
|
||||||
{Action::TOGGLE_WORLD_MAP, "toggle_world_map"},
|
{Action::TOGGLE_WORLD_MAP, "toggle_world_map"},
|
||||||
{Action::TOGGLE_NAMEPLATES, "toggle_nameplates"},
|
{Action::TOGGLE_NAMEPLATES, "toggle_nameplates"},
|
||||||
{Action::TOGGLE_RAID_FRAMES, "toggle_raid_frames"},
|
{Action::TOGGLE_RAID_FRAMES, "toggle_raid_frames"},
|
||||||
{Action::TOGGLE_QUEST_LOG, "toggle_quest_log"},
|
|
||||||
{Action::TOGGLE_ACHIEVEMENTS, "toggle_achievements"},
|
{Action::TOGGLE_ACHIEVEMENTS, "toggle_achievements"},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue