mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-03-22 23:30:14 +00:00
Compare commits
No commits in common. "0b8e1834f628ccbecb60954d4c53f86f4a0a2210" and "e0346c85df4f532cd7c1bdb9ad48c7824cbcb6e3" have entirely different histories.
0b8e1834f6
...
e0346c85df
24 changed files with 551 additions and 2293 deletions
|
|
@ -25,9 +25,6 @@ Implemented (working in normal use):
|
|||
- Talent tree UI with proper visuals and functionality
|
||||
- Pet tracking (SMSG_PET_SPELLS), dismiss pet button
|
||||
- Party: group invites, party list, out-of-range member health (SMSG_PARTY_MEMBER_STATS)
|
||||
- Nameplates: NPC subtitles, guild names, elite/boss/rare borders, quest/raid indicators, cast bars, debuff dots
|
||||
- Floating combat text: world-space damage/heal numbers above entities with 3D projection
|
||||
- Target/focus frames: guild name, creature type, rank badges, combo points, cast bars
|
||||
- Map exploration: subzone-level fog-of-war reveal
|
||||
- Warden anti-cheat: full module execution via Unicorn Engine x86 emulation; module caching
|
||||
- Audio: ambient, movement, combat, spell, and UI sound systems
|
||||
|
|
|
|||
|
|
@ -135,13 +135,6 @@ public:
|
|||
|
||||
bool isEntityMoving() const { return isMoving_; }
|
||||
|
||||
/// True only during the active interpolation phase (before reaching destination).
|
||||
/// Unlike isEntityMoving(), this does NOT include the dead-reckoning overrun window,
|
||||
/// so animations (Run/Walk) should use this to avoid "running in place" after arrival.
|
||||
bool isActivelyMoving() const {
|
||||
return isMoving_ && moveElapsed_ < moveDuration_;
|
||||
}
|
||||
|
||||
// Returns the latest server-authoritative position: destination if moving, current if not.
|
||||
// Unlike getX/Y/Z (which only update via updateMovement), this always reflects the
|
||||
// last known server position regardless of distance culling.
|
||||
|
|
@ -284,14 +277,18 @@ protected:
|
|||
|
||||
/**
|
||||
* Player entity
|
||||
* Name is inherited from Unit — do NOT redeclare it here or the
|
||||
* shadowed field will diverge from Unit::name, causing nameplates
|
||||
* and other Unit*-based lookups to read an empty string.
|
||||
*/
|
||||
class Player : public Unit {
|
||||
public:
|
||||
Player() { type = ObjectType::PLAYER; }
|
||||
explicit Player(uint64_t guid) : Unit(guid) { type = ObjectType::PLAYER; }
|
||||
|
||||
// Name
|
||||
const std::string& getName() const { return name; }
|
||||
void setName(const std::string& n) { name = n; }
|
||||
|
||||
protected:
|
||||
std::string name;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -453,7 +453,6 @@ public:
|
|||
uint32_t avgWaitTimeSec = 0; // server-estimated average wait (STATUS_WAIT_QUEUE)
|
||||
uint32_t timeInQueueSec = 0; // time already spent in queue (STATUS_WAIT_QUEUE)
|
||||
std::chrono::steady_clock::time_point inviteReceivedTime{};
|
||||
std::string bgName; // human-readable BG/arena name
|
||||
};
|
||||
|
||||
// Available BG list (populated by SMSG_BATTLEFIELD_LIST)
|
||||
|
|
@ -609,33 +608,6 @@ public:
|
|||
uint32_t getPetitionCost() const { return petitionCost_; }
|
||||
uint64_t getPetitionNpcGuid() const { return petitionNpcGuid_; }
|
||||
|
||||
// Petition signatures (guild charter signing flow)
|
||||
struct PetitionSignature {
|
||||
uint64_t playerGuid = 0;
|
||||
std::string playerName; // resolved later or empty
|
||||
};
|
||||
struct PetitionInfo {
|
||||
uint64_t petitionGuid = 0;
|
||||
uint64_t ownerGuid = 0;
|
||||
std::string guildName;
|
||||
uint32_t signatureCount = 0;
|
||||
uint32_t signaturesRequired = 9; // guild default; arena teams differ
|
||||
std::vector<PetitionSignature> signatures;
|
||||
bool showUI = false;
|
||||
};
|
||||
const PetitionInfo& getPetitionInfo() const { return petitionInfo_; }
|
||||
bool hasPetitionSignaturesUI() const { return petitionInfo_.showUI; }
|
||||
void clearPetitionSignaturesUI() { petitionInfo_.showUI = false; }
|
||||
void signPetition(uint64_t petitionGuid);
|
||||
void turnInPetition(uint64_t petitionGuid);
|
||||
|
||||
// Guild name lookup for other players' nameplates
|
||||
// Returns the guild name for a given guildId, or empty if unknown.
|
||||
// Automatically queries the server for unknown guild IDs.
|
||||
const std::string& lookupGuildName(uint32_t guildId);
|
||||
// Returns the guildId for a player entity (from PLAYER_GUILDID update field).
|
||||
uint32_t getEntityGuildId(uint64_t guid) const;
|
||||
|
||||
// Ready check
|
||||
struct ReadyCheckResult {
|
||||
std::string name;
|
||||
|
|
@ -695,16 +667,6 @@ public:
|
|||
auto it = creatureInfoCache.find(entry);
|
||||
return (it != creatureInfoCache.end()) ? static_cast<int>(it->second.rank) : -1;
|
||||
}
|
||||
// Returns creature type (1=Beast,2=Dragonkin,...,7=Humanoid,...) or 0 if not cached
|
||||
uint32_t getCreatureType(uint32_t entry) const {
|
||||
auto it = creatureInfoCache.find(entry);
|
||||
return (it != creatureInfoCache.end()) ? it->second.creatureType : 0;
|
||||
}
|
||||
// Returns creature family (e.g. pet family for beasts) or 0
|
||||
uint32_t getCreatureFamily(uint32_t entry) const {
|
||||
auto it = creatureInfoCache.find(entry);
|
||||
return (it != creatureInfoCache.end()) ? it->second.family : 0;
|
||||
}
|
||||
|
||||
// ---- Phase 2: Combat ----
|
||||
void startAutoAttack(uint64_t targetGuid);
|
||||
|
|
@ -969,10 +931,6 @@ public:
|
|||
using StandStateCallback = std::function<void(uint8_t standState)>;
|
||||
void setStandStateCallback(StandStateCallback cb) { standStateCallback_ = std::move(cb); }
|
||||
|
||||
// Appearance changed callback — fired when PLAYER_BYTES or facial features update (barber shop, etc.)
|
||||
using AppearanceChangedCallback = std::function<void()>;
|
||||
void setAppearanceChangedCallback(AppearanceChangedCallback cb) { appearanceChangedCallback_ = std::move(cb); }
|
||||
|
||||
// Ghost state callback — fired when player enters or leaves ghost (spirit) form
|
||||
using GhostStateCallback = std::function<void(bool isGhost)>;
|
||||
void setGhostStateCallback(GhostStateCallback cb) { ghostStateCallback_ = std::move(cb); }
|
||||
|
|
@ -1244,17 +1202,6 @@ public:
|
|||
uint32_t getPetUnlearnCost() const { return petUnlearnCost_; }
|
||||
void confirmPetUnlearn();
|
||||
void cancelPetUnlearn() { petUnlearnPending_ = false; }
|
||||
|
||||
// Barber shop
|
||||
bool isBarberShopOpen() const { return barberShopOpen_; }
|
||||
void closeBarberShop() { barberShopOpen_ = false; }
|
||||
void sendAlterAppearance(uint32_t hairStyle, uint32_t hairColor, uint32_t facialHair);
|
||||
|
||||
// Instance difficulty (0=5N, 1=5H, 2=25N, 3=25H for WotLK)
|
||||
uint32_t getInstanceDifficulty() const { return instanceDifficulty_; }
|
||||
bool isInstanceHeroic() const { return instanceIsHeroic_; }
|
||||
bool isInInstance() const { return inInstance_; }
|
||||
|
||||
/** True when ghost is within 40 yards of corpse position (same map). */
|
||||
bool canReclaimCorpse() const;
|
||||
/** Seconds remaining on the PvP corpse-reclaim delay, or 0 if the reclaim is available now. */
|
||||
|
|
@ -1451,11 +1398,8 @@ public:
|
|||
uint32_t seasonGames = 0;
|
||||
uint32_t seasonWins = 0;
|
||||
uint32_t rank = 0;
|
||||
std::string teamName;
|
||||
uint32_t teamType = 0; // 2, 3, or 5
|
||||
};
|
||||
const std::vector<ArenaTeamStats>& getArenaTeamStats() const { return arenaTeamStats_; }
|
||||
void requestArenaTeamRoster(uint32_t teamId);
|
||||
|
||||
// ---- Arena Team Roster ----
|
||||
struct ArenaTeamMember {
|
||||
|
|
@ -1507,7 +1451,6 @@ public:
|
|||
std::string itemName;
|
||||
uint8_t itemQuality = 0;
|
||||
uint32_t rollCountdownMs = 60000; // Duration of roll window in ms
|
||||
uint8_t voteMask = 0xFF; // Bitmask: 0x01=pass, 0x02=need, 0x04=greed, 0x08=disenchant
|
||||
std::chrono::steady_clock::time_point rollStartedAt{};
|
||||
|
||||
struct PlayerRollResult {
|
||||
|
|
@ -2068,7 +2011,6 @@ public:
|
|||
void openItemBySlot(int backpackIndex);
|
||||
void openItemInBag(int bagIndex, int slotIndex);
|
||||
void destroyItem(uint8_t bag, uint8_t slot, uint8_t count = 1);
|
||||
void splitItem(uint8_t srcBag, uint8_t srcSlot, uint8_t count);
|
||||
void swapContainerItems(uint8_t srcBag, uint8_t srcSlot, uint8_t dstBag, uint8_t dstSlot);
|
||||
void swapBagSlots(int srcBagIndex, int dstBagIndex);
|
||||
void useItemById(uint32_t itemId);
|
||||
|
|
@ -2395,9 +2337,6 @@ private:
|
|||
void handleGuildInvite(network::Packet& packet);
|
||||
void handleGuildCommandResult(network::Packet& packet);
|
||||
void handlePetitionShowlist(network::Packet& packet);
|
||||
void handlePetitionQueryResponse(network::Packet& packet);
|
||||
void handlePetitionShowSignatures(network::Packet& packet);
|
||||
void handlePetitionSignResults(network::Packet& packet);
|
||||
void handlePetSpells(network::Packet& packet);
|
||||
void handleTurnInPetitionResults(network::Packet& packet);
|
||||
|
||||
|
|
@ -2898,7 +2837,6 @@ private:
|
|||
// Instance difficulty
|
||||
uint32_t instanceDifficulty_ = 0;
|
||||
bool instanceIsHeroic_ = false;
|
||||
bool inInstance_ = false;
|
||||
|
||||
// Raid target markers (icon 0-7 -> guid; 0 = empty slot)
|
||||
std::array<uint64_t, kRaidMarkCount> raidTargetGuids_ = {};
|
||||
|
|
@ -3014,22 +2952,16 @@ private:
|
|||
GuildInfoData guildInfoData_;
|
||||
GuildQueryResponseData guildQueryData_;
|
||||
bool hasGuildRoster_ = false;
|
||||
std::unordered_map<uint32_t, std::string> guildNameCache_; // guildId → guild name
|
||||
std::unordered_set<uint32_t> pendingGuildNameQueries_; // in-flight guild queries
|
||||
bool pendingGuildInvite_ = false;
|
||||
std::string pendingGuildInviterName_;
|
||||
std::string pendingGuildInviteGuildName_;
|
||||
bool showPetitionDialog_ = false;
|
||||
uint32_t petitionCost_ = 0;
|
||||
uint64_t petitionNpcGuid_ = 0;
|
||||
PetitionInfo petitionInfo_;
|
||||
|
||||
uint64_t activeCharacterGuid_ = 0;
|
||||
Race playerRace_ = Race::HUMAN;
|
||||
|
||||
// Barber shop
|
||||
bool barberShopOpen_ = false;
|
||||
|
||||
// ---- Phase 5: Loot ----
|
||||
bool lootWindowOpen = false;
|
||||
bool autoLoot_ = false;
|
||||
|
|
@ -3385,7 +3317,6 @@ private:
|
|||
NpcAggroCallback npcAggroCallback_;
|
||||
NpcRespawnCallback npcRespawnCallback_;
|
||||
StandStateCallback standStateCallback_;
|
||||
AppearanceChangedCallback appearanceChangedCallback_;
|
||||
GhostStateCallback ghostStateCallback_;
|
||||
MeleeSwingCallback meleeSwingCallback_;
|
||||
uint64_t lastMeleeSwingMs_ = 0; // system_clock ms at last player auto-attack swing
|
||||
|
|
|
|||
|
|
@ -61,9 +61,6 @@ struct CombatTextEntry {
|
|||
float age = 0.0f; // Seconds since creation (for fadeout)
|
||||
bool isPlayerSource = false; // True if player dealt this
|
||||
uint8_t powerType = 0; // For ENERGIZE/POWER_DRAIN: 0=mana,1=rage,2=focus,3=energy,6=runicpower
|
||||
uint64_t srcGuid = 0; // Source entity (attacker/caster)
|
||||
uint64_t dstGuid = 0; // Destination entity (victim/target) — used for world-space positioning
|
||||
float xSeed = 0.0f; // Random horizontal offset seed (-1..1) to stagger overlapping text
|
||||
|
||||
static constexpr float LIFETIME = 2.5f;
|
||||
bool isExpired() const { return age >= LIFETIME; }
|
||||
|
|
|
|||
|
|
@ -2046,13 +2046,6 @@ public:
|
|||
static network::Packet build(uint8_t dstBag, uint8_t dstSlot, uint8_t srcBag, uint8_t srcSlot);
|
||||
};
|
||||
|
||||
/** CMSG_SPLIT_ITEM packet builder */
|
||||
class SplitItemPacket {
|
||||
public:
|
||||
static network::Packet build(uint8_t srcBag, uint8_t srcSlot,
|
||||
uint8_t dstBag, uint8_t dstSlot, uint8_t count);
|
||||
};
|
||||
|
||||
/** CMSG_SWAP_INV_ITEM packet builder */
|
||||
class SwapInvItemPacket {
|
||||
public:
|
||||
|
|
@ -2796,12 +2789,5 @@ public:
|
|||
static network::Packet build(int32_t titleBit);
|
||||
};
|
||||
|
||||
/** CMSG_ALTER_APPEARANCE – barber shop: change hair style, color, facial hair.
|
||||
* Payload: uint32 hairStyle, uint32 hairColor, uint32 facialHair. */
|
||||
class AlterAppearancePacket {
|
||||
public:
|
||||
static network::Packet build(uint32_t hairStyle, uint32_t hairColor, uint32_t facialHair);
|
||||
};
|
||||
|
||||
} // namespace game
|
||||
} // namespace wowee
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@
|
|||
#include <string>
|
||||
#include <optional>
|
||||
#include <random>
|
||||
#include <chrono>
|
||||
#include <future>
|
||||
|
||||
namespace wowee {
|
||||
|
|
@ -435,9 +434,6 @@ private:
|
|||
void* glowVBMapped_ = nullptr;
|
||||
|
||||
std::unordered_map<uint32_t, M2ModelGPU> models;
|
||||
// Grace period for model cleanup: track when a model first became instanceless.
|
||||
// Models are only evicted after 60 seconds with no instances.
|
||||
std::unordered_map<uint32_t, std::chrono::steady_clock::time_point> modelUnusedSince_;
|
||||
std::vector<M2Instance> instances;
|
||||
|
||||
// O(1) dedup: key = (modelId, quantized x, quantized y, quantized z) → instanceId
|
||||
|
|
|
|||
|
|
@ -154,9 +154,6 @@ public:
|
|||
void triggerLevelUpEffect(const glm::vec3& position);
|
||||
void cancelEmote();
|
||||
|
||||
// Screenshot capture — copies swapchain image to PNG file
|
||||
bool captureScreenshot(const std::string& outputPath);
|
||||
|
||||
// Spell visual effects (SMSG_PLAY_SPELL_VISUAL / SMSG_PLAY_SPELL_IMPACT)
|
||||
// useImpactKit=false → CastKit path; useImpactKit=true → ImpactKit path
|
||||
void playSpellVisual(uint32_t visualId, const glm::vec3& worldPosition,
|
||||
|
|
|
|||
|
|
@ -279,9 +279,6 @@ public:
|
|||
/** Process one ready tile (for loading screens with per-tile progress updates) */
|
||||
void processOneReadyTile();
|
||||
|
||||
/** Process a bounded batch of ready tiles with async GPU upload (no sync wait) */
|
||||
void processReadyTiles();
|
||||
|
||||
private:
|
||||
/**
|
||||
* Get tile coordinates from GL world position
|
||||
|
|
@ -320,6 +317,10 @@ private:
|
|||
*/
|
||||
void workerLoop();
|
||||
|
||||
/**
|
||||
* Main thread: poll for completed tiles and upload to GPU
|
||||
*/
|
||||
void processReadyTiles();
|
||||
void ensureGroundEffectTablesLoaded();
|
||||
void generateGroundClutterPlacements(std::shared_ptr<PendingTile>& pending,
|
||||
std::unordered_set<uint32_t>& preparedModelIds);
|
||||
|
|
|
|||
|
|
@ -86,8 +86,7 @@ private:
|
|||
bool showEntityWindow = false;
|
||||
bool showChatWindow = true;
|
||||
bool showMinimap_ = true; // M key toggles minimap
|
||||
bool showNameplates_ = true; // V key toggles enemy/NPC nameplates
|
||||
bool showFriendlyNameplates_ = true; // Shift+V toggles friendly player nameplates
|
||||
bool showNameplates_ = true; // V key toggles nameplates
|
||||
float nameplateScale_ = 1.0f; // Scale multiplier for nameplate bar dimensions
|
||||
uint64_t nameplateCtxGuid_ = 0; // GUID of nameplate right-clicked (0 = none)
|
||||
ImVec2 nameplateCtxPos_{}; // Screen position of nameplate right-click
|
||||
|
|
@ -366,7 +365,6 @@ private:
|
|||
void renderQuestOfferRewardWindow(game::GameHandler& gameHandler);
|
||||
void renderVendorWindow(game::GameHandler& gameHandler);
|
||||
void renderTrainerWindow(game::GameHandler& gameHandler);
|
||||
void renderBarberShopWindow(game::GameHandler& gameHandler);
|
||||
void renderStableWindow(game::GameHandler& gameHandler);
|
||||
void renderTaxiWindow(game::GameHandler& gameHandler);
|
||||
void renderLogoutCountdown(game::GameHandler& gameHandler);
|
||||
|
|
@ -400,7 +398,6 @@ private:
|
|||
void renderBattlegroundScore(game::GameHandler& gameHandler);
|
||||
void renderDPSMeter(game::GameHandler& gameHandler);
|
||||
void renderDurabilityWarning(game::GameHandler& gameHandler);
|
||||
void takeScreenshot(game::GameHandler& gameHandler);
|
||||
|
||||
/**
|
||||
* Inventory screen
|
||||
|
|
@ -535,24 +532,6 @@ private:
|
|||
// Vendor search filter
|
||||
char vendorSearchFilter_[128] = "";
|
||||
|
||||
// Vendor purchase confirmation for expensive items
|
||||
bool vendorConfirmOpen_ = false;
|
||||
uint64_t vendorConfirmGuid_ = 0;
|
||||
uint32_t vendorConfirmItemId_ = 0;
|
||||
uint32_t vendorConfirmSlot_ = 0;
|
||||
uint32_t vendorConfirmQty_ = 1;
|
||||
uint32_t vendorConfirmPrice_ = 0;
|
||||
std::string vendorConfirmItemName_;
|
||||
|
||||
// Barber shop UI state
|
||||
int barberHairStyle_ = 0;
|
||||
int barberHairColor_ = 0;
|
||||
int barberFacialHair_ = 0;
|
||||
int barberOrigHairStyle_ = 0;
|
||||
int barberOrigHairColor_ = 0;
|
||||
int barberOrigFacialHair_ = 0;
|
||||
bool barberInitialized_ = false;
|
||||
|
||||
// Trainer search filter
|
||||
char trainerSearchFilter_[128] = "";
|
||||
|
||||
|
|
@ -665,7 +644,6 @@ private:
|
|||
float resurrectFlashTimer_ = 0.0f;
|
||||
static constexpr float kResurrectFlashDuration = 3.0f;
|
||||
bool ghostStateCallbackSet_ = false;
|
||||
bool appearanceCallbackSet_ = false;
|
||||
bool ghostOpacityStateKnown_ = false;
|
||||
bool ghostOpacityLastState_ = false;
|
||||
uint32_t ghostOpacityLastInstanceId_ = 0;
|
||||
|
|
|
|||
|
|
@ -187,14 +187,6 @@ private:
|
|||
uint8_t destroyCount_ = 1;
|
||||
std::string destroyItemName_;
|
||||
|
||||
// Stack split popup state
|
||||
bool splitConfirmOpen_ = false;
|
||||
uint8_t splitBag_ = 0xFF;
|
||||
uint8_t splitSlot_ = 0;
|
||||
int splitMax_ = 1;
|
||||
int splitCount_ = 1;
|
||||
std::string splitItemName_;
|
||||
|
||||
// Pending chat item link from shift-click
|
||||
std::string pendingChatItemLink_;
|
||||
|
||||
|
|
|
|||
|
|
@ -45,12 +45,6 @@ private:
|
|||
std::unordered_map<uint32_t, std::string> spellTooltips; // spellId -> description
|
||||
std::unordered_map<uint32_t, VkDescriptorSet> bgTextureCache_; // tabId -> bg texture
|
||||
|
||||
// Talent learn confirmation
|
||||
bool talentConfirmOpen_ = false;
|
||||
uint32_t pendingTalentId_ = 0;
|
||||
uint32_t pendingTalentRank_ = 0;
|
||||
std::string pendingTalentName_;
|
||||
|
||||
// GlyphProperties.dbc cache: glyphId -> { spellId, isMajor }
|
||||
struct GlyphInfo { uint32_t spellId = 0; bool isMajor = false; };
|
||||
std::unordered_map<uint32_t, GlyphInfo> glyphProperties_; // glyphId -> info
|
||||
|
|
|
|||
|
|
@ -1647,11 +1647,7 @@ void Application::update(float deltaTime) {
|
|||
// startMoveTo() in handleMonsterMove, regardless of distance-cull.
|
||||
// This correctly detects movement for distant creatures (> 150u)
|
||||
// where updateMovement() is not called and getX/Y/Z() stays stale.
|
||||
// Use isActivelyMoving() (not isEntityMoving()) so the
|
||||
// Run/Walk animation stops when the creature reaches its
|
||||
// destination, rather than persisting through the dead-
|
||||
// reckoning overrun window.
|
||||
const bool entityIsMoving = entity->isActivelyMoving();
|
||||
const bool entityIsMoving = entity->isEntityMoving();
|
||||
const bool isMovingNow = !deadOrCorpse && (entityIsMoving || planarDist > 0.03f || dz > 0.08f);
|
||||
if (deadOrCorpse || largeCorrection) {
|
||||
charRenderer->setInstancePosition(instanceId, renderPos);
|
||||
|
|
@ -1720,110 +1716,6 @@ void Application::update(float deltaTime) {
|
|||
}
|
||||
}
|
||||
|
||||
// --- Online player render sync (position, orientation, animation) ---
|
||||
// Mirrors the creature sync loop above but without collision guard or
|
||||
// weapon-attach logic. Without this, online players never transition
|
||||
// back to Stand after movement stops ("run in place" bug).
|
||||
auto playerSyncStart = std::chrono::steady_clock::now();
|
||||
if (renderer && gameHandler && renderer->getCharacterRenderer()) {
|
||||
auto* charRenderer = renderer->getCharacterRenderer();
|
||||
glm::vec3 pPos(0.0f);
|
||||
bool havePPos = false;
|
||||
if (auto pe = gameHandler->getEntityManager().getEntity(gameHandler->getPlayerGuid())) {
|
||||
pPos = glm::vec3(pe->getX(), pe->getY(), pe->getZ());
|
||||
havePPos = true;
|
||||
}
|
||||
const float pSyncRadiusSq = 320.0f * 320.0f;
|
||||
|
||||
for (const auto& [guid, instanceId] : playerInstances_) {
|
||||
auto entity = gameHandler->getEntityManager().getEntity(guid);
|
||||
if (!entity || entity->getType() != game::ObjectType::PLAYER) continue;
|
||||
|
||||
// Distance cull
|
||||
if (havePPos) {
|
||||
glm::vec3 latestCanonical(entity->getLatestX(), entity->getLatestY(), entity->getLatestZ());
|
||||
glm::vec3 d = latestCanonical - pPos;
|
||||
if (glm::dot(d, d) > pSyncRadiusSq) continue;
|
||||
}
|
||||
|
||||
// Position sync
|
||||
glm::vec3 canonical(entity->getX(), entity->getY(), entity->getZ());
|
||||
glm::vec3 renderPos = core::coords::canonicalToRender(canonical);
|
||||
|
||||
auto posIt = creatureRenderPosCache_.find(guid);
|
||||
if (posIt == creatureRenderPosCache_.end()) {
|
||||
charRenderer->setInstancePosition(instanceId, renderPos);
|
||||
creatureRenderPosCache_[guid] = renderPos;
|
||||
} else {
|
||||
const glm::vec3 prevPos = posIt->second;
|
||||
const glm::vec2 delta2(renderPos.x - prevPos.x, renderPos.y - prevPos.y);
|
||||
float planarDist = glm::length(delta2);
|
||||
float dz = std::abs(renderPos.z - prevPos.z);
|
||||
|
||||
auto unitPtr = std::static_pointer_cast<game::Unit>(entity);
|
||||
const bool deadOrCorpse = unitPtr->getHealth() == 0;
|
||||
const bool largeCorrection = (planarDist > 6.0f) || (dz > 3.0f);
|
||||
const bool entityIsMoving = entity->isActivelyMoving();
|
||||
const bool isMovingNow = !deadOrCorpse && (entityIsMoving || planarDist > 0.03f || dz > 0.08f);
|
||||
|
||||
if (deadOrCorpse || largeCorrection) {
|
||||
charRenderer->setInstancePosition(instanceId, renderPos);
|
||||
} else if (planarDist > 0.03f || dz > 0.08f) {
|
||||
float duration = std::clamp(planarDist / 5.5f, 0.05f, 0.22f);
|
||||
charRenderer->moveInstanceTo(instanceId, renderPos, duration);
|
||||
}
|
||||
posIt->second = renderPos;
|
||||
|
||||
// Drive movement animation (same logic as creatures)
|
||||
const bool isSwimmingNow = creatureSwimmingState_.count(guid) > 0;
|
||||
const bool isWalkingNow = creatureWalkingState_.count(guid) > 0;
|
||||
const bool isFlyingNow = creatureFlyingState_.count(guid) > 0;
|
||||
bool prevMoving = creatureWasMoving_[guid];
|
||||
bool prevSwimming = creatureWasSwimming_[guid];
|
||||
bool prevFlying = creatureWasFlying_[guid];
|
||||
bool prevWalking = creatureWasWalking_[guid];
|
||||
const bool stateChanged = (isMovingNow != prevMoving) ||
|
||||
(isSwimmingNow != prevSwimming) ||
|
||||
(isFlyingNow != prevFlying) ||
|
||||
(isWalkingNow != prevWalking && isMovingNow);
|
||||
if (stateChanged) {
|
||||
creatureWasMoving_[guid] = isMovingNow;
|
||||
creatureWasSwimming_[guid] = isSwimmingNow;
|
||||
creatureWasFlying_[guid] = isFlyingNow;
|
||||
creatureWasWalking_[guid] = isWalkingNow;
|
||||
uint32_t curAnimId = 0; float curT = 0.0f, curDur = 0.0f;
|
||||
bool gotState = charRenderer->getAnimationState(instanceId, curAnimId, curT, curDur);
|
||||
if (!gotState || curAnimId != 1 /*Death*/) {
|
||||
uint32_t targetAnim;
|
||||
if (isMovingNow) {
|
||||
if (isFlyingNow) targetAnim = 159u; // FlyForward
|
||||
else if (isSwimmingNow) targetAnim = 42u; // Swim
|
||||
else if (isWalkingNow) targetAnim = 4u; // Walk
|
||||
else targetAnim = 5u; // Run
|
||||
} else {
|
||||
if (isFlyingNow) targetAnim = 158u; // FlyIdle (hover)
|
||||
else if (isSwimmingNow) targetAnim = 41u; // SwimIdle
|
||||
else targetAnim = 0u; // Stand
|
||||
}
|
||||
charRenderer->playAnimation(instanceId, targetAnim, /*loop=*/true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Orientation sync
|
||||
float renderYaw = entity->getOrientation() + glm::radians(90.0f);
|
||||
charRenderer->setInstanceRotation(instanceId, glm::vec3(0.0f, 0.0f, renderYaw));
|
||||
}
|
||||
}
|
||||
{
|
||||
float psMs = std::chrono::duration<float, std::milli>(
|
||||
std::chrono::steady_clock::now() - playerSyncStart).count();
|
||||
if (psMs > 5.0f) {
|
||||
LOG_WARNING("SLOW update stage 'player render sync': ", psMs, "ms (",
|
||||
playerInstances_.size(), " players)");
|
||||
}
|
||||
}
|
||||
|
||||
// Movement heartbeat is sent from GameHandler::update() to avoid
|
||||
// duplicate packets from multiple update loops.
|
||||
|
||||
|
|
@ -2098,7 +1990,7 @@ void Application::setupUICallbacks() {
|
|||
worldEntryMovementGraceTimer_ = 2.0f;
|
||||
taxiLandingClampTimer_ = 0.0f;
|
||||
lastTaxiFlight_ = false;
|
||||
renderer->getTerrainManager()->processReadyTiles();
|
||||
renderer->getTerrainManager()->processAllReadyTiles();
|
||||
{
|
||||
auto [tileX, tileY] = core::coords::worldToTile(renderPos.x, renderPos.y);
|
||||
std::vector<std::pair<int,int>> nearbyTiles;
|
||||
|
|
@ -2131,12 +2023,10 @@ void Application::setupUICallbacks() {
|
|||
renderer->getCameraController()->clearMovementInputs();
|
||||
renderer->getCameraController()->suppressMovementFor(0.5f);
|
||||
}
|
||||
// Kick off async upload for any tiles that finished background
|
||||
// parsing. Use the bounded processReadyTiles() instead of
|
||||
// processAllReadyTiles() to avoid multi-second main-thread stalls
|
||||
// when many tiles are ready (the rest will finalize over subsequent
|
||||
// frames via the normal terrain update loop).
|
||||
renderer->getTerrainManager()->processReadyTiles();
|
||||
// Flush any tiles that finished background parsing during the cast
|
||||
// (e.g. Hearthstone pre-loaded them) so they're GPU-uploaded before
|
||||
// the first frame at the new position.
|
||||
renderer->getTerrainManager()->processAllReadyTiles();
|
||||
|
||||
// Queue all remaining tiles within the load radius (8 tiles = 17x17)
|
||||
// at the new position. precacheTiles skips already-loaded/pending tiles,
|
||||
|
|
@ -2999,50 +2889,29 @@ void Application::setupUICallbacks() {
|
|||
}
|
||||
});
|
||||
|
||||
// NPC/player death callback (online mode) - play death animation
|
||||
// NPC death callback (online mode) - play death animation
|
||||
gameHandler->setNpcDeathCallback([this](uint64_t guid) {
|
||||
deadCreatureGuids_.insert(guid);
|
||||
if (!renderer || !renderer->getCharacterRenderer()) return;
|
||||
uint32_t instanceId = 0;
|
||||
auto it = creatureInstances_.find(guid);
|
||||
if (it != creatureInstances_.end()) instanceId = it->second;
|
||||
else {
|
||||
auto pit = playerInstances_.find(guid);
|
||||
if (pit != playerInstances_.end()) instanceId = pit->second;
|
||||
}
|
||||
if (instanceId != 0) {
|
||||
renderer->getCharacterRenderer()->playAnimation(instanceId, 1, false); // Death
|
||||
if (it != creatureInstances_.end() && renderer && renderer->getCharacterRenderer()) {
|
||||
renderer->getCharacterRenderer()->playAnimation(it->second, 1, false); // Death
|
||||
}
|
||||
});
|
||||
|
||||
// NPC/player respawn callback (online mode) - reset to idle animation
|
||||
// NPC respawn callback (online mode) - reset to idle animation
|
||||
gameHandler->setNpcRespawnCallback([this](uint64_t guid) {
|
||||
deadCreatureGuids_.erase(guid);
|
||||
if (!renderer || !renderer->getCharacterRenderer()) return;
|
||||
uint32_t instanceId = 0;
|
||||
auto it = creatureInstances_.find(guid);
|
||||
if (it != creatureInstances_.end()) instanceId = it->second;
|
||||
else {
|
||||
auto pit = playerInstances_.find(guid);
|
||||
if (pit != playerInstances_.end()) instanceId = pit->second;
|
||||
}
|
||||
if (instanceId != 0) {
|
||||
renderer->getCharacterRenderer()->playAnimation(instanceId, 0, true); // Idle
|
||||
if (it != creatureInstances_.end() && renderer && renderer->getCharacterRenderer()) {
|
||||
renderer->getCharacterRenderer()->playAnimation(it->second, 0, true); // Idle
|
||||
}
|
||||
});
|
||||
|
||||
// NPC/player swing callback (online mode) - play attack animation
|
||||
// NPC swing callback (online mode) - play attack animation
|
||||
gameHandler->setNpcSwingCallback([this](uint64_t guid) {
|
||||
if (!renderer || !renderer->getCharacterRenderer()) return;
|
||||
uint32_t instanceId = 0;
|
||||
auto it = creatureInstances_.find(guid);
|
||||
if (it != creatureInstances_.end()) instanceId = it->second;
|
||||
else {
|
||||
auto pit = playerInstances_.find(guid);
|
||||
if (pit != playerInstances_.end()) instanceId = pit->second;
|
||||
}
|
||||
if (instanceId != 0) {
|
||||
renderer->getCharacterRenderer()->playAnimation(instanceId, 16, false); // Attack
|
||||
if (it != creatureInstances_.end() && renderer && renderer->getCharacterRenderer()) {
|
||||
renderer->getCharacterRenderer()->playAnimation(it->second, 16, false); // Attack
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -4854,42 +4723,24 @@ void Application::loadOnlineWorldTerrain(uint32_t mapId, float x, float y, float
|
|||
|
||||
gameHandler->setNpcDeathCallback([cr, app](uint64_t guid) {
|
||||
app->deadCreatureGuids_.insert(guid);
|
||||
uint32_t instanceId = 0;
|
||||
auto it = app->creatureInstances_.find(guid);
|
||||
if (it != app->creatureInstances_.end()) instanceId = it->second;
|
||||
else {
|
||||
auto pit = app->playerInstances_.find(guid);
|
||||
if (pit != app->playerInstances_.end()) instanceId = pit->second;
|
||||
}
|
||||
if (instanceId != 0 && cr) {
|
||||
cr->playAnimation(instanceId, 1, false); // animation ID 1 = Death
|
||||
if (it != app->creatureInstances_.end() && cr) {
|
||||
cr->playAnimation(it->second, 1, false); // animation ID 1 = Death
|
||||
}
|
||||
});
|
||||
|
||||
gameHandler->setNpcRespawnCallback([cr, app](uint64_t guid) {
|
||||
app->deadCreatureGuids_.erase(guid);
|
||||
uint32_t instanceId = 0;
|
||||
auto it = app->creatureInstances_.find(guid);
|
||||
if (it != app->creatureInstances_.end()) instanceId = it->second;
|
||||
else {
|
||||
auto pit = app->playerInstances_.find(guid);
|
||||
if (pit != app->playerInstances_.end()) instanceId = pit->second;
|
||||
}
|
||||
if (instanceId != 0 && cr) {
|
||||
cr->playAnimation(instanceId, 0, true); // animation ID 0 = Idle
|
||||
if (it != app->creatureInstances_.end() && cr) {
|
||||
cr->playAnimation(it->second, 0, true); // animation ID 0 = Idle
|
||||
}
|
||||
});
|
||||
|
||||
gameHandler->setNpcSwingCallback([cr, app](uint64_t guid) {
|
||||
uint32_t instanceId = 0;
|
||||
auto it = app->creatureInstances_.find(guid);
|
||||
if (it != app->creatureInstances_.end()) instanceId = it->second;
|
||||
else {
|
||||
auto pit = app->playerInstances_.find(guid);
|
||||
if (pit != app->playerInstances_.end()) instanceId = pit->second;
|
||||
}
|
||||
if (instanceId != 0 && cr) {
|
||||
cr->playAnimation(instanceId, 16, false); // animation ID 16 = Attack1
|
||||
if (it != app->creatureInstances_.end() && cr) {
|
||||
cr->playAnimation(it->second, 16, false); // animation ID 16 = Attack1
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -7193,7 +7044,6 @@ void Application::despawnOnlinePlayer(uint64_t guid) {
|
|||
playerInstances_.erase(it);
|
||||
onlinePlayerAppearance_.erase(guid);
|
||||
pendingOnlinePlayerEquipment_.erase(guid);
|
||||
creatureRenderPosCache_.erase(guid);
|
||||
creatureSwimmingState_.erase(guid);
|
||||
creatureWalkingState_.erase(guid);
|
||||
creatureFlyingState_.erase(guid);
|
||||
|
|
|
|||
|
|
@ -759,8 +759,6 @@ void GameHandler::disconnect() {
|
|||
activeCharacterGuid_ = 0;
|
||||
playerNameCache.clear();
|
||||
pendingNameQueries.clear();
|
||||
guildNameCache_.clear();
|
||||
pendingGuildNameQueries_.clear();
|
||||
friendGuids_.clear();
|
||||
contacts_.clear();
|
||||
transportAttachments_.clear();
|
||||
|
|
@ -2344,7 +2342,7 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
/*uint32_t randProp =*/ packet.readUInt32();
|
||||
}
|
||||
uint32_t countdown = packet.readUInt32();
|
||||
uint8_t voteMask = packet.readUInt8();
|
||||
/*uint8_t voteMask =*/ packet.readUInt8();
|
||||
// Trigger the roll popup for local player
|
||||
pendingLootRollActive_ = true;
|
||||
pendingLootRoll_.objectGuid = objectGuid;
|
||||
|
|
@ -2358,10 +2356,9 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
pendingLootRoll_.itemName = info ? info->name : std::to_string(itemId);
|
||||
pendingLootRoll_.itemQuality = info ? static_cast<uint8_t>(info->quality) : 0;
|
||||
pendingLootRoll_.rollCountdownMs = (countdown > 0 && countdown <= 120000) ? countdown : 60000;
|
||||
pendingLootRoll_.voteMask = voteMask;
|
||||
pendingLootRoll_.rollStartedAt = std::chrono::steady_clock::now();
|
||||
LOG_INFO("SMSG_LOOT_START_ROLL: item=", itemId, " (", pendingLootRoll_.itemName,
|
||||
") slot=", slot, " voteMask=0x", std::hex, (int)voteMask, std::dec);
|
||||
") slot=", slot);
|
||||
break;
|
||||
}
|
||||
|
||||
|
|
@ -2681,8 +2678,8 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
}
|
||||
case Opcode::SMSG_ENABLE_BARBER_SHOP:
|
||||
// Sent by server when player sits in barber chair — triggers barber shop UI
|
||||
// No payload; we don't have barber shop UI yet, so just log
|
||||
LOG_INFO("SMSG_ENABLE_BARBER_SHOP: barber shop available");
|
||||
barberShopOpen_ = true;
|
||||
break;
|
||||
case Opcode::SMSG_FEIGN_DEATH_RESISTED:
|
||||
addUIError("Your Feign Death was resisted.");
|
||||
|
|
@ -2952,7 +2949,6 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
struct SpellMissLogEntry {
|
||||
uint64_t victimGuid = 0;
|
||||
uint8_t missInfo = 0;
|
||||
uint32_t reflectSpellId = 0; // Only valid when missInfo==11 (REFLECT)
|
||||
};
|
||||
std::vector<SpellMissLogEntry> parsedMisses;
|
||||
parsedMisses.reserve(storedLimit);
|
||||
|
|
@ -2971,18 +2967,17 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
}
|
||||
const uint8_t missInfo = packet.readUInt8();
|
||||
// REFLECT (11): extra uint32 reflectSpellId + uint8 reflectResult
|
||||
uint32_t reflectSpellId = 0;
|
||||
if (missInfo == 11) {
|
||||
if (packet.getSize() - packet.getReadPos() >= 5) {
|
||||
reflectSpellId = packet.readUInt32();
|
||||
/*uint8_t reflectResult =*/ packet.readUInt8();
|
||||
/*uint32_t reflectSpellId =*/ packet.readUInt32();
|
||||
/*uint8_t reflectResult =*/ packet.readUInt8();
|
||||
} else {
|
||||
truncated = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (i < storedLimit) {
|
||||
parsedMisses.push_back({victimGuid, missInfo, reflectSpellId});
|
||||
parsedMisses.push_back({victimGuid, missInfo});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -2995,15 +2990,12 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
const uint64_t victimGuid = miss.victimGuid;
|
||||
const uint8_t missInfo = miss.missInfo;
|
||||
CombatTextEntry::Type ct = combatTextTypeFromSpellMissInfo(missInfo);
|
||||
// For REFLECT, use the reflected spell ID so combat text shows the spell name
|
||||
uint32_t combatSpellId = (ct == CombatTextEntry::REFLECT && miss.reflectSpellId != 0)
|
||||
? miss.reflectSpellId : spellId;
|
||||
if (casterGuid == playerGuid) {
|
||||
// We cast a spell and it missed the target
|
||||
addCombatText(ct, 0, combatSpellId, true, 0, casterGuid, victimGuid);
|
||||
addCombatText(ct, 0, spellId, true, 0, casterGuid, victimGuid);
|
||||
} else if (victimGuid == playerGuid) {
|
||||
// Enemy spell missed us (we dodged/parried/blocked/resisted/etc.)
|
||||
addCombatText(ct, 0, combatSpellId, false, 0, casterGuid, victimGuid);
|
||||
addCombatText(ct, 0, spellId, false, 0, casterGuid, victimGuid);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
|
@ -4258,20 +4250,17 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
uint8_t auraType = packet.readUInt8();
|
||||
if (auraType == 3 || auraType == 89) {
|
||||
// Classic/TBC: damage(4)+school(4)+absorbed(4)+resisted(4) = 16 bytes
|
||||
// WotLK 3.3.5a: damage(4)+overkill(4)+school(4)+absorbed(4)+resisted(4)+isCrit(1) = 21 bytes
|
||||
// WotLK 3.3.5a: damage(4)+overkill(4)+school(4)+absorbed(4)+resisted(4) = 20 bytes
|
||||
const bool periodicWotlk = isActiveExpansion("wotlk");
|
||||
const size_t dotSz = periodicWotlk ? 21u : 16u;
|
||||
const size_t dotSz = periodicWotlk ? 20u : 16u;
|
||||
if (packet.getSize() - packet.getReadPos() < dotSz) break;
|
||||
uint32_t dmg = packet.readUInt32();
|
||||
if (periodicWotlk) /*uint32_t overkill=*/ packet.readUInt32();
|
||||
/*uint32_t school=*/ packet.readUInt32();
|
||||
uint32_t abs = packet.readUInt32();
|
||||
uint32_t res = packet.readUInt32();
|
||||
bool dotCrit = false;
|
||||
if (periodicWotlk) dotCrit = (packet.readUInt8() != 0);
|
||||
if (dmg > 0)
|
||||
addCombatText(dotCrit ? CombatTextEntry::CRIT_DAMAGE : CombatTextEntry::PERIODIC_DAMAGE,
|
||||
static_cast<int32_t>(dmg),
|
||||
addCombatText(CombatTextEntry::PERIODIC_DAMAGE, static_cast<int32_t>(dmg),
|
||||
spellId, isPlayerCaster, 0, casterGuid, victimGuid);
|
||||
if (abs > 0)
|
||||
addCombatText(CombatTextEntry::ABSORB, static_cast<int32_t>(abs),
|
||||
|
|
@ -4289,13 +4278,11 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
/*uint32_t max=*/ packet.readUInt32();
|
||||
/*uint32_t over=*/ packet.readUInt32();
|
||||
uint32_t hotAbs = 0;
|
||||
bool hotCrit = false;
|
||||
if (healWotlk) {
|
||||
hotAbs = packet.readUInt32();
|
||||
hotCrit = (packet.readUInt8() != 0);
|
||||
/*uint8_t isCrit=*/ packet.readUInt8();
|
||||
}
|
||||
addCombatText(hotCrit ? CombatTextEntry::CRIT_HEAL : CombatTextEntry::PERIODIC_HEAL,
|
||||
static_cast<int32_t>(heal),
|
||||
addCombatText(CombatTextEntry::PERIODIC_HEAL, static_cast<int32_t>(heal),
|
||||
spellId, isPlayerCaster, 0, casterGuid, victimGuid);
|
||||
if (hotAbs > 0)
|
||||
addCombatText(CombatTextEntry::ABSORB, static_cast<int32_t>(hotAbs),
|
||||
|
|
@ -4902,7 +4889,6 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
uint32_t result = packet.readUInt32();
|
||||
if (result == 0) {
|
||||
addSystemChatMessage("Hairstyle changed.");
|
||||
barberShopOpen_ = false;
|
||||
} else {
|
||||
const char* msg = (result == 1) ? "Not enough money for new hairstyle."
|
||||
: (result == 2) ? "You are not at a barber shop."
|
||||
|
|
@ -5806,31 +5792,7 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
case Opcode::SMSG_LFG_OFFER_CONTINUE:
|
||||
addSystemChatMessage("Dungeon Finder: You may continue your dungeon.");
|
||||
break;
|
||||
case Opcode::SMSG_LFG_ROLE_CHOSEN: {
|
||||
// uint64 guid + uint8 ready + uint32 roles
|
||||
if (packet.getSize() - packet.getReadPos() >= 13) {
|
||||
uint64_t roleGuid = packet.readUInt64();
|
||||
uint8_t ready = packet.readUInt8();
|
||||
uint32_t roles = packet.readUInt32();
|
||||
// Build a descriptive message for group chat
|
||||
std::string roleName;
|
||||
if (roles & 0x02) roleName += "Tank ";
|
||||
if (roles & 0x04) roleName += "Healer ";
|
||||
if (roles & 0x08) roleName += "DPS ";
|
||||
if (roleName.empty()) roleName = "None";
|
||||
// Find player name
|
||||
std::string pName = "A player";
|
||||
if (auto e = entityManager.getEntity(roleGuid))
|
||||
if (auto u = std::dynamic_pointer_cast<Unit>(e))
|
||||
pName = u->getName();
|
||||
if (ready)
|
||||
addSystemChatMessage(pName + " has chosen: " + roleName);
|
||||
LOG_DEBUG("SMSG_LFG_ROLE_CHOSEN: guid=", roleGuid,
|
||||
" ready=", (int)ready, " roles=", roles);
|
||||
}
|
||||
packet.setReadPos(packet.getSize());
|
||||
break;
|
||||
}
|
||||
case Opcode::SMSG_LFG_ROLE_CHOSEN:
|
||||
case Opcode::SMSG_LFG_UPDATE_SEARCH:
|
||||
case Opcode::SMSG_UPDATE_LFG_LIST:
|
||||
case Opcode::SMSG_LFG_PLAYER_INFO:
|
||||
|
|
@ -7654,13 +7616,9 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
break;
|
||||
}
|
||||
case Opcode::SMSG_PETITION_QUERY_RESPONSE:
|
||||
handlePetitionQueryResponse(packet);
|
||||
break;
|
||||
case Opcode::SMSG_PETITION_SHOW_SIGNATURES:
|
||||
handlePetitionShowSignatures(packet);
|
||||
break;
|
||||
case Opcode::SMSG_PETITION_SIGN_RESULTS:
|
||||
handlePetitionSignResults(packet);
|
||||
packet.setReadPos(packet.getSize());
|
||||
break;
|
||||
|
||||
// ---- Pet system ----
|
||||
|
|
@ -7714,17 +7672,13 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
break;
|
||||
}
|
||||
case Opcode::SMSG_PET_CAST_FAILED: {
|
||||
// WotLK: castCount(1) + spellId(4) + reason(1)
|
||||
// Classic/TBC: spellId(4) + reason(1) (no castCount)
|
||||
const bool hasCount = isActiveExpansion("wotlk");
|
||||
const size_t minSize = hasCount ? 6u : 5u;
|
||||
if (packet.getSize() - packet.getReadPos() >= minSize) {
|
||||
if (hasCount) /*uint8_t castCount =*/ packet.readUInt8();
|
||||
if (packet.getSize() - packet.getReadPos() >= 5) {
|
||||
uint8_t castCount = packet.readUInt8();
|
||||
uint32_t spellId = packet.readUInt32();
|
||||
uint8_t reason = (packet.getSize() - packet.getReadPos() >= 1)
|
||||
? packet.readUInt8() : 0;
|
||||
LOG_DEBUG("SMSG_PET_CAST_FAILED: spell=", spellId,
|
||||
" reason=", (int)reason);
|
||||
" reason=", (int)reason, " castCount=", (int)castCount);
|
||||
if (reason != 0) {
|
||||
const char* reasonStr = getSpellCastResultString(reason);
|
||||
const std::string& sName = getSpellName(spellId);
|
||||
|
|
@ -11339,10 +11293,10 @@ void GameHandler::applyUpdateObjectBlock(const UpdateBlock& block, bool& newItem
|
|||
// Track player-on-transport state
|
||||
if (block.guid == playerGuid) {
|
||||
if (block.onTransport) {
|
||||
setPlayerOnTransport(block.transportGuid, glm::vec3(0.0f));
|
||||
// Convert transport offset from server → canonical coordinates
|
||||
glm::vec3 serverOffset(block.transportX, block.transportY, block.transportZ);
|
||||
glm::vec3 canonicalOffset = core::coords::serverToCanonical(serverOffset);
|
||||
setPlayerOnTransport(block.transportGuid, canonicalOffset);
|
||||
playerTransportOffset_ = core::coords::serverToCanonical(serverOffset);
|
||||
if (transportManager_ && transportManager_->getTransport(playerTransportGuid_)) {
|
||||
glm::vec3 composed = transportManager_->getPlayerWorldPosition(playerTransportGuid_, playerTransportOffset_);
|
||||
entity->setPosition(composed.x, composed.y, composed.z, oCanonical);
|
||||
|
|
@ -11608,9 +11562,6 @@ void GameHandler::applyUpdateObjectBlock(const UpdateBlock& block, bool& newItem
|
|||
" displayId=", unit->getDisplayId(), " appearance extraction failed — model will not render");
|
||||
}
|
||||
}
|
||||
if (unitInitiallyDead && npcDeathCallback_) {
|
||||
npcDeathCallback_(block.guid);
|
||||
}
|
||||
} else if (creatureSpawnCallback_) {
|
||||
LOG_DEBUG("[Spawn] UNIT guid=0x", std::hex, block.guid, std::dec,
|
||||
" displayId=", unit->getDisplayId(), " at (",
|
||||
|
|
@ -11957,7 +11908,7 @@ void GameHandler::applyUpdateObjectBlock(const UpdateBlock& block, bool& newItem
|
|||
corpseX_, ",", corpseY_, ",", corpseZ_,
|
||||
") map=", corpseMapId_);
|
||||
}
|
||||
if ((entity->getType() == ObjectType::UNIT || entity->getType() == ObjectType::PLAYER) && npcDeathCallback_) {
|
||||
if (entity->getType() == ObjectType::UNIT && npcDeathCallback_) {
|
||||
npcDeathCallback_(block.guid);
|
||||
npcDeathNotified = true;
|
||||
}
|
||||
|
|
@ -11970,7 +11921,7 @@ void GameHandler::applyUpdateObjectBlock(const UpdateBlock& block, bool& newItem
|
|||
LOG_INFO("Player entered ghost form");
|
||||
}
|
||||
}
|
||||
if ((entity->getType() == ObjectType::UNIT || entity->getType() == ObjectType::PLAYER) && npcRespawnCallback_) {
|
||||
if (entity->getType() == ObjectType::UNIT && npcRespawnCallback_) {
|
||||
npcRespawnCallback_(block.guid);
|
||||
npcRespawnNotified = true;
|
||||
}
|
||||
|
|
@ -12001,7 +11952,7 @@ void GameHandler::applyUpdateObjectBlock(const UpdateBlock& block, bool& newItem
|
|||
selfResAvailable_ = false;
|
||||
LOG_INFO("Player resurrected (dynamic flags)");
|
||||
}
|
||||
} else if (entity->getType() == ObjectType::UNIT || entity->getType() == ObjectType::PLAYER) {
|
||||
} else if (entity->getType() == ObjectType::UNIT) {
|
||||
bool wasDead = (oldDyn & UNIT_DYNFLAG_DEAD) != 0;
|
||||
bool nowDead = (val & UNIT_DYNFLAG_DEAD) != 0;
|
||||
if (!wasDead && nowDead) {
|
||||
|
|
@ -12137,12 +12088,6 @@ void GameHandler::applyUpdateObjectBlock(const UpdateBlock& block, bool& newItem
|
|||
" displayId=", unit->getDisplayId(), " appearance extraction failed (VALUES update) — model will not render");
|
||||
}
|
||||
}
|
||||
bool isDeadNow = (unit->getHealth() == 0) ||
|
||||
((unit->getDynamicFlags() & (UNIT_DYNFLAG_DEAD | UNIT_DYNFLAG_LOOTABLE)) != 0);
|
||||
if (isDeadNow && !npcDeathNotified && npcDeathCallback_) {
|
||||
npcDeathCallback_(block.guid);
|
||||
npcDeathNotified = true;
|
||||
}
|
||||
} else if (creatureSpawnCallback_) {
|
||||
float unitScale2 = 1.0f;
|
||||
{
|
||||
|
|
@ -12209,7 +12154,6 @@ void GameHandler::applyUpdateObjectBlock(const UpdateBlock& block, bool& newItem
|
|||
const uint16_t ufCoinage = fieldIndex(UF::PLAYER_FIELD_COINAGE);
|
||||
const uint16_t ufPlayerFlags = fieldIndex(UF::PLAYER_FLAGS);
|
||||
const uint16_t ufArmor = fieldIndex(UF::UNIT_FIELD_RESISTANCES);
|
||||
const uint16_t ufPBytesV = fieldIndex(UF::PLAYER_BYTES);
|
||||
const uint16_t ufPBytes2v = fieldIndex(UF::PLAYER_BYTES_2);
|
||||
const uint16_t ufChosenTitle = fieldIndex(UF::PLAYER_CHOSEN_TITLE);
|
||||
const uint16_t ufStatsV[5] = {
|
||||
|
|
@ -12260,38 +12204,15 @@ void GameHandler::applyUpdateObjectBlock(const UpdateBlock& block, bool& newItem
|
|||
else if (ufArmor != 0xFFFF && key > ufArmor && key <= ufArmor + 6) {
|
||||
playerResistances_[key - ufArmor - 1] = static_cast<int32_t>(val);
|
||||
}
|
||||
else if (ufPBytesV != 0xFFFF && key == ufPBytesV) {
|
||||
// PLAYER_BYTES changed (barber shop, polymorph, etc.)
|
||||
// Update the Character struct so inventory preview refreshes
|
||||
for (auto& ch : characters) {
|
||||
if (ch.guid == playerGuid) {
|
||||
ch.appearanceBytes = val;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (appearanceChangedCallback_)
|
||||
appearanceChangedCallback_();
|
||||
}
|
||||
else if (ufPBytes2v != 0xFFFF && key == ufPBytes2v) {
|
||||
// Byte 0 (bits 0-7): facial hair / piercings
|
||||
uint8_t facialHair = static_cast<uint8_t>(val & 0xFF);
|
||||
for (auto& ch : characters) {
|
||||
if (ch.guid == playerGuid) {
|
||||
ch.facialFeatures = facialHair;
|
||||
break;
|
||||
}
|
||||
}
|
||||
uint8_t bankBagSlots = static_cast<uint8_t>((val >> 16) & 0xFF);
|
||||
LOG_DEBUG("PLAYER_BYTES_2 (VALUES): raw=0x", std::hex, val, std::dec,
|
||||
" bankBagSlots=", static_cast<int>(bankBagSlots),
|
||||
" facial=", static_cast<int>(facialHair));
|
||||
LOG_WARNING("PLAYER_BYTES_2 (VALUES): raw=0x", std::hex, val, std::dec,
|
||||
" bankBagSlots=", static_cast<int>(bankBagSlots));
|
||||
inventory.setPurchasedBankBagSlots(bankBagSlots);
|
||||
// Byte 3 (bits 24-31): REST_STATE
|
||||
// 0 = not resting, 1 = REST_TYPE_IN_TAVERN, 2 = REST_TYPE_IN_CITY
|
||||
uint8_t restStateByte = static_cast<uint8_t>((val >> 24) & 0xFF);
|
||||
isResting_ = (restStateByte != 0);
|
||||
if (appearanceChangedCallback_)
|
||||
appearanceChangedCallback_();
|
||||
}
|
||||
else if (ufChosenTitle != 0xFFFF && key == ufChosenTitle) {
|
||||
chosenTitleBit_ = static_cast<int32_t>(val);
|
||||
|
|
@ -12509,10 +12430,10 @@ void GameHandler::applyUpdateObjectBlock(const UpdateBlock& block, bool& newItem
|
|||
|
||||
// Track player-on-transport state from MOVEMENT updates
|
||||
if (block.onTransport) {
|
||||
setPlayerOnTransport(block.transportGuid, glm::vec3(0.0f));
|
||||
// Convert transport offset from server → canonical coordinates
|
||||
glm::vec3 serverOffset(block.transportX, block.transportY, block.transportZ);
|
||||
glm::vec3 canonicalOffset = core::coords::serverToCanonical(serverOffset);
|
||||
setPlayerOnTransport(block.transportGuid, canonicalOffset);
|
||||
playerTransportOffset_ = core::coords::serverToCanonical(serverOffset);
|
||||
if (transportManager_ && transportManager_->getTransport(playerTransportGuid_)) {
|
||||
glm::vec3 composed = transportManager_->getPlayerWorldPosition(playerTransportGuid_, playerTransportOffset_);
|
||||
entity->setPosition(composed.x, composed.y, composed.z, oCanonical);
|
||||
|
|
@ -12816,15 +12737,6 @@ void GameHandler::handleMessageChat(network::Packet& packet) {
|
|||
// Track whisper sender for /r command
|
||||
if (data.type == ChatType::WHISPER && !data.senderName.empty()) {
|
||||
lastWhisperSender_ = data.senderName;
|
||||
|
||||
// Auto-reply if AFK or DND
|
||||
if (afkStatus_ && !data.senderName.empty()) {
|
||||
std::string reply = afkMessage_.empty() ? "Away from Keyboard" : afkMessage_;
|
||||
sendChatMessage(ChatType::WHISPER, "<AFK> " + reply, data.senderName);
|
||||
} else if (dndStatus_ && !data.senderName.empty()) {
|
||||
std::string reply = dndMessage_.empty() ? "Do Not Disturb" : dndMessage_;
|
||||
sendChatMessage(ChatType::WHISPER, "<DND> " + reply, data.senderName);
|
||||
}
|
||||
}
|
||||
|
||||
// Trigger chat bubble for SAY/YELL messages from others
|
||||
|
|
@ -15632,12 +15544,6 @@ void GameHandler::addCombatText(CombatTextEntry::Type type, int32_t amount, uint
|
|||
entry.age = 0.0f;
|
||||
entry.isPlayerSource = isPlayerSource;
|
||||
entry.powerType = powerType;
|
||||
entry.srcGuid = srcGuid;
|
||||
entry.dstGuid = dstGuid;
|
||||
// Random horizontal stagger so simultaneous hits don't stack vertically
|
||||
static std::mt19937 rng(std::random_device{}());
|
||||
std::uniform_real_distribution<float> dist(-1.0f, 1.0f);
|
||||
entry.xSeed = dist(rng);
|
||||
combatText.push_back(entry);
|
||||
|
||||
// Persistent combat log — use explicit GUIDs if provided, else fall back to
|
||||
|
|
@ -16171,21 +16077,18 @@ void GameHandler::handleBattlefieldStatus(network::Packet& packet) {
|
|||
uint32_t statusId = packet.readUInt32();
|
||||
|
||||
// Map BG type IDs to their names (stable across all three expansions)
|
||||
// BattlemasterList.dbc IDs (3.3.5a)
|
||||
static const std::pair<uint32_t, const char*> kBgNames[] = {
|
||||
{1, "Alterac Valley"},
|
||||
{2, "Warsong Gulch"},
|
||||
{3, "Arathi Basin"},
|
||||
{4, "Nagrand Arena"},
|
||||
{5, "Blade's Edge Arena"},
|
||||
{6, "All Arenas"},
|
||||
{7, "Eye of the Storm"},
|
||||
{8, "Ruins of Lordaeron"},
|
||||
{6, "Eye of the Storm"},
|
||||
{9, "Strand of the Ancients"},
|
||||
{10, "Dalaran Sewers"},
|
||||
{11, "Ring of Valor"},
|
||||
{30, "Isle of Conquest"},
|
||||
{32, "Random Battleground"},
|
||||
{11, "Isle of Conquest"},
|
||||
{30, "Nagrand Arena"},
|
||||
{31, "Blade's Edge Arena"},
|
||||
{32, "Dalaran Sewers"},
|
||||
{33, "Ring of Valor"},
|
||||
{34, "Ruins of Lordaeron"},
|
||||
};
|
||||
std::string bgName = "Battleground";
|
||||
for (const auto& kv : kBgNames) {
|
||||
|
|
@ -16236,7 +16139,6 @@ void GameHandler::handleBattlefieldStatus(network::Packet& packet) {
|
|||
bgQueues_[queueSlot].bgTypeId = bgTypeId;
|
||||
bgQueues_[queueSlot].arenaType = arenaType;
|
||||
bgQueues_[queueSlot].statusId = statusId;
|
||||
bgQueues_[queueSlot].bgName = bgName;
|
||||
if (statusId == 1) {
|
||||
bgQueues_[queueSlot].avgWaitTimeSec = avgWaitSec;
|
||||
bgQueues_[queueSlot].timeInQueueSec = timeInQueueSec;
|
||||
|
|
@ -16490,7 +16392,6 @@ void GameHandler::handleInstanceDifficulty(network::Packet& packet) {
|
|||
} else {
|
||||
instanceIsHeroic_ = (instanceDifficulty_ == 1);
|
||||
}
|
||||
inInstance_ = true;
|
||||
LOG_INFO("Instance difficulty: ", instanceDifficulty_, " heroic=", instanceIsHeroic_);
|
||||
|
||||
// Announce difficulty change to the player (only when it actually changes)
|
||||
|
|
@ -17032,25 +16933,7 @@ void GameHandler::handleArenaTeamQueryResponse(network::Packet& packet) {
|
|||
if (packet.getSize() - packet.getReadPos() < 4) return;
|
||||
uint32_t teamId = packet.readUInt32();
|
||||
std::string teamName = packet.readString();
|
||||
uint32_t teamType = 0;
|
||||
if (packet.getSize() - packet.getReadPos() >= 4)
|
||||
teamType = packet.readUInt32();
|
||||
LOG_INFO("Arena team query response: id=", teamId, " name=", teamName, " type=", teamType);
|
||||
|
||||
// Store name and type in matching ArenaTeamStats entry
|
||||
for (auto& s : arenaTeamStats_) {
|
||||
if (s.teamId == teamId) {
|
||||
s.teamName = teamName;
|
||||
s.teamType = teamType;
|
||||
return;
|
||||
}
|
||||
}
|
||||
// No stats entry yet — create a placeholder so we can show the name
|
||||
ArenaTeamStats stub;
|
||||
stub.teamId = teamId;
|
||||
stub.teamName = teamName;
|
||||
stub.teamType = teamType;
|
||||
arenaTeamStats_.push_back(std::move(stub));
|
||||
LOG_INFO("Arena team query response: id=", teamId, " name=", teamName);
|
||||
}
|
||||
|
||||
void GameHandler::handleArenaTeamRoster(network::Packet& packet) {
|
||||
|
|
@ -17194,29 +17077,18 @@ void GameHandler::handleArenaTeamStats(network::Packet& packet) {
|
|||
stats.seasonWins = packet.readUInt32();
|
||||
stats.rank = packet.readUInt32();
|
||||
|
||||
// Update or insert for this team (preserve name/type from query response)
|
||||
// Update or insert for this team
|
||||
for (auto& s : arenaTeamStats_) {
|
||||
if (s.teamId == stats.teamId) {
|
||||
stats.teamName = std::move(s.teamName);
|
||||
stats.teamType = s.teamType;
|
||||
s = std::move(stats);
|
||||
LOG_INFO("SMSG_ARENA_TEAM_STATS: teamId=", s.teamId,
|
||||
" rating=", s.rating, " rank=", s.rank);
|
||||
s = stats;
|
||||
LOG_INFO("SMSG_ARENA_TEAM_STATS: teamId=", stats.teamId,
|
||||
" rating=", stats.rating, " rank=", stats.rank);
|
||||
return;
|
||||
}
|
||||
}
|
||||
arenaTeamStats_.push_back(std::move(stats));
|
||||
LOG_INFO("SMSG_ARENA_TEAM_STATS: teamId=", arenaTeamStats_.back().teamId,
|
||||
" rating=", arenaTeamStats_.back().rating,
|
||||
" rank=", arenaTeamStats_.back().rank);
|
||||
}
|
||||
|
||||
void GameHandler::requestArenaTeamRoster(uint32_t teamId) {
|
||||
if (!socket) return;
|
||||
network::Packet pkt(wireOpcode(Opcode::CMSG_ARENA_TEAM_ROSTER));
|
||||
pkt.writeUInt32(teamId);
|
||||
socket->send(pkt);
|
||||
LOG_INFO("Requesting arena team roster for teamId=", teamId);
|
||||
arenaTeamStats_.push_back(stats);
|
||||
LOG_INFO("SMSG_ARENA_TEAM_STATS: teamId=", stats.teamId,
|
||||
" rating=", stats.rating, " rank=", stats.rank);
|
||||
}
|
||||
|
||||
void GameHandler::handleArenaError(network::Packet& packet) {
|
||||
|
|
@ -18633,12 +18505,6 @@ void GameHandler::handleCastFailed(network::Packet& packet) {
|
|||
msg.language = ChatLanguage::UNIVERSAL;
|
||||
msg.message = errMsg;
|
||||
addLocalChatMessage(msg);
|
||||
|
||||
// Play error sound for cast failure feedback
|
||||
if (auto* renderer = core::Application::getInstance().getRenderer()) {
|
||||
if (auto* sfx = renderer->getUiSoundManager())
|
||||
sfx->playError();
|
||||
}
|
||||
}
|
||||
|
||||
static audio::SpellSoundManager::MagicSchool schoolMaskToMagicSchool(uint32_t mask) {
|
||||
|
|
@ -19271,13 +19137,6 @@ void GameHandler::confirmTalentWipe() {
|
|||
talentWipeCost_ = 0;
|
||||
}
|
||||
|
||||
void GameHandler::sendAlterAppearance(uint32_t hairStyle, uint32_t hairColor, uint32_t facialHair) {
|
||||
if (state != WorldState::IN_WORLD || !socket) return;
|
||||
auto pkt = AlterAppearancePacket::build(hairStyle, hairColor, facialHair);
|
||||
socket->send(pkt);
|
||||
LOG_INFO("sendAlterAppearance: hair=", hairStyle, " color=", hairColor, " facial=", facialHair);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Phase 4: Group/Party
|
||||
// ============================================================
|
||||
|
|
@ -19704,28 +19563,6 @@ void GameHandler::queryGuildInfo(uint32_t guildId) {
|
|||
LOG_INFO("Querying guild info: guildId=", guildId);
|
||||
}
|
||||
|
||||
static const std::string kEmptyString;
|
||||
|
||||
const std::string& GameHandler::lookupGuildName(uint32_t guildId) {
|
||||
if (guildId == 0) return kEmptyString;
|
||||
auto it = guildNameCache_.find(guildId);
|
||||
if (it != guildNameCache_.end()) return it->second;
|
||||
// Query the server if we haven't already
|
||||
if (pendingGuildNameQueries_.insert(guildId).second) {
|
||||
queryGuildInfo(guildId);
|
||||
}
|
||||
return kEmptyString;
|
||||
}
|
||||
|
||||
uint32_t GameHandler::getEntityGuildId(uint64_t guid) const {
|
||||
auto entity = entityManager.getEntity(guid);
|
||||
if (!entity || entity->getType() != ObjectType::PLAYER) return 0;
|
||||
// PLAYER_GUILDID = UNIT_END + 3 across all expansions
|
||||
const uint16_t ufUnitEnd = fieldIndex(UF::UNIT_END);
|
||||
if (ufUnitEnd == 0xFFFF) return 0;
|
||||
return entity->getField(ufUnitEnd + 3);
|
||||
}
|
||||
|
||||
void GameHandler::createGuild(const std::string& guildName) {
|
||||
if (state != WorldState::IN_WORLD || !socket) return;
|
||||
auto packet = GuildCreatePacket::build(guildName);
|
||||
|
|
@ -19774,118 +19611,6 @@ void GameHandler::handlePetitionShowlist(network::Packet& packet) {
|
|||
LOG_INFO("Petition showlist: cost=", data.cost);
|
||||
}
|
||||
|
||||
void GameHandler::handlePetitionQueryResponse(network::Packet& packet) {
|
||||
// SMSG_PETITION_QUERY_RESPONSE (3.3.5a):
|
||||
// uint32 petitionEntry, uint64 petitionGuid, string guildName,
|
||||
// string bodyText (empty), uint32 flags, uint32 minSignatures,
|
||||
// uint32 maxSignatures, ...plus more fields we can skip
|
||||
auto rem = [&]() { return packet.getSize() - packet.getReadPos(); };
|
||||
if (rem() < 12) return;
|
||||
|
||||
/*uint32_t entry =*/ packet.readUInt32();
|
||||
uint64_t petGuid = packet.readUInt64();
|
||||
std::string guildName = packet.readString();
|
||||
/*std::string body =*/ packet.readString();
|
||||
|
||||
// Update petition info if it matches our current petition
|
||||
if (petitionInfo_.petitionGuid == petGuid) {
|
||||
petitionInfo_.guildName = guildName;
|
||||
}
|
||||
|
||||
LOG_INFO("SMSG_PETITION_QUERY_RESPONSE: guid=", petGuid, " name=", guildName);
|
||||
packet.setReadPos(packet.getSize()); // skip remaining fields
|
||||
}
|
||||
|
||||
void GameHandler::handlePetitionShowSignatures(network::Packet& packet) {
|
||||
// SMSG_PETITION_SHOW_SIGNATURES (3.3.5a):
|
||||
// uint64 itemGuid (petition item in inventory)
|
||||
// uint64 ownerGuid
|
||||
// uint32 petitionGuid (low part / entry)
|
||||
// uint8 signatureCount
|
||||
// For each signature:
|
||||
// uint64 playerGuid
|
||||
// uint32 unk (always 0)
|
||||
auto rem = [&]() { return packet.getSize() - packet.getReadPos(); };
|
||||
if (rem() < 21) return;
|
||||
|
||||
petitionInfo_ = PetitionInfo{};
|
||||
petitionInfo_.petitionGuid = packet.readUInt64();
|
||||
petitionInfo_.ownerGuid = packet.readUInt64();
|
||||
/*uint32_t petEntry =*/ packet.readUInt32();
|
||||
uint8_t sigCount = packet.readUInt8();
|
||||
|
||||
petitionInfo_.signatureCount = sigCount;
|
||||
petitionInfo_.signatures.reserve(sigCount);
|
||||
|
||||
for (uint8_t i = 0; i < sigCount; ++i) {
|
||||
if (rem() < 12) break;
|
||||
PetitionSignature sig;
|
||||
sig.playerGuid = packet.readUInt64();
|
||||
/*uint32_t unk =*/ packet.readUInt32();
|
||||
petitionInfo_.signatures.push_back(sig);
|
||||
}
|
||||
|
||||
petitionInfo_.showUI = true;
|
||||
LOG_INFO("SMSG_PETITION_SHOW_SIGNATURES: petGuid=", petitionInfo_.petitionGuid,
|
||||
" owner=", petitionInfo_.ownerGuid,
|
||||
" sigs=", sigCount);
|
||||
}
|
||||
|
||||
void GameHandler::handlePetitionSignResults(network::Packet& packet) {
|
||||
// SMSG_PETITION_SIGN_RESULTS (3.3.5a):
|
||||
// uint64 petitionGuid, uint64 playerGuid, uint32 result
|
||||
auto rem = [&]() { return packet.getSize() - packet.getReadPos(); };
|
||||
if (rem() < 20) return;
|
||||
|
||||
uint64_t petGuid = packet.readUInt64();
|
||||
uint64_t playerGuid = packet.readUInt64();
|
||||
uint32_t result = packet.readUInt32();
|
||||
|
||||
switch (result) {
|
||||
case 0: // PETITION_SIGN_OK
|
||||
addSystemChatMessage("Petition signed successfully.");
|
||||
// Increment local count
|
||||
if (petitionInfo_.petitionGuid == petGuid) {
|
||||
petitionInfo_.signatureCount++;
|
||||
PetitionSignature sig;
|
||||
sig.playerGuid = playerGuid;
|
||||
petitionInfo_.signatures.push_back(sig);
|
||||
}
|
||||
break;
|
||||
case 1: // PETITION_SIGN_ALREADY_SIGNED
|
||||
addSystemChatMessage("You have already signed that petition.");
|
||||
break;
|
||||
case 2: // PETITION_SIGN_ALREADY_IN_GUILD
|
||||
addSystemChatMessage("You are already in a guild.");
|
||||
break;
|
||||
case 3: // PETITION_SIGN_CANT_SIGN_OWN
|
||||
addSystemChatMessage("You cannot sign your own petition.");
|
||||
break;
|
||||
default:
|
||||
addSystemChatMessage("Cannot sign petition (error " + std::to_string(result) + ").");
|
||||
break;
|
||||
}
|
||||
LOG_INFO("SMSG_PETITION_SIGN_RESULTS: pet=", petGuid, " player=", playerGuid,
|
||||
" result=", result);
|
||||
}
|
||||
|
||||
void GameHandler::signPetition(uint64_t petitionGuid) {
|
||||
if (!socket || state != WorldState::IN_WORLD) return;
|
||||
network::Packet pkt(wireOpcode(Opcode::CMSG_PETITION_SIGN));
|
||||
pkt.writeUInt64(petitionGuid);
|
||||
pkt.writeUInt8(0); // unk
|
||||
socket->send(pkt);
|
||||
LOG_INFO("Signing petition: ", petitionGuid);
|
||||
}
|
||||
|
||||
void GameHandler::turnInPetition(uint64_t petitionGuid) {
|
||||
if (!socket || state != WorldState::IN_WORLD) return;
|
||||
network::Packet pkt(wireOpcode(Opcode::CMSG_TURN_IN_PETITION));
|
||||
pkt.writeUInt64(petitionGuid);
|
||||
socket->send(pkt);
|
||||
LOG_INFO("Turning in petition: ", petitionGuid);
|
||||
}
|
||||
|
||||
void GameHandler::handleTurnInPetitionResults(network::Packet& packet) {
|
||||
uint32_t result = 0;
|
||||
if (!TurnInPetitionResultsParser::parse(packet, result)) return;
|
||||
|
|
@ -19922,30 +19647,18 @@ void GameHandler::handleGuildQueryResponse(network::Packet& packet) {
|
|||
GuildQueryResponseData data;
|
||||
if (!packetParsers_->parseGuildQueryResponse(packet, data)) return;
|
||||
|
||||
// Always cache the guild name for nameplate lookups
|
||||
if (data.guildId != 0 && !data.guildName.empty()) {
|
||||
guildNameCache_[data.guildId] = data.guildName;
|
||||
pendingGuildNameQueries_.erase(data.guildId);
|
||||
}
|
||||
|
||||
// Check if this is the local player's guild
|
||||
const Character* ch = getActiveCharacter();
|
||||
bool isLocalGuild = (ch && ch->hasGuild() && ch->guildId == data.guildId);
|
||||
|
||||
if (isLocalGuild) {
|
||||
const bool wasUnknown = guildName_.empty();
|
||||
guildName_ = data.guildName;
|
||||
guildQueryData_ = data;
|
||||
guildRankNames_.clear();
|
||||
for (uint32_t i = 0; i < 10; ++i) {
|
||||
guildRankNames_.push_back(data.rankNames[i]);
|
||||
}
|
||||
LOG_INFO("Guild name set to: ", guildName_);
|
||||
if (wasUnknown && !guildName_.empty())
|
||||
addSystemChatMessage("Guild: <" + guildName_ + ">");
|
||||
} else {
|
||||
LOG_INFO("Cached guild name: id=", data.guildId, " name=", data.guildName);
|
||||
const bool wasUnknown = guildName_.empty();
|
||||
guildName_ = data.guildName;
|
||||
guildQueryData_ = data;
|
||||
guildRankNames_.clear();
|
||||
for (uint32_t i = 0; i < 10; ++i) {
|
||||
guildRankNames_.push_back(data.rankNames[i]);
|
||||
}
|
||||
LOG_INFO("Guild name set to: ", guildName_);
|
||||
// Only announce once — when we first learn our own guild name at login.
|
||||
// Subsequent queries (e.g. querying other players' guilds) are silent.
|
||||
if (wasUnknown && !guildName_.empty())
|
||||
addSystemChatMessage("Guild: <" + guildName_ + ">");
|
||||
}
|
||||
|
||||
void GameHandler::handleGuildEvent(network::Packet& packet) {
|
||||
|
|
@ -20219,17 +19932,6 @@ void GameHandler::performGameObjectInteractionNow(uint64_t guid) {
|
|||
addSystemChatMessage("Too far away.");
|
||||
return;
|
||||
}
|
||||
// Stop movement before interacting — servers may reject GO use or
|
||||
// immediately cancel the resulting spell cast if the player is moving.
|
||||
const uint32_t moveFlags = movementInfo.flags;
|
||||
const bool isMoving = (moveFlags & 0x00000001u) || // FORWARD
|
||||
(moveFlags & 0x00000002u) || // BACKWARD
|
||||
(moveFlags & 0x00000004u) || // STRAFE_LEFT
|
||||
(moveFlags & 0x00000008u); // STRAFE_RIGHT
|
||||
if (isMoving) {
|
||||
movementInfo.flags &= ~0x0000000Fu; // clear directional movement flags
|
||||
sendMovement(Opcode::MSG_MOVE_STOP);
|
||||
}
|
||||
if (std::abs(dx) > 0.01f || std::abs(dy) > 0.01f) {
|
||||
movementInfo.orientation = std::atan2(-dy, dx);
|
||||
sendMovement(Opcode::MSG_MOVE_SET_FACING);
|
||||
|
|
@ -20237,12 +19939,6 @@ void GameHandler::performGameObjectInteractionNow(uint64_t guid) {
|
|||
sendMovement(Opcode::MSG_MOVE_HEARTBEAT);
|
||||
}
|
||||
|
||||
LOG_INFO("GO interaction: guid=0x", std::hex, guid, std::dec,
|
||||
" entry=", goEntry, " type=", goType,
|
||||
" name='", goName, "' dist=", entity ? std::sqrt(
|
||||
(entity->getX() - movementInfo.x) * (entity->getX() - movementInfo.x) +
|
||||
(entity->getY() - movementInfo.y) * (entity->getY() - movementInfo.y) +
|
||||
(entity->getZ() - movementInfo.z) * (entity->getZ() - movementInfo.z)) : -1.0f);
|
||||
auto packet = GameObjectUsePacket::build(guid);
|
||||
socket->send(packet);
|
||||
lastInteractedGoGuid_ = guid;
|
||||
|
|
@ -21387,40 +21083,6 @@ void GameHandler::destroyItem(uint8_t bag, uint8_t slot, uint8_t count) {
|
|||
socket->send(packet);
|
||||
}
|
||||
|
||||
void GameHandler::splitItem(uint8_t srcBag, uint8_t srcSlot, uint8_t count) {
|
||||
if (state != WorldState::IN_WORLD || !socket) return;
|
||||
if (count == 0) return;
|
||||
|
||||
// Find a free slot for the split destination: try backpack first, then bags
|
||||
int freeBp = inventory.findFreeBackpackSlot();
|
||||
if (freeBp >= 0) {
|
||||
uint8_t dstBag = 0xFF;
|
||||
uint8_t dstSlot = static_cast<uint8_t>(23 + freeBp);
|
||||
LOG_INFO("splitItem: src(bag=", (int)srcBag, " slot=", (int)srcSlot,
|
||||
") count=", (int)count, " -> dst(bag=0xFF slot=", (int)dstSlot, ")");
|
||||
auto packet = SplitItemPacket::build(srcBag, srcSlot, dstBag, dstSlot, count);
|
||||
socket->send(packet);
|
||||
return;
|
||||
}
|
||||
// Try equipped bags
|
||||
for (int b = 0; b < inventory.NUM_BAG_SLOTS; b++) {
|
||||
int bagSize = inventory.getBagSize(b);
|
||||
for (int s = 0; s < bagSize; s++) {
|
||||
if (inventory.getBagSlot(b, s).empty()) {
|
||||
uint8_t dstBag = static_cast<uint8_t>(19 + b);
|
||||
uint8_t dstSlot = static_cast<uint8_t>(s);
|
||||
LOG_INFO("splitItem: src(bag=", (int)srcBag, " slot=", (int)srcSlot,
|
||||
") count=", (int)count, " -> dst(bag=", (int)dstBag,
|
||||
" slot=", (int)dstSlot, ")");
|
||||
auto packet = SplitItemPacket::build(srcBag, srcSlot, dstBag, dstSlot, count);
|
||||
socket->send(packet);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
addSystemChatMessage("Cannot split: no free inventory slots.");
|
||||
}
|
||||
|
||||
void GameHandler::useItemBySlot(int backpackIndex) {
|
||||
if (backpackIndex < 0 || backpackIndex >= inventory.getBackpackSize()) return;
|
||||
const auto& slot = inventory.getBackpackSlot(backpackIndex);
|
||||
|
|
@ -21574,8 +21236,8 @@ void GameHandler::unstuckHearth() {
|
|||
}
|
||||
|
||||
void GameHandler::handleLootResponse(network::Packet& packet) {
|
||||
// All expansions use 22 bytes/item (slot+itemId+count+displayInfo+randSuffix+randProp+slotType).
|
||||
// WotLK adds a quest item list after the regular items.
|
||||
// Classic 1.12 and TBC 2.4.3 use 14 bytes/item (no randomSuffix/randomProp fields);
|
||||
// WotLK 3.3.5a uses 22 bytes/item.
|
||||
const bool wotlkLoot = isActiveExpansion("wotlk");
|
||||
if (!LootResponseParser::parse(packet, currentLoot, wotlkLoot)) return;
|
||||
const bool hasLoot = !currentLoot.items.empty() || currentLoot.gold > 0;
|
||||
|
|
@ -22560,7 +22222,6 @@ void GameHandler::handleNewWorld(network::Packet& packet) {
|
|||
}
|
||||
|
||||
currentMapId_ = mapId;
|
||||
inInstance_ = false; // cleared on map change; re-set if SMSG_INSTANCE_DIFFICULTY follows
|
||||
if (socket) {
|
||||
socket->tracePacketsFor(std::chrono::seconds(12), "new_world");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -189,8 +189,11 @@ uint32_t classicWireMoveFlags(uint32_t internalFlags) {
|
|||
// Same as TBC: u8 UpdateFlags, JUMPING=0x2000, 8 speeds, no pitchRate
|
||||
// ============================================================================
|
||||
bool ClassicPacketParsers::parseMovementBlock(network::Packet& packet, UpdateBlock& block) {
|
||||
auto rem = [&]() -> size_t { return packet.getSize() - packet.getReadPos(); };
|
||||
if (rem() < 1) return false;
|
||||
// Validate minimum packet size for updateFlags byte
|
||||
if (packet.getReadPos() >= packet.getSize()) {
|
||||
LOG_WARNING("[Classic] Movement block packet too small (need at least 1 byte for updateFlags)");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Classic: UpdateFlags is uint8 (same as TBC)
|
||||
uint8_t updateFlags = packet.readUInt8();
|
||||
|
|
@ -206,9 +209,6 @@ bool ClassicPacketParsers::parseMovementBlock(network::Packet& packet, UpdateBlo
|
|||
const uint8_t UPDATEFLAG_HAS_POSITION = 0x40;
|
||||
|
||||
if (updateFlags & UPDATEFLAG_LIVING) {
|
||||
// Minimum: moveFlags(4)+time(4)+position(16)+fallTime(4)+speeds(24) = 52 bytes
|
||||
if (rem() < 52) return false;
|
||||
|
||||
// Movement flags (u32 only — NO extra flags byte in Classic)
|
||||
uint32_t moveFlags = packet.readUInt32();
|
||||
/*uint32_t time =*/ packet.readUInt32();
|
||||
|
|
@ -225,29 +225,26 @@ bool ClassicPacketParsers::parseMovementBlock(network::Packet& packet, UpdateBlo
|
|||
|
||||
// Transport data (Classic: ONTRANSPORT=0x02000000, no timestamp)
|
||||
if (moveFlags & ClassicMoveFlags::ONTRANSPORT) {
|
||||
if (rem() < 1) return false;
|
||||
block.onTransport = true;
|
||||
block.transportGuid = UpdateObjectParser::readPackedGuid(packet);
|
||||
if (rem() < 16) return false; // 4 floats
|
||||
block.transportX = packet.readFloat();
|
||||
block.transportY = packet.readFloat();
|
||||
block.transportZ = packet.readFloat();
|
||||
block.transportO = packet.readFloat();
|
||||
// Classic: NO transport timestamp (TBC adds u32 timestamp)
|
||||
// Classic: NO transport seat byte
|
||||
}
|
||||
|
||||
// Pitch (Classic: only SWIMMING, no FLYING or ONTRANSPORT pitch)
|
||||
if (moveFlags & ClassicMoveFlags::SWIMMING) {
|
||||
if (rem() < 4) return false;
|
||||
/*float pitch =*/ packet.readFloat();
|
||||
}
|
||||
|
||||
// Fall time (always present)
|
||||
if (rem() < 4) return false;
|
||||
/*uint32_t fallTime =*/ packet.readUInt32();
|
||||
|
||||
// Jumping (Classic: JUMPING=0x2000, same as TBC)
|
||||
if (moveFlags & ClassicMoveFlags::JUMPING) {
|
||||
if (rem() < 16) return false;
|
||||
/*float jumpVelocity =*/ packet.readFloat();
|
||||
/*float jumpSinAngle =*/ packet.readFloat();
|
||||
/*float jumpCosAngle =*/ packet.readFloat();
|
||||
|
|
@ -256,12 +253,12 @@ bool ClassicPacketParsers::parseMovementBlock(network::Packet& packet, UpdateBlo
|
|||
|
||||
// Spline elevation
|
||||
if (moveFlags & ClassicMoveFlags::SPLINE_ELEVATION) {
|
||||
if (rem() < 4) return false;
|
||||
/*float splineElevation =*/ packet.readFloat();
|
||||
}
|
||||
|
||||
// Speeds (Classic: 6 values — no flight speeds, no pitchRate)
|
||||
if (rem() < 24) return false;
|
||||
// TBC added flying_speed + backwards_flying_speed (8 total)
|
||||
// WotLK added pitchRate (9 total)
|
||||
/*float walkSpeed =*/ packet.readFloat();
|
||||
float runSpeed = packet.readFloat();
|
||||
/*float runBackSpeed =*/ packet.readFloat();
|
||||
|
|
@ -274,34 +271,34 @@ bool ClassicPacketParsers::parseMovementBlock(network::Packet& packet, UpdateBlo
|
|||
|
||||
// Spline data (Classic: SPLINE_ENABLED=0x00400000)
|
||||
if (moveFlags & ClassicMoveFlags::SPLINE_ENABLED) {
|
||||
if (rem() < 4) return false;
|
||||
uint32_t splineFlags = packet.readUInt32();
|
||||
LOG_DEBUG(" [Classic] Spline: flags=0x", std::hex, splineFlags, std::dec);
|
||||
|
||||
if (splineFlags & 0x00010000) { // FINAL_POINT
|
||||
if (rem() < 12) return false;
|
||||
/*float finalX =*/ packet.readFloat();
|
||||
/*float finalY =*/ packet.readFloat();
|
||||
/*float finalZ =*/ packet.readFloat();
|
||||
} else if (splineFlags & 0x00020000) { // FINAL_TARGET
|
||||
if (rem() < 8) return false;
|
||||
/*uint64_t finalTarget =*/ packet.readUInt64();
|
||||
} else if (splineFlags & 0x00040000) { // FINAL_ANGLE
|
||||
if (rem() < 4) return false;
|
||||
/*float finalAngle =*/ packet.readFloat();
|
||||
}
|
||||
|
||||
// Classic spline: timePassed, duration, id, pointCount
|
||||
if (rem() < 16) return false;
|
||||
// Classic spline: timePassed, duration, id, nodes, finalNode (same as TBC)
|
||||
/*uint32_t timePassed =*/ packet.readUInt32();
|
||||
/*uint32_t duration =*/ packet.readUInt32();
|
||||
/*uint32_t splineId =*/ packet.readUInt32();
|
||||
|
||||
uint32_t pointCount = packet.readUInt32();
|
||||
if (pointCount > 256) return false;
|
||||
|
||||
// points + endPoint (no splineMode in Classic)
|
||||
if (rem() < static_cast<size_t>(pointCount) * 12 + 12) return false;
|
||||
if (pointCount > 256) {
|
||||
static uint32_t badClassicSplineCount = 0;
|
||||
++badClassicSplineCount;
|
||||
if (badClassicSplineCount <= 5 || (badClassicSplineCount % 100) == 0) {
|
||||
LOG_WARNING(" [Classic] Spline pointCount=", pointCount,
|
||||
" exceeds max, capping (occurrence=", badClassicSplineCount, ")");
|
||||
}
|
||||
pointCount = 0;
|
||||
}
|
||||
for (uint32_t i = 0; i < pointCount; i++) {
|
||||
/*float px =*/ packet.readFloat();
|
||||
/*float py =*/ packet.readFloat();
|
||||
|
|
@ -315,7 +312,6 @@ bool ClassicPacketParsers::parseMovementBlock(network::Packet& packet, UpdateBlo
|
|||
}
|
||||
}
|
||||
else if (updateFlags & UPDATEFLAG_HAS_POSITION) {
|
||||
if (rem() < 16) return false;
|
||||
block.x = packet.readFloat();
|
||||
block.y = packet.readFloat();
|
||||
block.z = packet.readFloat();
|
||||
|
|
@ -327,25 +323,21 @@ bool ClassicPacketParsers::parseMovementBlock(network::Packet& packet, UpdateBlo
|
|||
|
||||
// High GUID
|
||||
if (updateFlags & UPDATEFLAG_HIGHGUID) {
|
||||
if (rem() < 4) return false;
|
||||
/*uint32_t highGuid =*/ packet.readUInt32();
|
||||
}
|
||||
|
||||
// ALL/SELF extra uint32
|
||||
if (updateFlags & UPDATEFLAG_ALL) {
|
||||
if (rem() < 4) return false;
|
||||
/*uint32_t unkAll =*/ packet.readUInt32();
|
||||
}
|
||||
|
||||
// Current melee target as packed guid
|
||||
if (updateFlags & UPDATEFLAG_MELEE_ATTACKING) {
|
||||
if (rem() < 1) return false;
|
||||
/*uint64_t meleeTargetGuid =*/ UpdateObjectParser::readPackedGuid(packet);
|
||||
}
|
||||
|
||||
// Transport progress / world time
|
||||
if (updateFlags & UPDATEFLAG_TRANSPORT) {
|
||||
if (rem() < 4) return false;
|
||||
/*uint32_t transportTime =*/ packet.readUInt32();
|
||||
}
|
||||
|
||||
|
|
@ -1926,9 +1918,6 @@ namespace TurtleMoveFlags {
|
|||
}
|
||||
|
||||
bool TurtlePacketParsers::parseMovementBlock(network::Packet& packet, UpdateBlock& block) {
|
||||
auto rem = [&]() -> size_t { return packet.getSize() - packet.getReadPos(); };
|
||||
if (rem() < 1) return false;
|
||||
|
||||
uint8_t updateFlags = packet.readUInt8();
|
||||
block.updateFlags = static_cast<uint16_t>(updateFlags);
|
||||
|
||||
|
|
@ -1942,8 +1931,6 @@ bool TurtlePacketParsers::parseMovementBlock(network::Packet& packet, UpdateBloc
|
|||
const uint8_t UPDATEFLAG_HAS_POSITION = 0x40;
|
||||
|
||||
if (updateFlags & UPDATEFLAG_LIVING) {
|
||||
// Minimum: moveFlags(4)+time(4)+position(16)+fallTime(4)+speeds(24) = 52 bytes
|
||||
if (rem() < 52) return false;
|
||||
size_t livingStart = packet.getReadPos();
|
||||
|
||||
uint32_t moveFlags = packet.readUInt32();
|
||||
|
|
@ -1962,10 +1949,8 @@ bool TurtlePacketParsers::parseMovementBlock(network::Packet& packet, UpdateBloc
|
|||
|
||||
// Transport — Classic flag position 0x02000000
|
||||
if (moveFlags & TurtleMoveFlags::ONTRANSPORT) {
|
||||
if (rem() < 1) return false; // PackedGuid mask byte
|
||||
block.onTransport = true;
|
||||
block.transportGuid = UpdateObjectParser::readPackedGuid(packet);
|
||||
if (rem() < 20) return false; // 4 floats + u32 timestamp
|
||||
block.transportX = packet.readFloat();
|
||||
block.transportY = packet.readFloat();
|
||||
block.transportZ = packet.readFloat();
|
||||
|
|
@ -1975,17 +1960,14 @@ bool TurtlePacketParsers::parseMovementBlock(network::Packet& packet, UpdateBloc
|
|||
|
||||
// Pitch (swimming only, Classic-style)
|
||||
if (moveFlags & TurtleMoveFlags::SWIMMING) {
|
||||
if (rem() < 4) return false;
|
||||
/*float pitch =*/ packet.readFloat();
|
||||
}
|
||||
|
||||
// Fall time (always present)
|
||||
if (rem() < 4) return false;
|
||||
/*uint32_t fallTime =*/ packet.readUInt32();
|
||||
|
||||
// Jump data
|
||||
if (moveFlags & TurtleMoveFlags::JUMPING) {
|
||||
if (rem() < 16) return false;
|
||||
/*float jumpVelocity =*/ packet.readFloat();
|
||||
/*float jumpSinAngle =*/ packet.readFloat();
|
||||
/*float jumpCosAngle =*/ packet.readFloat();
|
||||
|
|
@ -1994,12 +1976,10 @@ bool TurtlePacketParsers::parseMovementBlock(network::Packet& packet, UpdateBloc
|
|||
|
||||
// Spline elevation
|
||||
if (moveFlags & TurtleMoveFlags::SPLINE_ELEVATION) {
|
||||
if (rem() < 4) return false;
|
||||
/*float splineElevation =*/ packet.readFloat();
|
||||
}
|
||||
|
||||
// Turtle: 6 speeds (same as Classic — no flight speeds)
|
||||
if (rem() < 24) return false; // 6 × float
|
||||
float walkSpeed = packet.readFloat();
|
||||
float runSpeed = packet.readFloat();
|
||||
float runBackSpeed = packet.readFloat();
|
||||
|
|
@ -2017,23 +1997,17 @@ bool TurtlePacketParsers::parseMovementBlock(network::Packet& packet, UpdateBloc
|
|||
bool hasSpline = (moveFlags & TurtleMoveFlags::SPLINE_CLASSIC) ||
|
||||
(moveFlags & TurtleMoveFlags::SPLINE_TBC);
|
||||
if (hasSpline) {
|
||||
if (rem() < 4) return false;
|
||||
uint32_t splineFlags = packet.readUInt32();
|
||||
LOG_DEBUG(" [Turtle] Spline: flags=0x", std::hex, splineFlags, std::dec);
|
||||
|
||||
if (splineFlags & 0x00010000) {
|
||||
if (rem() < 12) return false;
|
||||
packet.readFloat(); packet.readFloat(); packet.readFloat();
|
||||
} else if (splineFlags & 0x00020000) {
|
||||
if (rem() < 8) return false;
|
||||
packet.readUInt64();
|
||||
} else if (splineFlags & 0x00040000) {
|
||||
if (rem() < 4) return false;
|
||||
packet.readFloat();
|
||||
}
|
||||
|
||||
// timePassed + duration + splineId + pointCount = 16 bytes
|
||||
if (rem() < 16) return false;
|
||||
/*uint32_t timePassed =*/ packet.readUInt32();
|
||||
/*uint32_t duration =*/ packet.readUInt32();
|
||||
/*uint32_t splineId =*/ packet.readUInt32();
|
||||
|
|
@ -2044,12 +2018,10 @@ bool TurtlePacketParsers::parseMovementBlock(network::Packet& packet, UpdateBloc
|
|||
++badTurtleSplineCount;
|
||||
if (badTurtleSplineCount <= 5 || (badTurtleSplineCount % 100) == 0) {
|
||||
LOG_WARNING(" [Turtle] Spline pointCount=", pointCount,
|
||||
" exceeds max (occurrence=", badTurtleSplineCount, ")");
|
||||
" exceeds max, capping (occurrence=", badTurtleSplineCount, ")");
|
||||
}
|
||||
return false;
|
||||
pointCount = 0;
|
||||
}
|
||||
// points + endPoint
|
||||
if (rem() < static_cast<size_t>(pointCount) * 12 + 12) return false;
|
||||
for (uint32_t i = 0; i < pointCount; i++) {
|
||||
packet.readFloat(); packet.readFloat(); packet.readFloat();
|
||||
}
|
||||
|
|
@ -2062,7 +2034,6 @@ bool TurtlePacketParsers::parseMovementBlock(network::Packet& packet, UpdateBloc
|
|||
" bytes, readPos now=", packet.getReadPos());
|
||||
}
|
||||
else if (updateFlags & UPDATEFLAG_HAS_POSITION) {
|
||||
if (rem() < 16) return false;
|
||||
block.x = packet.readFloat();
|
||||
block.y = packet.readFloat();
|
||||
block.z = packet.readFloat();
|
||||
|
|
@ -2074,22 +2045,18 @@ bool TurtlePacketParsers::parseMovementBlock(network::Packet& packet, UpdateBloc
|
|||
|
||||
// High GUID — 1×u32
|
||||
if (updateFlags & UPDATEFLAG_HIGHGUID) {
|
||||
if (rem() < 4) return false;
|
||||
/*uint32_t highGuid =*/ packet.readUInt32();
|
||||
}
|
||||
|
||||
if (updateFlags & UPDATEFLAG_ALL) {
|
||||
if (rem() < 4) return false;
|
||||
/*uint32_t unkAll =*/ packet.readUInt32();
|
||||
}
|
||||
|
||||
if (updateFlags & UPDATEFLAG_MELEE_ATTACKING) {
|
||||
if (rem() < 1) return false;
|
||||
/*uint64_t meleeTargetGuid =*/ UpdateObjectParser::readPackedGuid(packet);
|
||||
}
|
||||
|
||||
if (updateFlags & UPDATEFLAG_TRANSPORT) {
|
||||
if (rem() < 4) return false;
|
||||
/*uint32_t transportTime =*/ packet.readUInt32();
|
||||
}
|
||||
|
||||
|
|
@ -2218,10 +2185,12 @@ bool TurtlePacketParsers::parseUpdateObject(network::Packet& packet, UpdateObjec
|
|||
return this->TbcPacketParsers::parseMovementBlock(p, b);
|
||||
}, "tbc");
|
||||
}
|
||||
// NOTE: Do NOT fall back to WotLK parseMovementBlock here.
|
||||
// WotLK uses uint16 updateFlags and 9 speeds vs Classic's uint8
|
||||
// and 6 speeds. A false-positive WotLK parse consumes wrong bytes,
|
||||
// corrupting subsequent update fields and losing NPC data.
|
||||
if (!ok) {
|
||||
ok = parseMovementVariant(
|
||||
[](network::Packet& p, UpdateBlock& b) {
|
||||
return UpdateObjectParser::parseMovementBlock(p, b);
|
||||
}, "wotlk");
|
||||
}
|
||||
break;
|
||||
case UpdateType::OUT_OF_RANGE_OBJECTS:
|
||||
case UpdateType::NEAR_OBJECTS:
|
||||
|
|
|
|||
|
|
@ -30,8 +30,11 @@ namespace TbcMoveFlags {
|
|||
// - Flag 0x08 (HIGH_GUID) reads 2 u32s (Classic: 1 u32)
|
||||
// ============================================================================
|
||||
bool TbcPacketParsers::parseMovementBlock(network::Packet& packet, UpdateBlock& block) {
|
||||
auto rem = [&]() -> size_t { return packet.getSize() - packet.getReadPos(); };
|
||||
if (rem() < 1) return false;
|
||||
// Validate minimum packet size for updateFlags byte
|
||||
if (packet.getReadPos() >= packet.getSize()) {
|
||||
LOG_WARNING("[TBC] Movement block packet too small (need at least 1 byte for updateFlags)");
|
||||
return false;
|
||||
}
|
||||
|
||||
// TBC 2.4.3: UpdateFlags is uint8 (1 byte)
|
||||
uint8_t updateFlags = packet.readUInt8();
|
||||
|
|
@ -55,9 +58,6 @@ bool TbcPacketParsers::parseMovementBlock(network::Packet& packet, UpdateBlock&
|
|||
const uint8_t UPDATEFLAG_HIGHGUID = 0x10;
|
||||
|
||||
if (updateFlags & UPDATEFLAG_LIVING) {
|
||||
// Minimum: moveFlags(4)+moveFlags2(1)+time(4)+position(16)+fallTime(4)+speeds(32) = 61
|
||||
if (rem() < 61) return false;
|
||||
|
||||
// Full movement block for living units
|
||||
uint32_t moveFlags = packet.readUInt32();
|
||||
uint8_t moveFlags2 = packet.readUInt8(); // TBC: uint8, not uint16
|
||||
|
|
@ -76,33 +76,29 @@ bool TbcPacketParsers::parseMovementBlock(network::Packet& packet, UpdateBlock&
|
|||
|
||||
// Transport data
|
||||
if (moveFlags & TbcMoveFlags::ON_TRANSPORT) {
|
||||
if (rem() < 1) return false;
|
||||
block.onTransport = true;
|
||||
block.transportGuid = UpdateObjectParser::readPackedGuid(packet);
|
||||
if (rem() < 20) return false; // 4 floats + 1 uint32
|
||||
block.transportX = packet.readFloat();
|
||||
block.transportY = packet.readFloat();
|
||||
block.transportZ = packet.readFloat();
|
||||
block.transportO = packet.readFloat();
|
||||
/*uint32_t tTime =*/ packet.readUInt32();
|
||||
// TBC: NO transport seat byte
|
||||
// TBC: NO interpolated movement check
|
||||
}
|
||||
|
||||
// Pitch: SWIMMING, or else ONTRANSPORT (TBC-specific secondary pitch)
|
||||
if (moveFlags & TbcMoveFlags::SWIMMING) {
|
||||
if (rem() < 4) return false;
|
||||
/*float pitch =*/ packet.readFloat();
|
||||
} else if (moveFlags & TbcMoveFlags::ONTRANSPORT) {
|
||||
if (rem() < 4) return false;
|
||||
/*float pitch =*/ packet.readFloat();
|
||||
}
|
||||
|
||||
// Fall time (always present)
|
||||
if (rem() < 4) return false;
|
||||
/*uint32_t fallTime =*/ packet.readUInt32();
|
||||
|
||||
// Jumping (TBC: JUMPING=0x2000, WotLK: FALLING=0x1000)
|
||||
if (moveFlags & TbcMoveFlags::JUMPING) {
|
||||
if (rem() < 16) return false;
|
||||
/*float jumpVelocity =*/ packet.readFloat();
|
||||
/*float jumpSinAngle =*/ packet.readFloat();
|
||||
/*float jumpCosAngle =*/ packet.readFloat();
|
||||
|
|
@ -111,12 +107,11 @@ bool TbcPacketParsers::parseMovementBlock(network::Packet& packet, UpdateBlock&
|
|||
|
||||
// Spline elevation (TBC: 0x02000000, WotLK: 0x04000000)
|
||||
if (moveFlags & TbcMoveFlags::SPLINE_ELEVATION) {
|
||||
if (rem() < 4) return false;
|
||||
/*float splineElevation =*/ packet.readFloat();
|
||||
}
|
||||
|
||||
// Speeds (TBC: 8 values — walk, run, runBack, swim, fly, flyBack, swimBack, turn)
|
||||
if (rem() < 32) return false;
|
||||
// WotLK adds pitchRate (9 total)
|
||||
/*float walkSpeed =*/ packet.readFloat();
|
||||
float runSpeed = packet.readFloat();
|
||||
/*float runBackSpeed =*/ packet.readFloat();
|
||||
|
|
@ -131,47 +126,49 @@ bool TbcPacketParsers::parseMovementBlock(network::Packet& packet, UpdateBlock&
|
|||
|
||||
// Spline data (TBC/WotLK: SPLINE_ENABLED = 0x08000000)
|
||||
if (moveFlags & TbcMoveFlags::SPLINE_ENABLED) {
|
||||
if (rem() < 4) return false;
|
||||
uint32_t splineFlags = packet.readUInt32();
|
||||
LOG_DEBUG(" [TBC] Spline: flags=0x", std::hex, splineFlags, std::dec);
|
||||
|
||||
if (splineFlags & 0x00010000) { // FINAL_POINT
|
||||
if (rem() < 12) return false;
|
||||
/*float finalX =*/ packet.readFloat();
|
||||
/*float finalY =*/ packet.readFloat();
|
||||
/*float finalZ =*/ packet.readFloat();
|
||||
} else if (splineFlags & 0x00020000) { // FINAL_TARGET
|
||||
if (rem() < 8) return false;
|
||||
/*uint64_t finalTarget =*/ packet.readUInt64();
|
||||
} else if (splineFlags & 0x00040000) { // FINAL_ANGLE
|
||||
if (rem() < 4) return false;
|
||||
/*float finalAngle =*/ packet.readFloat();
|
||||
}
|
||||
|
||||
// TBC spline: timePassed, duration, id, pointCount
|
||||
if (rem() < 16) return false;
|
||||
// TBC spline: timePassed, duration, id, nodes, finalNode
|
||||
// (no durationMod, durationModNext, verticalAccel, effectStartTime, splineMode)
|
||||
/*uint32_t timePassed =*/ packet.readUInt32();
|
||||
/*uint32_t duration =*/ packet.readUInt32();
|
||||
/*uint32_t splineId =*/ packet.readUInt32();
|
||||
|
||||
uint32_t pointCount = packet.readUInt32();
|
||||
if (pointCount > 256) return false;
|
||||
|
||||
// points + endPoint (no splineMode in TBC)
|
||||
if (rem() < static_cast<size_t>(pointCount) * 12 + 12) return false;
|
||||
if (pointCount > 256) {
|
||||
static uint32_t badTbcSplineCount = 0;
|
||||
++badTbcSplineCount;
|
||||
if (badTbcSplineCount <= 5 || (badTbcSplineCount % 100) == 0) {
|
||||
LOG_WARNING(" [TBC] Spline pointCount=", pointCount,
|
||||
" exceeds max, capping (occurrence=", badTbcSplineCount, ")");
|
||||
}
|
||||
pointCount = 0;
|
||||
}
|
||||
for (uint32_t i = 0; i < pointCount; i++) {
|
||||
/*float px =*/ packet.readFloat();
|
||||
/*float py =*/ packet.readFloat();
|
||||
/*float pz =*/ packet.readFloat();
|
||||
}
|
||||
|
||||
// TBC: NO splineMode byte (WotLK adds it)
|
||||
/*float endPointX =*/ packet.readFloat();
|
||||
/*float endPointY =*/ packet.readFloat();
|
||||
/*float endPointZ =*/ packet.readFloat();
|
||||
}
|
||||
}
|
||||
else if (updateFlags & UPDATEFLAG_HAS_POSITION) {
|
||||
if (rem() < 16) return false;
|
||||
// TBC: Simple stationary position (same as WotLK STATIONARY)
|
||||
block.x = packet.readFloat();
|
||||
block.y = packet.readFloat();
|
||||
block.z = packet.readFloat();
|
||||
|
|
@ -180,29 +177,29 @@ bool TbcPacketParsers::parseMovementBlock(network::Packet& packet, UpdateBlock&
|
|||
|
||||
LOG_DEBUG(" [TBC] STATIONARY: (", block.x, ", ", block.y, ", ", block.z, ")");
|
||||
}
|
||||
// TBC: No UPDATEFLAG_POSITION (0x0100) code path
|
||||
|
||||
// Target GUID
|
||||
if (updateFlags & UPDATEFLAG_HAS_TARGET) {
|
||||
if (rem() < 1) return false;
|
||||
/*uint64_t targetGuid =*/ UpdateObjectParser::readPackedGuid(packet);
|
||||
}
|
||||
|
||||
// Transport time
|
||||
if (updateFlags & UPDATEFLAG_TRANSPORT) {
|
||||
if (rem() < 4) return false;
|
||||
/*uint32_t transportTime =*/ packet.readUInt32();
|
||||
}
|
||||
|
||||
// LOWGUID (0x08) — TBC has 2 u32s, Classic has 1 u32
|
||||
// TBC: No VEHICLE flag (WotLK 0x0080)
|
||||
// TBC: No ROTATION flag (WotLK 0x0200)
|
||||
|
||||
// HIGH_GUID (0x08) — TBC has 2 u32s, Classic has 1 u32
|
||||
if (updateFlags & UPDATEFLAG_LOWGUID) {
|
||||
if (rem() < 8) return false;
|
||||
/*uint32_t unknown0 =*/ packet.readUInt32();
|
||||
/*uint32_t unknown1 =*/ packet.readUInt32();
|
||||
}
|
||||
|
||||
// HIGHGUID (0x10)
|
||||
// ALL (0x10)
|
||||
if (updateFlags & UPDATEFLAG_HIGHGUID) {
|
||||
if (rem() < 4) return false;
|
||||
/*uint32_t unknown2 =*/ packet.readUInt32();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -913,9 +913,6 @@ bool UpdateObjectParser::parseMovementBlock(network::Packet& packet, UpdateBlock
|
|||
// 1. UpdateFlags (1 byte, sometimes 2)
|
||||
// 2. Movement data depends on update flags
|
||||
|
||||
auto rem = [&]() -> size_t { return packet.getSize() - packet.getReadPos(); };
|
||||
if (rem() < 2) return false;
|
||||
|
||||
// Update flags (3.3.5a uses 2 bytes for flags)
|
||||
uint16_t updateFlags = packet.readUInt16();
|
||||
block.updateFlags = updateFlags;
|
||||
|
|
@ -960,9 +957,6 @@ bool UpdateObjectParser::parseMovementBlock(network::Packet& packet, UpdateBlock
|
|||
const uint16_t UPDATEFLAG_HIGHGUID = 0x0010;
|
||||
|
||||
if (updateFlags & UPDATEFLAG_LIVING) {
|
||||
// Minimum: moveFlags(4)+moveFlags2(2)+time(4)+position(16)+fallTime(4)+speeds(36) = 66
|
||||
if (rem() < 66) return false;
|
||||
|
||||
// Full movement block for living units
|
||||
uint32_t moveFlags = packet.readUInt32();
|
||||
uint16_t moveFlags2 = packet.readUInt16();
|
||||
|
|
@ -980,10 +974,8 @@ bool UpdateObjectParser::parseMovementBlock(network::Packet& packet, UpdateBlock
|
|||
|
||||
// Transport data (if on transport)
|
||||
if (moveFlags & 0x00000200) { // MOVEMENTFLAG_ONTRANSPORT
|
||||
if (rem() < 1) return false;
|
||||
block.onTransport = true;
|
||||
block.transportGuid = readPackedGuid(packet);
|
||||
if (rem() < 21) return false; // 4 floats + uint32 + uint8
|
||||
block.transportX = packet.readFloat();
|
||||
block.transportY = packet.readFloat();
|
||||
block.transportZ = packet.readFloat();
|
||||
|
|
@ -995,7 +987,6 @@ bool UpdateObjectParser::parseMovementBlock(network::Packet& packet, UpdateBlock
|
|||
" offset=(", block.transportX, ", ", block.transportY, ", ", block.transportZ, ")");
|
||||
|
||||
if (moveFlags2 & 0x0200) { // MOVEMENTFLAG2_INTERPOLATED_MOVEMENT
|
||||
if (rem() < 4) return false;
|
||||
/*uint32_t tTime2 =*/ packet.readUInt32();
|
||||
}
|
||||
}
|
||||
|
|
@ -1014,17 +1005,14 @@ bool UpdateObjectParser::parseMovementBlock(network::Packet& packet, UpdateBlock
|
|||
if ((moveFlags & 0x00200000) /* SWIMMING */ ||
|
||||
(moveFlags & 0x01000000) /* FLYING */ ||
|
||||
(moveFlags2 & 0x0010) /* MOVEMENTFLAG2_ALWAYS_ALLOW_PITCHING */) {
|
||||
if (rem() < 4) return false;
|
||||
/*float pitch =*/ packet.readFloat();
|
||||
}
|
||||
|
||||
// Fall time
|
||||
if (rem() < 4) return false;
|
||||
/*uint32_t fallTime =*/ packet.readUInt32();
|
||||
|
||||
// Jumping
|
||||
if (moveFlags & 0x00001000) { // MOVEMENTFLAG_FALLING
|
||||
if (rem() < 16) return false;
|
||||
/*float jumpVelocity =*/ packet.readFloat();
|
||||
/*float jumpSinAngle =*/ packet.readFloat();
|
||||
/*float jumpCosAngle =*/ packet.readFloat();
|
||||
|
|
@ -1033,12 +1021,10 @@ bool UpdateObjectParser::parseMovementBlock(network::Packet& packet, UpdateBlock
|
|||
|
||||
// Spline elevation
|
||||
if (moveFlags & 0x04000000) { // MOVEMENTFLAG_SPLINE_ELEVATION
|
||||
if (rem() < 4) return false;
|
||||
/*float splineElevation =*/ packet.readFloat();
|
||||
}
|
||||
|
||||
// Speeds (9 values in WotLK: walk/run/runBack/swim/swimBack/flight/flightBack/turn/pitch)
|
||||
if (rem() < 36) return false;
|
||||
// Speeds (7 speed values)
|
||||
/*float walkSpeed =*/ packet.readFloat();
|
||||
float runSpeed = packet.readFloat();
|
||||
/*float runBackSpeed =*/ packet.readFloat();
|
||||
|
|
@ -1072,60 +1058,46 @@ bool UpdateObjectParser::parseMovementBlock(network::Packet& packet, UpdateBlock
|
|||
/*float finalAngle =*/ packet.readFloat();
|
||||
}
|
||||
|
||||
// Spline data layout varies by expansion:
|
||||
// Classic/Vanilla: timePassed(4)+duration(4)+splineId(4)+pointCount(4)+points+mode(1)+endPoint(12)
|
||||
// WotLK: timePassed(4)+duration(4)+splineId(4)+durationMod(4)+durationModNext(4)
|
||||
// +[ANIMATION(5)]+[PARABOLIC(8)]+pointCount(4)+points+mode(1)+endPoint(12)
|
||||
// Since the parser has no expansion context, auto-detect by trying Classic first.
|
||||
// Legacy UPDATE_OBJECT spline layout used by many servers:
|
||||
// timePassed, duration, splineId, durationMod, durationModNext,
|
||||
// [ANIMATION: animType(1)+animTime(4) if SPLINEFLAG_ANIMATION(0x00400000)],
|
||||
// verticalAccel, effectStartTime, pointCount, points, splineMode, endPoint.
|
||||
const size_t legacyStart = packet.getReadPos();
|
||||
if (!bytesAvailable(16)) return false; // minimum: 12 common + 4 pointCount
|
||||
if (!bytesAvailable(12 + 8 + 8 + 4)) return false;
|
||||
/*uint32_t timePassed =*/ packet.readUInt32();
|
||||
/*uint32_t duration =*/ packet.readUInt32();
|
||||
/*uint32_t splineId =*/ packet.readUInt32();
|
||||
const size_t afterSplineId = packet.getReadPos();
|
||||
|
||||
// Helper: try to parse uncompressed spline points from current read position.
|
||||
auto tryParseUncompressedSpline = [&](const char* tag) -> bool {
|
||||
if (!bytesAvailable(4)) return false;
|
||||
uint32_t pc = packet.readUInt32();
|
||||
if (pc > 256) return false;
|
||||
size_t needed = static_cast<size_t>(pc) * 12ull + 13ull;
|
||||
if (!bytesAvailable(needed)) return false;
|
||||
for (uint32_t i = 0; i < pc; i++) {
|
||||
packet.readFloat(); packet.readFloat(); packet.readFloat();
|
||||
}
|
||||
packet.readUInt8(); // splineMode
|
||||
packet.readFloat(); packet.readFloat(); packet.readFloat(); // endPoint
|
||||
LOG_DEBUG(" Spline pointCount=", pc, " (", tag, ")");
|
||||
return true;
|
||||
};
|
||||
|
||||
// --- Try 1: Classic format (pointCount immediately after splineId) ---
|
||||
bool splineParsed = tryParseUncompressedSpline("classic");
|
||||
|
||||
// --- Try 2: WotLK format (durationMod+durationModNext+conditional+pointCount) ---
|
||||
if (!splineParsed) {
|
||||
packet.setReadPos(afterSplineId);
|
||||
bool wotlkOk = bytesAvailable(8); // durationMod + durationModNext
|
||||
if (wotlkOk) {
|
||||
/*float durationMod =*/ packet.readFloat();
|
||||
/*float durationModNext =*/ packet.readFloat();
|
||||
if (splineFlags & 0x00400000) { // SPLINEFLAG_ANIMATION
|
||||
if (!bytesAvailable(5)) { wotlkOk = false; }
|
||||
else { packet.readUInt8(); packet.readUInt32(); }
|
||||
}
|
||||
}
|
||||
if (wotlkOk && (splineFlags & 0x00000800)) { // SPLINEFLAG_PARABOLIC
|
||||
if (!bytesAvailable(8)) { wotlkOk = false; }
|
||||
else { packet.readFloat(); packet.readUInt32(); }
|
||||
}
|
||||
if (wotlkOk) {
|
||||
splineParsed = tryParseUncompressedSpline("wotlk");
|
||||
}
|
||||
/*float durationMod =*/ packet.readFloat();
|
||||
/*float durationModNext =*/ packet.readFloat();
|
||||
// Animation flag inserts 5 bytes (uint8 type + int32 time) before verticalAccel
|
||||
if (splineFlags & 0x00400000) { // SPLINEFLAG_ANIMATION
|
||||
if (!bytesAvailable(5)) return false;
|
||||
packet.readUInt8(); // animationType
|
||||
packet.readUInt32(); // animTime
|
||||
}
|
||||
/*float verticalAccel =*/ packet.readFloat();
|
||||
/*uint32_t effectStartTime =*/ packet.readUInt32();
|
||||
uint32_t pointCount = packet.readUInt32();
|
||||
|
||||
// --- Try 3: Compact layout (compressed points) as final recovery ---
|
||||
if (!splineParsed) {
|
||||
const size_t remainingAfterCount = packet.getSize() - packet.getReadPos();
|
||||
const bool legacyCountLooksValid = (pointCount <= 256);
|
||||
const size_t legacyPointsBytes = static_cast<size_t>(pointCount) * 12ull;
|
||||
const bool legacyPayloadFits = (legacyPointsBytes + 13ull) <= remainingAfterCount;
|
||||
|
||||
if (legacyCountLooksValid && legacyPayloadFits) {
|
||||
for (uint32_t i = 0; i < pointCount; i++) {
|
||||
/*float px =*/ packet.readFloat();
|
||||
/*float py =*/ packet.readFloat();
|
||||
/*float pz =*/ packet.readFloat();
|
||||
}
|
||||
/*uint8_t splineMode =*/ packet.readUInt8();
|
||||
/*float endPointX =*/ packet.readFloat();
|
||||
/*float endPointY =*/ packet.readFloat();
|
||||
/*float endPointZ =*/ packet.readFloat();
|
||||
LOG_DEBUG(" Spline pointCount=", pointCount, " (legacy)");
|
||||
} else {
|
||||
// Legacy pointCount looks invalid; try compact WotLK layout as recovery.
|
||||
// This keeps malformed/variant packets from desyncing the whole update block.
|
||||
packet.setReadPos(legacyStart);
|
||||
const size_t afterFinalFacingPos = packet.getReadPos();
|
||||
if (splineFlags & 0x00400000) { // Animation
|
||||
|
|
@ -1146,7 +1118,8 @@ bool UpdateObjectParser::parseMovementBlock(network::Packet& packet, UpdateBlock
|
|||
static uint32_t badSplineCount = 0;
|
||||
++badSplineCount;
|
||||
if (badSplineCount <= 5 || (badSplineCount % 100) == 0) {
|
||||
LOG_WARNING(" Spline invalid (classic+wotlk+compact) at readPos=",
|
||||
LOG_WARNING(" Spline pointCount=", pointCount,
|
||||
" invalid (legacy+compact) at readPos=",
|
||||
afterFinalFacingPos, "/", packet.getSize(),
|
||||
", occurrence=", badSplineCount);
|
||||
}
|
||||
|
|
@ -1166,14 +1139,12 @@ bool UpdateObjectParser::parseMovementBlock(network::Packet& packet, UpdateBlock
|
|||
if (!bytesAvailable(compactPayloadBytes)) return false;
|
||||
packet.setReadPos(packet.getReadPos() + compactPayloadBytes);
|
||||
}
|
||||
} // end compact fallback
|
||||
} // end else (compact fallback)
|
||||
}
|
||||
}
|
||||
else if (updateFlags & UPDATEFLAG_POSITION) {
|
||||
// Transport position update (UPDATEFLAG_POSITION = 0x0100)
|
||||
if (rem() < 1) return false;
|
||||
uint64_t transportGuid = readPackedGuid(packet);
|
||||
if (rem() < 32) return false; // 8 floats
|
||||
block.x = packet.readFloat();
|
||||
block.y = packet.readFloat();
|
||||
block.z = packet.readFloat();
|
||||
|
|
@ -1202,7 +1173,7 @@ bool UpdateObjectParser::parseMovementBlock(network::Packet& packet, UpdateBlock
|
|||
}
|
||||
}
|
||||
else if (updateFlags & UPDATEFLAG_STATIONARY_POSITION) {
|
||||
if (rem() < 16) return false;
|
||||
// Simple stationary position (4 floats)
|
||||
block.x = packet.readFloat();
|
||||
block.y = packet.readFloat();
|
||||
block.z = packet.readFloat();
|
||||
|
|
@ -1214,38 +1185,32 @@ bool UpdateObjectParser::parseMovementBlock(network::Packet& packet, UpdateBlock
|
|||
|
||||
// Target GUID (for units with target)
|
||||
if (updateFlags & UPDATEFLAG_HAS_TARGET) {
|
||||
if (rem() < 1) return false;
|
||||
/*uint64_t targetGuid =*/ readPackedGuid(packet);
|
||||
}
|
||||
|
||||
// Transport time
|
||||
if (updateFlags & UPDATEFLAG_TRANSPORT) {
|
||||
if (rem() < 4) return false;
|
||||
/*uint32_t transportTime =*/ packet.readUInt32();
|
||||
}
|
||||
|
||||
// Vehicle
|
||||
if (updateFlags & UPDATEFLAG_VEHICLE) {
|
||||
if (rem() < 8) return false;
|
||||
/*uint32_t vehicleId =*/ packet.readUInt32();
|
||||
/*float vehicleOrientation =*/ packet.readFloat();
|
||||
}
|
||||
|
||||
// Rotation (GameObjects)
|
||||
if (updateFlags & UPDATEFLAG_ROTATION) {
|
||||
if (rem() < 8) return false;
|
||||
/*int64_t rotation =*/ packet.readUInt64();
|
||||
}
|
||||
|
||||
// Low GUID
|
||||
if (updateFlags & UPDATEFLAG_LOWGUID) {
|
||||
if (rem() < 4) return false;
|
||||
/*uint32_t lowGuid =*/ packet.readUInt32();
|
||||
}
|
||||
|
||||
// High GUID
|
||||
if (updateFlags & UPDATEFLAG_HIGHGUID) {
|
||||
if (rem() < 4) return false;
|
||||
/*uint32_t highGuid =*/ packet.readUInt32();
|
||||
}
|
||||
|
||||
|
|
@ -1255,8 +1220,6 @@ bool UpdateObjectParser::parseMovementBlock(network::Packet& packet, UpdateBlock
|
|||
bool UpdateObjectParser::parseUpdateFields(network::Packet& packet, UpdateBlock& block) {
|
||||
size_t startPos = packet.getReadPos();
|
||||
|
||||
if (packet.getReadPos() >= packet.getSize()) return false;
|
||||
|
||||
// Read number of blocks (each block is 32 fields = 32 bits)
|
||||
uint8_t blockCount = packet.readUInt8();
|
||||
|
||||
|
|
@ -1344,8 +1307,6 @@ bool UpdateObjectParser::parseUpdateFields(network::Packet& packet, UpdateBlock&
|
|||
}
|
||||
|
||||
bool UpdateObjectParser::parseUpdateBlock(network::Packet& packet, UpdateBlock& block) {
|
||||
if (packet.getReadPos() >= packet.getSize()) return false;
|
||||
|
||||
// Read update type
|
||||
uint8_t updateTypeVal = packet.readUInt8();
|
||||
block.updateType = static_cast<UpdateType>(updateTypeVal);
|
||||
|
|
@ -1355,7 +1316,6 @@ bool UpdateObjectParser::parseUpdateBlock(network::Packet& packet, UpdateBlock&
|
|||
switch (block.updateType) {
|
||||
case UpdateType::VALUES: {
|
||||
// Partial update - changed fields only
|
||||
if (packet.getReadPos() >= packet.getSize()) return false;
|
||||
block.guid = readPackedGuid(packet);
|
||||
LOG_DEBUG(" VALUES update for GUID: 0x", std::hex, block.guid, std::dec);
|
||||
|
||||
|
|
@ -1364,7 +1324,6 @@ bool UpdateObjectParser::parseUpdateBlock(network::Packet& packet, UpdateBlock&
|
|||
|
||||
case UpdateType::MOVEMENT: {
|
||||
// Movement update
|
||||
if (packet.getReadPos() + 8 > packet.getSize()) return false;
|
||||
block.guid = packet.readUInt64();
|
||||
LOG_DEBUG(" MOVEMENT update for GUID: 0x", std::hex, block.guid, std::dec);
|
||||
|
||||
|
|
@ -1374,12 +1333,10 @@ bool UpdateObjectParser::parseUpdateBlock(network::Packet& packet, UpdateBlock&
|
|||
case UpdateType::CREATE_OBJECT:
|
||||
case UpdateType::CREATE_OBJECT2: {
|
||||
// Create new object with full data
|
||||
if (packet.getReadPos() >= packet.getSize()) return false;
|
||||
block.guid = readPackedGuid(packet);
|
||||
LOG_DEBUG(" CREATE_OBJECT for GUID: 0x", std::hex, block.guid, std::dec);
|
||||
|
||||
// Read object type
|
||||
if (packet.getReadPos() >= packet.getSize()) return false;
|
||||
uint8_t objectTypeVal = packet.readUInt8();
|
||||
block.objectType = static_cast<ObjectType>(objectTypeVal);
|
||||
LOG_DEBUG(" Object type: ", (int)objectTypeVal);
|
||||
|
|
@ -3252,11 +3209,12 @@ bool MonsterMoveParser::parse(network::Packet& packet, MonsterMoveData& data) {
|
|||
|
||||
if (pointCount == 0) return true;
|
||||
|
||||
// Cap pointCount to prevent excessive iteration from malformed packets.
|
||||
constexpr uint32_t kMaxSplinePoints = 1000;
|
||||
if (pointCount > kMaxSplinePoints) {
|
||||
LOG_WARNING("SMSG_MONSTER_MOVE: pointCount=", pointCount, " exceeds max ", kMaxSplinePoints,
|
||||
" (guid=0x", std::hex, data.guid, std::dec, ")");
|
||||
return false;
|
||||
" (guid=0x", std::hex, data.guid, std::dec, "), capping");
|
||||
pointCount = kMaxSplinePoints;
|
||||
}
|
||||
|
||||
// Catmullrom or Flying → all waypoints stored as absolute float3 (uncompressed).
|
||||
|
|
@ -3907,13 +3865,13 @@ bool SpellGoParser::parse(network::Packet& packet, SpellGoData& data) {
|
|||
|
||||
data.hitTargets.reserve(storedHitLimit);
|
||||
for (uint16_t i = 0; i < rawHitCount; ++i) {
|
||||
// WotLK 3.3.5a hit targets are full uint64 GUIDs (not PackedGuid).
|
||||
if (packet.getSize() - packet.getReadPos() < 8) {
|
||||
// WotLK hit targets are packed GUIDs, like the caster and miss targets.
|
||||
if (!hasFullPackedGuid(packet)) {
|
||||
LOG_WARNING("Spell go: truncated hit targets at index ", i, "/", (int)rawHitCount);
|
||||
truncatedTargets = true;
|
||||
break;
|
||||
}
|
||||
const uint64_t targetGuid = packet.readUInt64();
|
||||
const uint64_t targetGuid = UpdateObjectParser::readPackedGuid(packet);
|
||||
if (i < storedHitLimit) {
|
||||
data.hitTargets.push_back(targetGuid);
|
||||
}
|
||||
|
|
@ -3931,27 +3889,7 @@ bool SpellGoParser::parse(network::Packet& packet, SpellGoData& data) {
|
|||
return false;
|
||||
}
|
||||
|
||||
const size_t missCountPos = packet.getReadPos();
|
||||
const uint8_t rawMissCount = packet.readUInt8();
|
||||
if (rawMissCount > 20) {
|
||||
// Likely offset error — dump context bytes for diagnostics.
|
||||
const auto& raw = packet.getData();
|
||||
std::string hexCtx;
|
||||
size_t dumpStart = (missCountPos >= 8) ? missCountPos - 8 : startPos;
|
||||
size_t dumpEnd = std::min(missCountPos + 16, raw.size());
|
||||
for (size_t i = dumpStart; i < dumpEnd; ++i) {
|
||||
char buf[4];
|
||||
std::snprintf(buf, sizeof(buf), "%02x ", raw[i]);
|
||||
hexCtx += buf;
|
||||
if (i == missCountPos - 1) hexCtx += "[";
|
||||
if (i == missCountPos) hexCtx += "] ";
|
||||
}
|
||||
LOG_WARNING("Spell go: suspect missCount=", (int)rawMissCount,
|
||||
" spell=", data.spellId, " hits=", (int)data.hitCount,
|
||||
" castFlags=0x", std::hex, data.castFlags, std::dec,
|
||||
" missCountPos=", missCountPos, " pktSize=", packet.getSize(),
|
||||
" ctx=", hexCtx);
|
||||
}
|
||||
if (rawMissCount > 128) {
|
||||
LOG_WARNING("Spell go: missCount capped (requested=", (int)rawMissCount,
|
||||
") spell=", data.spellId, " hits=", (int)data.hitCount,
|
||||
|
|
@ -3961,16 +3899,22 @@ bool SpellGoParser::parse(network::Packet& packet, SpellGoData& data) {
|
|||
|
||||
data.missTargets.reserve(storedMissLimit);
|
||||
for (uint16_t i = 0; i < rawMissCount; ++i) {
|
||||
// WotLK 3.3.5a miss targets are full uint64 GUIDs + uint8 missType.
|
||||
// Each miss entry: packed GUID(1-8 bytes) + missType(1 byte).
|
||||
// REFLECT additionally appends uint8 reflectResult.
|
||||
if (packet.getSize() - packet.getReadPos() < 9) { // 8 GUID + 1 missType
|
||||
if (!hasFullPackedGuid(packet)) {
|
||||
LOG_WARNING("Spell go: truncated miss targets at index ", i, "/", (int)rawMissCount,
|
||||
" spell=", data.spellId, " hits=", (int)data.hitCount);
|
||||
truncatedTargets = true;
|
||||
break;
|
||||
}
|
||||
SpellGoMissEntry m;
|
||||
m.targetGuid = packet.readUInt64();
|
||||
m.targetGuid = UpdateObjectParser::readPackedGuid(packet); // packed GUID in WotLK
|
||||
if (packet.getSize() - packet.getReadPos() < 1) {
|
||||
LOG_WARNING("Spell go: missing missType at miss index ", i, "/", (int)rawMissCount,
|
||||
" spell=", data.spellId);
|
||||
truncatedTargets = true;
|
||||
break;
|
||||
}
|
||||
m.missType = packet.readUInt8();
|
||||
if (m.missType == 11) { // SPELL_MISS_REFLECT
|
||||
if (packet.getSize() - packet.getReadPos() < 1) {
|
||||
|
|
@ -4358,17 +4302,6 @@ network::Packet SwapItemPacket::build(uint8_t dstBag, uint8_t dstSlot, uint8_t s
|
|||
return packet;
|
||||
}
|
||||
|
||||
network::Packet SplitItemPacket::build(uint8_t srcBag, uint8_t srcSlot,
|
||||
uint8_t dstBag, uint8_t dstSlot, uint8_t count) {
|
||||
network::Packet packet(wireOpcode(Opcode::CMSG_SPLIT_ITEM));
|
||||
packet.writeUInt8(srcBag);
|
||||
packet.writeUInt8(srcSlot);
|
||||
packet.writeUInt8(dstBag);
|
||||
packet.writeUInt8(dstSlot);
|
||||
packet.writeUInt8(count);
|
||||
return packet;
|
||||
}
|
||||
|
||||
network::Packet SwapInvItemPacket::build(uint8_t srcSlot, uint8_t dstSlot) {
|
||||
network::Packet packet(wireOpcode(Opcode::CMSG_SWAP_INV_ITEM));
|
||||
packet.writeUInt8(srcSlot);
|
||||
|
|
@ -5878,14 +5811,5 @@ network::Packet SetTitlePacket::build(int32_t titleBit) {
|
|||
return p;
|
||||
}
|
||||
|
||||
network::Packet AlterAppearancePacket::build(uint32_t hairStyle, uint32_t hairColor, uint32_t facialHair) {
|
||||
// CMSG_ALTER_APPEARANCE: uint32 hairStyle + uint32 hairColor + uint32 facialHair
|
||||
network::Packet p(wireOpcode(Opcode::CMSG_ALTER_APPEARANCE));
|
||||
p.writeUInt32(hairStyle);
|
||||
p.writeUInt32(hairColor);
|
||||
p.writeUInt32(facialHair);
|
||||
return p;
|
||||
}
|
||||
|
||||
} // namespace game
|
||||
} // namespace wowee
|
||||
|
|
|
|||
|
|
@ -273,9 +273,8 @@ void CameraController::update(float deltaTime) {
|
|||
keyW = keyS = keyA = keyD = keyQ = keyE = nowJump = false;
|
||||
}
|
||||
|
||||
// Tilde or NumLock toggles auto-run; any forward/backward key cancels it
|
||||
bool tildeDown = !uiWantsKeyboard && (input.isKeyPressed(SDL_SCANCODE_GRAVE) ||
|
||||
input.isKeyPressed(SDL_SCANCODE_NUMLOCKCLEAR));
|
||||
// Tilde toggles auto-run; any forward/backward key cancels it
|
||||
bool tildeDown = !uiWantsKeyboard && input.isKeyPressed(SDL_SCANCODE_GRAVE);
|
||||
if (tildeDown && !tildeWasDown) {
|
||||
autoRunning = !autoRunning;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1753,7 +1753,6 @@ uint32_t M2Renderer::createInstance(uint32_t modelId, const glm::vec3& position,
|
|||
return 0;
|
||||
}
|
||||
const auto& mdlRef = modelIt->second;
|
||||
modelUnusedSince_.erase(modelId);
|
||||
|
||||
// Deduplicate: skip if same model already at nearly the same position.
|
||||
// Uses hash map for O(1) lookup instead of O(N) scan.
|
||||
|
|
@ -1865,7 +1864,6 @@ uint32_t M2Renderer::createInstanceWithMatrix(uint32_t modelId, const glm::mat4&
|
|||
LOG_WARNING("Cannot create instance: model ", modelId, " not loaded");
|
||||
return 0;
|
||||
}
|
||||
modelUnusedSince_.erase(modelId);
|
||||
|
||||
// Deduplicate: O(1) hash lookup
|
||||
{
|
||||
|
|
@ -4278,28 +4276,11 @@ void M2Renderer::cleanupUnusedModels() {
|
|||
usedModelIds.insert(instance.modelId);
|
||||
}
|
||||
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
constexpr auto kGracePeriod = std::chrono::seconds(60);
|
||||
|
||||
// Find models with no instances that have exceeded the grace period.
|
||||
// Models that just lost their last instance get tracked but not evicted
|
||||
// immediately — this prevents thrashing when GO models are briefly
|
||||
// instance-free between despawn and respawn cycles.
|
||||
// Find and remove models with no instances
|
||||
std::vector<uint32_t> toRemove;
|
||||
for (const auto& [id, model] : models) {
|
||||
if (usedModelIds.find(id) != usedModelIds.end()) {
|
||||
// Model still in use — clear any pending unused timestamp
|
||||
modelUnusedSince_.erase(id);
|
||||
continue;
|
||||
}
|
||||
auto unusedIt = modelUnusedSince_.find(id);
|
||||
if (unusedIt == modelUnusedSince_.end()) {
|
||||
// First cycle with no instances — start the grace timer
|
||||
modelUnusedSince_[id] = now;
|
||||
} else if (now - unusedIt->second >= kGracePeriod) {
|
||||
// Grace period expired — mark for removal
|
||||
if (usedModelIds.find(id) == usedModelIds.end()) {
|
||||
toRemove.push_back(id);
|
||||
modelUnusedSince_.erase(unusedIt);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -67,10 +67,6 @@
|
|||
#include <cctype>
|
||||
#include <cmath>
|
||||
#include <chrono>
|
||||
#include <filesystem>
|
||||
|
||||
#define STB_IMAGE_WRITE_IMPLEMENTATION
|
||||
#include "stb_image_write.h"
|
||||
#include <cstdlib>
|
||||
#include <optional>
|
||||
#include <unordered_map>
|
||||
|
|
@ -2578,101 +2574,6 @@ void Renderer::cancelEmote() {
|
|||
emoteLoop = false;
|
||||
}
|
||||
|
||||
bool Renderer::captureScreenshot(const std::string& outputPath) {
|
||||
if (!vkCtx) return false;
|
||||
|
||||
VkDevice device = vkCtx->getDevice();
|
||||
VmaAllocator alloc = vkCtx->getAllocator();
|
||||
VkExtent2D extent = vkCtx->getSwapchainExtent();
|
||||
const auto& images = vkCtx->getSwapchainImages();
|
||||
|
||||
if (images.empty() || currentImageIndex >= images.size()) return false;
|
||||
|
||||
VkImage srcImage = images[currentImageIndex];
|
||||
uint32_t w = extent.width;
|
||||
uint32_t h = extent.height;
|
||||
VkDeviceSize bufSize = static_cast<VkDeviceSize>(w) * h * 4;
|
||||
|
||||
// Stall GPU so the swapchain image is idle
|
||||
vkDeviceWaitIdle(device);
|
||||
|
||||
// Create staging buffer
|
||||
VkBufferCreateInfo bufInfo{VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO};
|
||||
bufInfo.size = bufSize;
|
||||
bufInfo.usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT;
|
||||
|
||||
VmaAllocationCreateInfo allocCI{};
|
||||
allocCI.usage = VMA_MEMORY_USAGE_CPU_ONLY;
|
||||
|
||||
VkBuffer stagingBuf = VK_NULL_HANDLE;
|
||||
VmaAllocation stagingAlloc = VK_NULL_HANDLE;
|
||||
if (vmaCreateBuffer(alloc, &bufInfo, &allocCI, &stagingBuf, &stagingAlloc, nullptr) != VK_SUCCESS) {
|
||||
LOG_WARNING("Screenshot: failed to create staging buffer");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Record copy commands
|
||||
VkCommandBuffer cmd = vkCtx->beginSingleTimeCommands();
|
||||
|
||||
// Transition swapchain image: PRESENT_SRC → TRANSFER_SRC
|
||||
VkImageMemoryBarrier toTransfer{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER};
|
||||
toTransfer.srcAccessMask = VK_ACCESS_MEMORY_READ_BIT;
|
||||
toTransfer.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT;
|
||||
toTransfer.oldLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR;
|
||||
toTransfer.newLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL;
|
||||
toTransfer.image = srcImage;
|
||||
toTransfer.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
|
||||
vkCmdPipelineBarrier(cmd,
|
||||
VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT,
|
||||
0, 0, nullptr, 0, nullptr, 1, &toTransfer);
|
||||
|
||||
// Copy image to buffer
|
||||
VkBufferImageCopy region{};
|
||||
region.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1};
|
||||
region.imageExtent = {w, h, 1};
|
||||
vkCmdCopyImageToBuffer(cmd, srcImage, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
|
||||
stagingBuf, 1, ®ion);
|
||||
|
||||
// Transition back: TRANSFER_SRC → PRESENT_SRC
|
||||
VkImageMemoryBarrier toPresent = toTransfer;
|
||||
toPresent.srcAccessMask = VK_ACCESS_TRANSFER_READ_BIT;
|
||||
toPresent.dstAccessMask = VK_ACCESS_MEMORY_READ_BIT;
|
||||
toPresent.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL;
|
||||
toPresent.newLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR;
|
||||
vkCmdPipelineBarrier(cmd,
|
||||
VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT,
|
||||
0, 0, nullptr, 0, nullptr, 1, &toPresent);
|
||||
|
||||
vkCtx->endSingleTimeCommands(cmd);
|
||||
|
||||
// Map and convert BGRA → RGBA
|
||||
void* mapped = nullptr;
|
||||
vmaMapMemory(alloc, stagingAlloc, &mapped);
|
||||
auto* pixels = static_cast<uint8_t*>(mapped);
|
||||
for (uint32_t i = 0; i < w * h; ++i) {
|
||||
std::swap(pixels[i * 4 + 0], pixels[i * 4 + 2]); // B ↔ R
|
||||
}
|
||||
|
||||
// Ensure output directory exists
|
||||
std::filesystem::path outPath(outputPath);
|
||||
if (outPath.has_parent_path())
|
||||
std::filesystem::create_directories(outPath.parent_path());
|
||||
|
||||
int ok = stbi_write_png(outputPath.c_str(),
|
||||
static_cast<int>(w), static_cast<int>(h),
|
||||
4, pixels, static_cast<int>(w * 4));
|
||||
|
||||
vmaUnmapMemory(alloc, stagingAlloc);
|
||||
vmaDestroyBuffer(alloc, stagingBuf, stagingAlloc);
|
||||
|
||||
if (ok) {
|
||||
LOG_INFO("Screenshot saved: ", outputPath);
|
||||
} else {
|
||||
LOG_WARNING("Screenshot: stbi_write_png failed for ", outputPath);
|
||||
}
|
||||
return ok != 0;
|
||||
}
|
||||
|
||||
void Renderer::triggerLevelUpEffect(const glm::vec3& position) {
|
||||
if (!levelUpEffect) return;
|
||||
|
||||
|
|
|
|||
|
|
@ -206,8 +206,8 @@ void AuthScreen::render(auth::AuthHandler& authHandler) {
|
|||
}
|
||||
}
|
||||
}
|
||||
// Login screen music
|
||||
if (renderer) {
|
||||
// Login screen music disabled
|
||||
if (false && renderer) {
|
||||
auto* music = renderer->getMusicManager();
|
||||
if (music) {
|
||||
if (!loginMusicVolumeAdjusted_) {
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -871,35 +871,6 @@ void InventoryScreen::render(game::Inventory& inventory, uint64_t moneyCopper) {
|
|||
ImGui::EndPopup();
|
||||
}
|
||||
|
||||
// Stack split popup
|
||||
if (splitConfirmOpen_) {
|
||||
ImVec2 mousePos = ImGui::GetIO().MousePos;
|
||||
ImGui::SetNextWindowPos(ImVec2(mousePos.x - 80.0f, mousePos.y - 20.0f), ImGuiCond_Always);
|
||||
ImGui::OpenPopup("##SplitStack");
|
||||
splitConfirmOpen_ = false;
|
||||
}
|
||||
if (ImGui::BeginPopup("##SplitStack", ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoTitleBar)) {
|
||||
ImGui::Text("Split %s", splitItemName_.c_str());
|
||||
ImGui::Spacing();
|
||||
ImGui::SetNextItemWidth(120.0f);
|
||||
ImGui::SliderInt("##splitcount", &splitCount_, 1, splitMax_ - 1);
|
||||
ImGui::Spacing();
|
||||
if (ImGui::Button("OK", ImVec2(55, 0))) {
|
||||
if (gameHandler_ && splitCount_ > 0 && splitCount_ < splitMax_) {
|
||||
gameHandler_->splitItem(splitBag_, splitSlot_, static_cast<uint8_t>(splitCount_));
|
||||
}
|
||||
splitItemName_.clear();
|
||||
inventoryDirty = true;
|
||||
ImGui::CloseCurrentPopup();
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Cancel", ImVec2(55, 0))) {
|
||||
splitItemName_.clear();
|
||||
ImGui::CloseCurrentPopup();
|
||||
}
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
|
||||
// Draw held item at cursor
|
||||
renderHeldItem();
|
||||
}
|
||||
|
|
@ -2331,39 +2302,22 @@ void InventoryScreen::renderItemSlot(game::Inventory& inventory, const game::Ite
|
|||
}
|
||||
}
|
||||
|
||||
// Shift+right-click: split stack (if stackable >1) or destroy confirmation
|
||||
// Shift+right-click: open destroy confirmation for non-quest items
|
||||
if (ImGui::IsItemHovered() && ImGui::IsMouseClicked(ImGuiMouseButton_Right) &&
|
||||
!holdingItem && ImGui::GetIO().KeyShift && item.itemId != 0) {
|
||||
if (item.stackCount > 1 && item.maxStack > 1) {
|
||||
// Open split popup for stackable items
|
||||
splitConfirmOpen_ = true;
|
||||
splitItemName_ = item.name;
|
||||
splitMax_ = static_cast<int>(item.stackCount);
|
||||
splitCount_ = splitMax_ / 2;
|
||||
if (splitCount_ < 1) splitCount_ = 1;
|
||||
if (kind == SlotKind::BACKPACK && backpackIndex >= 0) {
|
||||
splitBag_ = 0xFF;
|
||||
splitSlot_ = static_cast<uint8_t>(23 + backpackIndex);
|
||||
} else if (kind == SlotKind::BACKPACK && isBagSlot) {
|
||||
splitBag_ = static_cast<uint8_t>(19 + bagIndex);
|
||||
splitSlot_ = static_cast<uint8_t>(bagSlotIndex);
|
||||
}
|
||||
} else if (item.bindType != 4) {
|
||||
// Destroy confirmation for non-quest, non-stackable items
|
||||
destroyConfirmOpen_ = true;
|
||||
destroyItemName_ = item.name;
|
||||
destroyCount_ = static_cast<uint8_t>(std::clamp<uint32_t>(
|
||||
std::max<uint32_t>(1u, item.stackCount), 1u, 255u));
|
||||
if (kind == SlotKind::BACKPACK && backpackIndex >= 0) {
|
||||
destroyBag_ = 0xFF;
|
||||
destroySlot_ = static_cast<uint8_t>(23 + backpackIndex);
|
||||
} else if (kind == SlotKind::BACKPACK && isBagSlot) {
|
||||
destroyBag_ = static_cast<uint8_t>(19 + bagIndex);
|
||||
destroySlot_ = static_cast<uint8_t>(bagSlotIndex);
|
||||
} else if (kind == SlotKind::EQUIPMENT) {
|
||||
destroyBag_ = 0xFF;
|
||||
destroySlot_ = static_cast<uint8_t>(equipSlot);
|
||||
}
|
||||
!holdingItem && ImGui::GetIO().KeyShift && item.itemId != 0 && item.bindType != 4) {
|
||||
destroyConfirmOpen_ = true;
|
||||
destroyItemName_ = item.name;
|
||||
destroyCount_ = static_cast<uint8_t>(std::clamp<uint32_t>(
|
||||
std::max<uint32_t>(1u, item.stackCount), 1u, 255u));
|
||||
if (kind == SlotKind::BACKPACK && backpackIndex >= 0) {
|
||||
destroyBag_ = 0xFF;
|
||||
destroySlot_ = static_cast<uint8_t>(23 + backpackIndex);
|
||||
} else if (kind == SlotKind::BACKPACK && isBagSlot) {
|
||||
destroyBag_ = static_cast<uint8_t>(19 + bagIndex);
|
||||
destroySlot_ = static_cast<uint8_t>(bagSlotIndex);
|
||||
} else if (kind == SlotKind::EQUIPMENT) {
|
||||
destroyBag_ = 0xFF;
|
||||
destroySlot_ = static_cast<uint8_t>(equipSlot);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -82,14 +82,6 @@ std::string replaceGenderPlaceholders(const std::string& text, game::GameHandler
|
|||
pos += replacement.length();
|
||||
}
|
||||
|
||||
// Resolve class and race names for $C and $R placeholders
|
||||
std::string className = "Adventurer";
|
||||
std::string raceName = "Unknown";
|
||||
if (character) {
|
||||
className = game::getClassName(character->characterClass);
|
||||
raceName = game::getRaceName(character->race);
|
||||
}
|
||||
|
||||
// Replace simple placeholders
|
||||
pos = 0;
|
||||
while ((pos = result.find('$', pos)) != std::string::npos) {
|
||||
|
|
@ -100,12 +92,11 @@ std::string replaceGenderPlaceholders(const std::string& text, game::GameHandler
|
|||
|
||||
switch (code) {
|
||||
case 'n': case 'N': replacement = playerName; break;
|
||||
case 'c': case 'C': replacement = className; break;
|
||||
case 'r': case 'R': replacement = raceName; break;
|
||||
case 'p': replacement = pronouns.subject; break;
|
||||
case 'o': replacement = pronouns.object; break;
|
||||
case 's': replacement = pronouns.possessive; break;
|
||||
case 'S': replacement = pronouns.possessiveP; break;
|
||||
case 'r': replacement = pronouns.object; break;
|
||||
case 'b': case 'B': replacement = "\n"; break;
|
||||
case 'g': case 'G': pos++; continue;
|
||||
default: pos++; continue;
|
||||
|
|
|
|||
|
|
@ -176,29 +176,6 @@ void TalentScreen::renderTalentTrees(game::GameHandler& gameHandler) {
|
|||
|
||||
ImGui::EndTabBar();
|
||||
}
|
||||
|
||||
// Talent learn confirmation popup
|
||||
if (talentConfirmOpen_) {
|
||||
ImGui::OpenPopup("Learn Talent?##talent_confirm");
|
||||
talentConfirmOpen_ = false;
|
||||
}
|
||||
if (ImGui::BeginPopupModal("Learn Talent?##talent_confirm", nullptr,
|
||||
ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoMove)) {
|
||||
ImGui::TextColored(ImVec4(1.0f, 1.0f, 0.3f, 1.0f), "%s", pendingTalentName_.c_str());
|
||||
ImGui::Text("Rank %u", pendingTalentRank_ + 1);
|
||||
ImGui::Spacing();
|
||||
ImGui::TextWrapped("Spend a talent point?");
|
||||
ImGui::Spacing();
|
||||
if (ImGui::Button("Learn", ImVec2(80, 0))) {
|
||||
gameHandler.learnTalent(pendingTalentId_, pendingTalentRank_);
|
||||
ImGui::CloseCurrentPopup();
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Cancel", ImVec2(80, 0))) {
|
||||
ImGui::CloseCurrentPopup();
|
||||
}
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
}
|
||||
|
||||
void TalentScreen::renderTalentTree(game::GameHandler& gameHandler, uint32_t tabId,
|
||||
|
|
@ -597,15 +574,10 @@ void TalentScreen::renderTalent(game::GameHandler& gameHandler,
|
|||
ImGui::EndTooltip();
|
||||
}
|
||||
|
||||
// Handle click — open confirmation dialog instead of learning directly
|
||||
// Handle click — currentRank is 1-indexed (0=not learned, 1=rank1, ...)
|
||||
// CMSG_LEARN_TALENT requestedRank must equal current count of learned ranks (same value)
|
||||
if (clicked && canLearn && prereqsMet) {
|
||||
talentConfirmOpen_ = true;
|
||||
pendingTalentId_ = talent.talentId;
|
||||
pendingTalentRank_ = currentRank;
|
||||
uint32_t nextSpell = (currentRank < 5) ? talent.rankSpells[currentRank] : 0;
|
||||
pendingTalentName_ = nextSpell ? gameHandler.getSpellName(nextSpell) : "";
|
||||
if (pendingTalentName_.empty())
|
||||
pendingTalentName_ = spellId ? gameHandler.getSpellName(spellId) : "Talent";
|
||||
gameHandler.learnTalent(talent.talentId, currentRank);
|
||||
}
|
||||
|
||||
ImGui::PopID();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue