mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-03-22 23:30:14 +00:00
Compare commits
71 commits
3eded6772d
...
29ca9809b1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
29ca9809b1 | ||
|
|
16cdde82b3 | ||
|
|
967cedba0e | ||
|
|
7270a4e690 | ||
|
|
528b796dff | ||
|
|
8dd4bc80ec | ||
|
|
23878e530f | ||
|
|
ab0828a4ce | ||
|
|
c19edd407a | ||
|
|
a0979b9cd8 | ||
|
|
04f22376ce | ||
|
|
d3ec230cec | ||
|
|
8014f2650c | ||
|
|
cb0dfddf59 | ||
|
|
b15a21a957 | ||
|
|
5f06c18a54 | ||
|
|
c011d724c6 | ||
|
|
5d2bc9503d | ||
|
|
9cf331fdab | ||
|
|
3d2bade521 | ||
|
|
deea701222 | ||
|
|
e122d725f6 | ||
|
|
abf9ef0b5f | ||
|
|
e11d0956fb | ||
|
|
011b1c8295 | ||
|
|
f31fa29616 | ||
|
|
d72912714b | ||
|
|
640eaacb8c | ||
|
|
07d0485a31 | ||
|
|
1c85b7a46d | ||
|
|
4d39736d29 | ||
|
|
6951b7803d | ||
|
|
06a628dae2 | ||
|
|
52c1fed6ab | ||
|
|
d339734143 | ||
|
|
3e5760aefe | ||
|
|
f63b75c388 | ||
|
|
c44477fbee | ||
|
|
c6e39707de | ||
|
|
ede380ec60 | ||
|
|
edd7e5e591 | ||
|
|
d5de031c23 | ||
|
|
8f0d2cc4ab | ||
|
|
0a6f88e8ad | ||
|
|
921c83df2e | ||
|
|
1b2c7f595e | ||
|
|
6d21f77d32 | ||
|
|
63d8200303 | ||
|
|
b4f744d000 | ||
|
|
1c967e9628 | ||
|
|
4d1be18c18 | ||
|
|
8561d5c58c | ||
|
|
38333df260 | ||
|
|
9d1616a11b | ||
|
|
e0d47040d3 | ||
|
|
a49c013c89 | ||
|
|
3f64f81ec0 | ||
|
|
95e8fcb88e | ||
|
|
0562139868 | ||
|
|
cc61732106 | ||
|
|
5024e8cb32 | ||
|
|
926bcbb50e | ||
|
|
151303a20a | ||
|
|
1c1cdf0f23 | ||
|
|
b6dfa8b747 | ||
|
|
9f340ef456 | ||
|
|
1d33ebbfe4 | ||
|
|
e56d3ca7de | ||
|
|
63c8dfa304 | ||
|
|
e12e399c0a | ||
|
|
d4ea416dd6 |
35 changed files with 3284 additions and 372 deletions
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"Spell": {
|
"Spell": {
|
||||||
"ID": 0, "Attributes": 5, "IconID": 117,
|
"ID": 0, "Attributes": 5, "IconID": 117,
|
||||||
"Name": 120, "Tooltip": 147, "Rank": 129
|
"Name": 120, "Tooltip": 147, "Rank": 129, "SchoolEnum": 1
|
||||||
},
|
},
|
||||||
"ItemDisplayInfo": {
|
"ItemDisplayInfo": {
|
||||||
"ID": 0, "LeftModel": 1, "LeftModelTexture": 3,
|
"ID": 0, "LeftModel": 1, "LeftModelTexture": 3,
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"Spell": {
|
"Spell": {
|
||||||
"ID": 0, "Attributes": 5, "IconID": 124,
|
"ID": 0, "Attributes": 5, "IconID": 124,
|
||||||
"Name": 127, "Tooltip": 154, "Rank": 136
|
"Name": 127, "Tooltip": 154, "Rank": 136, "SchoolMask": 215
|
||||||
},
|
},
|
||||||
"ItemDisplayInfo": {
|
"ItemDisplayInfo": {
|
||||||
"ID": 0, "LeftModel": 1, "LeftModelTexture": 3,
|
"ID": 0, "LeftModel": 1, "LeftModelTexture": 3,
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"Spell": {
|
"Spell": {
|
||||||
"ID": 0, "Attributes": 5, "IconID": 117,
|
"ID": 0, "Attributes": 5, "IconID": 117,
|
||||||
"Name": 120, "Tooltip": 147, "Rank": 129
|
"Name": 120, "Tooltip": 147, "Rank": 129, "SchoolEnum": 1
|
||||||
},
|
},
|
||||||
"ItemDisplayInfo": {
|
"ItemDisplayInfo": {
|
||||||
"ID": 0, "LeftModel": 1, "LeftModelTexture": 3,
|
"ID": 0, "LeftModel": 1, "LeftModelTexture": 3,
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"Spell": {
|
"Spell": {
|
||||||
"ID": 0, "Attributes": 4, "IconID": 133,
|
"ID": 0, "Attributes": 4, "IconID": 133,
|
||||||
"Name": 136, "Tooltip": 139, "Rank": 153
|
"Name": 136, "Tooltip": 139, "Rank": 153, "SchoolMask": 225
|
||||||
},
|
},
|
||||||
"ItemDisplayInfo": {
|
"ItemDisplayInfo": {
|
||||||
"ID": 0, "LeftModel": 1, "LeftModelTexture": 3,
|
"ID": 0, "LeftModel": 1, "LeftModelTexture": 3,
|
||||||
|
|
@ -28,6 +28,7 @@
|
||||||
"ReputationBase0": 10, "ReputationBase1": 11,
|
"ReputationBase0": 10, "ReputationBase1": 11,
|
||||||
"ReputationBase2": 12, "ReputationBase3": 13
|
"ReputationBase2": 12, "ReputationBase3": 13
|
||||||
},
|
},
|
||||||
|
"Achievement": { "ID": 0, "Title": 4, "Description": 21 },
|
||||||
"AreaTable": { "ID": 0, "ExploreFlag": 3 },
|
"AreaTable": { "ID": 0, "ExploreFlag": 3 },
|
||||||
"CreatureDisplayInfoExtra": {
|
"CreatureDisplayInfoExtra": {
|
||||||
"ID": 0, "RaceID": 1, "SexID": 2, "SkinID": 3, "FaceID": 4,
|
"ID": 0, "RaceID": 1, "SexID": 2, "SkinID": 3, "FaceID": 4,
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,9 @@ layout(set = 1, binding = 1) uniform WMOMaterial {
|
||||||
float heightMapVariance;
|
float heightMapVariance;
|
||||||
float normalMapStrength;
|
float normalMapStrength;
|
||||||
int isLava;
|
int isLava;
|
||||||
|
float wmoAmbientR;
|
||||||
|
float wmoAmbientG;
|
||||||
|
float wmoAmbientB;
|
||||||
};
|
};
|
||||||
|
|
||||||
layout(set = 1, binding = 2) uniform sampler2D uNormalHeightMap;
|
layout(set = 1, binding = 2) uniform sampler2D uNormalHeightMap;
|
||||||
|
|
@ -185,7 +188,13 @@ void main() {
|
||||||
} else if (unlit != 0) {
|
} else if (unlit != 0) {
|
||||||
result = texColor.rgb * shadow;
|
result = texColor.rgb * shadow;
|
||||||
} else if (isInterior != 0) {
|
} else if (isInterior != 0) {
|
||||||
vec3 mocv = max(VertColor.rgb, vec3(0.5));
|
// WMO interior: vertex colors (MOCV) are pre-baked lighting from the artist.
|
||||||
|
// The MOHD ambient color tints/floors the vertex colors so dark spots don't
|
||||||
|
// go completely black, matching the WoW client's interior shading.
|
||||||
|
vec3 wmoAmbient = vec3(wmoAmbientR, wmoAmbientG, wmoAmbientB);
|
||||||
|
// Clamp ambient to at least 0.3 to avoid total darkness when MOHD color is zero
|
||||||
|
wmoAmbient = max(wmoAmbient, vec3(0.3));
|
||||||
|
vec3 mocv = max(VertColor.rgb, wmoAmbient);
|
||||||
result = texColor.rgb * mocv * shadow;
|
result = texColor.rgb * mocv * shadow;
|
||||||
} else {
|
} else {
|
||||||
vec3 ldir = normalize(-lightDir.xyz);
|
vec3 ldir = normalize(-lightDir.xyz);
|
||||||
|
|
|
||||||
Binary file not shown.
|
|
@ -45,6 +45,11 @@ public:
|
||||||
bool playSound2D(const std::vector<uint8_t>& wavData, float volume = 1.0f, float pitch = 1.0f);
|
bool playSound2D(const std::vector<uint8_t>& wavData, float volume = 1.0f, float pitch = 1.0f);
|
||||||
bool playSound2D(const std::string& mpqPath, float volume = 1.0f, float pitch = 1.0f);
|
bool playSound2D(const std::string& mpqPath, float volume = 1.0f, float pitch = 1.0f);
|
||||||
|
|
||||||
|
// Stoppable 2D sound — returns a non-zero handle, or 0 on failure
|
||||||
|
uint32_t playSound2DStoppable(const std::vector<uint8_t>& wavData, float volume = 1.0f);
|
||||||
|
// Stop a sound started with playSound2DStoppable (no-op if already finished)
|
||||||
|
void stopSound(uint32_t id);
|
||||||
|
|
||||||
// 3D positional sound playback
|
// 3D positional sound playback
|
||||||
bool playSound3D(const std::vector<uint8_t>& wavData, const glm::vec3& position,
|
bool playSound3D(const std::vector<uint8_t>& wavData, const glm::vec3& position,
|
||||||
float volume = 1.0f, float pitch = 1.0f, float maxDistance = 100.0f);
|
float volume = 1.0f, float pitch = 1.0f, float maxDistance = 100.0f);
|
||||||
|
|
@ -70,8 +75,10 @@ private:
|
||||||
ma_sound* sound;
|
ma_sound* sound;
|
||||||
void* buffer; // ma_audio_buffer* - Keep audio buffer alive
|
void* buffer; // ma_audio_buffer* - Keep audio buffer alive
|
||||||
std::shared_ptr<const std::vector<uint8_t>> pcmDataRef; // Keep decoded PCM alive
|
std::shared_ptr<const std::vector<uint8_t>> pcmDataRef; // Keep decoded PCM alive
|
||||||
|
uint32_t id = 0; // 0 = anonymous (not stoppable)
|
||||||
};
|
};
|
||||||
std::vector<ActiveSound> activeSounds_;
|
std::vector<ActiveSound> activeSounds_;
|
||||||
|
uint32_t nextSoundId_ = 1;
|
||||||
|
|
||||||
// Music track state
|
// Music track state
|
||||||
ma_sound* musicSound_ = nullptr;
|
ma_sound* musicSound_ = nullptr;
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,7 @@ public:
|
||||||
|
|
||||||
// Spell casting sounds
|
// Spell casting sounds
|
||||||
void playPrecast(MagicSchool school, SpellPower power); // Channeling/preparation
|
void playPrecast(MagicSchool school, SpellPower power); // Channeling/preparation
|
||||||
|
void stopPrecast(); // Stop precast sound early
|
||||||
void playCast(MagicSchool school); // When spell fires
|
void playCast(MagicSchool school); // When spell fires
|
||||||
void playImpact(MagicSchool school, SpellPower power); // When spell hits target
|
void playImpact(MagicSchool school, SpellPower power); // When spell hits target
|
||||||
|
|
||||||
|
|
@ -96,6 +97,7 @@ private:
|
||||||
// State tracking
|
// State tracking
|
||||||
float volumeScale_ = 1.0f;
|
float volumeScale_ = 1.0f;
|
||||||
bool initialized_ = false;
|
bool initialized_ = false;
|
||||||
|
uint32_t activePrecastId_ = 0; // Handle from AudioEngine::playSound2DStoppable()
|
||||||
|
|
||||||
// Helper methods
|
// Helper methods
|
||||||
bool loadSound(const std::string& path, SpellSample& sample, pipeline::AssetManager* assets);
|
bool loadSound(const std::string& path, SpellSample& sample, pipeline::AssetManager* assets);
|
||||||
|
|
|
||||||
|
|
@ -456,7 +456,55 @@ public:
|
||||||
void dismissPet();
|
void dismissPet();
|
||||||
bool hasPet() const { return petGuid_ != 0; }
|
bool hasPet() const { return petGuid_ != 0; }
|
||||||
uint64_t getPetGuid() const { return petGuid_; }
|
uint64_t getPetGuid() const { return petGuid_; }
|
||||||
|
|
||||||
|
// ---- Pet state (populated by SMSG_PET_SPELLS / SMSG_PET_MODE) ----
|
||||||
|
// 10 action bar slots; each entry is a packed uint32:
|
||||||
|
// bits 0-23 = spell ID (or 0 for empty)
|
||||||
|
// bits 24-31 = action type (0x00=cast, 0xC0=autocast on, 0x40=autocast off)
|
||||||
|
static constexpr int PET_ACTION_BAR_SLOTS = 10;
|
||||||
|
uint32_t getPetActionSlot(int idx) const {
|
||||||
|
if (idx < 0 || idx >= PET_ACTION_BAR_SLOTS) return 0;
|
||||||
|
return petActionSlots_[idx];
|
||||||
|
}
|
||||||
|
// Pet command/react state from SMSG_PET_MODE or SMSG_PET_SPELLS
|
||||||
|
uint8_t getPetCommand() const { return petCommand_; } // 0=stay,1=follow,2=attack,3=dismiss
|
||||||
|
uint8_t getPetReact() const { return petReact_; } // 0=passive,1=defensive,2=aggressive
|
||||||
|
// Spells the pet knows (from SMSG_PET_SPELLS spell list)
|
||||||
|
const std::vector<uint32_t>& getPetSpells() const { return petSpellList_; }
|
||||||
|
// Pet autocast set (spellIds that have autocast enabled)
|
||||||
|
bool isPetSpellAutocast(uint32_t spellId) const {
|
||||||
|
return petAutocastSpells_.count(spellId) != 0;
|
||||||
|
}
|
||||||
|
// Send CMSG_PET_ACTION to issue a pet command
|
||||||
|
void sendPetAction(uint32_t action, uint64_t targetGuid = 0);
|
||||||
const std::unordered_set<uint32_t>& getKnownSpells() const { return knownSpells; }
|
const std::unordered_set<uint32_t>& getKnownSpells() const { return knownSpells; }
|
||||||
|
|
||||||
|
// Player proficiency bitmasks (from SMSG_SET_PROFICIENCY)
|
||||||
|
// itemClass 2 = Weapon (subClassMask bits: 0=Axe1H,1=Axe2H,2=Bow,3=Gun,4=Mace1H,5=Mace2H,6=Polearm,7=Sword1H,8=Sword2H,10=Staff,13=Fist,14=Misc,15=Dagger,16=Thrown,17=Crossbow,18=Wand,19=Fishing)
|
||||||
|
// itemClass 4 = Armor (subClassMask bits: 1=Cloth,2=Leather,3=Mail,4=Plate,6=Shield)
|
||||||
|
uint32_t getWeaponProficiency() const { return weaponProficiency_; }
|
||||||
|
uint32_t getArmorProficiency() const { return armorProficiency_; }
|
||||||
|
bool canUseWeaponSubclass(uint32_t subClass) const { return (weaponProficiency_ >> subClass) & 1u; }
|
||||||
|
bool canUseArmorSubclass(uint32_t subClass) const { return (armorProficiency_ >> subClass) & 1u; }
|
||||||
|
|
||||||
|
// Minimap pings from party members
|
||||||
|
struct MinimapPing {
|
||||||
|
uint64_t senderGuid = 0;
|
||||||
|
float wowX = 0.0f; // canonical WoW X (north)
|
||||||
|
float wowY = 0.0f; // canonical WoW Y (west)
|
||||||
|
float age = 0.0f; // seconds since received
|
||||||
|
static constexpr float LIFETIME = 5.0f;
|
||||||
|
bool isExpired() const { return age >= LIFETIME; }
|
||||||
|
};
|
||||||
|
const std::vector<MinimapPing>& getMinimapPings() const { return minimapPings_; }
|
||||||
|
void tickMinimapPings(float dt) {
|
||||||
|
for (auto& p : minimapPings_) p.age += dt;
|
||||||
|
minimapPings_.erase(
|
||||||
|
std::remove_if(minimapPings_.begin(), minimapPings_.end(),
|
||||||
|
[](const MinimapPing& p){ return p.isExpired(); }),
|
||||||
|
minimapPings_.end());
|
||||||
|
}
|
||||||
|
|
||||||
bool isCasting() const { return casting; }
|
bool isCasting() const { return casting; }
|
||||||
bool isGameObjectInteractionCasting() const {
|
bool isGameObjectInteractionCasting() const {
|
||||||
return casting && currentCastSpellId == 0 && pendingGameObjectInteractGuid_ != 0;
|
return casting && currentCastSpellId == 0 && pendingGameObjectInteractGuid_ != 0;
|
||||||
|
|
@ -465,6 +513,34 @@ public:
|
||||||
float getCastProgress() const { return castTimeTotal > 0 ? (castTimeTotal - castTimeRemaining) / castTimeTotal : 0.0f; }
|
float getCastProgress() const { return castTimeTotal > 0 ? (castTimeTotal - castTimeRemaining) / castTimeTotal : 0.0f; }
|
||||||
float getCastTimeRemaining() const { return castTimeRemaining; }
|
float getCastTimeRemaining() const { return castTimeRemaining; }
|
||||||
|
|
||||||
|
// Unit cast state (tracked per GUID for target frame + boss frames)
|
||||||
|
struct UnitCastState {
|
||||||
|
bool casting = false;
|
||||||
|
uint32_t spellId = 0;
|
||||||
|
float timeRemaining = 0.0f;
|
||||||
|
float timeTotal = 0.0f;
|
||||||
|
};
|
||||||
|
// Returns cast state for any unit by GUID (empty/non-casting if not found)
|
||||||
|
const UnitCastState* getUnitCastState(uint64_t guid) const {
|
||||||
|
auto it = unitCastStates_.find(guid);
|
||||||
|
return (it != unitCastStates_.end() && it->second.casting) ? &it->second : nullptr;
|
||||||
|
}
|
||||||
|
// Convenience helpers for the current target
|
||||||
|
bool isTargetCasting() const { return getUnitCastState(targetGuid) != nullptr; }
|
||||||
|
uint32_t getTargetCastSpellId() const {
|
||||||
|
auto* s = getUnitCastState(targetGuid);
|
||||||
|
return s ? s->spellId : 0;
|
||||||
|
}
|
||||||
|
float getTargetCastProgress() const {
|
||||||
|
auto* s = getUnitCastState(targetGuid);
|
||||||
|
return (s && s->timeTotal > 0.0f)
|
||||||
|
? (s->timeTotal - s->timeRemaining) / s->timeTotal : 0.0f;
|
||||||
|
}
|
||||||
|
float getTargetCastTimeRemaining() const {
|
||||||
|
auto* s = getUnitCastState(targetGuid);
|
||||||
|
return s ? s->timeRemaining : 0.0f;
|
||||||
|
}
|
||||||
|
|
||||||
// Talents
|
// Talents
|
||||||
uint8_t getActiveTalentSpec() const { return activeTalentSpec_; }
|
uint8_t getActiveTalentSpec() const { return activeTalentSpec_; }
|
||||||
uint8_t getUnspentTalentPoints() const { return unspentTalentPoints_[activeTalentSpec_]; }
|
uint8_t getUnspentTalentPoints() const { return unspentTalentPoints_[activeTalentSpec_]; }
|
||||||
|
|
@ -583,6 +659,12 @@ public:
|
||||||
using BindPointCallback = std::function<void(uint32_t mapId, float x, float y, float z)>;
|
using BindPointCallback = std::function<void(uint32_t mapId, float x, float y, float z)>;
|
||||||
void setBindPointCallback(BindPointCallback cb) { bindPointCallback_ = std::move(cb); }
|
void setBindPointCallback(BindPointCallback cb) { bindPointCallback_ = std::move(cb); }
|
||||||
|
|
||||||
|
// Called when the player starts casting Hearthstone so terrain at the bind
|
||||||
|
// point can be pre-loaded during the cast time.
|
||||||
|
// Parameters: mapId and canonical (x, y, z) of the bind location.
|
||||||
|
using HearthstonePreloadCallback = std::function<void(uint32_t mapId, float x, float y, float z)>;
|
||||||
|
void setHearthstonePreloadCallback(HearthstonePreloadCallback cb) { hearthstonePreloadCallback_ = std::move(cb); }
|
||||||
|
|
||||||
// Creature spawn callback (online mode - triggered when creature enters view)
|
// Creature spawn callback (online mode - triggered when creature enters view)
|
||||||
// Parameters: guid, displayId, x, y, z (canonical), orientation
|
// Parameters: guid, displayId, x, y, z (canonical), orientation
|
||||||
using CreatureSpawnCallback = std::function<void(uint64_t guid, uint32_t displayId, float x, float y, float z, float orientation)>;
|
using CreatureSpawnCallback = std::function<void(uint64_t guid, uint32_t displayId, float x, float y, float z, float orientation)>;
|
||||||
|
|
@ -702,6 +784,11 @@ public:
|
||||||
bool isPlayerGhost() const { return releasedSpirit_; }
|
bool isPlayerGhost() const { return releasedSpirit_; }
|
||||||
bool showDeathDialog() const { return playerDead_ && !releasedSpirit_; }
|
bool showDeathDialog() const { return playerDead_ && !releasedSpirit_; }
|
||||||
bool showResurrectDialog() const { return resurrectRequestPending_; }
|
bool showResurrectDialog() const { return resurrectRequestPending_; }
|
||||||
|
const std::string& getResurrectCasterName() const { return resurrectCasterName_; }
|
||||||
|
/** True when ghost is within 40 yards of corpse position (same map). */
|
||||||
|
bool canReclaimCorpse() const;
|
||||||
|
/** Send CMSG_RECLAIM_CORPSE; noop if not a ghost or not near corpse. */
|
||||||
|
void reclaimCorpse();
|
||||||
void releaseSpirit();
|
void releaseSpirit();
|
||||||
void acceptResurrect();
|
void acceptResurrect();
|
||||||
void declineResurrect();
|
void declineResurrect();
|
||||||
|
|
@ -773,6 +860,13 @@ public:
|
||||||
};
|
};
|
||||||
const std::vector<InstanceLockout>& getInstanceLockouts() const { return instanceLockouts_; }
|
const std::vector<InstanceLockout>& getInstanceLockouts() const { return instanceLockouts_; }
|
||||||
|
|
||||||
|
// Boss encounter unit tracking (SMSG_UPDATE_INSTANCE_ENCOUNTER_UNIT)
|
||||||
|
static constexpr uint32_t kMaxEncounterSlots = 5;
|
||||||
|
// Returns boss unit guid for the given encounter slot (0 if none)
|
||||||
|
uint64_t getEncounterUnitGuid(uint32_t slot) const {
|
||||||
|
return (slot < kMaxEncounterSlots) ? encounterUnitGuids_[slot] : 0;
|
||||||
|
}
|
||||||
|
|
||||||
// ---- LFG / Dungeon Finder ----
|
// ---- LFG / Dungeon Finder ----
|
||||||
enum class LfgState : uint8_t {
|
enum class LfgState : uint8_t {
|
||||||
None = 0,
|
None = 0,
|
||||||
|
|
@ -1016,6 +1110,10 @@ public:
|
||||||
};
|
};
|
||||||
const std::unordered_map<uint32_t, TaxiNode>& getTaxiNodes() const { return taxiNodes_; }
|
const std::unordered_map<uint32_t, TaxiNode>& getTaxiNodes() const { return taxiNodes_; }
|
||||||
uint32_t getTaxiCostTo(uint32_t destNodeId) const;
|
uint32_t getTaxiCostTo(uint32_t destNodeId) const;
|
||||||
|
bool taxiNpcHasRoutes(uint64_t guid) const {
|
||||||
|
auto it = taxiNpcHasRoutes_.find(guid);
|
||||||
|
return it != taxiNpcHasRoutes_.end() && it->second;
|
||||||
|
}
|
||||||
|
|
||||||
// Vendor
|
// Vendor
|
||||||
void openVendor(uint64_t npcGuid);
|
void openVendor(uint64_t npcGuid);
|
||||||
|
|
@ -1439,6 +1537,8 @@ private:
|
||||||
void handleWho(network::Packet& packet);
|
void handleWho(network::Packet& packet);
|
||||||
|
|
||||||
// ---- Social handlers ----
|
// ---- Social handlers ----
|
||||||
|
void handleFriendList(network::Packet& packet); // Classic SMSG_FRIEND_LIST
|
||||||
|
void handleContactList(network::Packet& packet); // WotLK SMSG_CONTACT_LIST (full parse)
|
||||||
void handleFriendStatus(network::Packet& packet);
|
void handleFriendStatus(network::Packet& packet);
|
||||||
void handleRandomRoll(network::Packet& packet);
|
void handleRandomRoll(network::Packet& packet);
|
||||||
|
|
||||||
|
|
@ -1558,6 +1658,7 @@ private:
|
||||||
|
|
||||||
// ---- Friend list cache ----
|
// ---- Friend list cache ----
|
||||||
std::unordered_map<std::string, uint64_t> friendsCache; // name -> guid
|
std::unordered_map<std::string, uint64_t> friendsCache; // name -> guid
|
||||||
|
std::unordered_set<uint64_t> friendGuids_; // all known friend GUIDs (for name backfill)
|
||||||
uint32_t lastContactListMask_ = 0;
|
uint32_t lastContactListMask_ = 0;
|
||||||
uint32_t lastContactListCount_ = 0;
|
uint32_t lastContactListCount_ = 0;
|
||||||
|
|
||||||
|
|
@ -1645,6 +1746,7 @@ private:
|
||||||
UnstuckCallback unstuckGyCallback_;
|
UnstuckCallback unstuckGyCallback_;
|
||||||
UnstuckCallback unstuckHearthCallback_;
|
UnstuckCallback unstuckHearthCallback_;
|
||||||
BindPointCallback bindPointCallback_;
|
BindPointCallback bindPointCallback_;
|
||||||
|
HearthstonePreloadCallback hearthstonePreloadCallback_;
|
||||||
CreatureSpawnCallback creatureSpawnCallback_;
|
CreatureSpawnCallback creatureSpawnCallback_;
|
||||||
CreatureDespawnCallback creatureDespawnCallback_;
|
CreatureDespawnCallback creatureDespawnCallback_;
|
||||||
PlayerSpawnCallback playerSpawnCallback_;
|
PlayerSpawnCallback playerSpawnCallback_;
|
||||||
|
|
@ -1676,10 +1778,15 @@ private:
|
||||||
std::unique_ptr<TransportManager> transportManager_; // Transport movement manager
|
std::unique_ptr<TransportManager> transportManager_; // Transport movement manager
|
||||||
std::unordered_set<uint32_t> knownSpells;
|
std::unordered_set<uint32_t> knownSpells;
|
||||||
std::unordered_map<uint32_t, float> spellCooldowns; // spellId -> remaining seconds
|
std::unordered_map<uint32_t, float> spellCooldowns; // spellId -> remaining seconds
|
||||||
|
uint32_t weaponProficiency_ = 0; // bitmask from SMSG_SET_PROFICIENCY itemClass=2
|
||||||
|
uint32_t armorProficiency_ = 0; // bitmask from SMSG_SET_PROFICIENCY itemClass=4
|
||||||
|
std::vector<MinimapPing> minimapPings_;
|
||||||
uint8_t castCount = 0;
|
uint8_t castCount = 0;
|
||||||
bool casting = false;
|
bool casting = false;
|
||||||
uint32_t currentCastSpellId = 0;
|
uint32_t currentCastSpellId = 0;
|
||||||
float castTimeRemaining = 0.0f;
|
float castTimeRemaining = 0.0f;
|
||||||
|
// Per-unit cast state (keyed by GUID, populated from SMSG_SPELL_START)
|
||||||
|
std::unordered_map<uint64_t, UnitCastState> unitCastStates_;
|
||||||
uint64_t pendingGameObjectInteractGuid_ = 0;
|
uint64_t pendingGameObjectInteractGuid_ = 0;
|
||||||
|
|
||||||
// Talents (dual-spec support)
|
// Talents (dual-spec support)
|
||||||
|
|
@ -1710,6 +1817,11 @@ private:
|
||||||
std::vector<AuraSlot> playerAuras;
|
std::vector<AuraSlot> playerAuras;
|
||||||
std::vector<AuraSlot> targetAuras;
|
std::vector<AuraSlot> targetAuras;
|
||||||
uint64_t petGuid_ = 0;
|
uint64_t petGuid_ = 0;
|
||||||
|
uint32_t petActionSlots_[10] = {}; // SMSG_PET_SPELLS action bar (10 slots)
|
||||||
|
uint8_t petCommand_ = 1; // 0=stay,1=follow,2=attack,3=dismiss
|
||||||
|
uint8_t petReact_ = 1; // 0=passive,1=defensive,2=aggressive
|
||||||
|
std::vector<uint32_t> petSpellList_; // known pet spells
|
||||||
|
std::unordered_set<uint32_t> petAutocastSpells_; // spells with autocast on
|
||||||
|
|
||||||
// ---- Battleground queue state ----
|
// ---- Battleground queue state ----
|
||||||
struct BgQueueSlot {
|
struct BgQueueSlot {
|
||||||
|
|
@ -1734,6 +1846,9 @@ private:
|
||||||
// Instance / raid lockouts
|
// Instance / raid lockouts
|
||||||
std::vector<InstanceLockout> instanceLockouts_;
|
std::vector<InstanceLockout> instanceLockouts_;
|
||||||
|
|
||||||
|
// Instance encounter boss units (slots 0-4 from SMSG_UPDATE_INSTANCE_ENCOUNTER_UNIT)
|
||||||
|
std::array<uint64_t, kMaxEncounterSlots> encounterUnitGuids_ = {}; // 0 = empty slot
|
||||||
|
|
||||||
// LFG / Dungeon Finder state
|
// LFG / Dungeon Finder state
|
||||||
LfgState lfgState_ = LfgState::None;
|
LfgState lfgState_ = LfgState::None;
|
||||||
uint32_t lfgDungeonId_ = 0; // current dungeon entry
|
uint32_t lfgDungeonId_ = 0; // current dungeon entry
|
||||||
|
|
@ -1742,7 +1857,9 @@ private:
|
||||||
uint32_t lfgTimeInQueueMs_= 0; // ms already in queue
|
uint32_t lfgTimeInQueueMs_= 0; // ms already in queue
|
||||||
|
|
||||||
// Ready check state
|
// Ready check state
|
||||||
bool pendingReadyCheck_ = false;
|
bool pendingReadyCheck_ = false;
|
||||||
|
uint32_t readyCheckReadyCount_ = 0;
|
||||||
|
uint32_t readyCheckNotReadyCount_ = 0;
|
||||||
std::string readyCheckInitiator_;
|
std::string readyCheckInitiator_;
|
||||||
|
|
||||||
// Faction standings (factionId → absolute standing value)
|
// Faction standings (factionId → absolute standing value)
|
||||||
|
|
@ -1878,6 +1995,7 @@ private:
|
||||||
}
|
}
|
||||||
|
|
||||||
// Taxi / Flight Paths
|
// Taxi / Flight Paths
|
||||||
|
std::unordered_map<uint64_t, bool> taxiNpcHasRoutes_; // guid -> has new/available routes
|
||||||
std::unordered_map<uint32_t, TaxiNode> taxiNodes_;
|
std::unordered_map<uint32_t, TaxiNode> taxiNodes_;
|
||||||
std::vector<TaxiPathEdge> taxiPathEdges_;
|
std::vector<TaxiPathEdge> taxiPathEdges_;
|
||||||
std::unordered_map<uint32_t, std::vector<TaxiPathNode>> taxiPathNodes_; // pathId -> ordered waypoints
|
std::unordered_map<uint32_t, std::vector<TaxiPathNode>> taxiPathNodes_; // pathId -> ordered waypoints
|
||||||
|
|
@ -1971,9 +2089,14 @@ private:
|
||||||
// Trainer
|
// Trainer
|
||||||
bool trainerWindowOpen_ = false;
|
bool trainerWindowOpen_ = false;
|
||||||
TrainerListData currentTrainerList_;
|
TrainerListData currentTrainerList_;
|
||||||
struct SpellNameEntry { std::string name; std::string rank; };
|
struct SpellNameEntry { std::string name; std::string rank; uint32_t schoolMask = 0; };
|
||||||
std::unordered_map<uint32_t, SpellNameEntry> spellNameCache_;
|
std::unordered_map<uint32_t, SpellNameEntry> spellNameCache_;
|
||||||
bool spellNameCacheLoaded_ = false;
|
bool spellNameCacheLoaded_ = false;
|
||||||
|
|
||||||
|
// Achievement name cache (lazy-loaded from Achievement.dbc on first earned event)
|
||||||
|
std::unordered_map<uint32_t, std::string> achievementNameCache_;
|
||||||
|
bool achievementNameCacheLoaded_ = false;
|
||||||
|
void loadAchievementNameCache();
|
||||||
std::vector<TrainerTab> trainerTabs_;
|
std::vector<TrainerTab> trainerTabs_;
|
||||||
void handleTrainerList(network::Packet& packet);
|
void handleTrainerList(network::Packet& packet);
|
||||||
void loadSpellNameCache();
|
void loadSpellNameCache();
|
||||||
|
|
@ -2090,6 +2213,8 @@ private:
|
||||||
float serverPitchRate_ = 3.14159f;
|
float serverPitchRate_ = 3.14159f;
|
||||||
bool playerDead_ = false;
|
bool playerDead_ = false;
|
||||||
bool releasedSpirit_ = false;
|
bool releasedSpirit_ = false;
|
||||||
|
uint32_t corpseMapId_ = 0;
|
||||||
|
float corpseX_ = 0.0f, corpseY_ = 0.0f, corpseZ_ = 0.0f;
|
||||||
// Death Knight runes (class 6): slots 0-1=Blood, 2-3=Unholy, 4-5=Frost initially
|
// Death Knight runes (class 6): slots 0-1=Blood, 2-3=Unholy, 4-5=Frost initially
|
||||||
std::array<RuneSlot, 6> playerRunes_ = [] {
|
std::array<RuneSlot, 6> playerRunes_ = [] {
|
||||||
std::array<RuneSlot, 6> r{};
|
std::array<RuneSlot, 6> r{};
|
||||||
|
|
@ -2101,7 +2226,9 @@ private:
|
||||||
uint64_t pendingSpiritHealerGuid_ = 0;
|
uint64_t pendingSpiritHealerGuid_ = 0;
|
||||||
bool resurrectPending_ = false;
|
bool resurrectPending_ = false;
|
||||||
bool resurrectRequestPending_ = false;
|
bool resurrectRequestPending_ = false;
|
||||||
|
bool resurrectIsSpiritHealer_ = false; // true = SMSG_SPIRIT_HEALER_CONFIRM, false = SMSG_RESURRECT_REQUEST
|
||||||
uint64_t resurrectCasterGuid_ = 0;
|
uint64_t resurrectCasterGuid_ = 0;
|
||||||
|
std::string resurrectCasterName_;
|
||||||
bool repopPending_ = false;
|
bool repopPending_ = false;
|
||||||
uint64_t lastRepopRequestMs_ = 0;
|
uint64_t lastRepopRequestMs_ = 0;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -93,6 +93,11 @@ public:
|
||||||
return SpellDamageLogParser::parse(packet, data);
|
return SpellDamageLogParser::parse(packet, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Parse SMSG_SPELLHEALLOG */
|
||||||
|
virtual bool parseSpellHealLog(network::Packet& packet, SpellHealLogData& data) {
|
||||||
|
return SpellHealLogParser::parse(packet, data);
|
||||||
|
}
|
||||||
|
|
||||||
// --- Spells ---
|
// --- Spells ---
|
||||||
|
|
||||||
/** Parse SMSG_INITIAL_SPELLS */
|
/** Parse SMSG_INITIAL_SPELLS */
|
||||||
|
|
@ -100,11 +105,34 @@ public:
|
||||||
return InitialSpellsParser::parse(packet, data);
|
return InitialSpellsParser::parse(packet, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Parse SMSG_SPELL_START */
|
||||||
|
virtual bool parseSpellStart(network::Packet& packet, SpellStartData& data) {
|
||||||
|
return SpellStartParser::parse(packet, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Parse SMSG_SPELL_GO */
|
||||||
|
virtual bool parseSpellGo(network::Packet& packet, SpellGoData& data) {
|
||||||
|
return SpellGoParser::parse(packet, data);
|
||||||
|
}
|
||||||
|
|
||||||
/** Parse SMSG_CAST_FAILED */
|
/** Parse SMSG_CAST_FAILED */
|
||||||
virtual bool parseCastFailed(network::Packet& packet, CastFailedData& data) {
|
virtual bool parseCastFailed(network::Packet& packet, CastFailedData& data) {
|
||||||
return CastFailedParser::parse(packet, data);
|
return CastFailedParser::parse(packet, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Parse SMSG_CAST_RESULT header (spellId + result), expansion-aware.
|
||||||
|
* WotLK: castCount(u8) + spellId(u32) + result(u8)
|
||||||
|
* TBC/Classic: spellId(u32) + result(u8) (no castCount prefix)
|
||||||
|
*/
|
||||||
|
virtual bool parseCastResult(network::Packet& packet, uint32_t& spellId, uint8_t& result) {
|
||||||
|
// WotLK default: skip castCount, read spellId + result
|
||||||
|
if (packet.getSize() - packet.getReadPos() < 6) return false;
|
||||||
|
packet.readUInt8(); // castCount
|
||||||
|
spellId = packet.readUInt32();
|
||||||
|
result = packet.readUInt8();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
/** Parse SMSG_AURA_UPDATE / SMSG_AURA_UPDATE_ALL */
|
/** Parse SMSG_AURA_UPDATE / SMSG_AURA_UPDATE_ALL */
|
||||||
virtual bool parseAuraUpdate(network::Packet& packet, AuraUpdateData& data, bool isAll = false) {
|
virtual bool parseAuraUpdate(network::Packet& packet, AuraUpdateData& data, bool isAll = false) {
|
||||||
return AuraUpdateParser::parse(packet, data, isAll);
|
return AuraUpdateParser::parse(packet, data, isAll);
|
||||||
|
|
@ -122,6 +150,13 @@ public:
|
||||||
return NameQueryResponseParser::parse(packet, data);
|
return NameQueryResponseParser::parse(packet, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Creature Query ---
|
||||||
|
|
||||||
|
/** Parse SMSG_CREATURE_QUERY_RESPONSE */
|
||||||
|
virtual bool parseCreatureQueryResponse(network::Packet& packet, CreatureQueryResponseData& data) {
|
||||||
|
return CreatureQueryResponseParser::parse(packet, data);
|
||||||
|
}
|
||||||
|
|
||||||
// --- Item Query ---
|
// --- Item Query ---
|
||||||
|
|
||||||
/** Build CMSG_ITEM_QUERY_SINGLE */
|
/** Build CMSG_ITEM_QUERY_SINGLE */
|
||||||
|
|
@ -287,6 +322,37 @@ public:
|
||||||
bool parseNameQueryResponse(network::Packet& packet, NameQueryResponseData& data) override;
|
bool parseNameQueryResponse(network::Packet& packet, NameQueryResponseData& data) override;
|
||||||
bool parseItemQueryResponse(network::Packet& packet, ItemQueryResponseData& data) override;
|
bool parseItemQueryResponse(network::Packet& packet, ItemQueryResponseData& data) override;
|
||||||
network::Packet buildAcceptQuestPacket(uint64_t npcGuid, uint32_t questId) override;
|
network::Packet buildAcceptQuestPacket(uint64_t npcGuid, uint32_t questId) override;
|
||||||
|
// TBC 2.4.3 CMSG_CAST_SPELL has no castFlags byte (WotLK added it)
|
||||||
|
network::Packet buildCastSpell(uint32_t spellId, uint64_t targetGuid, uint8_t castCount) override;
|
||||||
|
// TBC 2.4.3 CMSG_USE_ITEM has no glyphIndex field (WotLK added it)
|
||||||
|
network::Packet buildUseItem(uint8_t bagIndex, uint8_t slotIndex, uint64_t itemGuid, uint32_t spellId = 0) override;
|
||||||
|
// TBC 2.4.3 SMSG_MONSTER_MOVE has no unk byte after packed GUID (WotLK added it)
|
||||||
|
bool parseMonsterMove(network::Packet& packet, MonsterMoveData& data) override;
|
||||||
|
// TBC 2.4.3 SMSG_GOSSIP_MESSAGE quests lack questFlags(u32)+isRepeatable(u8) (WotLK added them)
|
||||||
|
bool parseGossipMessage(network::Packet& packet, GossipMessageData& data) override;
|
||||||
|
// TBC 2.4.3 SMSG_CAST_RESULT: spellId(u32) + result(u8) — WotLK added castCount(u8) prefix
|
||||||
|
bool parseCastResult(network::Packet& packet, uint32_t& spellId, uint8_t& result) override;
|
||||||
|
// TBC 2.4.3 SMSG_CAST_FAILED: spellId(u32) + result(u8) — WotLK added castCount(u8) prefix
|
||||||
|
bool parseCastFailed(network::Packet& packet, CastFailedData& data) override;
|
||||||
|
// TBC 2.4.3 SMSG_SPELL_START: full uint64 GUIDs (WotLK uses packed GUIDs)
|
||||||
|
bool parseSpellStart(network::Packet& packet, SpellStartData& data) override;
|
||||||
|
// TBC 2.4.3 SMSG_SPELL_GO: full uint64 GUIDs, no timestamp field (WotLK added one)
|
||||||
|
bool parseSpellGo(network::Packet& packet, SpellGoData& data) override;
|
||||||
|
// TBC 2.4.3 SMSG_MAIL_LIST_RESULT: uint8 count (not uint32+uint8), no body field,
|
||||||
|
// attachment uses uint64 itemGuid (not uint32), enchants are 7×u32 id-only (not 7×{id+dur+charges})
|
||||||
|
bool parseMailList(network::Packet& packet, std::vector<MailMessage>& inbox) override;
|
||||||
|
// TBC 2.4.3 SMSG_ATTACKERSTATEUPDATE uses full uint64 GUIDs (WotLK uses packed GUIDs)
|
||||||
|
bool parseAttackerStateUpdate(network::Packet& packet, AttackerStateUpdateData& data) override;
|
||||||
|
// TBC 2.4.3 SMSG_SPELLNONMELEEDAMAGELOG uses full uint64 GUIDs (WotLK uses packed GUIDs)
|
||||||
|
bool parseSpellDamageLog(network::Packet& packet, SpellDamageLogData& data) override;
|
||||||
|
// TBC 2.4.3 SMSG_SPELLHEALLOG uses full uint64 GUIDs (WotLK uses packed GUIDs)
|
||||||
|
bool parseSpellHealLog(network::Packet& packet, SpellHealLogData& data) override;
|
||||||
|
// TBC 2.4.3 quest log has 4 update fields per slot (questId, state, counts, timer)
|
||||||
|
// WotLK expands this to 5 (splits counts into two fields).
|
||||||
|
uint8_t questLogStride() const override { return 4; }
|
||||||
|
// TBC 2.4.3 CMSG_QUESTGIVER_QUERY_QUEST: guid(8) + questId(4) — no trailing
|
||||||
|
// isDialogContinued byte that WotLK added
|
||||||
|
network::Packet buildQueryQuestPacket(uint64_t npcGuid, uint32_t questId) override;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -317,6 +383,8 @@ public:
|
||||||
bool parseCastFailed(network::Packet& packet, CastFailedData& data) override;
|
bool parseCastFailed(network::Packet& packet, CastFailedData& data) override;
|
||||||
bool parseMessageChat(network::Packet& packet, MessageChatData& data) override;
|
bool parseMessageChat(network::Packet& packet, MessageChatData& data) override;
|
||||||
bool parseGameObjectQueryResponse(network::Packet& packet, GameObjectQueryResponseData& data) override;
|
bool parseGameObjectQueryResponse(network::Packet& packet, GameObjectQueryResponseData& data) override;
|
||||||
|
// Classic 1.12 SMSG_CREATURE_QUERY_RESPONSE lacks the iconName string that TBC/WotLK include
|
||||||
|
bool parseCreatureQueryResponse(network::Packet& packet, CreatureQueryResponseData& data) override;
|
||||||
bool parseGossipMessage(network::Packet& packet, GossipMessageData& data) override;
|
bool parseGossipMessage(network::Packet& packet, GossipMessageData& data) override;
|
||||||
bool parseGuildRoster(network::Packet& packet, GuildRosterData& data) override;
|
bool parseGuildRoster(network::Packet& packet, GuildRosterData& data) override;
|
||||||
bool parseGuildQueryResponse(network::Packet& packet, GuildQueryResponseData& data) override;
|
bool parseGuildQueryResponse(network::Packet& packet, GuildQueryResponseData& data) override;
|
||||||
|
|
@ -339,6 +407,19 @@ public:
|
||||||
bool parseMonsterMove(network::Packet& packet, MonsterMoveData& data) override {
|
bool parseMonsterMove(network::Packet& packet, MonsterMoveData& data) override {
|
||||||
return MonsterMoveParser::parseVanilla(packet, data);
|
return MonsterMoveParser::parseVanilla(packet, data);
|
||||||
}
|
}
|
||||||
|
// Classic 1.12 uses PackedGuid (not full uint64) and uint16 castFlags (not uint32)
|
||||||
|
bool parseSpellStart(network::Packet& packet, SpellStartData& data) override;
|
||||||
|
bool parseSpellGo(network::Packet& packet, SpellGoData& data) override;
|
||||||
|
// Classic 1.12 melee/spell log packets use PackedGuid (not full uint64)
|
||||||
|
bool parseAttackerStateUpdate(network::Packet& packet, AttackerStateUpdateData& data) override;
|
||||||
|
bool parseSpellDamageLog(network::Packet& packet, SpellDamageLogData& data) override;
|
||||||
|
bool parseSpellHealLog(network::Packet& packet, SpellHealLogData& data) override;
|
||||||
|
// Classic 1.12 has SMSG_AURA_UPDATE (unlike TBC which doesn't);
|
||||||
|
// format differs from WotLK: no caster GUID, DURATION flag is 0x10 not 0x20
|
||||||
|
bool parseAuraUpdate(network::Packet& packet, AuraUpdateData& data, bool isAll = false) override;
|
||||||
|
// Classic 1.12 SMSG_NAME_QUERY_RESPONSE: full uint64 guid + name + realmName CString +
|
||||||
|
// uint32 race + uint32 gender + uint32 class (TBC Variant A skips the realmName CString)
|
||||||
|
bool parseNameQueryResponse(network::Packet& packet, NameQueryResponseData& data) override;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,7 @@ struct CombatTextEntry {
|
||||||
enum Type : uint8_t {
|
enum Type : uint8_t {
|
||||||
MELEE_DAMAGE, SPELL_DAMAGE, HEAL, MISS, DODGE, PARRY, BLOCK,
|
MELEE_DAMAGE, SPELL_DAMAGE, HEAL, MISS, DODGE, PARRY, BLOCK,
|
||||||
CRIT_DAMAGE, CRIT_HEAL, PERIODIC_DAMAGE, PERIODIC_HEAL, ENVIRONMENTAL,
|
CRIT_DAMAGE, CRIT_HEAL, PERIODIC_DAMAGE, PERIODIC_HEAL, ENVIRONMENTAL,
|
||||||
ENERGIZE, XP_GAIN
|
ENERGIZE, XP_GAIN, IMMUNE
|
||||||
};
|
};
|
||||||
Type type;
|
Type type;
|
||||||
int32_t amount = 0;
|
int32_t amount = 0;
|
||||||
|
|
|
||||||
|
|
@ -717,7 +717,9 @@ struct TextEmoteData {
|
||||||
*/
|
*/
|
||||||
class TextEmoteParser {
|
class TextEmoteParser {
|
||||||
public:
|
public:
|
||||||
static bool parse(network::Packet& packet, TextEmoteData& data);
|
// legacyFormat: Classic 1.12 and TBC 2.4.3 send textEmoteId+emoteNum first, then senderGuid.
|
||||||
|
// WotLK 3.3.5a reverses this: senderGuid first, then textEmoteId+emoteNum.
|
||||||
|
static bool parse(network::Packet& packet, TextEmoteData& data, bool legacyFormat = false);
|
||||||
};
|
};
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
@ -1729,7 +1731,8 @@ public:
|
||||||
/** CMSG_PET_ACTION packet builder */
|
/** CMSG_PET_ACTION packet builder */
|
||||||
class PetActionPacket {
|
class PetActionPacket {
|
||||||
public:
|
public:
|
||||||
static network::Packet build(uint64_t petGuid, uint32_t action);
|
/** CMSG_PET_ACTION: petGuid + action + targetGuid (0 = no target) */
|
||||||
|
static network::Packet build(uint64_t petGuid, uint32_t action, uint64_t targetGuid = 0);
|
||||||
};
|
};
|
||||||
|
|
||||||
/** SMSG_CAST_FAILED data */
|
/** SMSG_CAST_FAILED data */
|
||||||
|
|
@ -1765,6 +1768,11 @@ public:
|
||||||
};
|
};
|
||||||
|
|
||||||
/** SMSG_SPELL_GO data (simplified) */
|
/** SMSG_SPELL_GO data (simplified) */
|
||||||
|
struct SpellGoMissEntry {
|
||||||
|
uint64_t targetGuid = 0;
|
||||||
|
uint8_t missType = 0; // 0=MISS 1=DODGE 2=PARRY 3=BLOCK 4=EVADE 5=IMMUNE 6=DEFLECT 7=ABSORB 8=RESIST
|
||||||
|
};
|
||||||
|
|
||||||
struct SpellGoData {
|
struct SpellGoData {
|
||||||
uint64_t casterGuid = 0;
|
uint64_t casterGuid = 0;
|
||||||
uint64_t casterUnit = 0;
|
uint64_t casterUnit = 0;
|
||||||
|
|
@ -1772,8 +1780,9 @@ struct SpellGoData {
|
||||||
uint32_t spellId = 0;
|
uint32_t spellId = 0;
|
||||||
uint32_t castFlags = 0;
|
uint32_t castFlags = 0;
|
||||||
uint8_t hitCount = 0;
|
uint8_t hitCount = 0;
|
||||||
std::vector<uint64_t> hitTargets;
|
std::vector<uint64_t> hitTargets;
|
||||||
uint8_t missCount = 0;
|
uint8_t missCount = 0;
|
||||||
|
std::vector<SpellGoMissEntry> missTargets;
|
||||||
|
|
||||||
bool isValid() const { return spellId != 0; }
|
bool isValid() const { return spellId != 0; }
|
||||||
};
|
};
|
||||||
|
|
@ -1848,7 +1857,9 @@ public:
|
||||||
/** SMSG_GROUP_LIST parser */
|
/** SMSG_GROUP_LIST parser */
|
||||||
class GroupListParser {
|
class GroupListParser {
|
||||||
public:
|
public:
|
||||||
static bool parse(network::Packet& packet, GroupListData& data);
|
// hasRoles: WotLK 3.3.5a added a roles byte at group level and per-member for LFD.
|
||||||
|
// Classic 1.12 and TBC 2.4.3 do not send this byte.
|
||||||
|
static bool parse(network::Packet& packet, GroupListData& data, bool hasRoles = true);
|
||||||
};
|
};
|
||||||
|
|
||||||
/** SMSG_PARTY_COMMAND_RESULT data */
|
/** SMSG_PARTY_COMMAND_RESULT data */
|
||||||
|
|
@ -2220,7 +2231,9 @@ struct TrainerListData {
|
||||||
|
|
||||||
class TrainerListParser {
|
class TrainerListParser {
|
||||||
public:
|
public:
|
||||||
static bool parse(network::Packet& packet, TrainerListData& data);
|
// isClassic: Classic 1.12 per-spell layout has no profDialog/profButton fields
|
||||||
|
// (reqLevel immediately follows cost), plus a trailing unk uint32 per entry.
|
||||||
|
static bool parse(network::Packet& packet, TrainerListData& data, bool isClassic = false);
|
||||||
};
|
};
|
||||||
|
|
||||||
class TrainerBuySpellPacket {
|
class TrainerBuySpellPacket {
|
||||||
|
|
@ -2266,6 +2279,13 @@ public:
|
||||||
static network::Packet build(bool accept);
|
static network::Packet build(bool accept);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** CMSG_SET_ACTIVE_TALENT_GROUP_OBSOLETE (0x4C3) — switch dual-spec talent group */
|
||||||
|
class ActivateTalentGroupPacket {
|
||||||
|
public:
|
||||||
|
/** @param group 0 = primary spec, 1 = secondary spec */
|
||||||
|
static network::Packet build(uint32_t group);
|
||||||
|
};
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Taxi / Flight Paths
|
// Taxi / Flight Paths
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
@ -2612,7 +2632,8 @@ public:
|
||||||
/** SMSG_AUCTION_LIST_RESULT parser (shared for browse/owner/bidder) */
|
/** SMSG_AUCTION_LIST_RESULT parser (shared for browse/owner/bidder) */
|
||||||
class AuctionListResultParser {
|
class AuctionListResultParser {
|
||||||
public:
|
public:
|
||||||
static bool parse(network::Packet& packet, AuctionListResult& data);
|
// numEnchantSlots: Classic 1.12 = 1, TBC/WotLK = 3 (extra enchant slots per entry)
|
||||||
|
static bool parse(network::Packet& packet, AuctionListResult& data, int numEnchantSlots = 3);
|
||||||
};
|
};
|
||||||
|
|
||||||
/** SMSG_AUCTION_COMMAND_RESULT parser */
|
/** SMSG_AUCTION_COMMAND_RESULT parser */
|
||||||
|
|
|
||||||
|
|
@ -91,6 +91,20 @@ inline bool isWouldBlock(int err) {
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Returns true for errors that mean the peer closed the connection cleanly.
|
||||||
|
// On Windows, WSAENOTCONN / WSAECONNRESET / WSAESHUTDOWN can be returned by
|
||||||
|
// recv() when the server closes the connection, rather than returning 0.
|
||||||
|
inline bool isConnectionClosed(int err) {
|
||||||
|
#ifdef _WIN32
|
||||||
|
return err == WSAENOTCONN || // socket not connected (server closed)
|
||||||
|
err == WSAECONNRESET || // connection reset by peer
|
||||||
|
err == WSAESHUTDOWN || // socket shut down
|
||||||
|
err == WSAECONNABORTED; // connection aborted
|
||||||
|
#else
|
||||||
|
return err == ENOTCONN || err == ECONNRESET;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
inline bool isInProgress(int err) {
|
inline bool isInProgress(int err) {
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
return err == WSAEWOULDBLOCK || err == WSAEALREADY;
|
return err == WSAEWOULDBLOCK || err == WSAEALREADY;
|
||||||
|
|
|
||||||
|
|
@ -185,6 +185,7 @@ struct WMOModel {
|
||||||
uint32_t nDoodadDefs;
|
uint32_t nDoodadDefs;
|
||||||
uint32_t nDoodadSets;
|
uint32_t nDoodadSets;
|
||||||
|
|
||||||
|
glm::vec3 ambientColor; // MOHD ambient color (used for interior group lighting)
|
||||||
glm::vec3 boundingBoxMin;
|
glm::vec3 boundingBoxMin;
|
||||||
glm::vec3 boundingBoxMax;
|
glm::vec3 boundingBoxMax;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -122,6 +122,7 @@ struct M2ModelGPU {
|
||||||
bool isKoboldFlame = false; // Model name matches kobold+(candle/torch/mine) (precomputed)
|
bool isKoboldFlame = false; // Model name matches kobold+(candle/torch/mine) (precomputed)
|
||||||
bool isLavaModel = false; // Model name contains lava/molten/magma (UV scroll fallback)
|
bool isLavaModel = false; // Model name contains lava/molten/magma (UV scroll fallback)
|
||||||
bool hasTextureAnimation = false; // True if any batch has UV animation
|
bool hasTextureAnimation = false; // True if any batch has UV animation
|
||||||
|
bool hasTransparentBatches = false; // True if any batch uses alpha-blend or additive (blendMode >= 2)
|
||||||
uint8_t availableLODs = 0; // Bitmask: bit N set if any batch has submeshLevel==N
|
uint8_t availableLODs = 0; // Bitmask: bit N set if any batch has submeshLevel==N
|
||||||
|
|
||||||
// Particle emitter data (kept from M2Model)
|
// Particle emitter data (kept from M2Model)
|
||||||
|
|
|
||||||
|
|
@ -241,13 +241,17 @@ private:
|
||||||
std::unique_ptr<game::ZoneManager> zoneManager;
|
std::unique_ptr<game::ZoneManager> zoneManager;
|
||||||
// Shadow mapping (Vulkan)
|
// Shadow mapping (Vulkan)
|
||||||
static constexpr uint32_t SHADOW_MAP_SIZE = 4096;
|
static constexpr uint32_t SHADOW_MAP_SIZE = 4096;
|
||||||
VkImage shadowDepthImage = VK_NULL_HANDLE;
|
// Per-frame shadow resources: each in-flight frame has its own depth image and
|
||||||
VmaAllocation shadowDepthAlloc = VK_NULL_HANDLE;
|
// framebuffer so that frame N's shadow read and frame N+1's shadow write don't
|
||||||
VkImageView shadowDepthView = VK_NULL_HANDLE;
|
// race on the same image across concurrent GPU submissions.
|
||||||
|
// Array size must match MAX_FRAMES (= 2, defined in the private section below).
|
||||||
|
VkImage shadowDepthImage[2] = {};
|
||||||
|
VmaAllocation shadowDepthAlloc[2] = {};
|
||||||
|
VkImageView shadowDepthView[2] = {};
|
||||||
VkSampler shadowSampler = VK_NULL_HANDLE;
|
VkSampler shadowSampler = VK_NULL_HANDLE;
|
||||||
VkRenderPass shadowRenderPass = VK_NULL_HANDLE;
|
VkRenderPass shadowRenderPass = VK_NULL_HANDLE;
|
||||||
VkFramebuffer shadowFramebuffer = VK_NULL_HANDLE;
|
VkFramebuffer shadowFramebuffer[2] = {};
|
||||||
VkImageLayout shadowDepthLayout_ = VK_IMAGE_LAYOUT_UNDEFINED;
|
VkImageLayout shadowDepthLayout_[2] = {};
|
||||||
glm::mat4 lightSpaceMatrix = glm::mat4(1.0f);
|
glm::mat4 lightSpaceMatrix = glm::mat4(1.0f);
|
||||||
glm::vec3 shadowCenter = glm::vec3(0.0f);
|
glm::vec3 shadowCenter = glm::vec3(0.0f);
|
||||||
bool shadowCenterInitialized = false;
|
bool shadowCenterInitialized = false;
|
||||||
|
|
|
||||||
|
|
@ -152,9 +152,11 @@ struct FinalizingTile {
|
||||||
FinalizationPhase phase = FinalizationPhase::TERRAIN;
|
FinalizationPhase phase = FinalizationPhase::TERRAIN;
|
||||||
|
|
||||||
// Progress indices within current phase
|
// Progress indices within current phase
|
||||||
size_t m2ModelIndex = 0; // Next M2 model to upload
|
size_t m2ModelIndex = 0; // Next M2 model to upload
|
||||||
size_t wmoModelIndex = 0; // Next WMO model to upload
|
size_t m2InstanceIndex = 0; // Next M2 placement to instantiate
|
||||||
size_t wmoDoodadIndex = 0; // Next WMO doodad to upload
|
size_t wmoModelIndex = 0; // Next WMO model to upload
|
||||||
|
size_t wmoInstanceIndex = 0; // Next WMO placement to instantiate
|
||||||
|
size_t wmoDoodadIndex = 0; // Next WMO doodad to upload
|
||||||
|
|
||||||
// Incremental terrain upload state (splits TERRAIN phase across frames)
|
// Incremental terrain upload state (splits TERRAIN phase across frames)
|
||||||
bool terrainPreloaded = false; // True after preloaded textures uploaded
|
bool terrainPreloaded = false; // True after preloaded textures uploaded
|
||||||
|
|
|
||||||
|
|
@ -353,7 +353,9 @@ private:
|
||||||
float heightMapVariance; // 40 (low variance = skip POM)
|
float heightMapVariance; // 40 (low variance = skip POM)
|
||||||
float normalMapStrength; // 44 (0=flat, 1=full, 2=exaggerated)
|
float normalMapStrength; // 44 (0=flat, 1=full, 2=exaggerated)
|
||||||
int32_t isLava; // 48 (1=lava/magma UV scroll)
|
int32_t isLava; // 48 (1=lava/magma UV scroll)
|
||||||
float pad[3]; // 52-60 padding to 64 bytes
|
float wmoAmbientR; // 52 (interior ambient color R)
|
||||||
|
float wmoAmbientG; // 56 (interior ambient color G)
|
||||||
|
float wmoAmbientB; // 60 (interior ambient color B)
|
||||||
}; // 64 bytes total
|
}; // 64 bytes total
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -472,6 +474,7 @@ private:
|
||||||
std::vector<GroupResources> groups;
|
std::vector<GroupResources> groups;
|
||||||
glm::vec3 boundingBoxMin;
|
glm::vec3 boundingBoxMin;
|
||||||
glm::vec3 boundingBoxMax;
|
glm::vec3 boundingBoxMax;
|
||||||
|
glm::vec3 wmoAmbientColor{0.5f, 0.5f, 0.5f}; // From MOHD, used for interior lighting
|
||||||
bool isLowPlatform = false;
|
bool isLowPlatform = false;
|
||||||
|
|
||||||
// Doodad templates (M2 models placed in WMO, stored for instancing)
|
// Doodad templates (M2 models placed in WMO, stored for instancing)
|
||||||
|
|
|
||||||
|
|
@ -210,6 +210,7 @@ private:
|
||||||
void renderMirrorTimers(game::GameHandler& gameHandler);
|
void renderMirrorTimers(game::GameHandler& gameHandler);
|
||||||
void renderCombatText(game::GameHandler& gameHandler);
|
void renderCombatText(game::GameHandler& gameHandler);
|
||||||
void renderPartyFrames(game::GameHandler& gameHandler);
|
void renderPartyFrames(game::GameHandler& gameHandler);
|
||||||
|
void renderBossFrames(game::GameHandler& gameHandler);
|
||||||
void renderGroupInvitePopup(game::GameHandler& gameHandler);
|
void renderGroupInvitePopup(game::GameHandler& gameHandler);
|
||||||
void renderDuelRequestPopup(game::GameHandler& gameHandler);
|
void renderDuelRequestPopup(game::GameHandler& gameHandler);
|
||||||
void renderLootRollPopup(game::GameHandler& gameHandler);
|
void renderLootRollPopup(game::GameHandler& gameHandler);
|
||||||
|
|
@ -227,6 +228,7 @@ private:
|
||||||
void renderTrainerWindow(game::GameHandler& gameHandler);
|
void renderTrainerWindow(game::GameHandler& gameHandler);
|
||||||
void renderTaxiWindow(game::GameHandler& gameHandler);
|
void renderTaxiWindow(game::GameHandler& gameHandler);
|
||||||
void renderDeathScreen(game::GameHandler& gameHandler);
|
void renderDeathScreen(game::GameHandler& gameHandler);
|
||||||
|
void renderReclaimCorpseButton(game::GameHandler& gameHandler);
|
||||||
void renderResurrectDialog(game::GameHandler& gameHandler);
|
void renderResurrectDialog(game::GameHandler& gameHandler);
|
||||||
void renderEscapeMenu();
|
void renderEscapeMenu();
|
||||||
void renderSettingsWindow();
|
void renderSettingsWindow();
|
||||||
|
|
@ -245,6 +247,7 @@ private:
|
||||||
void renderDungeonFinderWindow(game::GameHandler& gameHandler);
|
void renderDungeonFinderWindow(game::GameHandler& gameHandler);
|
||||||
void renderInstanceLockouts(game::GameHandler& gameHandler);
|
void renderInstanceLockouts(game::GameHandler& gameHandler);
|
||||||
void renderNameplates(game::GameHandler& gameHandler);
|
void renderNameplates(game::GameHandler& gameHandler);
|
||||||
|
void renderBattlegroundScore(game::GameHandler& gameHandler);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Inventory screen
|
* Inventory screen
|
||||||
|
|
|
||||||
|
|
@ -288,11 +288,77 @@ bool AudioEngine::playSound2D(const std::vector<uint8_t>& wavData, float volume,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Track this sound for cleanup (decoded PCM shared across plays)
|
// Track this sound for cleanup (decoded PCM shared across plays)
|
||||||
activeSounds_.push_back({sound, audioBuffer, decoded.pcmData});
|
activeSounds_.push_back({sound, audioBuffer, decoded.pcmData, 0u});
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
uint32_t AudioEngine::playSound2DStoppable(const std::vector<uint8_t>& wavData, float volume) {
|
||||||
|
if (!initialized_ || !engine_ || wavData.empty()) return 0;
|
||||||
|
if (masterVolume_ <= 0.0f) return 0;
|
||||||
|
|
||||||
|
DecodedWavCacheEntry decoded;
|
||||||
|
if (!decodeWavCached(wavData, decoded) || !decoded.pcmData || decoded.frames == 0) return 0;
|
||||||
|
|
||||||
|
ma_audio_buffer_config bufferConfig = ma_audio_buffer_config_init(
|
||||||
|
decoded.format, decoded.channels, decoded.frames, decoded.pcmData->data(), nullptr);
|
||||||
|
bufferConfig.sampleRate = decoded.sampleRate;
|
||||||
|
|
||||||
|
ma_audio_buffer* audioBuffer = static_cast<ma_audio_buffer*>(std::malloc(sizeof(ma_audio_buffer)));
|
||||||
|
if (!audioBuffer) return 0;
|
||||||
|
if (ma_audio_buffer_init(&bufferConfig, audioBuffer) != MA_SUCCESS) {
|
||||||
|
std::free(audioBuffer);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
ma_sound* sound = static_cast<ma_sound*>(std::malloc(sizeof(ma_sound)));
|
||||||
|
if (!sound) {
|
||||||
|
ma_audio_buffer_uninit(audioBuffer);
|
||||||
|
std::free(audioBuffer);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
ma_result result = ma_sound_init_from_data_source(
|
||||||
|
engine_, audioBuffer,
|
||||||
|
MA_SOUND_FLAG_DECODE | MA_SOUND_FLAG_ASYNC | MA_SOUND_FLAG_NO_PITCH | MA_SOUND_FLAG_NO_SPATIALIZATION,
|
||||||
|
nullptr, sound);
|
||||||
|
if (result != MA_SUCCESS) {
|
||||||
|
ma_audio_buffer_uninit(audioBuffer);
|
||||||
|
std::free(audioBuffer);
|
||||||
|
std::free(sound);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
ma_sound_set_volume(sound, volume);
|
||||||
|
if (ma_sound_start(sound) != MA_SUCCESS) {
|
||||||
|
ma_sound_uninit(sound);
|
||||||
|
ma_audio_buffer_uninit(audioBuffer);
|
||||||
|
std::free(audioBuffer);
|
||||||
|
std::free(sound);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t id = nextSoundId_++;
|
||||||
|
if (nextSoundId_ == 0) nextSoundId_ = 1; // Skip 0 (sentinel)
|
||||||
|
activeSounds_.push_back({sound, audioBuffer, decoded.pcmData, id});
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
void AudioEngine::stopSound(uint32_t id) {
|
||||||
|
if (id == 0) return;
|
||||||
|
for (auto it = activeSounds_.begin(); it != activeSounds_.end(); ++it) {
|
||||||
|
if (it->id == id) {
|
||||||
|
ma_sound_stop(it->sound);
|
||||||
|
ma_sound_uninit(it->sound);
|
||||||
|
std::free(it->sound);
|
||||||
|
ma_audio_buffer* buffer = static_cast<ma_audio_buffer*>(it->buffer);
|
||||||
|
ma_audio_buffer_uninit(buffer);
|
||||||
|
std::free(buffer);
|
||||||
|
activeSounds_.erase(it);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
bool AudioEngine::playSound2D(const std::string& mpqPath, float volume, float pitch) {
|
bool AudioEngine::playSound2D(const std::string& mpqPath, float volume, float pitch) {
|
||||||
if (!assetManager_) {
|
if (!assetManager_) {
|
||||||
LOG_WARNING("AudioEngine::playSound2D(path): no AssetManager set");
|
LOG_WARNING("AudioEngine::playSound2D(path): no AssetManager set");
|
||||||
|
|
|
||||||
|
|
@ -220,12 +220,22 @@ void SpellSoundManager::playPrecast(MagicSchool school, SpellPower power) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (library) {
|
if (library && !library->empty() && (*library)[0].loaded) {
|
||||||
playSound(*library);
|
stopPrecast(); // Stop any previous precast still playing
|
||||||
|
float volume = 0.75f * volumeScale_;
|
||||||
|
activePrecastId_ = AudioEngine::instance().playSound2DStoppable((*library)[0].data, volume);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void SpellSoundManager::stopPrecast() {
|
||||||
|
if (activePrecastId_ != 0) {
|
||||||
|
AudioEngine::instance().stopSound(activePrecastId_);
|
||||||
|
activePrecastId_ = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void SpellSoundManager::playCast(MagicSchool school) {
|
void SpellSoundManager::playCast(MagicSchool school) {
|
||||||
|
stopPrecast(); // Ensure precast doesn't overlap the cast sound
|
||||||
switch (school) {
|
switch (school) {
|
||||||
case MagicSchool::FIRE:
|
case MagicSchool::FIRE:
|
||||||
playSound(castFireSounds_);
|
playSound(castFireSounds_);
|
||||||
|
|
|
||||||
|
|
@ -1108,8 +1108,8 @@ void Application::update(float deltaTime) {
|
||||||
// Taxi flights move fast (32 u/s) — load further ahead so terrain is ready
|
// Taxi flights move fast (32 u/s) — load further ahead so terrain is ready
|
||||||
// before the camera arrives. Keep updates frequent to spot new tiles early.
|
// before the camera arrives. Keep updates frequent to spot new tiles early.
|
||||||
renderer->getTerrainManager()->setUpdateInterval(onTaxi ? 0.033f : 0.033f);
|
renderer->getTerrainManager()->setUpdateInterval(onTaxi ? 0.033f : 0.033f);
|
||||||
renderer->getTerrainManager()->setLoadRadius(onTaxi ? 6 : 4);
|
renderer->getTerrainManager()->setLoadRadius(onTaxi ? 8 : 4);
|
||||||
renderer->getTerrainManager()->setUnloadRadius(onTaxi ? 9 : 7);
|
renderer->getTerrainManager()->setUnloadRadius(onTaxi ? 12 : 7);
|
||||||
renderer->getTerrainManager()->setTaxiStreamingMode(onTaxi);
|
renderer->getTerrainManager()->setTaxiStreamingMode(onTaxi);
|
||||||
}
|
}
|
||||||
lastTaxiFlight_ = onTaxi;
|
lastTaxiFlight_ = onTaxi;
|
||||||
|
|
@ -1710,6 +1710,10 @@ void Application::setupUICallbacks() {
|
||||||
renderer->getCameraController()->clearMovementInputs();
|
renderer->getCameraController()->clearMovementInputs();
|
||||||
renderer->getCameraController()->suppressMovementFor(0.5f);
|
renderer->getCameraController()->suppressMovementFor(0.5f);
|
||||||
}
|
}
|
||||||
|
// 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();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1950,6 +1954,51 @@ void Application::setupUICallbacks() {
|
||||||
LOG_INFO("Bindpoint set: mapId=", mapId, " pos=(", x, ", ", y, ", ", z, ")");
|
LOG_INFO("Bindpoint set: mapId=", mapId, " pos=(", x, ", ", y, ", ", z, ")");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Hearthstone preload callback: begin loading terrain at the bind point as soon as
|
||||||
|
// the player starts casting Hearthstone. The ~10 s cast gives enough time for
|
||||||
|
// the background streaming workers to bring tiles into the cache so the player
|
||||||
|
// lands on solid ground instead of falling through un-loaded terrain.
|
||||||
|
gameHandler->setHearthstonePreloadCallback([this](uint32_t mapId, float x, float y, float z) {
|
||||||
|
if (!renderer || !assetManager) return;
|
||||||
|
|
||||||
|
auto* terrainMgr = renderer->getTerrainManager();
|
||||||
|
if (!terrainMgr) return;
|
||||||
|
|
||||||
|
// Resolve map name from the cached Map.dbc table
|
||||||
|
std::string mapName;
|
||||||
|
if (auto it = mapNameById_.find(mapId); it != mapNameById_.end()) {
|
||||||
|
mapName = it->second;
|
||||||
|
} else {
|
||||||
|
mapName = mapIdToName(mapId);
|
||||||
|
}
|
||||||
|
if (mapName.empty()) mapName = "Azeroth";
|
||||||
|
|
||||||
|
if (mapId == loadedMapId_) {
|
||||||
|
// Same map: pre-enqueue tiles around the bind point so workers start
|
||||||
|
// loading them now. Uses render-space coords (canonicalToRender).
|
||||||
|
glm::vec3 renderPos = core::coords::canonicalToRender(glm::vec3(x, y, z));
|
||||||
|
auto [tileX, tileY] = core::coords::worldToTile(renderPos.x, renderPos.y);
|
||||||
|
|
||||||
|
std::vector<std::pair<int,int>> tiles;
|
||||||
|
tiles.reserve(25);
|
||||||
|
for (int dy = -2; dy <= 2; dy++)
|
||||||
|
for (int dx = -2; dx <= 2; dx++)
|
||||||
|
tiles.push_back({tileX + dx, tileY + dy});
|
||||||
|
|
||||||
|
terrainMgr->precacheTiles(tiles);
|
||||||
|
LOG_INFO("Hearthstone preload: enqueued ", tiles.size(),
|
||||||
|
" tiles around bind point (same map) tile=[", tileX, ",", tileY, "]");
|
||||||
|
} else {
|
||||||
|
// Different map: warm the file cache so ADT parsing is fast when
|
||||||
|
// loadOnlineWorldTerrain runs its blocking load loop.
|
||||||
|
// homeBindPos_ is canonical; startWorldPreload expects server coords.
|
||||||
|
glm::vec3 server = core::coords::canonicalToServer(glm::vec3(x, y, z));
|
||||||
|
startWorldPreload(mapId, mapName, server.x, server.y);
|
||||||
|
LOG_INFO("Hearthstone preload: started file cache warm for map '", mapName,
|
||||||
|
"' (id=", mapId, ")");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Faction hostility map is built in buildFactionHostilityMap() when character enters world
|
// Faction hostility map is built in buildFactionHostilityMap() when character enters world
|
||||||
|
|
||||||
// Creature spawn callback (online mode) - spawn creature models
|
// Creature spawn callback (online mode) - spawn creature models
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -314,6 +314,308 @@ network::Packet ClassicPacketParsers::buildUseItem(uint8_t bagIndex, uint8_t slo
|
||||||
return packet;
|
return packet;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Classic parseSpellStart — Vanilla 1.12 SMSG_SPELL_START
|
||||||
|
//
|
||||||
|
// Key differences from TBC:
|
||||||
|
// - GUIDs are PackedGuid (variable-length byte mask + non-zero bytes),
|
||||||
|
// NOT full uint64 as in TBC/WotLK.
|
||||||
|
// - castFlags is uint16 (NOT uint32 as in TBC/WotLK).
|
||||||
|
// - SpellCastTargets uses uint16 targetFlags (NOT uint32 as in TBC).
|
||||||
|
//
|
||||||
|
// Format: PackedGuid(casterObj) + PackedGuid(casterUnit) + uint8(castCount)
|
||||||
|
// + uint32(spellId) + uint16(castFlags) + uint32(castTime)
|
||||||
|
// + uint16(targetFlags) [+ PackedGuid(unitTarget) if TARGET_FLAG_UNIT]
|
||||||
|
// ============================================================================
|
||||||
|
bool ClassicPacketParsers::parseSpellStart(network::Packet& packet, SpellStartData& data) {
|
||||||
|
auto rem = [&]() { return packet.getSize() - packet.getReadPos(); };
|
||||||
|
if (rem() < 2) return false;
|
||||||
|
|
||||||
|
data.casterGuid = UpdateObjectParser::readPackedGuid(packet);
|
||||||
|
if (rem() < 1) return false;
|
||||||
|
data.casterUnit = UpdateObjectParser::readPackedGuid(packet);
|
||||||
|
|
||||||
|
// uint8 castCount + uint32 spellId + uint16 castFlags + uint32 castTime = 11 bytes
|
||||||
|
if (rem() < 11) return false;
|
||||||
|
data.castCount = packet.readUInt8();
|
||||||
|
data.spellId = packet.readUInt32();
|
||||||
|
data.castFlags = packet.readUInt16(); // uint16 in Vanilla (uint32 in TBC/WotLK)
|
||||||
|
data.castTime = packet.readUInt32();
|
||||||
|
|
||||||
|
// SpellCastTargets: uint16 targetFlags in Vanilla (uint32 in TBC/WotLK)
|
||||||
|
if (rem() < 2) return true;
|
||||||
|
uint16_t targetFlags = packet.readUInt16();
|
||||||
|
// TARGET_FLAG_UNIT (0x02) or TARGET_FLAG_OBJECT (0x800) carry a packed GUID
|
||||||
|
if (((targetFlags & 0x02) || (targetFlags & 0x800)) && rem() >= 1) {
|
||||||
|
data.targetGuid = UpdateObjectParser::readPackedGuid(packet);
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_DEBUG("[Classic] Spell start: spell=", data.spellId, " castTime=", data.castTime, "ms");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Classic parseSpellGo — Vanilla 1.12 SMSG_SPELL_GO
|
||||||
|
//
|
||||||
|
// Same GUID and castFlags format differences as parseSpellStart:
|
||||||
|
// - GUIDs are PackedGuid (not full uint64)
|
||||||
|
// - castFlags is uint16 (not uint32)
|
||||||
|
// - Hit/miss target GUIDs are also PackedGuid in Vanilla
|
||||||
|
//
|
||||||
|
// Format: PackedGuid(casterObj) + PackedGuid(casterUnit) + uint8(castCount)
|
||||||
|
// + uint32(spellId) + uint16(castFlags)
|
||||||
|
// + uint8(hitCount) + [PackedGuid(hitTarget) × hitCount]
|
||||||
|
// + uint8(missCount) + [PackedGuid(missTarget) + uint8(missType)] × missCount
|
||||||
|
// ============================================================================
|
||||||
|
bool ClassicPacketParsers::parseSpellGo(network::Packet& packet, SpellGoData& data) {
|
||||||
|
auto rem = [&]() { return packet.getSize() - packet.getReadPos(); };
|
||||||
|
if (rem() < 2) return false;
|
||||||
|
|
||||||
|
data.casterGuid = UpdateObjectParser::readPackedGuid(packet);
|
||||||
|
if (rem() < 1) return false;
|
||||||
|
data.casterUnit = UpdateObjectParser::readPackedGuid(packet);
|
||||||
|
|
||||||
|
// uint8 castCount + uint32 spellId + uint16 castFlags = 7 bytes
|
||||||
|
if (rem() < 7) return false;
|
||||||
|
data.castCount = packet.readUInt8();
|
||||||
|
data.spellId = packet.readUInt32();
|
||||||
|
data.castFlags = packet.readUInt16(); // uint16 in Vanilla (uint32 in TBC/WotLK)
|
||||||
|
|
||||||
|
// Hit targets
|
||||||
|
if (rem() < 1) return true;
|
||||||
|
data.hitCount = packet.readUInt8();
|
||||||
|
data.hitTargets.reserve(data.hitCount);
|
||||||
|
for (uint8_t i = 0; i < data.hitCount && rem() >= 1; ++i) {
|
||||||
|
data.hitTargets.push_back(UpdateObjectParser::readPackedGuid(packet));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Miss targets
|
||||||
|
if (rem() < 1) return true;
|
||||||
|
data.missCount = packet.readUInt8();
|
||||||
|
data.missTargets.reserve(data.missCount);
|
||||||
|
for (uint8_t i = 0; i < data.missCount && rem() >= 2; ++i) {
|
||||||
|
SpellGoMissEntry m;
|
||||||
|
m.targetGuid = UpdateObjectParser::readPackedGuid(packet);
|
||||||
|
if (rem() < 1) break;
|
||||||
|
m.missType = packet.readUInt8();
|
||||||
|
data.missTargets.push_back(m);
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_DEBUG("[Classic] Spell go: spell=", data.spellId, " hits=", (int)data.hitCount,
|
||||||
|
" misses=", (int)data.missCount);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Classic parseAttackerStateUpdate — Vanilla 1.12 SMSG_ATTACKERSTATEUPDATE
|
||||||
|
//
|
||||||
|
// Identical to TBC format except GUIDs are PackedGuid (not full uint64).
|
||||||
|
// Format: uint32(hitInfo) + PackedGuid(attacker) + PackedGuid(target)
|
||||||
|
// + int32(totalDamage) + uint8(subDamageCount)
|
||||||
|
// + [per sub: uint32(schoolMask) + float(damage) + uint32(intDamage)
|
||||||
|
// + uint32(absorbed) + uint32(resisted)]
|
||||||
|
// + uint32(victimState) + int32(overkill) [+ uint32(blocked)]
|
||||||
|
// ============================================================================
|
||||||
|
bool ClassicPacketParsers::parseAttackerStateUpdate(network::Packet& packet, AttackerStateUpdateData& data) {
|
||||||
|
auto rem = [&]() { return packet.getSize() - packet.getReadPos(); };
|
||||||
|
if (rem() < 5) return false; // hitInfo(4) + at least GUID mask byte(1)
|
||||||
|
|
||||||
|
data.hitInfo = packet.readUInt32();
|
||||||
|
data.attackerGuid = UpdateObjectParser::readPackedGuid(packet); // PackedGuid in Vanilla
|
||||||
|
if (rem() < 1) return false;
|
||||||
|
data.targetGuid = UpdateObjectParser::readPackedGuid(packet); // PackedGuid in Vanilla
|
||||||
|
|
||||||
|
if (rem() < 5) return false; // int32 totalDamage + uint8 subDamageCount
|
||||||
|
data.totalDamage = static_cast<int32_t>(packet.readUInt32());
|
||||||
|
data.subDamageCount = packet.readUInt8();
|
||||||
|
|
||||||
|
for (uint8_t i = 0; i < data.subDamageCount && rem() >= 20; ++i) {
|
||||||
|
SubDamage sub;
|
||||||
|
sub.schoolMask = packet.readUInt32();
|
||||||
|
sub.damage = packet.readFloat();
|
||||||
|
sub.intDamage = packet.readUInt32();
|
||||||
|
sub.absorbed = packet.readUInt32();
|
||||||
|
sub.resisted = packet.readUInt32();
|
||||||
|
data.subDamages.push_back(sub);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rem() < 8) return true;
|
||||||
|
data.victimState = packet.readUInt32();
|
||||||
|
data.overkill = static_cast<int32_t>(packet.readUInt32());
|
||||||
|
|
||||||
|
if (rem() >= 4) {
|
||||||
|
data.blocked = packet.readUInt32();
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_INFO("[Classic] Melee hit: ", data.totalDamage, " damage",
|
||||||
|
data.isCrit() ? " (CRIT)" : "",
|
||||||
|
data.isMiss() ? " (MISS)" : "");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Classic parseSpellDamageLog — Vanilla 1.12 SMSG_SPELLNONMELEEDAMAGELOG
|
||||||
|
//
|
||||||
|
// Identical to TBC except GUIDs are PackedGuid (not full uint64).
|
||||||
|
// Format: PackedGuid(target) + PackedGuid(caster) + uint32(spellId)
|
||||||
|
// + uint32(damage) + uint8(schoolMask) + uint32(absorbed) + uint32(resisted)
|
||||||
|
// + uint8(periodicLog) + uint8(unused) + uint32(blocked) + uint32(flags)
|
||||||
|
// ============================================================================
|
||||||
|
bool ClassicPacketParsers::parseSpellDamageLog(network::Packet& packet, SpellDamageLogData& data) {
|
||||||
|
auto rem = [&]() { return packet.getSize() - packet.getReadPos(); };
|
||||||
|
if (rem() < 2) return false;
|
||||||
|
|
||||||
|
data.targetGuid = UpdateObjectParser::readPackedGuid(packet); // PackedGuid in Vanilla
|
||||||
|
if (rem() < 1) return false;
|
||||||
|
data.attackerGuid = UpdateObjectParser::readPackedGuid(packet); // PackedGuid in Vanilla
|
||||||
|
|
||||||
|
// uint32(spellId) + uint32(damage) + uint8(schoolMask) + uint32(absorbed)
|
||||||
|
// + uint32(resisted) + uint8 + uint8 + uint32(blocked) + uint32(flags) = 21 bytes
|
||||||
|
if (rem() < 21) return false;
|
||||||
|
data.spellId = packet.readUInt32();
|
||||||
|
data.damage = packet.readUInt32();
|
||||||
|
data.schoolMask = packet.readUInt8();
|
||||||
|
data.absorbed = packet.readUInt32();
|
||||||
|
data.resisted = packet.readUInt32();
|
||||||
|
packet.readUInt8(); // periodicLog
|
||||||
|
packet.readUInt8(); // unused
|
||||||
|
packet.readUInt32(); // blocked
|
||||||
|
uint32_t flags = packet.readUInt32();
|
||||||
|
data.isCrit = (flags & 0x02) != 0;
|
||||||
|
data.overkill = 0; // no overkill field in Vanilla (same as TBC)
|
||||||
|
|
||||||
|
LOG_INFO("[Classic] Spell damage: spellId=", data.spellId, " dmg=", data.damage,
|
||||||
|
data.isCrit ? " CRIT" : "");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Classic parseSpellHealLog — Vanilla 1.12 SMSG_SPELLHEALLOG
|
||||||
|
//
|
||||||
|
// Identical to TBC except GUIDs are PackedGuid (not full uint64).
|
||||||
|
// Format: PackedGuid(target) + PackedGuid(caster) + uint32(spellId)
|
||||||
|
// + uint32(heal) + uint32(overheal) + uint8(crit)
|
||||||
|
// ============================================================================
|
||||||
|
bool ClassicPacketParsers::parseSpellHealLog(network::Packet& packet, SpellHealLogData& data) {
|
||||||
|
auto rem = [&]() { return packet.getSize() - packet.getReadPos(); };
|
||||||
|
if (rem() < 2) return false;
|
||||||
|
|
||||||
|
data.targetGuid = UpdateObjectParser::readPackedGuid(packet); // PackedGuid in Vanilla
|
||||||
|
if (rem() < 1) return false;
|
||||||
|
data.casterGuid = UpdateObjectParser::readPackedGuid(packet); // PackedGuid in Vanilla
|
||||||
|
|
||||||
|
if (rem() < 13) return false; // uint32 + uint32 + uint32 + uint8 = 13 bytes
|
||||||
|
data.spellId = packet.readUInt32();
|
||||||
|
data.heal = packet.readUInt32();
|
||||||
|
data.overheal = packet.readUInt32();
|
||||||
|
data.isCrit = (packet.readUInt8() != 0);
|
||||||
|
|
||||||
|
LOG_INFO("[Classic] Spell heal: spellId=", data.spellId, " heal=", data.heal,
|
||||||
|
data.isCrit ? " CRIT" : "");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Classic parseAuraUpdate — Vanilla 1.12 SMSG_AURA_UPDATE / SMSG_AURA_UPDATE_ALL
|
||||||
|
//
|
||||||
|
// Classic has SMSG_AURA_UPDATE (TBC does not — TBC uses a different aura system
|
||||||
|
// and the TBC override returns false with a warning). Classic inherits TBC's
|
||||||
|
// override by default, so this override is needed to restore aura tracking.
|
||||||
|
//
|
||||||
|
// Classic aura flags differ from WotLK:
|
||||||
|
// 0x01/0x02/0x04 = effect indices active (same as WotLK)
|
||||||
|
// 0x08 = CANCELABLE / NOT-NEGATIVE (WotLK: 0x08 = NOT_CASTER)
|
||||||
|
// 0x10 = DURATION (WotLK: 0x20 = DURATION)
|
||||||
|
// 0x20 = NOT_CASTER (WotLK: no caster GUID at all if 0x08)
|
||||||
|
// 0x40 = POSITIVE (WotLK: 0x40 = EFFECT_AMOUNTS)
|
||||||
|
//
|
||||||
|
// Key differences from WotLK parser:
|
||||||
|
// - No caster GUID field in Classic SMSG_AURA_UPDATE packets
|
||||||
|
// - DURATION bit is 0x10, not 0x20
|
||||||
|
// - No effect amounts field (WotLK 0x40 = EFFECT_AMOUNTS does not exist here)
|
||||||
|
//
|
||||||
|
// Format: PackedGuid(entity) + [uint8(slot) + uint32(spellId)
|
||||||
|
// [+ uint8(flags) + uint8(level) + uint8(charges)
|
||||||
|
// + [uint32(maxDuration) + uint32(duration) if flags & 0x10]]*
|
||||||
|
// ============================================================================
|
||||||
|
bool ClassicPacketParsers::parseAuraUpdate(network::Packet& packet, AuraUpdateData& data, bool isAll) {
|
||||||
|
auto rem = [&]() { return packet.getSize() - packet.getReadPos(); };
|
||||||
|
if (rem() < 1) return false;
|
||||||
|
|
||||||
|
data.guid = UpdateObjectParser::readPackedGuid(packet);
|
||||||
|
|
||||||
|
while (rem() > 0) {
|
||||||
|
if (rem() < 1) break;
|
||||||
|
uint8_t slot = packet.readUInt8();
|
||||||
|
if (rem() < 4) break;
|
||||||
|
uint32_t spellId = packet.readUInt32();
|
||||||
|
|
||||||
|
AuraSlot aura;
|
||||||
|
if (spellId != 0) {
|
||||||
|
aura.spellId = spellId;
|
||||||
|
if (rem() < 3) { data.updates.push_back({slot, aura}); break; }
|
||||||
|
aura.flags = packet.readUInt8();
|
||||||
|
aura.level = packet.readUInt8();
|
||||||
|
aura.charges = packet.readUInt8();
|
||||||
|
|
||||||
|
// Classic DURATION flag is 0x10 (WotLK uses 0x20)
|
||||||
|
if ((aura.flags & 0x10) && rem() >= 8) {
|
||||||
|
aura.maxDurationMs = static_cast<int32_t>(packet.readUInt32());
|
||||||
|
aura.durationMs = static_cast<int32_t>(packet.readUInt32());
|
||||||
|
}
|
||||||
|
// No caster GUID field in Classic (WotLK added it gated by 0x08 NOT_CASTER)
|
||||||
|
// No effect amounts field in Classic (WotLK added it gated by 0x40)
|
||||||
|
}
|
||||||
|
|
||||||
|
data.updates.push_back({slot, aura});
|
||||||
|
if (!isAll) break;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_DEBUG("[Classic] Aura update for 0x", std::hex, data.guid, std::dec,
|
||||||
|
": ", data.updates.size(), " slots");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Classic SMSG_NAME_QUERY_RESPONSE format (1.12 / vmangos):
|
||||||
|
// uint64 guid (full, GetObjectGuid)
|
||||||
|
// CString name
|
||||||
|
// CString realmName (usually empty = single \0 byte)
|
||||||
|
// uint32 race
|
||||||
|
// uint32 gender
|
||||||
|
// uint32 class
|
||||||
|
//
|
||||||
|
// TBC Variant A (inherited from TbcPacketParsers) skips the realmName CString,
|
||||||
|
// causing it to misread the uint32 race field (absorbs the realmName \0 byte
|
||||||
|
// as the low byte), producing race=0 and shifted gender/class values.
|
||||||
|
// ============================================================================
|
||||||
|
bool ClassicPacketParsers::parseNameQueryResponse(network::Packet& packet, NameQueryResponseData& data) {
|
||||||
|
data = NameQueryResponseData{};
|
||||||
|
|
||||||
|
auto rem = [&]() { return packet.getSize() - packet.getReadPos(); };
|
||||||
|
if (rem() < 8) return false;
|
||||||
|
|
||||||
|
data.guid = packet.readUInt64(); // full uint64, not PackedGuid
|
||||||
|
data.name = packet.readString(); // null-terminated name
|
||||||
|
if (rem() == 0) return !data.name.empty();
|
||||||
|
|
||||||
|
data.realmName = packet.readString(); // null-terminated realm name (usually "")
|
||||||
|
if (rem() < 12) return !data.name.empty();
|
||||||
|
|
||||||
|
uint32_t race = packet.readUInt32();
|
||||||
|
uint32_t gender = packet.readUInt32();
|
||||||
|
uint32_t cls = packet.readUInt32();
|
||||||
|
data.race = static_cast<uint8_t>(race & 0xFF);
|
||||||
|
data.gender = static_cast<uint8_t>(gender & 0xFF);
|
||||||
|
data.classId = static_cast<uint8_t>(cls & 0xFF);
|
||||||
|
data.found = 0;
|
||||||
|
|
||||||
|
LOG_DEBUG("[Classic] Name query response: ", data.name,
|
||||||
|
" (race=", (int)data.race, " gender=", (int)data.gender,
|
||||||
|
" class=", (int)data.classId, ")");
|
||||||
|
return !data.name.empty();
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Classic SMSG_CAST_FAILED: no castCount byte (added in TBC/WotLK)
|
// Classic SMSG_CAST_FAILED: no castCount byte (added in TBC/WotLK)
|
||||||
// Format: spellId(u32) + result(u8)
|
// Format: spellId(u32) + result(u8)
|
||||||
|
|
@ -1282,6 +1584,16 @@ bool ClassicPacketParsers::parseQuestDetails(network::Packet& packet, QuestDetai
|
||||||
/*activateAccept*/ packet.readUInt8();
|
/*activateAccept*/ packet.readUInt8();
|
||||||
data.suggestedPlayers = packet.readUInt32();
|
data.suggestedPlayers = packet.readUInt32();
|
||||||
|
|
||||||
|
// Vanilla 1.12: emote section before reward items
|
||||||
|
// Format: emoteCount(u32) + [delay(u32) + type(u32)] × emoteCount
|
||||||
|
if (packet.getReadPos() + 4 <= packet.getSize()) {
|
||||||
|
uint32_t emoteCount = packet.readUInt32();
|
||||||
|
for (uint32_t i = 0; i < emoteCount && packet.getReadPos() + 8 <= packet.getSize(); ++i) {
|
||||||
|
packet.readUInt32(); // delay
|
||||||
|
packet.readUInt32(); // type
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Choice reward items: variable count + 3 uint32s each
|
// Choice reward items: variable count + 3 uint32s each
|
||||||
if (packet.getReadPos() + 4 <= packet.getSize()) {
|
if (packet.getReadPos() + 4 <= packet.getSize()) {
|
||||||
uint32_t choiceCount = packet.readUInt32();
|
uint32_t choiceCount = packet.readUInt32();
|
||||||
|
|
@ -1309,5 +1621,42 @@ bool ClassicPacketParsers::parseQuestDetails(network::Packet& packet, QuestDetai
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// ClassicPacketParsers::parseCreatureQueryResponse
|
||||||
|
//
|
||||||
|
// Classic 1.12 SMSG_CREATURE_QUERY_RESPONSE lacks the iconName CString field
|
||||||
|
// that TBC 2.4.3 and WotLK 3.3.5a include between subName and typeFlags.
|
||||||
|
// Without this override, the TBC/WotLK parser reads typeFlags bytes as the
|
||||||
|
// iconName string, shifting typeFlags/creatureType/family/rank by 1-4 bytes.
|
||||||
|
// ============================================================================
|
||||||
|
bool ClassicPacketParsers::parseCreatureQueryResponse(network::Packet& packet,
|
||||||
|
CreatureQueryResponseData& data) {
|
||||||
|
data.entry = packet.readUInt32();
|
||||||
|
if (data.entry & 0x80000000) {
|
||||||
|
data.entry &= ~0x80000000;
|
||||||
|
data.name = "";
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
data.name = packet.readString();
|
||||||
|
packet.readString(); // name2
|
||||||
|
packet.readString(); // name3
|
||||||
|
packet.readString(); // name4
|
||||||
|
data.subName = packet.readString();
|
||||||
|
// NOTE: NO iconName field in Classic 1.12 — goes straight to typeFlags
|
||||||
|
if (packet.getReadPos() + 16 > packet.getSize()) {
|
||||||
|
LOG_WARNING("[Classic] Creature query: truncated at typeFlags (entry=", data.entry, ")");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
data.typeFlags = packet.readUInt32();
|
||||||
|
data.creatureType = packet.readUInt32();
|
||||||
|
data.family = packet.readUInt32();
|
||||||
|
data.rank = packet.readUInt32();
|
||||||
|
|
||||||
|
LOG_DEBUG("[Classic] Creature query: ", data.name, " type=", data.creatureType,
|
||||||
|
" rank=", data.rank);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace game
|
} // namespace game
|
||||||
} // namespace wowee
|
} // namespace wowee
|
||||||
|
|
|
||||||
|
|
@ -497,6 +497,199 @@ bool TbcPacketParsers::parseUpdateObject(network::Packet& packet, UpdateObjectDa
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// TBC 2.4.3 SMSG_GOSSIP_MESSAGE
|
||||||
|
// Identical to WotLK except each quest entry lacks questFlags(u32) and
|
||||||
|
// isRepeatable(u8) that WotLK added. Without this override the WotLK parser
|
||||||
|
// reads those 5 bytes as part of the quest title, corrupting all gossip quests.
|
||||||
|
// ============================================================================
|
||||||
|
bool TbcPacketParsers::parseGossipMessage(network::Packet& packet, GossipMessageData& data) {
|
||||||
|
if (packet.getSize() - packet.getReadPos() < 16) return false;
|
||||||
|
|
||||||
|
data.npcGuid = packet.readUInt64();
|
||||||
|
data.menuId = packet.readUInt32(); // TBC added menuId (Classic doesn't have it)
|
||||||
|
data.titleTextId = packet.readUInt32();
|
||||||
|
uint32_t optionCount = packet.readUInt32();
|
||||||
|
|
||||||
|
data.options.clear();
|
||||||
|
data.options.reserve(optionCount);
|
||||||
|
for (uint32_t i = 0; i < optionCount; ++i) {
|
||||||
|
GossipOption opt;
|
||||||
|
opt.id = packet.readUInt32();
|
||||||
|
opt.icon = packet.readUInt8();
|
||||||
|
opt.isCoded = (packet.readUInt8() != 0);
|
||||||
|
opt.boxMoney = packet.readUInt32();
|
||||||
|
opt.text = packet.readString();
|
||||||
|
opt.boxText = packet.readString();
|
||||||
|
data.options.push_back(opt);
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t questCount = packet.readUInt32();
|
||||||
|
data.quests.clear();
|
||||||
|
data.quests.reserve(questCount);
|
||||||
|
for (uint32_t i = 0; i < questCount; ++i) {
|
||||||
|
GossipQuestItem quest;
|
||||||
|
quest.questId = packet.readUInt32();
|
||||||
|
quest.questIcon = packet.readUInt32();
|
||||||
|
quest.questLevel = static_cast<int32_t>(packet.readUInt32());
|
||||||
|
// TBC 2.4.3: NO questFlags(u32) and NO isRepeatable(u8) here
|
||||||
|
// WotLK adds these 5 bytes — reading them from TBC garbles the quest title
|
||||||
|
quest.questFlags = 0;
|
||||||
|
quest.isRepeatable = 0;
|
||||||
|
quest.title = normalizeWowTextTokens(packet.readString());
|
||||||
|
data.quests.push_back(quest);
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_INFO("[TBC] Gossip: ", optionCount, " options, ", questCount, " quests");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// TBC 2.4.3 SMSG_MONSTER_MOVE
|
||||||
|
// Identical to WotLK except WotLK added a uint8 unk byte immediately after the
|
||||||
|
// packed GUID (toggles MOVEMENTFLAG2_UNK7). TBC does NOT have this byte.
|
||||||
|
// Without this override, all NPC movement positions/durations are offset by 1
|
||||||
|
// byte and parse as garbage.
|
||||||
|
// ============================================================================
|
||||||
|
bool TbcPacketParsers::parseMonsterMove(network::Packet& packet, MonsterMoveData& data) {
|
||||||
|
data.guid = UpdateObjectParser::readPackedGuid(packet);
|
||||||
|
if (data.guid == 0) return false;
|
||||||
|
// No unk byte here in TBC 2.4.3
|
||||||
|
|
||||||
|
if (packet.getReadPos() + 12 > packet.getSize()) return false;
|
||||||
|
data.x = packet.readFloat();
|
||||||
|
data.y = packet.readFloat();
|
||||||
|
data.z = packet.readFloat();
|
||||||
|
|
||||||
|
if (packet.getReadPos() + 4 > packet.getSize()) return false;
|
||||||
|
packet.readUInt32(); // splineId
|
||||||
|
|
||||||
|
if (packet.getReadPos() >= packet.getSize()) return false;
|
||||||
|
data.moveType = packet.readUInt8();
|
||||||
|
|
||||||
|
if (data.moveType == 1) {
|
||||||
|
data.destX = data.x;
|
||||||
|
data.destY = data.y;
|
||||||
|
data.destZ = data.z;
|
||||||
|
data.hasDest = false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.moveType == 2) {
|
||||||
|
if (packet.getReadPos() + 12 > packet.getSize()) return false;
|
||||||
|
packet.readFloat(); packet.readFloat(); packet.readFloat();
|
||||||
|
} else if (data.moveType == 3) {
|
||||||
|
if (packet.getReadPos() + 8 > packet.getSize()) return false;
|
||||||
|
data.facingTarget = packet.readUInt64();
|
||||||
|
} else if (data.moveType == 4) {
|
||||||
|
if (packet.getReadPos() + 4 > packet.getSize()) return false;
|
||||||
|
data.facingAngle = packet.readFloat();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (packet.getReadPos() + 4 > packet.getSize()) return false;
|
||||||
|
data.splineFlags = packet.readUInt32();
|
||||||
|
|
||||||
|
// TBC 2.4.3 SplineFlags animation bit is same as WotLK: 0x00400000
|
||||||
|
if (data.splineFlags & 0x00400000) {
|
||||||
|
if (packet.getReadPos() + 5 > packet.getSize()) return false;
|
||||||
|
packet.readUInt8(); // animationType
|
||||||
|
packet.readUInt32(); // effectStartTime
|
||||||
|
}
|
||||||
|
|
||||||
|
if (packet.getReadPos() + 4 > packet.getSize()) return false;
|
||||||
|
data.duration = packet.readUInt32();
|
||||||
|
|
||||||
|
if (data.splineFlags & 0x00000800) {
|
||||||
|
if (packet.getReadPos() + 8 > packet.getSize()) return false;
|
||||||
|
packet.readFloat(); // verticalAcceleration
|
||||||
|
packet.readUInt32(); // effectStartTime
|
||||||
|
}
|
||||||
|
|
||||||
|
if (packet.getReadPos() + 4 > packet.getSize()) return false;
|
||||||
|
uint32_t pointCount = packet.readUInt32();
|
||||||
|
if (pointCount == 0) return true;
|
||||||
|
if (pointCount > 16384) return false;
|
||||||
|
|
||||||
|
bool uncompressed = (data.splineFlags & (0x00080000 | 0x00002000)) != 0;
|
||||||
|
if (uncompressed) {
|
||||||
|
for (uint32_t i = 0; i < pointCount - 1; i++) {
|
||||||
|
if (packet.getReadPos() + 12 > packet.getSize()) return true;
|
||||||
|
packet.readFloat(); packet.readFloat(); packet.readFloat();
|
||||||
|
}
|
||||||
|
if (packet.getReadPos() + 12 > packet.getSize()) return true;
|
||||||
|
data.destX = packet.readFloat();
|
||||||
|
data.destY = packet.readFloat();
|
||||||
|
data.destZ = packet.readFloat();
|
||||||
|
data.hasDest = true;
|
||||||
|
} else {
|
||||||
|
if (packet.getReadPos() + 12 > packet.getSize()) return true;
|
||||||
|
data.destX = packet.readFloat();
|
||||||
|
data.destY = packet.readFloat();
|
||||||
|
data.destZ = packet.readFloat();
|
||||||
|
data.hasDest = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_DEBUG("[TBC] MonsterMove: guid=0x", std::hex, data.guid, std::dec,
|
||||||
|
" type=", (int)data.moveType, " dur=", data.duration, "ms",
|
||||||
|
" dest=(", data.destX, ",", data.destY, ",", data.destZ, ")");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// TBC 2.4.3 CMSG_CAST_SPELL
|
||||||
|
// Format: castCount(u8) + spellId(u32) + SpellCastTargets
|
||||||
|
// WotLK 3.3.5a adds castFlags(u8) between spellId and targets — TBC does NOT.
|
||||||
|
// ============================================================================
|
||||||
|
network::Packet TbcPacketParsers::buildCastSpell(uint32_t spellId, uint64_t targetGuid, uint8_t castCount) {
|
||||||
|
network::Packet packet(wireOpcode(LogicalOpcode::CMSG_CAST_SPELL));
|
||||||
|
packet.writeUInt8(castCount);
|
||||||
|
packet.writeUInt32(spellId);
|
||||||
|
// No castFlags byte in TBC 2.4.3
|
||||||
|
|
||||||
|
if (targetGuid != 0) {
|
||||||
|
packet.writeUInt32(0x02); // TARGET_FLAG_UNIT
|
||||||
|
// Write packed GUID
|
||||||
|
uint8_t mask = 0;
|
||||||
|
uint8_t bytes[8];
|
||||||
|
int byteCount = 0;
|
||||||
|
uint64_t g = targetGuid;
|
||||||
|
for (int i = 0; i < 8; ++i) {
|
||||||
|
uint8_t b = g & 0xFF;
|
||||||
|
if (b != 0) {
|
||||||
|
mask |= (1 << i);
|
||||||
|
bytes[byteCount++] = b;
|
||||||
|
}
|
||||||
|
g >>= 8;
|
||||||
|
}
|
||||||
|
packet.writeUInt8(mask);
|
||||||
|
for (int i = 0; i < byteCount; ++i)
|
||||||
|
packet.writeUInt8(bytes[i]);
|
||||||
|
} else {
|
||||||
|
packet.writeUInt32(0x00); // TARGET_FLAG_SELF
|
||||||
|
}
|
||||||
|
|
||||||
|
return packet;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// TBC 2.4.3 CMSG_USE_ITEM
|
||||||
|
// Format: bag(u8) + slot(u8) + castCount(u8) + spellId(u32) + itemGuid(u64) +
|
||||||
|
// castFlags(u8) + SpellCastTargets
|
||||||
|
// WotLK 3.3.5a adds glyphIndex(u32) between itemGuid and castFlags — TBC does NOT.
|
||||||
|
// ============================================================================
|
||||||
|
network::Packet TbcPacketParsers::buildUseItem(uint8_t bagIndex, uint8_t slotIndex, uint64_t itemGuid, uint32_t spellId) {
|
||||||
|
network::Packet packet(wireOpcode(LogicalOpcode::CMSG_USE_ITEM));
|
||||||
|
packet.writeUInt8(bagIndex);
|
||||||
|
packet.writeUInt8(slotIndex);
|
||||||
|
packet.writeUInt8(0); // cast count
|
||||||
|
packet.writeUInt32(spellId); // on-use spell id
|
||||||
|
packet.writeUInt64(itemGuid); // full 8-byte GUID
|
||||||
|
// No glyph index field in TBC 2.4.3
|
||||||
|
packet.writeUInt8(0); // cast flags
|
||||||
|
packet.writeUInt32(0x00); // SpellCastTargets: TARGET_FLAG_SELF
|
||||||
|
return packet;
|
||||||
|
}
|
||||||
|
|
||||||
network::Packet TbcPacketParsers::buildAcceptQuestPacket(uint64_t npcGuid, uint32_t questId) {
|
network::Packet TbcPacketParsers::buildAcceptQuestPacket(uint64_t npcGuid, uint32_t questId) {
|
||||||
network::Packet packet(wireOpcode(Opcode::CMSG_QUESTGIVER_ACCEPT_QUEST));
|
network::Packet packet(wireOpcode(Opcode::CMSG_QUESTGIVER_ACCEPT_QUEST));
|
||||||
packet.writeUInt64(npcGuid);
|
packet.writeUInt64(npcGuid);
|
||||||
|
|
@ -505,6 +698,20 @@ network::Packet TbcPacketParsers::buildAcceptQuestPacket(uint64_t npcGuid, uint3
|
||||||
return packet;
|
return packet;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// TBC 2.4.3 CMSG_QUESTGIVER_QUERY_QUEST
|
||||||
|
//
|
||||||
|
// WotLK adds a trailing uint8 isDialogContinued byte; TBC does not.
|
||||||
|
// TBC format: guid(8) + questId(4) = 12 bytes.
|
||||||
|
// ============================================================================
|
||||||
|
network::Packet TbcPacketParsers::buildQueryQuestPacket(uint64_t npcGuid, uint32_t questId) {
|
||||||
|
network::Packet packet(wireOpcode(Opcode::CMSG_QUESTGIVER_QUERY_QUEST));
|
||||||
|
packet.writeUInt64(npcGuid);
|
||||||
|
packet.writeUInt32(questId);
|
||||||
|
// No isDialogContinued byte (WotLK-only addition)
|
||||||
|
return packet;
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// TBC parseAuraUpdate - SMSG_AURA_UPDATE doesn't exist in TBC
|
// TBC parseAuraUpdate - SMSG_AURA_UPDATE doesn't exist in TBC
|
||||||
// TBC uses inline aura update fields + SMSG_INIT_EXTRA_AURA_INFO_OBSOLETE (0x3A3) /
|
// TBC uses inline aura update fields + SMSG_INIT_EXTRA_AURA_INFO_OBSOLETE (0x3A3) /
|
||||||
|
|
@ -696,5 +903,294 @@ bool TbcPacketParsers::parseItemQueryResponse(network::Packet& packet, ItemQuery
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// TbcPacketParsers::parseMailList — TBC 2.4.3 SMSG_MAIL_LIST_RESULT
|
||||||
|
//
|
||||||
|
// Differences from WotLK 3.3.5a (base implementation):
|
||||||
|
// - Header: uint8 count only (WotLK: uint32 totalCount + uint8 shownCount)
|
||||||
|
// - No body field — subject IS the full text (WotLK added body when mailTemplateId==0)
|
||||||
|
// - Attachment item GUID: full uint64 (WotLK: uint32 low GUID)
|
||||||
|
// - Attachment enchants: 7 × uint32 id only (WotLK: 7 × {id+duration+charges} = 84 bytes)
|
||||||
|
// - Header fields: cod + itemTextId + stationery (WotLK has extra unknown uint32 between
|
||||||
|
// itemTextId and stationery)
|
||||||
|
// ============================================================================
|
||||||
|
bool TbcPacketParsers::parseMailList(network::Packet& packet, std::vector<MailMessage>& inbox) {
|
||||||
|
size_t remaining = packet.getSize() - packet.getReadPos();
|
||||||
|
if (remaining < 1) return false;
|
||||||
|
|
||||||
|
uint8_t count = packet.readUInt8();
|
||||||
|
LOG_INFO("SMSG_MAIL_LIST_RESULT (TBC): count=", (int)count);
|
||||||
|
|
||||||
|
inbox.clear();
|
||||||
|
inbox.reserve(count);
|
||||||
|
|
||||||
|
for (uint8_t i = 0; i < count; ++i) {
|
||||||
|
remaining = packet.getSize() - packet.getReadPos();
|
||||||
|
if (remaining < 2) break;
|
||||||
|
|
||||||
|
uint16_t msgSize = packet.readUInt16();
|
||||||
|
size_t startPos = packet.getReadPos();
|
||||||
|
|
||||||
|
MailMessage msg;
|
||||||
|
if (remaining < static_cast<size_t>(msgSize) + 2) {
|
||||||
|
LOG_WARNING("[TBC] Mail entry ", i, " truncated");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
msg.messageId = packet.readUInt32();
|
||||||
|
msg.messageType = packet.readUInt8();
|
||||||
|
|
||||||
|
switch (msg.messageType) {
|
||||||
|
case 0: msg.senderGuid = packet.readUInt64(); break;
|
||||||
|
default: msg.senderEntry = packet.readUInt32(); break;
|
||||||
|
}
|
||||||
|
|
||||||
|
msg.cod = packet.readUInt32();
|
||||||
|
packet.readUInt32(); // itemTextId
|
||||||
|
// NOTE: TBC has NO extra unknown uint32 here (WotLK added one between itemTextId and stationery)
|
||||||
|
msg.stationeryId = packet.readUInt32();
|
||||||
|
msg.money = packet.readUInt32();
|
||||||
|
msg.flags = packet.readUInt32();
|
||||||
|
msg.expirationTime = packet.readFloat();
|
||||||
|
msg.mailTemplateId = packet.readUInt32();
|
||||||
|
msg.subject = packet.readString();
|
||||||
|
// TBC has no separate body field at all
|
||||||
|
|
||||||
|
uint8_t attachCount = packet.readUInt8();
|
||||||
|
msg.attachments.reserve(attachCount);
|
||||||
|
for (uint8_t j = 0; j < attachCount; ++j) {
|
||||||
|
MailAttachment att;
|
||||||
|
att.slot = packet.readUInt8();
|
||||||
|
uint64_t itemGuid = packet.readUInt64(); // full 64-bit GUID (TBC)
|
||||||
|
att.itemGuidLow = static_cast<uint32_t>(itemGuid & 0xFFFFFFFF);
|
||||||
|
att.itemId = packet.readUInt32();
|
||||||
|
// TBC: 7 × uint32 enchant ID only (no duration/charges per slot)
|
||||||
|
for (int e = 0; e < 7; ++e) {
|
||||||
|
uint32_t enchId = packet.readUInt32();
|
||||||
|
if (e == 0) att.enchantId = enchId;
|
||||||
|
}
|
||||||
|
att.randomPropertyId = packet.readUInt32();
|
||||||
|
att.randomSuffix = packet.readUInt32();
|
||||||
|
att.stackCount = packet.readUInt32();
|
||||||
|
att.chargesOrDurability = packet.readUInt32();
|
||||||
|
att.maxDurability = packet.readUInt32();
|
||||||
|
packet.readUInt32(); // current durability (separate from chargesOrDurability)
|
||||||
|
msg.attachments.push_back(att);
|
||||||
|
}
|
||||||
|
|
||||||
|
msg.read = (msg.flags & 0x01) != 0;
|
||||||
|
inbox.push_back(std::move(msg));
|
||||||
|
|
||||||
|
// Skip any unread bytes within this mail entry
|
||||||
|
size_t consumed = packet.getReadPos() - startPos;
|
||||||
|
if (consumed < static_cast<size_t>(msgSize)) {
|
||||||
|
packet.setReadPos(startPos + msgSize);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return !inbox.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// TbcPacketParsers::parseSpellStart — TBC 2.4.3 SMSG_SPELL_START
|
||||||
|
//
|
||||||
|
// TBC uses full uint64 GUIDs for casterGuid and casterUnit.
|
||||||
|
// WotLK uses packed (variable-length) GUIDs.
|
||||||
|
// TBC also lacks the castCount byte — format:
|
||||||
|
// casterGuid(u64) + casterUnit(u64) + castCount(u8) + spellId(u32) + castFlags(u32) + castTime(u32)
|
||||||
|
// Wait: TBC DOES have castCount. But WotLK removed spellId in some paths.
|
||||||
|
// Correct TBC format (cmangos-tbc): objectGuid(u64) + casterGuid(u64) + castCount(u8) + spellId(u32) + castFlags(u32) + castTime(u32)
|
||||||
|
// ============================================================================
|
||||||
|
bool TbcPacketParsers::parseSpellStart(network::Packet& packet, SpellStartData& data) {
|
||||||
|
if (packet.getSize() - packet.getReadPos() < 22) return false;
|
||||||
|
|
||||||
|
data.casterGuid = packet.readUInt64(); // full GUID (object)
|
||||||
|
data.casterUnit = packet.readUInt64(); // full GUID (caster unit)
|
||||||
|
data.castCount = packet.readUInt8();
|
||||||
|
data.spellId = packet.readUInt32();
|
||||||
|
data.castFlags = packet.readUInt32();
|
||||||
|
data.castTime = packet.readUInt32();
|
||||||
|
|
||||||
|
if (packet.getReadPos() + 4 <= packet.getSize()) {
|
||||||
|
uint32_t targetFlags = packet.readUInt32();
|
||||||
|
if ((targetFlags & 0x02) && packet.getReadPos() + 8 <= packet.getSize()) {
|
||||||
|
data.targetGuid = packet.readUInt64(); // full GUID in TBC
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_DEBUG("[TBC] Spell start: spell=", data.spellId, " castTime=", data.castTime, "ms");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// TbcPacketParsers::parseSpellGo — TBC 2.4.3 SMSG_SPELL_GO
|
||||||
|
//
|
||||||
|
// TBC uses full uint64 GUIDs, no timestamp field after castFlags.
|
||||||
|
// WotLK uses packed GUIDs and adds a timestamp (u32) after castFlags.
|
||||||
|
// ============================================================================
|
||||||
|
bool TbcPacketParsers::parseSpellGo(network::Packet& packet, SpellGoData& data) {
|
||||||
|
if (packet.getSize() - packet.getReadPos() < 19) return false;
|
||||||
|
|
||||||
|
data.casterGuid = packet.readUInt64(); // full GUID in TBC
|
||||||
|
data.casterUnit = packet.readUInt64(); // full GUID in TBC
|
||||||
|
data.castCount = packet.readUInt8();
|
||||||
|
data.spellId = packet.readUInt32();
|
||||||
|
data.castFlags = packet.readUInt32();
|
||||||
|
// NOTE: NO timestamp field here in TBC (WotLK added packet.readUInt32())
|
||||||
|
|
||||||
|
if (packet.getReadPos() >= packet.getSize()) {
|
||||||
|
LOG_DEBUG("[TBC] Spell go: spell=", data.spellId, " (no hit data)");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
data.hitCount = packet.readUInt8();
|
||||||
|
data.hitTargets.reserve(data.hitCount);
|
||||||
|
for (uint8_t i = 0; i < data.hitCount && packet.getReadPos() + 8 <= packet.getSize(); ++i) {
|
||||||
|
data.hitTargets.push_back(packet.readUInt64()); // full GUID in TBC
|
||||||
|
}
|
||||||
|
|
||||||
|
if (packet.getReadPos() < packet.getSize()) {
|
||||||
|
data.missCount = packet.readUInt8();
|
||||||
|
data.missTargets.reserve(data.missCount);
|
||||||
|
for (uint8_t i = 0; i < data.missCount && packet.getReadPos() + 9 <= packet.getSize(); ++i) {
|
||||||
|
SpellGoMissEntry m;
|
||||||
|
m.targetGuid = packet.readUInt64(); // full GUID in TBC
|
||||||
|
m.missType = packet.readUInt8();
|
||||||
|
data.missTargets.push_back(m);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_DEBUG("[TBC] Spell go: spell=", data.spellId, " hits=", (int)data.hitCount,
|
||||||
|
" misses=", (int)data.missCount);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// TbcPacketParsers::parseCastResult — TBC 2.4.3 SMSG_CAST_RESULT
|
||||||
|
//
|
||||||
|
// TBC format: spellId(u32) + result(u8) = 5 bytes
|
||||||
|
// WotLK adds a castCount(u8) prefix making it 6 bytes.
|
||||||
|
// Without this override, WotLK parser reads spellId[0] as castCount,
|
||||||
|
// then the remaining 4 bytes as spellId (off by one), producing wrong result.
|
||||||
|
// ============================================================================
|
||||||
|
bool TbcPacketParsers::parseCastResult(network::Packet& packet, uint32_t& spellId, uint8_t& result) {
|
||||||
|
if (packet.getSize() - packet.getReadPos() < 5) return false;
|
||||||
|
spellId = packet.readUInt32(); // No castCount prefix in TBC
|
||||||
|
result = packet.readUInt8();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// TbcPacketParsers::parseCastFailed — TBC 2.4.3 SMSG_CAST_FAILED
|
||||||
|
//
|
||||||
|
// TBC format: spellId(u32) + result(u8)
|
||||||
|
// WotLK added castCount(u8) before spellId; reading it on TBC would shift
|
||||||
|
// the spellId by one byte and corrupt all subsequent fields.
|
||||||
|
// Classic has the same layout, but the result enum starts differently (offset +1);
|
||||||
|
// TBC uses the same result values as WotLK so no offset is needed.
|
||||||
|
// ============================================================================
|
||||||
|
bool TbcPacketParsers::parseCastFailed(network::Packet& packet, CastFailedData& data) {
|
||||||
|
if (packet.getSize() - packet.getReadPos() < 5) return false;
|
||||||
|
data.castCount = 0; // not present in TBC
|
||||||
|
data.spellId = packet.readUInt32();
|
||||||
|
data.result = packet.readUInt8(); // same enum as WotLK
|
||||||
|
LOG_DEBUG("[TBC] Cast failed: spell=", data.spellId, " result=", (int)data.result);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// TbcPacketParsers::parseAttackerStateUpdate — TBC 2.4.3 SMSG_ATTACKERSTATEUPDATE
|
||||||
|
//
|
||||||
|
// TBC uses full uint64 GUIDs for attacker and target.
|
||||||
|
// WotLK uses packed (variable-length) GUIDs — using the WotLK reader here
|
||||||
|
// would mis-parse TBC's GUIDs and corrupt all subsequent damage fields.
|
||||||
|
// ============================================================================
|
||||||
|
bool TbcPacketParsers::parseAttackerStateUpdate(network::Packet& packet, AttackerStateUpdateData& data) {
|
||||||
|
if (packet.getSize() - packet.getReadPos() < 21) return false;
|
||||||
|
|
||||||
|
data.hitInfo = packet.readUInt32();
|
||||||
|
data.attackerGuid = packet.readUInt64(); // full GUID in TBC
|
||||||
|
data.targetGuid = packet.readUInt64(); // full GUID in TBC
|
||||||
|
data.totalDamage = static_cast<int32_t>(packet.readUInt32());
|
||||||
|
data.subDamageCount = packet.readUInt8();
|
||||||
|
|
||||||
|
for (uint8_t i = 0; i < data.subDamageCount; ++i) {
|
||||||
|
SubDamage sub;
|
||||||
|
sub.schoolMask = packet.readUInt32();
|
||||||
|
sub.damage = packet.readFloat();
|
||||||
|
sub.intDamage = packet.readUInt32();
|
||||||
|
sub.absorbed = packet.readUInt32();
|
||||||
|
sub.resisted = packet.readUInt32();
|
||||||
|
data.subDamages.push_back(sub);
|
||||||
|
}
|
||||||
|
|
||||||
|
data.victimState = packet.readUInt32();
|
||||||
|
data.overkill = static_cast<int32_t>(packet.readUInt32());
|
||||||
|
|
||||||
|
if (packet.getReadPos() < packet.getSize()) {
|
||||||
|
data.blocked = packet.readUInt32();
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_INFO("[TBC] Melee hit: ", data.totalDamage, " damage",
|
||||||
|
data.isCrit() ? " (CRIT)" : "",
|
||||||
|
data.isMiss() ? " (MISS)" : "");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// TbcPacketParsers::parseSpellDamageLog — TBC 2.4.3 SMSG_SPELLNONMELEEDAMAGELOG
|
||||||
|
//
|
||||||
|
// TBC uses full uint64 GUIDs; WotLK uses packed GUIDs.
|
||||||
|
// ============================================================================
|
||||||
|
bool TbcPacketParsers::parseSpellDamageLog(network::Packet& packet, SpellDamageLogData& data) {
|
||||||
|
if (packet.getSize() - packet.getReadPos() < 29) return false;
|
||||||
|
|
||||||
|
data.targetGuid = packet.readUInt64(); // full GUID in TBC
|
||||||
|
data.attackerGuid = packet.readUInt64(); // full GUID in TBC
|
||||||
|
data.spellId = packet.readUInt32();
|
||||||
|
data.damage = packet.readUInt32();
|
||||||
|
data.schoolMask = packet.readUInt8();
|
||||||
|
data.absorbed = packet.readUInt32();
|
||||||
|
data.resisted = packet.readUInt32();
|
||||||
|
|
||||||
|
uint8_t periodicLog = packet.readUInt8();
|
||||||
|
(void)periodicLog;
|
||||||
|
packet.readUInt8(); // unused
|
||||||
|
packet.readUInt32(); // blocked
|
||||||
|
uint32_t flags = packet.readUInt32();
|
||||||
|
data.isCrit = (flags & 0x02) != 0;
|
||||||
|
|
||||||
|
// TBC does not have an overkill field here
|
||||||
|
data.overkill = 0;
|
||||||
|
|
||||||
|
LOG_INFO("[TBC] Spell damage: spellId=", data.spellId, " dmg=", data.damage,
|
||||||
|
data.isCrit ? " CRIT" : "");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// TbcPacketParsers::parseSpellHealLog — TBC 2.4.3 SMSG_SPELLHEALLOG
|
||||||
|
//
|
||||||
|
// TBC uses full uint64 GUIDs; WotLK uses packed GUIDs.
|
||||||
|
// ============================================================================
|
||||||
|
bool TbcPacketParsers::parseSpellHealLog(network::Packet& packet, SpellHealLogData& data) {
|
||||||
|
if (packet.getSize() - packet.getReadPos() < 25) return false;
|
||||||
|
|
||||||
|
data.targetGuid = packet.readUInt64(); // full GUID in TBC
|
||||||
|
data.casterGuid = packet.readUInt64(); // full GUID in TBC
|
||||||
|
data.spellId = packet.readUInt32();
|
||||||
|
data.heal = packet.readUInt32();
|
||||||
|
data.overheal = packet.readUInt32();
|
||||||
|
// TBC has no absorbed field in SMSG_SPELLHEALLOG; skip crit flag
|
||||||
|
if (packet.getReadPos() < packet.getSize()) {
|
||||||
|
uint8_t critFlag = packet.readUInt8();
|
||||||
|
data.isCrit = (critFlag != 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_INFO("[TBC] Spell heal: spellId=", data.spellId, " heal=", data.heal,
|
||||||
|
data.isCrit ? " CRIT" : "");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace game
|
} // namespace game
|
||||||
} // namespace wowee
|
} // namespace wowee
|
||||||
|
|
|
||||||
|
|
@ -1510,20 +1510,30 @@ network::Packet TextEmotePacket::build(uint32_t textEmoteId, uint64_t targetGuid
|
||||||
return packet;
|
return packet;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool TextEmoteParser::parse(network::Packet& packet, TextEmoteData& data) {
|
bool TextEmoteParser::parse(network::Packet& packet, TextEmoteData& data, bool legacyFormat) {
|
||||||
size_t bytesLeft = packet.getSize() - packet.getReadPos();
|
size_t bytesLeft = packet.getSize() - packet.getReadPos();
|
||||||
if (bytesLeft < 20) {
|
if (bytesLeft < 20) {
|
||||||
LOG_WARNING("SMSG_TEXT_EMOTE too short: ", bytesLeft, " bytes");
|
LOG_WARNING("SMSG_TEXT_EMOTE too short: ", bytesLeft, " bytes");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
data.senderGuid = packet.readUInt64();
|
|
||||||
data.textEmoteId = packet.readUInt32();
|
if (legacyFormat) {
|
||||||
data.emoteNum = packet.readUInt32();
|
// Classic 1.12 / TBC 2.4.3: textEmoteId(u32) + emoteNum(u32) + senderGuid(u64)
|
||||||
|
data.textEmoteId = packet.readUInt32();
|
||||||
|
data.emoteNum = packet.readUInt32();
|
||||||
|
data.senderGuid = packet.readUInt64();
|
||||||
|
} else {
|
||||||
|
// WotLK 3.3.5a: senderGuid(u64) + textEmoteId(u32) + emoteNum(u32)
|
||||||
|
data.senderGuid = packet.readUInt64();
|
||||||
|
data.textEmoteId = packet.readUInt32();
|
||||||
|
data.emoteNum = packet.readUInt32();
|
||||||
|
}
|
||||||
|
|
||||||
uint32_t nameLen = packet.readUInt32();
|
uint32_t nameLen = packet.readUInt32();
|
||||||
if (nameLen > 0 && nameLen <= 256) {
|
if (nameLen > 0 && nameLen <= 256) {
|
||||||
data.targetName = packet.readString();
|
data.targetName = packet.readString();
|
||||||
} else if (nameLen > 0) {
|
} else if (nameLen > 0) {
|
||||||
// Skip garbage
|
// Implausible name length — misaligned read
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -2934,10 +2944,12 @@ network::Packet CancelAuraPacket::build(uint32_t spellId) {
|
||||||
return packet;
|
return packet;
|
||||||
}
|
}
|
||||||
|
|
||||||
network::Packet PetActionPacket::build(uint64_t petGuid, uint32_t action) {
|
network::Packet PetActionPacket::build(uint64_t petGuid, uint32_t action, uint64_t targetGuid) {
|
||||||
|
// CMSG_PET_ACTION: petGuid(8) + action(4) + targetGuid(8)
|
||||||
network::Packet packet(wireOpcode(Opcode::CMSG_PET_ACTION));
|
network::Packet packet(wireOpcode(Opcode::CMSG_PET_ACTION));
|
||||||
packet.writeUInt64(petGuid);
|
packet.writeUInt64(petGuid);
|
||||||
packet.writeUInt32(action);
|
packet.writeUInt32(action);
|
||||||
|
packet.writeUInt64(targetGuid);
|
||||||
return packet;
|
return packet;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2985,7 +2997,13 @@ bool SpellGoParser::parse(network::Packet& packet, SpellGoData& data) {
|
||||||
}
|
}
|
||||||
|
|
||||||
data.missCount = packet.readUInt8();
|
data.missCount = packet.readUInt8();
|
||||||
// Skip miss details for now
|
data.missTargets.reserve(data.missCount);
|
||||||
|
for (uint8_t i = 0; i < data.missCount && packet.getReadPos() + 2 <= packet.getSize(); ++i) {
|
||||||
|
SpellGoMissEntry m;
|
||||||
|
m.targetGuid = UpdateObjectParser::readPackedGuid(packet); // packed GUID in WotLK
|
||||||
|
m.missType = (packet.getReadPos() < packet.getSize()) ? packet.readUInt8() : 0;
|
||||||
|
data.missTargets.push_back(m);
|
||||||
|
}
|
||||||
|
|
||||||
LOG_DEBUG("Spell go: spell=", data.spellId, " hits=", (int)data.hitCount,
|
LOG_DEBUG("Spell go: spell=", data.spellId, " hits=", (int)data.hitCount,
|
||||||
" misses=", (int)data.missCount);
|
" misses=", (int)data.missCount);
|
||||||
|
|
@ -3082,47 +3100,82 @@ network::Packet GroupDeclinePacket::build() {
|
||||||
return packet;
|
return packet;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool GroupListParser::parse(network::Packet& packet, GroupListData& data) {
|
bool GroupListParser::parse(network::Packet& packet, GroupListData& data, bool hasRoles) {
|
||||||
data.groupType = packet.readUInt8();
|
auto rem = [&]() { return packet.getSize() - packet.getReadPos(); };
|
||||||
data.subGroup = packet.readUInt8();
|
|
||||||
data.flags = packet.readUInt8();
|
|
||||||
data.roles = packet.readUInt8();
|
|
||||||
|
|
||||||
// Skip LFG data if present
|
if (rem() < 3) return false;
|
||||||
if (data.groupType & 0x04) {
|
data.groupType = packet.readUInt8();
|
||||||
packet.readUInt8(); // lfg state
|
data.subGroup = packet.readUInt8();
|
||||||
packet.readUInt32(); // lfg entry
|
data.flags = packet.readUInt8();
|
||||||
packet.readUInt8(); // lfg flags (3.3.5a may not have this)
|
|
||||||
|
// WotLK 3.3.5a added a roles byte (tank/healer/dps) for the dungeon finder.
|
||||||
|
// Classic 1.12 and TBC 2.4.3 do not have this byte.
|
||||||
|
if (hasRoles) {
|
||||||
|
if (rem() < 1) return false;
|
||||||
|
data.roles = packet.readUInt8();
|
||||||
|
} else {
|
||||||
|
data.roles = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
packet.readUInt64(); // group GUID
|
// WotLK: LFG data gated by groupType bit 0x04 (LFD group type)
|
||||||
packet.readUInt32(); // counter
|
if (hasRoles && (data.groupType & 0x04)) {
|
||||||
|
if (rem() < 5) return false;
|
||||||
|
packet.readUInt8(); // lfg state
|
||||||
|
packet.readUInt32(); // lfg entry
|
||||||
|
// WotLK 3.3.5a may or may not send the lfg flags byte — read it only if present
|
||||||
|
if (rem() >= 13) { // enough for lfgFlags(1)+groupGuid(8)+counter(4)
|
||||||
|
packet.readUInt8(); // lfg flags
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rem() < 12) return false;
|
||||||
|
packet.readUInt64(); // group GUID
|
||||||
|
packet.readUInt32(); // update counter
|
||||||
|
|
||||||
|
if (rem() < 4) return false;
|
||||||
data.memberCount = packet.readUInt32();
|
data.memberCount = packet.readUInt32();
|
||||||
|
if (data.memberCount > 40) {
|
||||||
|
LOG_WARNING("GroupListParser: implausible memberCount=", data.memberCount, ", clamping");
|
||||||
|
data.memberCount = 40;
|
||||||
|
}
|
||||||
data.members.reserve(data.memberCount);
|
data.members.reserve(data.memberCount);
|
||||||
|
|
||||||
for (uint32_t i = 0; i < data.memberCount; ++i) {
|
for (uint32_t i = 0; i < data.memberCount; ++i) {
|
||||||
|
if (rem() == 0) break;
|
||||||
GroupMember member;
|
GroupMember member;
|
||||||
member.name = packet.readString();
|
member.name = packet.readString();
|
||||||
member.guid = packet.readUInt64();
|
if (rem() < 8) break;
|
||||||
|
member.guid = packet.readUInt64();
|
||||||
|
if (rem() < 3) break;
|
||||||
member.isOnline = packet.readUInt8();
|
member.isOnline = packet.readUInt8();
|
||||||
member.subGroup = packet.readUInt8();
|
member.subGroup = packet.readUInt8();
|
||||||
member.flags = packet.readUInt8();
|
member.flags = packet.readUInt8();
|
||||||
member.roles = packet.readUInt8();
|
// WotLK added per-member roles byte; Classic/TBC do not have it.
|
||||||
|
if (hasRoles) {
|
||||||
|
if (rem() < 1) break;
|
||||||
|
member.roles = packet.readUInt8();
|
||||||
|
} else {
|
||||||
|
member.roles = 0;
|
||||||
|
}
|
||||||
data.members.push_back(member);
|
data.members.push_back(member);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (rem() < 8) {
|
||||||
|
LOG_INFO("Group list: ", data.memberCount, " members (no leader GUID in packet)");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
data.leaderGuid = packet.readUInt64();
|
data.leaderGuid = packet.readUInt64();
|
||||||
|
|
||||||
if (data.memberCount > 0 && packet.getReadPos() < packet.getSize()) {
|
if (data.memberCount > 0 && rem() >= 10) {
|
||||||
data.lootMethod = packet.readUInt8();
|
data.lootMethod = packet.readUInt8();
|
||||||
data.looterGuid = packet.readUInt64();
|
data.looterGuid = packet.readUInt64();
|
||||||
data.lootThreshold = packet.readUInt8();
|
data.lootThreshold = packet.readUInt8();
|
||||||
data.difficultyId = packet.readUInt8();
|
// Dungeon difficulty (heroic/normal) — Classic doesn't send this; TBC/WotLK do
|
||||||
data.raidDifficultyId = packet.readUInt8();
|
if (rem() >= 1) data.difficultyId = packet.readUInt8();
|
||||||
if (packet.getReadPos() < packet.getSize()) {
|
// Raid difficulty — WotLK only
|
||||||
packet.readUInt8(); // unknown byte
|
if (rem() >= 1) data.raidDifficultyId = packet.readUInt8();
|
||||||
}
|
// Extra byte in some 3.3.5a builds
|
||||||
|
if (hasRoles && rem() >= 1) packet.readUInt8();
|
||||||
}
|
}
|
||||||
|
|
||||||
LOG_INFO("Group list: ", data.memberCount, " members, leader=0x",
|
LOG_INFO("Group list: ", data.memberCount, " members, leader=0x",
|
||||||
|
|
@ -3780,7 +3833,11 @@ bool ListInventoryParser::parse(network::Packet& packet, ListInventoryData& data
|
||||||
// Trainer
|
// Trainer
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
bool TrainerListParser::parse(network::Packet& packet, TrainerListData& data) {
|
bool TrainerListParser::parse(network::Packet& packet, TrainerListData& data, bool isClassic) {
|
||||||
|
// WotLK per-entry: spellId(4) + state(1) + cost(4) + profDialog(4) + profButton(4) +
|
||||||
|
// reqLevel(1) + reqSkill(4) + reqSkillValue(4) + chain×3(12) = 38 bytes
|
||||||
|
// Classic per-entry: spellId(4) + state(1) + cost(4) + reqLevel(1) +
|
||||||
|
// reqSkill(4) + reqSkillValue(4) + chain×3(12) + unk(4) = 34 bytes
|
||||||
data = TrainerListData{};
|
data = TrainerListData{};
|
||||||
data.trainerGuid = packet.readUInt64();
|
data.trainerGuid = packet.readUInt64();
|
||||||
data.trainerType = packet.readUInt32();
|
data.trainerType = packet.readUInt32();
|
||||||
|
|
@ -3794,23 +3851,35 @@ bool TrainerListParser::parse(network::Packet& packet, TrainerListData& data) {
|
||||||
data.spells.reserve(spellCount);
|
data.spells.reserve(spellCount);
|
||||||
for (uint32_t i = 0; i < spellCount; ++i) {
|
for (uint32_t i = 0; i < spellCount; ++i) {
|
||||||
TrainerSpell spell;
|
TrainerSpell spell;
|
||||||
spell.spellId = packet.readUInt32();
|
spell.spellId = packet.readUInt32();
|
||||||
spell.state = packet.readUInt8();
|
spell.state = packet.readUInt8();
|
||||||
spell.spellCost = packet.readUInt32();
|
spell.spellCost = packet.readUInt32();
|
||||||
spell.profDialog = packet.readUInt32();
|
if (isClassic) {
|
||||||
spell.profButton = packet.readUInt32();
|
// Classic 1.12: reqLevel immediately after cost; no profDialog/profButton
|
||||||
spell.reqLevel = packet.readUInt8();
|
spell.profDialog = 0;
|
||||||
spell.reqSkill = packet.readUInt32();
|
spell.profButton = 0;
|
||||||
|
spell.reqLevel = packet.readUInt8();
|
||||||
|
} else {
|
||||||
|
// TBC / WotLK: profDialog + profButton before reqLevel
|
||||||
|
spell.profDialog = packet.readUInt32();
|
||||||
|
spell.profButton = packet.readUInt32();
|
||||||
|
spell.reqLevel = packet.readUInt8();
|
||||||
|
}
|
||||||
|
spell.reqSkill = packet.readUInt32();
|
||||||
spell.reqSkillValue = packet.readUInt32();
|
spell.reqSkillValue = packet.readUInt32();
|
||||||
spell.chainNode1 = packet.readUInt32();
|
spell.chainNode1 = packet.readUInt32();
|
||||||
spell.chainNode2 = packet.readUInt32();
|
spell.chainNode2 = packet.readUInt32();
|
||||||
spell.chainNode3 = packet.readUInt32();
|
spell.chainNode3 = packet.readUInt32();
|
||||||
|
if (isClassic) {
|
||||||
|
packet.readUInt32(); // trailing unk / sort index
|
||||||
|
}
|
||||||
data.spells.push_back(spell);
|
data.spells.push_back(spell);
|
||||||
}
|
}
|
||||||
|
|
||||||
data.greeting = packet.readString();
|
data.greeting = packet.readString();
|
||||||
|
|
||||||
LOG_INFO("Trainer list: ", spellCount, " spells, type=", data.trainerType,
|
LOG_INFO("Trainer list (", isClassic ? "Classic" : "TBC/WotLK", "): ",
|
||||||
|
spellCount, " spells, type=", data.trainerType,
|
||||||
", greeting=\"", data.greeting, "\"");
|
", greeting=\"", data.greeting, "\"");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -3935,6 +4004,14 @@ network::Packet TalentWipeConfirmPacket::build(bool accept) {
|
||||||
return packet;
|
return packet;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
network::Packet ActivateTalentGroupPacket::build(uint32_t group) {
|
||||||
|
// CMSG_SET_ACTIVE_TALENT_GROUP_OBSOLETE (0x4C3 in WotLK 3.3.5a)
|
||||||
|
// Payload: uint32 group (0 = primary, 1 = secondary)
|
||||||
|
network::Packet packet(wireOpcode(Opcode::CMSG_SET_ACTIVE_TALENT_GROUP_OBSOLETE));
|
||||||
|
packet.writeUInt32(group);
|
||||||
|
return packet;
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Death/Respawn
|
// Death/Respawn
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
@ -4437,45 +4514,54 @@ network::Packet AuctionListBidderItemsPacket::build(
|
||||||
return p;
|
return p;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool AuctionListResultParser::parse(network::Packet& packet, AuctionListResult& data) {
|
bool AuctionListResultParser::parse(network::Packet& packet, AuctionListResult& data, int numEnchantSlots) {
|
||||||
|
// Per-entry fixed size: auctionId(4) + itemEntry(4) + enchantSlots×3×4 +
|
||||||
|
// randProp(4) + suffix(4) + stack(4) + charges(4) + flags(4) +
|
||||||
|
// ownerGuid(8) + startBid(4) + outbid(4) + buyout(4) + expire(4) +
|
||||||
|
// bidderGuid(8) + curBid(4)
|
||||||
|
// Classic: numEnchantSlots=1 → 80 bytes/entry
|
||||||
|
// TBC/WotLK: numEnchantSlots=3 → 104 bytes/entry
|
||||||
if (packet.getSize() - packet.getReadPos() < 4) return false;
|
if (packet.getSize() - packet.getReadPos() < 4) return false;
|
||||||
|
|
||||||
uint32_t count = packet.readUInt32();
|
uint32_t count = packet.readUInt32();
|
||||||
data.auctions.clear();
|
data.auctions.clear();
|
||||||
data.auctions.reserve(count);
|
data.auctions.reserve(count);
|
||||||
|
|
||||||
|
const size_t minPerEntry = static_cast<size_t>(8 + numEnchantSlots * 12 + 28 + 8 + 8);
|
||||||
for (uint32_t i = 0; i < count; ++i) {
|
for (uint32_t i = 0; i < count; ++i) {
|
||||||
if (packet.getReadPos() + 64 > packet.getSize()) break;
|
if (packet.getReadPos() + minPerEntry > packet.getSize()) break;
|
||||||
AuctionEntry e;
|
AuctionEntry e;
|
||||||
e.auctionId = packet.readUInt32();
|
e.auctionId = packet.readUInt32();
|
||||||
e.itemEntry = packet.readUInt32();
|
e.itemEntry = packet.readUInt32();
|
||||||
// 3 enchant slots: enchantId, duration, charges
|
// First enchant slot always present
|
||||||
e.enchantId = packet.readUInt32();
|
e.enchantId = packet.readUInt32();
|
||||||
packet.readUInt32(); // enchant duration
|
packet.readUInt32(); // enchant1 duration
|
||||||
packet.readUInt32(); // enchant charges
|
packet.readUInt32(); // enchant1 charges
|
||||||
packet.readUInt32(); // enchant2 id
|
// Extra enchant slots for TBC/WotLK
|
||||||
packet.readUInt32(); // enchant2 duration
|
for (int s = 1; s < numEnchantSlots; ++s) {
|
||||||
packet.readUInt32(); // enchant2 charges
|
packet.readUInt32(); // enchant N id
|
||||||
packet.readUInt32(); // enchant3 id
|
packet.readUInt32(); // enchant N duration
|
||||||
packet.readUInt32(); // enchant3 duration
|
packet.readUInt32(); // enchant N charges
|
||||||
packet.readUInt32(); // enchant3 charges
|
}
|
||||||
e.randomPropertyId = packet.readUInt32();
|
e.randomPropertyId = packet.readUInt32();
|
||||||
e.suffixFactor = packet.readUInt32();
|
e.suffixFactor = packet.readUInt32();
|
||||||
e.stackCount = packet.readUInt32();
|
e.stackCount = packet.readUInt32();
|
||||||
packet.readUInt32(); // item charges
|
packet.readUInt32(); // item charges
|
||||||
packet.readUInt32(); // item flags (unused)
|
packet.readUInt32(); // item flags (unused)
|
||||||
e.ownerGuid = packet.readUInt64();
|
e.ownerGuid = packet.readUInt64();
|
||||||
e.startBid = packet.readUInt32();
|
e.startBid = packet.readUInt32();
|
||||||
e.minBidIncrement = packet.readUInt32();
|
e.minBidIncrement = packet.readUInt32();
|
||||||
e.buyoutPrice = packet.readUInt32();
|
e.buyoutPrice = packet.readUInt32();
|
||||||
e.timeLeftMs = packet.readUInt32();
|
e.timeLeftMs = packet.readUInt32();
|
||||||
e.bidderGuid = packet.readUInt64();
|
e.bidderGuid = packet.readUInt64();
|
||||||
e.currentBid = packet.readUInt32();
|
e.currentBid = packet.readUInt32();
|
||||||
data.auctions.push_back(e);
|
data.auctions.push_back(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
data.totalCount = packet.readUInt32();
|
if (packet.getSize() - packet.getReadPos() >= 8) {
|
||||||
data.searchDelay = packet.readUInt32();
|
data.totalCount = packet.readUInt32();
|
||||||
|
data.searchDelay = packet.readUInt32();
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -153,6 +153,11 @@ void TCPSocket::update() {
|
||||||
if (net::isWouldBlock(err)) {
|
if (net::isWouldBlock(err)) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
if (net::isConnectionClosed(err)) {
|
||||||
|
// Peer closed the connection — treat the same as recv() returning 0
|
||||||
|
sawClose = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
LOG_ERROR("Receive failed: ", net::errorString(err));
|
LOG_ERROR("Receive failed: ", net::errorString(err));
|
||||||
disconnect();
|
disconnect();
|
||||||
|
|
|
||||||
|
|
@ -128,6 +128,39 @@ bool WorldSocket::connect(const std::string& host, uint16_t port) {
|
||||||
sockfd = INVALID_SOCK;
|
sockfd = INVALID_SOCK;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Non-blocking connect in progress — wait up to 10s for completion.
|
||||||
|
// On Windows, calling recv() before the connect completes returns
|
||||||
|
// WSAENOTCONN; we must poll writability before declaring connected.
|
||||||
|
fd_set writefds, errfds;
|
||||||
|
FD_ZERO(&writefds);
|
||||||
|
FD_ZERO(&errfds);
|
||||||
|
FD_SET(sockfd, &writefds);
|
||||||
|
FD_SET(sockfd, &errfds);
|
||||||
|
|
||||||
|
struct timeval tv;
|
||||||
|
tv.tv_sec = 10;
|
||||||
|
tv.tv_usec = 0;
|
||||||
|
|
||||||
|
int sel = ::select(static_cast<int>(sockfd) + 1, nullptr, &writefds, &errfds, &tv);
|
||||||
|
if (sel <= 0) {
|
||||||
|
LOG_ERROR("World server connection timed out (", host, ":", port, ")");
|
||||||
|
net::closeSocket(sockfd);
|
||||||
|
sockfd = INVALID_SOCK;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify the socket error code — writeable doesn't guarantee success on all platforms
|
||||||
|
int sockErr = 0;
|
||||||
|
socklen_t errLen = sizeof(sockErr);
|
||||||
|
getsockopt(sockfd, SOL_SOCKET, SO_ERROR,
|
||||||
|
reinterpret_cast<char*>(&sockErr), &errLen);
|
||||||
|
if (sockErr != 0) {
|
||||||
|
LOG_ERROR("Failed to connect to world server: ", net::errorString(sockErr));
|
||||||
|
net::closeSocket(sockfd);
|
||||||
|
sockfd = INVALID_SOCK;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
connected = true;
|
connected = true;
|
||||||
|
|
@ -369,6 +402,11 @@ void WorldSocket::update() {
|
||||||
if (net::isWouldBlock(err)) {
|
if (net::isWouldBlock(err)) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
if (net::isConnectionClosed(err)) {
|
||||||
|
// Peer closed the connection — treat the same as recv() returning 0
|
||||||
|
sawClose = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
LOG_ERROR("Receive failed: ", net::errorString(err));
|
LOG_ERROR("Receive failed: ", net::errorString(err));
|
||||||
disconnect();
|
disconnect();
|
||||||
|
|
|
||||||
|
|
@ -109,7 +109,11 @@ WMOModel WMOLoader::load(const std::vector<uint8_t>& wmoData) {
|
||||||
model.nDoodadDefs = read<uint32_t>(wmoData, offset);
|
model.nDoodadDefs = read<uint32_t>(wmoData, offset);
|
||||||
model.nDoodadSets = read<uint32_t>(wmoData, offset);
|
model.nDoodadSets = read<uint32_t>(wmoData, offset);
|
||||||
|
|
||||||
[[maybe_unused]] uint32_t ambColor = read<uint32_t>(wmoData, offset); // Ambient color (BGRA)
|
uint32_t ambColor = read<uint32_t>(wmoData, offset); // Ambient color (BGRA)
|
||||||
|
// Unpack BGRA bytes to normalized [0,1] RGB
|
||||||
|
model.ambientColor.r = ((ambColor >> 16) & 0xFF) / 255.0f;
|
||||||
|
model.ambientColor.g = ((ambColor >> 8) & 0xFF) / 255.0f;
|
||||||
|
model.ambientColor.b = ((ambColor >> 0) & 0xFF) / 255.0f;
|
||||||
[[maybe_unused]] uint32_t wmoID = read<uint32_t>(wmoData, offset);
|
[[maybe_unused]] uint32_t wmoID = read<uint32_t>(wmoData, offset);
|
||||||
|
|
||||||
model.boundingBoxMin.x = read<float>(wmoData, offset);
|
model.boundingBoxMin.x = read<float>(wmoData, offset);
|
||||||
|
|
|
||||||
|
|
@ -543,9 +543,24 @@ bool CharacterPreview::applyEquipment(const std::vector<game::EquipmentItem>& eq
|
||||||
|
|
||||||
auto displayInfoDbc = assetManager_->loadDBC("ItemDisplayInfo.dbc");
|
auto displayInfoDbc = assetManager_->loadDBC("ItemDisplayInfo.dbc");
|
||||||
if (!displayInfoDbc || !displayInfoDbc->isLoaded()) {
|
if (!displayInfoDbc || !displayInfoDbc->isLoaded()) {
|
||||||
|
LOG_WARNING("applyEquipment: ItemDisplayInfo.dbc not loaded");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Diagnostic: log equipment vector and DBC state
|
||||||
|
LOG_INFO("applyEquipment: ", equipment.size(), " items, ItemDisplayInfo.dbc records=",
|
||||||
|
displayInfoDbc->getRecordCount(), " fields=", displayInfoDbc->getFieldCount(),
|
||||||
|
" bodySkin=", bodySkinPath_.empty() ? "(empty)" : bodySkinPath_);
|
||||||
|
for (size_t ei = 0; ei < equipment.size(); ++ei) {
|
||||||
|
const auto& it = equipment[ei];
|
||||||
|
if (it.displayModel == 0) continue;
|
||||||
|
int32_t dbcRec = displayInfoDbc->findRecordById(it.displayModel);
|
||||||
|
LOG_INFO(" slot[", ei, "]: displayModel=", it.displayModel,
|
||||||
|
" invType=", (int)it.inventoryType,
|
||||||
|
" dbcRec=", dbcRec,
|
||||||
|
(dbcRec >= 0 ? " (found)" : " (NOT FOUND in ItemDisplayInfo.dbc)"));
|
||||||
|
}
|
||||||
|
|
||||||
auto hasInvType = [&](std::initializer_list<uint8_t> types) -> bool {
|
auto hasInvType = [&](std::initializer_list<uint8_t> types) -> bool {
|
||||||
for (const auto& it : equipment) {
|
for (const auto& it : equipment) {
|
||||||
if (it.displayModel == 0) continue;
|
if (it.displayModel == 0) continue;
|
||||||
|
|
@ -560,7 +575,7 @@ bool CharacterPreview::applyEquipment(const std::vector<game::EquipmentItem>& eq
|
||||||
for (const auto& it : equipment) {
|
for (const auto& it : equipment) {
|
||||||
if (it.displayModel == 0) continue;
|
if (it.displayModel == 0) continue;
|
||||||
for (uint8_t t : types) {
|
for (uint8_t t : types) {
|
||||||
if (it.inventoryType == t) return it.displayModel; // ItemDisplayInfo ID (3.3.5a char enum)
|
if (it.inventoryType == t) return it.displayModel;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
|
|
@ -570,7 +585,12 @@ bool CharacterPreview::applyEquipment(const std::vector<game::EquipmentItem>& eq
|
||||||
if (displayInfoId == 0) return 0;
|
if (displayInfoId == 0) return 0;
|
||||||
int32_t recIdx = displayInfoDbc->findRecordById(displayInfoId);
|
int32_t recIdx = displayInfoDbc->findRecordById(displayInfoId);
|
||||||
if (recIdx < 0) return 0;
|
if (recIdx < 0) return 0;
|
||||||
return displayInfoDbc->getUInt32(static_cast<uint32_t>(recIdx), 7 + groupField);
|
uint32_t val = displayInfoDbc->getUInt32(static_cast<uint32_t>(recIdx), 7 + groupField);
|
||||||
|
if (val > 0) {
|
||||||
|
LOG_INFO(" getGeosetGroup: displayInfoId=", displayInfoId,
|
||||||
|
" groupField=", groupField, " field=", (7 + groupField), " val=", val);
|
||||||
|
}
|
||||||
|
return val;
|
||||||
};
|
};
|
||||||
|
|
||||||
// --- Geosets ---
|
// --- Geosets ---
|
||||||
|
|
@ -654,6 +674,9 @@ bool CharacterPreview::applyEquipment(const std::vector<game::EquipmentItem>& eq
|
||||||
std::string texName = displayInfoDbc->getString(static_cast<uint32_t>(recIdx), fieldIdx);
|
std::string texName = displayInfoDbc->getString(static_cast<uint32_t>(recIdx), fieldIdx);
|
||||||
if (texName.empty()) continue;
|
if (texName.empty()) continue;
|
||||||
|
|
||||||
|
LOG_INFO(" texture region ", region, " (field ", fieldIdx, "): texName=", texName,
|
||||||
|
" for displayModel=", it.displayModel);
|
||||||
|
|
||||||
std::string base = "Item\\TextureComponents\\" +
|
std::string base = "Item\\TextureComponents\\" +
|
||||||
std::string(componentDirs[region]) + "\\" + texName;
|
std::string(componentDirs[region]) + "\\" + texName;
|
||||||
|
|
||||||
|
|
@ -669,6 +692,7 @@ bool CharacterPreview::applyEquipment(const std::vector<game::EquipmentItem>& eq
|
||||||
} else if (assetManager_->fileExists(basePath)) {
|
} else if (assetManager_->fileExists(basePath)) {
|
||||||
fullPath = basePath;
|
fullPath = basePath;
|
||||||
} else {
|
} else {
|
||||||
|
LOG_INFO(" texture path not found: ", base, " (_M/_F/_U/.blp)");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
regionLayers.emplace_back(region, fullPath);
|
regionLayers.emplace_back(region, fullPath);
|
||||||
|
|
|
||||||
|
|
@ -1185,7 +1185,7 @@ bool M2Renderer::loadModel(const pipeline::M2Model& model, uint32_t modelId) {
|
||||||
" tris, grid ", gpuModel.collision.gridCellsX, "x", gpuModel.collision.gridCellsY);
|
" tris, grid ", gpuModel.collision.gridCellsX, "x", gpuModel.collision.gridCellsY);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Flag smoke models for UV scroll animation (particle emitters not implemented)
|
// Flag smoke models for UV scroll animation (in addition to particle emitters)
|
||||||
{
|
{
|
||||||
std::string smokeName = model.name;
|
std::string smokeName = model.name;
|
||||||
std::transform(smokeName.begin(), smokeName.end(), smokeName.begin(),
|
std::transform(smokeName.begin(), smokeName.end(), smokeName.begin(),
|
||||||
|
|
@ -1357,6 +1357,7 @@ bool M2Renderer::loadModel(const pipeline::M2Model& model, uint32_t modelId) {
|
||||||
if (batch.materialIndex < model.materials.size()) {
|
if (batch.materialIndex < model.materials.size()) {
|
||||||
bgpu.blendMode = model.materials[batch.materialIndex].blendMode;
|
bgpu.blendMode = model.materials[batch.materialIndex].blendMode;
|
||||||
bgpu.materialFlags = model.materials[batch.materialIndex].flags;
|
bgpu.materialFlags = model.materials[batch.materialIndex].flags;
|
||||||
|
if (bgpu.blendMode >= 2) gpuModel.hasTransparentBatches = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Copy LOD level from batch
|
// Copy LOD level from batch
|
||||||
|
|
@ -2349,7 +2350,11 @@ void M2Renderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, const
|
||||||
sortedVisible_.push_back({i, instance.modelId, distSq, effectiveMaxDistSq});
|
sortedVisible_.push_back({i, instance.modelId, distSq, effectiveMaxDistSq});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort by modelId to minimize vertex/index buffer rebinds
|
// Two-pass rendering: opaque/alpha-test first (depth write ON), then transparent/additive
|
||||||
|
// (depth write OFF, sorted back-to-front) so transparent geometry composites correctly
|
||||||
|
// against all opaque geometry rather than only against what was rendered before it.
|
||||||
|
|
||||||
|
// Pass 1: sort by modelId for minimum buffer rebinds (opaque batches)
|
||||||
std::sort(sortedVisible_.begin(), sortedVisible_.end(),
|
std::sort(sortedVisible_.begin(), sortedVisible_.end(),
|
||||||
[](const VisibleEntry& a, const VisibleEntry& b) { return a.modelId < b.modelId; });
|
[](const VisibleEntry& a, const VisibleEntry& b) { return a.modelId < b.modelId; });
|
||||||
|
|
||||||
|
|
@ -2377,6 +2382,7 @@ void M2Renderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, const
|
||||||
// Start with opaque pipeline
|
// Start with opaque pipeline
|
||||||
vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, opaquePipeline_);
|
vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, opaquePipeline_);
|
||||||
currentPipeline = opaquePipeline_;
|
currentPipeline = opaquePipeline_;
|
||||||
|
bool opaquePass = true; // Pass 1 = opaque, pass 2 = transparent (set below for second pass)
|
||||||
|
|
||||||
for (const auto& entry : sortedVisible_) {
|
for (const auto& entry : sortedVisible_) {
|
||||||
if (entry.index >= instances.size()) continue;
|
if (entry.index >= instances.size()) continue;
|
||||||
|
|
@ -2475,6 +2481,15 @@ void M2Renderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, const
|
||||||
if (!model.isGroundDetail && batch.submeshLevel != targetLOD) continue;
|
if (!model.isGroundDetail && batch.submeshLevel != targetLOD) continue;
|
||||||
if (batch.batchOpacity < 0.01f) continue;
|
if (batch.batchOpacity < 0.01f) continue;
|
||||||
|
|
||||||
|
// Two-pass gate: pass 1 = opaque/cutout only, pass 2 = transparent/additive only.
|
||||||
|
// Alpha-test (blendMode==1) and spell effects that force-additive are handled
|
||||||
|
// by their effective blend mode below; gate on raw blendMode here.
|
||||||
|
{
|
||||||
|
const bool rawTransparent = (batch.blendMode >= 2) || model.isSpellEffect;
|
||||||
|
if (opaquePass && rawTransparent) continue; // skip transparent in opaque pass
|
||||||
|
if (!opaquePass && !rawTransparent) continue; // skip opaque in transparent pass
|
||||||
|
}
|
||||||
|
|
||||||
const bool koboldFlameCard = batch.colorKeyBlack && model.isKoboldFlame;
|
const bool koboldFlameCard = batch.colorKeyBlack && model.isKoboldFlame;
|
||||||
const bool smallCardLikeBatch =
|
const bool smallCardLikeBatch =
|
||||||
(batch.glowSize <= 1.35f) ||
|
(batch.glowSize <= 1.35f) ||
|
||||||
|
|
@ -2628,6 +2643,162 @@ void M2Renderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, const
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Pass 2: transparent/additive batches — sort back-to-front by distance so
|
||||||
|
// overlapping transparent geometry composites in the correct painter's order.
|
||||||
|
opaquePass = false;
|
||||||
|
std::sort(sortedVisible_.begin(), sortedVisible_.end(),
|
||||||
|
[](const VisibleEntry& a, const VisibleEntry& b) { return a.distSq > b.distSq; });
|
||||||
|
|
||||||
|
currentModelId = UINT32_MAX;
|
||||||
|
currentModel = nullptr;
|
||||||
|
// Reset pipeline to opaque so the first transparent bind always sets explicitly
|
||||||
|
currentPipeline = opaquePipeline_;
|
||||||
|
|
||||||
|
for (const auto& entry : sortedVisible_) {
|
||||||
|
if (entry.index >= instances.size()) continue;
|
||||||
|
auto& instance = instances[entry.index];
|
||||||
|
|
||||||
|
// Quick skip: if model has no transparent batches at all, skip it entirely
|
||||||
|
if (entry.modelId != currentModelId) {
|
||||||
|
auto mdlIt = models.find(entry.modelId);
|
||||||
|
if (mdlIt == models.end()) continue;
|
||||||
|
if (!mdlIt->second.hasTransparentBatches && !mdlIt->second.isSpellEffect) continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reuse the same rendering logic as pass 1 (via fallthrough — the batch gate
|
||||||
|
// `!opaquePass && !rawTransparent → continue` handles opaque skipping)
|
||||||
|
if (entry.modelId != currentModelId) {
|
||||||
|
currentModelId = entry.modelId;
|
||||||
|
auto mdlIt = models.find(currentModelId);
|
||||||
|
if (mdlIt == models.end()) continue;
|
||||||
|
currentModel = &mdlIt->second;
|
||||||
|
if (!currentModel->vertexBuffer) continue;
|
||||||
|
VkDeviceSize offset = 0;
|
||||||
|
vkCmdBindVertexBuffers(cmd, 0, 1, ¤tModel->vertexBuffer, &offset);
|
||||||
|
vkCmdBindIndexBuffer(cmd, currentModel->indexBuffer, 0, VK_INDEX_TYPE_UINT16);
|
||||||
|
}
|
||||||
|
|
||||||
|
const M2ModelGPU& model = *currentModel;
|
||||||
|
|
||||||
|
// Distance-based fade alpha (same as pass 1)
|
||||||
|
float fadeAlpha = 1.0f;
|
||||||
|
float fadeFrac = model.disableAnimation ? 0.55f : fadeStartFraction;
|
||||||
|
float fadeStartDistSq = entry.effectiveMaxDistSq * fadeFrac * fadeFrac;
|
||||||
|
if (entry.distSq > fadeStartDistSq) {
|
||||||
|
fadeAlpha = std::clamp((entry.effectiveMaxDistSq - entry.distSq) /
|
||||||
|
(entry.effectiveMaxDistSq - fadeStartDistSq), 0.0f, 1.0f);
|
||||||
|
}
|
||||||
|
float instanceFadeAlpha = fadeAlpha;
|
||||||
|
if (model.isGroundDetail) instanceFadeAlpha *= 0.82f;
|
||||||
|
if (model.isInstancePortal) instanceFadeAlpha *= 0.12f;
|
||||||
|
|
||||||
|
bool modelNeedsAnimation = model.hasAnimation && !model.disableAnimation;
|
||||||
|
if (modelNeedsAnimation && instance.boneMatrices.empty()) continue;
|
||||||
|
bool needsBones = modelNeedsAnimation && !instance.boneMatrices.empty();
|
||||||
|
if (needsBones && (!instance.boneBuffer[frameIndex] || !instance.boneSet[frameIndex])) continue;
|
||||||
|
bool useBones = needsBones;
|
||||||
|
if (useBones && instance.boneSet[frameIndex]) {
|
||||||
|
vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS,
|
||||||
|
pipelineLayout_, 2, 1, &instance.boneSet[frameIndex], 0, nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
uint16_t desiredLOD = 0;
|
||||||
|
if (entry.distSq > 150.0f * 150.0f) desiredLOD = 3;
|
||||||
|
else if (entry.distSq > 80.0f * 80.0f) desiredLOD = 2;
|
||||||
|
else if (entry.distSq > 40.0f * 40.0f) desiredLOD = 1;
|
||||||
|
uint16_t targetLOD = desiredLOD;
|
||||||
|
if (desiredLOD > 0 && !(model.availableLODs & (1u << desiredLOD))) targetLOD = 0;
|
||||||
|
|
||||||
|
const bool particleDominantEffect = model.isSpellEffect &&
|
||||||
|
!model.particleEmitters.empty() && model.batches.size() <= 2;
|
||||||
|
|
||||||
|
for (const auto& batch : model.batches) {
|
||||||
|
if (batch.indexCount == 0) continue;
|
||||||
|
if (!model.isGroundDetail && batch.submeshLevel != targetLOD) continue;
|
||||||
|
if (batch.batchOpacity < 0.01f) continue;
|
||||||
|
|
||||||
|
// Pass 2 gate: only transparent/additive batches
|
||||||
|
{
|
||||||
|
const bool rawTransparent = (batch.blendMode >= 2) || model.isSpellEffect;
|
||||||
|
if (!rawTransparent) continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip glow sprites (handled after loop)
|
||||||
|
const bool batchUnlit = (batch.materialFlags & 0x01) != 0;
|
||||||
|
const bool shouldUseGlowSprite =
|
||||||
|
!batch.colorKeyBlack &&
|
||||||
|
(model.isElvenLike || model.isLanternLike) &&
|
||||||
|
!model.isSpellEffect &&
|
||||||
|
(batch.glowSize <= 1.35f || (batch.lanternGlowHint && batch.glowSize <= 6.0f)) &&
|
||||||
|
(batch.lanternGlowHint || (batch.blendMode >= 3) ||
|
||||||
|
(batch.colorKeyBlack && batchUnlit && batch.blendMode >= 1));
|
||||||
|
if (shouldUseGlowSprite) {
|
||||||
|
const bool cardLikeSkipMesh = (batch.blendMode >= 3) || batch.colorKeyBlack || batchUnlit;
|
||||||
|
if ((batch.glowCardLike && model.isLanternLike) || (cardLikeSkipMesh && !model.isLanternLike))
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
glm::vec2 uvOffset(0.0f, 0.0f);
|
||||||
|
if (batch.textureAnimIndex != 0xFFFF && model.hasTextureAnimation) {
|
||||||
|
uint16_t lookupIdx = batch.textureAnimIndex;
|
||||||
|
if (lookupIdx < model.textureTransformLookup.size()) {
|
||||||
|
uint16_t transformIdx = model.textureTransformLookup[lookupIdx];
|
||||||
|
if (transformIdx < model.textureTransforms.size()) {
|
||||||
|
const auto& tt = model.textureTransforms[transformIdx];
|
||||||
|
glm::vec3 trans = interpVec3(tt.translation,
|
||||||
|
instance.currentSequenceIndex, instance.animTime,
|
||||||
|
glm::vec3(0.0f), model.globalSequenceDurations);
|
||||||
|
uvOffset = glm::vec2(trans.x, trans.y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (model.isLavaModel && uvOffset == glm::vec2(0.0f)) {
|
||||||
|
static auto startTime2 = std::chrono::steady_clock::now();
|
||||||
|
float t = std::chrono::duration<float>(std::chrono::steady_clock::now() - startTime2).count();
|
||||||
|
uvOffset = glm::vec2(t * 0.03f, -t * 0.08f);
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t effectiveBlendMode = batch.blendMode;
|
||||||
|
if (model.isSpellEffect) {
|
||||||
|
if (effectiveBlendMode <= 1) effectiveBlendMode = 3;
|
||||||
|
else if (effectiveBlendMode == 4 || effectiveBlendMode == 5) effectiveBlendMode = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
VkPipeline desiredPipeline;
|
||||||
|
switch (effectiveBlendMode) {
|
||||||
|
case 2: desiredPipeline = alphaPipeline_; break;
|
||||||
|
default: desiredPipeline = additivePipeline_; break;
|
||||||
|
}
|
||||||
|
if (desiredPipeline != currentPipeline) {
|
||||||
|
vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, desiredPipeline);
|
||||||
|
currentPipeline = desiredPipeline;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (batch.materialUBOMapped) {
|
||||||
|
auto* mat = static_cast<M2MaterialUBO*>(batch.materialUBOMapped);
|
||||||
|
mat->interiorDarken = insideInterior ? 1.0f : 0.0f;
|
||||||
|
if (batch.colorKeyBlack)
|
||||||
|
mat->colorKeyThreshold = (effectiveBlendMode == 4 || effectiveBlendMode == 5) ? 0.7f : 0.08f;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!batch.materialSet) continue;
|
||||||
|
vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS,
|
||||||
|
pipelineLayout_, 1, 1, &batch.materialSet, 0, nullptr);
|
||||||
|
|
||||||
|
M2PushConstants pc;
|
||||||
|
pc.model = instance.modelMatrix;
|
||||||
|
pc.uvOffset = uvOffset;
|
||||||
|
pc.texCoordSet = static_cast<int>(batch.textureUnit);
|
||||||
|
pc.useBones = useBones ? 1 : 0;
|
||||||
|
pc.isFoliage = model.shadowWindFoliage ? 1 : 0;
|
||||||
|
pc.fadeAlpha = instanceFadeAlpha;
|
||||||
|
if (particleDominantEffect) continue; // emission-only mesh
|
||||||
|
vkCmdPushConstants(cmd, pipelineLayout_, VK_SHADER_STAGE_VERTEX_BIT, 0, sizeof(pc), &pc);
|
||||||
|
vkCmdDrawIndexed(cmd, batch.indexCount, 1, batch.indexStart, 0, 0);
|
||||||
|
lastDrawCallCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Render glow sprites as billboarded additive point lights
|
// Render glow sprites as billboarded additive point lights
|
||||||
if (!glowSprites_.empty() && particleAdditivePipeline_ && glowVB_ && glowTexDescSet_) {
|
if (!glowSprites_.empty() && particleAdditivePipeline_ && glowVB_ && glowTexDescSet_) {
|
||||||
vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, particleAdditivePipeline_);
|
vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, particleAdditivePipeline_);
|
||||||
|
|
|
||||||
|
|
@ -288,7 +288,9 @@ Renderer::~Renderer() = default;
|
||||||
bool Renderer::createPerFrameResources() {
|
bool Renderer::createPerFrameResources() {
|
||||||
VkDevice device = vkCtx->getDevice();
|
VkDevice device = vkCtx->getDevice();
|
||||||
|
|
||||||
// --- Create shadow depth image ---
|
// --- Create per-frame shadow depth images (one per in-flight frame) ---
|
||||||
|
// Each frame slot has its own depth image so that frame N's shadow read and
|
||||||
|
// frame N+1's shadow write cannot race on the same image.
|
||||||
VkImageCreateInfo imgCI{};
|
VkImageCreateInfo imgCI{};
|
||||||
imgCI.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
|
imgCI.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
|
||||||
imgCI.imageType = VK_IMAGE_TYPE_2D;
|
imgCI.imageType = VK_IMAGE_TYPE_2D;
|
||||||
|
|
@ -301,26 +303,30 @@ bool Renderer::createPerFrameResources() {
|
||||||
imgCI.usage = VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT;
|
imgCI.usage = VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT;
|
||||||
VmaAllocationCreateInfo imgAllocCI{};
|
VmaAllocationCreateInfo imgAllocCI{};
|
||||||
imgAllocCI.usage = VMA_MEMORY_USAGE_GPU_ONLY;
|
imgAllocCI.usage = VMA_MEMORY_USAGE_GPU_ONLY;
|
||||||
if (vmaCreateImage(vkCtx->getAllocator(), &imgCI, &imgAllocCI,
|
for (uint32_t i = 0; i < MAX_FRAMES; i++) {
|
||||||
&shadowDepthImage, &shadowDepthAlloc, nullptr) != VK_SUCCESS) {
|
if (vmaCreateImage(vkCtx->getAllocator(), &imgCI, &imgAllocCI,
|
||||||
LOG_ERROR("Failed to create shadow depth image");
|
&shadowDepthImage[i], &shadowDepthAlloc[i], nullptr) != VK_SUCCESS) {
|
||||||
return false;
|
LOG_ERROR("Failed to create shadow depth image [", i, "]");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
shadowDepthLayout_[i] = VK_IMAGE_LAYOUT_UNDEFINED;
|
||||||
}
|
}
|
||||||
shadowDepthLayout_ = VK_IMAGE_LAYOUT_UNDEFINED;
|
|
||||||
|
|
||||||
// --- Create shadow depth image view ---
|
// --- Create per-frame shadow depth image views ---
|
||||||
VkImageViewCreateInfo viewCI{};
|
VkImageViewCreateInfo viewCI{};
|
||||||
viewCI.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
|
viewCI.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
|
||||||
viewCI.image = shadowDepthImage;
|
|
||||||
viewCI.viewType = VK_IMAGE_VIEW_TYPE_2D;
|
viewCI.viewType = VK_IMAGE_VIEW_TYPE_2D;
|
||||||
viewCI.format = VK_FORMAT_D32_SFLOAT;
|
viewCI.format = VK_FORMAT_D32_SFLOAT;
|
||||||
viewCI.subresourceRange = {VK_IMAGE_ASPECT_DEPTH_BIT, 0, 1, 0, 1};
|
viewCI.subresourceRange = {VK_IMAGE_ASPECT_DEPTH_BIT, 0, 1, 0, 1};
|
||||||
if (vkCreateImageView(device, &viewCI, nullptr, &shadowDepthView) != VK_SUCCESS) {
|
for (uint32_t i = 0; i < MAX_FRAMES; i++) {
|
||||||
LOG_ERROR("Failed to create shadow depth image view");
|
viewCI.image = shadowDepthImage[i];
|
||||||
return false;
|
if (vkCreateImageView(device, &viewCI, nullptr, &shadowDepthView[i]) != VK_SUCCESS) {
|
||||||
|
LOG_ERROR("Failed to create shadow depth image view [", i, "]");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Create shadow sampler ---
|
// --- Create shadow sampler (shared — read-only, no per-frame needed) ---
|
||||||
VkSamplerCreateInfo sampCI{};
|
VkSamplerCreateInfo sampCI{};
|
||||||
sampCI.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO;
|
sampCI.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO;
|
||||||
sampCI.magFilter = VK_FILTER_LINEAR;
|
sampCI.magFilter = VK_FILTER_LINEAR;
|
||||||
|
|
@ -377,18 +383,20 @@ bool Renderer::createPerFrameResources() {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Create shadow framebuffer ---
|
// --- Create per-frame shadow framebuffers ---
|
||||||
VkFramebufferCreateInfo fbCI{};
|
VkFramebufferCreateInfo fbCI{};
|
||||||
fbCI.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO;
|
fbCI.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO;
|
||||||
fbCI.renderPass = shadowRenderPass;
|
fbCI.renderPass = shadowRenderPass;
|
||||||
fbCI.attachmentCount = 1;
|
fbCI.attachmentCount = 1;
|
||||||
fbCI.pAttachments = &shadowDepthView;
|
|
||||||
fbCI.width = SHADOW_MAP_SIZE;
|
fbCI.width = SHADOW_MAP_SIZE;
|
||||||
fbCI.height = SHADOW_MAP_SIZE;
|
fbCI.height = SHADOW_MAP_SIZE;
|
||||||
fbCI.layers = 1;
|
fbCI.layers = 1;
|
||||||
if (vkCreateFramebuffer(device, &fbCI, nullptr, &shadowFramebuffer) != VK_SUCCESS) {
|
for (uint32_t i = 0; i < MAX_FRAMES; i++) {
|
||||||
LOG_ERROR("Failed to create shadow framebuffer");
|
fbCI.pAttachments = &shadowDepthView[i];
|
||||||
return false;
|
if (vkCreateFramebuffer(device, &fbCI, nullptr, &shadowFramebuffer[i]) != VK_SUCCESS) {
|
||||||
|
LOG_ERROR("Failed to create shadow framebuffer [", i, "]");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Create descriptor set layout for set 0 (per-frame UBO + shadow sampler) ---
|
// --- Create descriptor set layout for set 0 (per-frame UBO + shadow sampler) ---
|
||||||
|
|
@ -470,7 +478,7 @@ bool Renderer::createPerFrameResources() {
|
||||||
|
|
||||||
VkDescriptorImageInfo shadowImgInfo{};
|
VkDescriptorImageInfo shadowImgInfo{};
|
||||||
shadowImgInfo.sampler = shadowSampler;
|
shadowImgInfo.sampler = shadowSampler;
|
||||||
shadowImgInfo.imageView = shadowDepthView;
|
shadowImgInfo.imageView = shadowDepthView[i];
|
||||||
shadowImgInfo.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
|
shadowImgInfo.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
|
||||||
|
|
||||||
VkWriteDescriptorSet writes[2]{};
|
VkWriteDescriptorSet writes[2]{};
|
||||||
|
|
@ -527,7 +535,7 @@ bool Renderer::createPerFrameResources() {
|
||||||
|
|
||||||
VkDescriptorImageInfo shadowImgInfo{};
|
VkDescriptorImageInfo shadowImgInfo{};
|
||||||
shadowImgInfo.sampler = shadowSampler;
|
shadowImgInfo.sampler = shadowSampler;
|
||||||
shadowImgInfo.imageView = shadowDepthView;
|
shadowImgInfo.imageView = shadowDepthView[0]; // reflection uses frame 0 shadow view
|
||||||
shadowImgInfo.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
|
shadowImgInfo.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
|
||||||
|
|
||||||
VkWriteDescriptorSet writes[2]{};
|
VkWriteDescriptorSet writes[2]{};
|
||||||
|
|
@ -576,13 +584,15 @@ void Renderer::destroyPerFrameResources() {
|
||||||
perFrameSetLayout = VK_NULL_HANDLE;
|
perFrameSetLayout = VK_NULL_HANDLE;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Destroy shadow resources
|
// Destroy per-frame shadow resources
|
||||||
if (shadowFramebuffer) { vkDestroyFramebuffer(device, shadowFramebuffer, nullptr); shadowFramebuffer = VK_NULL_HANDLE; }
|
for (uint32_t i = 0; i < MAX_FRAMES; i++) {
|
||||||
|
if (shadowFramebuffer[i]) { vkDestroyFramebuffer(device, shadowFramebuffer[i], nullptr); shadowFramebuffer[i] = VK_NULL_HANDLE; }
|
||||||
|
if (shadowDepthView[i]) { vkDestroyImageView(device, shadowDepthView[i], nullptr); shadowDepthView[i] = VK_NULL_HANDLE; }
|
||||||
|
if (shadowDepthImage[i]) { vmaDestroyImage(vkCtx->getAllocator(), shadowDepthImage[i], shadowDepthAlloc[i]); shadowDepthImage[i] = VK_NULL_HANDLE; shadowDepthAlloc[i] = VK_NULL_HANDLE; }
|
||||||
|
shadowDepthLayout_[i] = VK_IMAGE_LAYOUT_UNDEFINED;
|
||||||
|
}
|
||||||
if (shadowRenderPass) { vkDestroyRenderPass(device, shadowRenderPass, nullptr); shadowRenderPass = VK_NULL_HANDLE; }
|
if (shadowRenderPass) { vkDestroyRenderPass(device, shadowRenderPass, nullptr); shadowRenderPass = VK_NULL_HANDLE; }
|
||||||
if (shadowDepthView) { vkDestroyImageView(device, shadowDepthView, nullptr); shadowDepthView = VK_NULL_HANDLE; }
|
|
||||||
if (shadowDepthImage) { vmaDestroyImage(vkCtx->getAllocator(), shadowDepthImage, shadowDepthAlloc); shadowDepthImage = VK_NULL_HANDLE; shadowDepthAlloc = VK_NULL_HANDLE; }
|
|
||||||
if (shadowSampler) { vkDestroySampler(device, shadowSampler, nullptr); shadowSampler = VK_NULL_HANDLE; }
|
if (shadowSampler) { vkDestroySampler(device, shadowSampler, nullptr); shadowSampler = VK_NULL_HANDLE; }
|
||||||
shadowDepthLayout_ = VK_IMAGE_LAYOUT_UNDEFINED;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void Renderer::updatePerFrameUBO() {
|
void Renderer::updatePerFrameUBO() {
|
||||||
|
|
@ -1088,7 +1098,7 @@ void Renderer::beginFrame() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Shadow pre-pass (before main render pass)
|
// Shadow pre-pass (before main render pass)
|
||||||
if (shadowsEnabled && shadowDepthImage != VK_NULL_HANDLE) {
|
if (shadowsEnabled && shadowDepthImage[0] != VK_NULL_HANDLE) {
|
||||||
renderShadowPass();
|
renderShadowPass();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -5669,7 +5679,7 @@ void Renderer::renderReflectionPass() {
|
||||||
void Renderer::renderShadowPass() {
|
void Renderer::renderShadowPass() {
|
||||||
static const bool skipShadows = (std::getenv("WOWEE_SKIP_SHADOWS") != nullptr);
|
static const bool skipShadows = (std::getenv("WOWEE_SKIP_SHADOWS") != nullptr);
|
||||||
if (skipShadows) return;
|
if (skipShadows) return;
|
||||||
if (!shadowsEnabled || shadowDepthImage == VK_NULL_HANDLE) return;
|
if (!shadowsEnabled || shadowDepthImage[0] == VK_NULL_HANDLE) return;
|
||||||
if (currentCmd == VK_NULL_HANDLE) return;
|
if (currentCmd == VK_NULL_HANDLE) return;
|
||||||
|
|
||||||
// Shadows render every frame — throttling causes visible flicker on player/NPCs
|
// Shadows render every frame — throttling causes visible flicker on player/NPCs
|
||||||
|
|
@ -5686,21 +5696,21 @@ void Renderer::renderShadowPass() {
|
||||||
ubo->shadowParams.y = 0.8f;
|
ubo->shadowParams.y = 0.8f;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Barrier 1: transition shadow map into writable depth layout.
|
// Barrier 1: transition this frame's shadow map into writable depth layout.
|
||||||
VkImageMemoryBarrier b1{};
|
VkImageMemoryBarrier b1{};
|
||||||
b1.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
|
b1.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
|
||||||
b1.oldLayout = shadowDepthLayout_;
|
b1.oldLayout = shadowDepthLayout_[frame];
|
||||||
b1.newLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;
|
b1.newLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;
|
||||||
b1.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
|
b1.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
|
||||||
b1.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
|
b1.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
|
||||||
b1.srcAccessMask = (shadowDepthLayout_ == VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL)
|
b1.srcAccessMask = (shadowDepthLayout_[frame] == VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL)
|
||||||
? VK_ACCESS_SHADER_READ_BIT
|
? VK_ACCESS_SHADER_READ_BIT
|
||||||
: 0;
|
: 0;
|
||||||
b1.dstAccessMask = VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT |
|
b1.dstAccessMask = VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT |
|
||||||
VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT;
|
VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT;
|
||||||
b1.image = shadowDepthImage;
|
b1.image = shadowDepthImage[frame];
|
||||||
b1.subresourceRange = {VK_IMAGE_ASPECT_DEPTH_BIT, 0, 1, 0, 1};
|
b1.subresourceRange = {VK_IMAGE_ASPECT_DEPTH_BIT, 0, 1, 0, 1};
|
||||||
VkPipelineStageFlags srcStage = (shadowDepthLayout_ == VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL)
|
VkPipelineStageFlags srcStage = (shadowDepthLayout_[frame] == VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL)
|
||||||
? VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT
|
? VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT
|
||||||
: VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
|
: VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
|
||||||
vkCmdPipelineBarrier(currentCmd,
|
vkCmdPipelineBarrier(currentCmd,
|
||||||
|
|
@ -5711,7 +5721,7 @@ void Renderer::renderShadowPass() {
|
||||||
VkRenderPassBeginInfo rpInfo{};
|
VkRenderPassBeginInfo rpInfo{};
|
||||||
rpInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO;
|
rpInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO;
|
||||||
rpInfo.renderPass = shadowRenderPass;
|
rpInfo.renderPass = shadowRenderPass;
|
||||||
rpInfo.framebuffer = shadowFramebuffer;
|
rpInfo.framebuffer = shadowFramebuffer[frame];
|
||||||
rpInfo.renderArea = {{0, 0}, {SHADOW_MAP_SIZE, SHADOW_MAP_SIZE}};
|
rpInfo.renderArea = {{0, 0}, {SHADOW_MAP_SIZE, SHADOW_MAP_SIZE}};
|
||||||
VkClearValue clear{};
|
VkClearValue clear{};
|
||||||
clear.depthStencil = {1.0f, 0};
|
clear.depthStencil = {1.0f, 0};
|
||||||
|
|
@ -5750,12 +5760,12 @@ void Renderer::renderShadowPass() {
|
||||||
b2.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
|
b2.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
|
||||||
b2.srcAccessMask = VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT;
|
b2.srcAccessMask = VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT;
|
||||||
b2.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;
|
b2.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;
|
||||||
b2.image = shadowDepthImage;
|
b2.image = shadowDepthImage[frame];
|
||||||
b2.subresourceRange = {VK_IMAGE_ASPECT_DEPTH_BIT, 0, 1, 0, 1};
|
b2.subresourceRange = {VK_IMAGE_ASPECT_DEPTH_BIT, 0, 1, 0, 1};
|
||||||
vkCmdPipelineBarrier(currentCmd,
|
vkCmdPipelineBarrier(currentCmd,
|
||||||
VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT,
|
VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT,
|
||||||
0, 0, nullptr, 0, nullptr, 1, &b2);
|
0, 0, nullptr, 0, nullptr, 1, &b2);
|
||||||
shadowDepthLayout_ = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
|
shadowDepthLayout_[frame] = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace rendering
|
} // namespace rendering
|
||||||
|
|
|
||||||
|
|
@ -885,13 +885,15 @@ bool TerrainManager::advanceFinalization(FinalizingTile& ft) {
|
||||||
}
|
}
|
||||||
|
|
||||||
case FinalizationPhase::M2_INSTANCES: {
|
case FinalizationPhase::M2_INSTANCES: {
|
||||||
// Create all M2 instances (lightweight struct allocation, no GPU work)
|
// Create M2 instances incrementally to avoid main-thread stalls.
|
||||||
if (m2Renderer) {
|
// createInstance includes an O(n) bone-sibling scan that becomes expensive
|
||||||
int loadedDoodads = 0;
|
// on dense tiles with many placements and a large existing instance list.
|
||||||
int skippedDedup = 0;
|
if (m2Renderer && ft.m2InstanceIndex < pending->m2Placements.size()) {
|
||||||
for (const auto& p : pending->m2Placements) {
|
constexpr size_t kInstancesPerStep = 32;
|
||||||
|
size_t created = 0;
|
||||||
|
while (ft.m2InstanceIndex < pending->m2Placements.size() && created < kInstancesPerStep) {
|
||||||
|
const auto& p = pending->m2Placements[ft.m2InstanceIndex++];
|
||||||
if (p.uniqueId != 0 && placedDoodadIds.count(p.uniqueId)) {
|
if (p.uniqueId != 0 && placedDoodadIds.count(p.uniqueId)) {
|
||||||
skippedDedup++;
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
uint32_t instId = m2Renderer->createInstance(p.modelId, p.position, p.rotation, p.scale);
|
uint32_t instId = m2Renderer->createInstance(p.modelId, p.position, p.rotation, p.scale);
|
||||||
|
|
@ -901,12 +903,14 @@ bool TerrainManager::advanceFinalization(FinalizingTile& ft) {
|
||||||
placedDoodadIds.insert(p.uniqueId);
|
placedDoodadIds.insert(p.uniqueId);
|
||||||
ft.tileUniqueIds.push_back(p.uniqueId);
|
ft.tileUniqueIds.push_back(p.uniqueId);
|
||||||
}
|
}
|
||||||
loadedDoodads++;
|
created++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (ft.m2InstanceIndex < pending->m2Placements.size()) {
|
||||||
|
return false; // More instances to create — yield
|
||||||
|
}
|
||||||
LOG_DEBUG(" Loaded doodads for tile [", x, ",", y, "]: ",
|
LOG_DEBUG(" Loaded doodads for tile [", x, ",", y, "]: ",
|
||||||
loadedDoodads, " instances (", ft.uploadedM2ModelIds.size(), " new models, ",
|
ft.m2InstanceIds.size(), " instances (", ft.uploadedM2ModelIds.size(), " new models)");
|
||||||
skippedDedup, " dedup skipped)");
|
|
||||||
}
|
}
|
||||||
ft.phase = FinalizationPhase::WMO_MODELS;
|
ft.phase = FinalizationPhase::WMO_MODELS;
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -948,17 +952,15 @@ bool TerrainManager::advanceFinalization(FinalizingTile& ft) {
|
||||||
}
|
}
|
||||||
|
|
||||||
case FinalizationPhase::WMO_INSTANCES: {
|
case FinalizationPhase::WMO_INSTANCES: {
|
||||||
// Create all WMO instances + load WMO liquids
|
// Create WMO instances incrementally to avoid stalls on tiles with many WMOs.
|
||||||
if (wmoRenderer) {
|
if (wmoRenderer && ft.wmoInstanceIndex < pending->wmoModels.size()) {
|
||||||
int loadedWMOs = 0;
|
constexpr size_t kWmoInstancesPerStep = 4;
|
||||||
int loadedLiquids = 0;
|
size_t created = 0;
|
||||||
int skippedWmoDedup = 0;
|
while (ft.wmoInstanceIndex < pending->wmoModels.size() && created < kWmoInstancesPerStep) {
|
||||||
for (auto& wmoReady : pending->wmoModels) {
|
auto& wmoReady = pending->wmoModels[ft.wmoInstanceIndex++];
|
||||||
if (wmoReady.uniqueId != 0 && placedWmoIds.count(wmoReady.uniqueId)) {
|
if (wmoReady.uniqueId != 0 && placedWmoIds.count(wmoReady.uniqueId)) {
|
||||||
skippedWmoDedup++;
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint32_t wmoInstId = wmoRenderer->createInstance(wmoReady.modelId, wmoReady.position, wmoReady.rotation);
|
uint32_t wmoInstId = wmoRenderer->createInstance(wmoReady.modelId, wmoReady.position, wmoReady.rotation);
|
||||||
if (wmoInstId) {
|
if (wmoInstId) {
|
||||||
ft.wmoInstanceIds.push_back(wmoInstId);
|
ft.wmoInstanceIds.push_back(wmoInstId);
|
||||||
|
|
@ -966,8 +968,6 @@ bool TerrainManager::advanceFinalization(FinalizingTile& ft) {
|
||||||
placedWmoIds.insert(wmoReady.uniqueId);
|
placedWmoIds.insert(wmoReady.uniqueId);
|
||||||
ft.tileWmoUniqueIds.push_back(wmoReady.uniqueId);
|
ft.tileWmoUniqueIds.push_back(wmoReady.uniqueId);
|
||||||
}
|
}
|
||||||
loadedWMOs++;
|
|
||||||
|
|
||||||
// Load WMO liquids (canals, pools, etc.)
|
// Load WMO liquids (canals, pools, etc.)
|
||||||
if (waterRenderer) {
|
if (waterRenderer) {
|
||||||
glm::mat4 modelMatrix = glm::mat4(1.0f);
|
glm::mat4 modelMatrix = glm::mat4(1.0f);
|
||||||
|
|
@ -977,25 +977,21 @@ bool TerrainManager::advanceFinalization(FinalizingTile& ft) {
|
||||||
modelMatrix = glm::rotate(modelMatrix, wmoReady.rotation.x, glm::vec3(1.0f, 0.0f, 0.0f));
|
modelMatrix = glm::rotate(modelMatrix, wmoReady.rotation.x, glm::vec3(1.0f, 0.0f, 0.0f));
|
||||||
for (const auto& group : wmoReady.model.groups) {
|
for (const auto& group : wmoReady.model.groups) {
|
||||||
if (!group.liquid.hasLiquid()) continue;
|
if (!group.liquid.hasLiquid()) continue;
|
||||||
// Skip interior water/ocean but keep magma/slime (e.g. Ironforge lava)
|
|
||||||
if (group.flags & 0x2000) {
|
if (group.flags & 0x2000) {
|
||||||
uint16_t lt = group.liquid.materialId;
|
uint16_t lt = group.liquid.materialId;
|
||||||
uint8_t basicType = (lt == 0) ? 0 : ((lt - 1) % 4);
|
uint8_t basicType = (lt == 0) ? 0 : ((lt - 1) % 4);
|
||||||
if (basicType < 2) continue;
|
if (basicType < 2) continue;
|
||||||
}
|
}
|
||||||
waterRenderer->loadFromWMO(group.liquid, modelMatrix, wmoInstId);
|
waterRenderer->loadFromWMO(group.liquid, modelMatrix, wmoInstId);
|
||||||
loadedLiquids++;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
created++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (loadedWMOs > 0 || skippedWmoDedup > 0) {
|
if (ft.wmoInstanceIndex < pending->wmoModels.size()) {
|
||||||
LOG_DEBUG(" Loaded WMOs for tile [", x, ",", y, "]: ",
|
return false; // More WMO instances to create — yield
|
||||||
loadedWMOs, " instances, ", skippedWmoDedup, " dedup skipped");
|
|
||||||
}
|
|
||||||
if (loadedLiquids > 0) {
|
|
||||||
LOG_DEBUG(" Loaded WMO liquids for tile [", x, ",", y, "]: ", loadedLiquids);
|
|
||||||
}
|
}
|
||||||
|
LOG_DEBUG(" Loaded WMOs for tile [", x, ",", y, "]: ", ft.wmoInstanceIds.size(), " instances");
|
||||||
}
|
}
|
||||||
ft.phase = FinalizationPhase::WMO_DOODADS;
|
ft.phase = FinalizationPhase::WMO_DOODADS;
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -2213,10 +2209,16 @@ void TerrainManager::streamTiles() {
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Enqueue tiles in radius around current tile for async loading
|
// Enqueue tiles in radius around current tile for async loading.
|
||||||
|
// Collect all newly-needed tiles, then sort by distance so the closest
|
||||||
|
// (most visible) tiles get loaded first. This is critical during taxi
|
||||||
|
// flight where new tiles enter the radius faster than they can load.
|
||||||
{
|
{
|
||||||
std::lock_guard<std::mutex> lock(queueMutex);
|
std::lock_guard<std::mutex> lock(queueMutex);
|
||||||
|
|
||||||
|
struct PendingEntry { TileCoord coord; int distSq; };
|
||||||
|
std::vector<PendingEntry> newTiles;
|
||||||
|
|
||||||
for (int dy = -loadRadius; dy <= loadRadius; dy++) {
|
for (int dy = -loadRadius; dy <= loadRadius; dy++) {
|
||||||
for (int dx = -loadRadius; dx <= loadRadius; dx++) {
|
for (int dx = -loadRadius; dx <= loadRadius; dx++) {
|
||||||
int tileX = currentTile.x + dx;
|
int tileX = currentTile.x + dx;
|
||||||
|
|
@ -2240,10 +2242,19 @@ void TerrainManager::streamTiles() {
|
||||||
if (failedTiles.find(coord) != failedTiles.end()) continue;
|
if (failedTiles.find(coord) != failedTiles.end()) continue;
|
||||||
if (shouldSkipMissingAdt(coord)) continue;
|
if (shouldSkipMissingAdt(coord)) continue;
|
||||||
|
|
||||||
loadQueue.push_back(coord);
|
newTiles.push_back({coord, dx*dx + dy*dy});
|
||||||
pendingTiles[coord] = true;
|
pendingTiles[coord] = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sort nearest tiles first so workers service the most visible tiles
|
||||||
|
std::sort(newTiles.begin(), newTiles.end(),
|
||||||
|
[](const PendingEntry& a, const PendingEntry& b) { return a.distSq < b.distSq; });
|
||||||
|
|
||||||
|
// Insert at front so new close tiles preempt any distant tiles already queued
|
||||||
|
for (auto it = newTiles.rbegin(); it != newTiles.rend(); ++it) {
|
||||||
|
loadQueue.push_front(it->coord);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Notify workers that there's work
|
// Notify workers that there's work
|
||||||
|
|
|
||||||
|
|
@ -414,6 +414,7 @@ bool WMORenderer::loadModel(const pipeline::WMOModel& model, uint32_t id) {
|
||||||
modelData.id = id;
|
modelData.id = id;
|
||||||
modelData.boundingBoxMin = model.boundingBoxMin;
|
modelData.boundingBoxMin = model.boundingBoxMin;
|
||||||
modelData.boundingBoxMax = model.boundingBoxMax;
|
modelData.boundingBoxMax = model.boundingBoxMax;
|
||||||
|
modelData.wmoAmbientColor = model.ambientColor;
|
||||||
{
|
{
|
||||||
glm::vec3 ext = model.boundingBoxMax - model.boundingBoxMin;
|
glm::vec3 ext = model.boundingBoxMax - model.boundingBoxMin;
|
||||||
float horiz = std::max(ext.x, ext.y);
|
float horiz = std::max(ext.x, ext.y);
|
||||||
|
|
@ -681,6 +682,9 @@ bool WMORenderer::loadModel(const pipeline::WMOModel& model, uint32_t id) {
|
||||||
matData.heightMapVariance = mb.heightMapVariance;
|
matData.heightMapVariance = mb.heightMapVariance;
|
||||||
matData.normalMapStrength = normalMapStrength_;
|
matData.normalMapStrength = normalMapStrength_;
|
||||||
matData.isLava = mb.isLava ? 1 : 0;
|
matData.isLava = mb.isLava ? 1 : 0;
|
||||||
|
matData.wmoAmbientR = modelData.wmoAmbientColor.r;
|
||||||
|
matData.wmoAmbientG = modelData.wmoAmbientColor.g;
|
||||||
|
matData.wmoAmbientB = modelData.wmoAmbientColor.b;
|
||||||
if (matBuf.info.pMappedData) {
|
if (matBuf.info.pMappedData) {
|
||||||
memcpy(matBuf.info.pMappedData, &matData, sizeof(matData));
|
memcpy(matBuf.info.pMappedData, &matData, sizeof(matData));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -401,8 +401,10 @@ void GameScreen::render(game::GameHandler& gameHandler) {
|
||||||
renderMirrorTimers(gameHandler);
|
renderMirrorTimers(gameHandler);
|
||||||
renderQuestObjectiveTracker(gameHandler);
|
renderQuestObjectiveTracker(gameHandler);
|
||||||
if (showNameplates_) renderNameplates(gameHandler);
|
if (showNameplates_) renderNameplates(gameHandler);
|
||||||
|
renderBattlegroundScore(gameHandler);
|
||||||
renderCombatText(gameHandler);
|
renderCombatText(gameHandler);
|
||||||
renderPartyFrames(gameHandler);
|
renderPartyFrames(gameHandler);
|
||||||
|
renderBossFrames(gameHandler);
|
||||||
renderGroupInvitePopup(gameHandler);
|
renderGroupInvitePopup(gameHandler);
|
||||||
renderDuelRequestPopup(gameHandler);
|
renderDuelRequestPopup(gameHandler);
|
||||||
renderLootRollPopup(gameHandler);
|
renderLootRollPopup(gameHandler);
|
||||||
|
|
@ -432,6 +434,7 @@ void GameScreen::render(game::GameHandler& gameHandler) {
|
||||||
// renderQuestMarkers(gameHandler); // Disabled - using 3D billboard markers now
|
// renderQuestMarkers(gameHandler); // Disabled - using 3D billboard markers now
|
||||||
renderMinimapMarkers(gameHandler);
|
renderMinimapMarkers(gameHandler);
|
||||||
renderDeathScreen(gameHandler);
|
renderDeathScreen(gameHandler);
|
||||||
|
renderReclaimCorpseButton(gameHandler);
|
||||||
renderResurrectDialog(gameHandler);
|
renderResurrectDialog(gameHandler);
|
||||||
renderChatBubbles(gameHandler);
|
renderChatBubbles(gameHandler);
|
||||||
renderEscapeMenu();
|
renderEscapeMenu();
|
||||||
|
|
@ -1865,6 +1868,37 @@ void GameScreen::renderPlayerFrame(game::GameHandler& gameHandler) {
|
||||||
}
|
}
|
||||||
ImGui::Dummy(ImVec2(totalW, squareH));
|
ImGui::Dummy(ImVec2(totalW, squareH));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Combo point display — Rogue (4) and Druid (11) in Cat Form
|
||||||
|
{
|
||||||
|
uint8_t cls = gameHandler.getPlayerClass();
|
||||||
|
const bool isRogue = (cls == 4);
|
||||||
|
const bool isDruid = (cls == 11);
|
||||||
|
if (isRogue || isDruid) {
|
||||||
|
uint8_t cp = gameHandler.getComboPoints();
|
||||||
|
if (cp > 0 || isRogue) { // always show for rogue; only when non-zero for druid
|
||||||
|
ImGui::Spacing();
|
||||||
|
ImVec2 cursor = ImGui::GetCursorScreenPos();
|
||||||
|
float totalW = ImGui::GetContentRegionAvail().x;
|
||||||
|
constexpr int MAX_CP = 5;
|
||||||
|
constexpr float DOT_R = 7.0f;
|
||||||
|
constexpr float SPACING = 4.0f;
|
||||||
|
float totalDotsW = MAX_CP * (DOT_R * 2.0f) + (MAX_CP - 1) * SPACING;
|
||||||
|
float startX = cursor.x + (totalW - totalDotsW) * 0.5f;
|
||||||
|
float cy = cursor.y + DOT_R;
|
||||||
|
ImDrawList* dl = ImGui::GetWindowDrawList();
|
||||||
|
for (int i = 0; i < MAX_CP; ++i) {
|
||||||
|
float cx = startX + i * (DOT_R * 2.0f + SPACING) + DOT_R;
|
||||||
|
ImU32 col = (i < static_cast<int>(cp))
|
||||||
|
? IM_COL32(255, 210, 0, 240) // bright gold — active
|
||||||
|
: IM_COL32(60, 60, 60, 160); // dark — empty
|
||||||
|
dl->AddCircleFilled(ImVec2(cx, cy), DOT_R, col);
|
||||||
|
dl->AddCircle(ImVec2(cx, cy), DOT_R, IM_COL32(160, 140, 0, 180), 0, 1.5f);
|
||||||
|
}
|
||||||
|
ImGui::Dummy(ImVec2(totalW, DOT_R * 2.0f));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
ImGui::End();
|
ImGui::End();
|
||||||
|
|
||||||
|
|
@ -2090,6 +2124,22 @@ void GameScreen::renderTargetFrame(game::GameHandler& gameHandler) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Target cast bar — shown when the target is casting
|
||||||
|
if (gameHandler.isTargetCasting()) {
|
||||||
|
float castPct = gameHandler.getTargetCastProgress();
|
||||||
|
float castLeft = gameHandler.getTargetCastTimeRemaining();
|
||||||
|
uint32_t tspell = gameHandler.getTargetCastSpellId();
|
||||||
|
const std::string& castName = (tspell != 0) ? gameHandler.getSpellName(tspell) : "";
|
||||||
|
ImGui::PushStyleColor(ImGuiCol_PlotHistogram, ImVec4(0.9f, 0.3f, 0.2f, 1.0f));
|
||||||
|
char castLabel[72];
|
||||||
|
if (!castName.empty())
|
||||||
|
snprintf(castLabel, sizeof(castLabel), "%s (%.1fs)", castName.c_str(), castLeft);
|
||||||
|
else
|
||||||
|
snprintf(castLabel, sizeof(castLabel), "Casting... (%.1fs)", castLeft);
|
||||||
|
ImGui::ProgressBar(castPct, ImVec2(-1, 14), castLabel);
|
||||||
|
ImGui::PopStyleColor();
|
||||||
|
}
|
||||||
|
|
||||||
// Distance
|
// Distance
|
||||||
const auto& movement = gameHandler.getMovementInfo();
|
const auto& movement = gameHandler.getMovementInfo();
|
||||||
float dx = target->getX() - movement.x;
|
float dx = target->getX() - movement.x;
|
||||||
|
|
@ -4670,6 +4720,10 @@ void GameScreen::renderCombatText(game::GameHandler& gameHandler) {
|
||||||
snprintf(text, sizeof(text), "+%d XP", entry.amount);
|
snprintf(text, sizeof(text), "+%d XP", entry.amount);
|
||||||
color = ImVec4(0.7f, 0.3f, 1.0f, alpha); // Purple for XP
|
color = ImVec4(0.7f, 0.3f, 1.0f, alpha); // Purple for XP
|
||||||
break;
|
break;
|
||||||
|
case game::CombatTextEntry::IMMUNE:
|
||||||
|
snprintf(text, sizeof(text), "Immune!");
|
||||||
|
color = ImVec4(0.9f, 0.9f, 0.9f, alpha); // White for immune
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
snprintf(text, sizeof(text), "%d", entry.amount);
|
snprintf(text, sizeof(text), "%d", entry.amount);
|
||||||
color = ImVec4(1.0f, 1.0f, 1.0f, alpha);
|
color = ImVec4(1.0f, 1.0f, 1.0f, alpha);
|
||||||
|
|
@ -4883,6 +4937,21 @@ void GameScreen::renderPartyFrames(game::GameHandler& gameHandler) {
|
||||||
ImGui::PopStyleColor();
|
ImGui::PopStyleColor();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Party member cast bar — shows when the party member is casting
|
||||||
|
if (auto* cs = gameHandler.getUnitCastState(member.guid)) {
|
||||||
|
float castPct = (cs->timeTotal > 0.0f)
|
||||||
|
? (cs->timeTotal - cs->timeRemaining) / cs->timeTotal : 0.0f;
|
||||||
|
ImGui::PushStyleColor(ImGuiCol_PlotHistogram, ImVec4(0.8f, 0.8f, 0.2f, 1.0f));
|
||||||
|
char pcastLabel[48];
|
||||||
|
const std::string& spellNm = gameHandler.getSpellName(cs->spellId);
|
||||||
|
if (!spellNm.empty())
|
||||||
|
snprintf(pcastLabel, sizeof(pcastLabel), "%s (%.1fs)", spellNm.c_str(), cs->timeRemaining);
|
||||||
|
else
|
||||||
|
snprintf(pcastLabel, sizeof(pcastLabel), "Casting... (%.1fs)", cs->timeRemaining);
|
||||||
|
ImGui::ProgressBar(castPct, ImVec2(-1, 10), pcastLabel);
|
||||||
|
ImGui::PopStyleColor();
|
||||||
|
}
|
||||||
|
|
||||||
ImGui::Separator();
|
ImGui::Separator();
|
||||||
ImGui::PopID();
|
ImGui::PopID();
|
||||||
}
|
}
|
||||||
|
|
@ -4893,6 +4962,97 @@ void GameScreen::renderPartyFrames(game::GameHandler& gameHandler) {
|
||||||
ImGui::PopStyleVar();
|
ImGui::PopStyleVar();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Boss Encounter Frames
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
void GameScreen::renderBossFrames(game::GameHandler& gameHandler) {
|
||||||
|
// Collect active boss unit slots
|
||||||
|
struct BossSlot { uint32_t slot; uint64_t guid; };
|
||||||
|
std::vector<BossSlot> active;
|
||||||
|
for (uint32_t s = 0; s < game::GameHandler::kMaxEncounterSlots; ++s) {
|
||||||
|
uint64_t g = gameHandler.getEncounterUnitGuid(s);
|
||||||
|
if (g != 0) active.push_back({s, g});
|
||||||
|
}
|
||||||
|
if (active.empty()) return;
|
||||||
|
|
||||||
|
const float frameW = 200.0f;
|
||||||
|
const float startX = ImGui::GetIO().DisplaySize.x - frameW - 10.0f;
|
||||||
|
float frameY = 120.0f;
|
||||||
|
|
||||||
|
ImGuiWindowFlags flags = ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove |
|
||||||
|
ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar |
|
||||||
|
ImGuiWindowFlags_AlwaysAutoResize;
|
||||||
|
|
||||||
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 4.0f);
|
||||||
|
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.15f, 0.05f, 0.05f, 0.85f));
|
||||||
|
|
||||||
|
ImGui::SetNextWindowPos(ImVec2(startX, frameY), ImGuiCond_Always);
|
||||||
|
ImGui::SetNextWindowSize(ImVec2(frameW, 0.0f), ImGuiCond_Always);
|
||||||
|
|
||||||
|
if (ImGui::Begin("##BossFrames", nullptr, flags)) {
|
||||||
|
for (const auto& bs : active) {
|
||||||
|
ImGui::PushID(static_cast<int>(bs.guid));
|
||||||
|
|
||||||
|
// Try to resolve name and health from entity manager
|
||||||
|
std::string name = "Boss";
|
||||||
|
uint32_t hp = 0, maxHp = 0;
|
||||||
|
auto entity = gameHandler.getEntityManager().getEntity(bs.guid);
|
||||||
|
if (entity && (entity->getType() == game::ObjectType::UNIT ||
|
||||||
|
entity->getType() == game::ObjectType::PLAYER)) {
|
||||||
|
auto unit = std::static_pointer_cast<game::Unit>(entity);
|
||||||
|
const auto& n = unit->getName();
|
||||||
|
if (!n.empty()) name = n;
|
||||||
|
hp = unit->getHealth();
|
||||||
|
maxHp = unit->getMaxHealth();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clickable name to target
|
||||||
|
if (ImGui::Selectable(name.c_str(), gameHandler.getTargetGuid() == bs.guid)) {
|
||||||
|
gameHandler.setTarget(bs.guid);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (maxHp > 0) {
|
||||||
|
float pct = static_cast<float>(hp) / static_cast<float>(maxHp);
|
||||||
|
// Boss health bar in red shades
|
||||||
|
ImGui::PushStyleColor(ImGuiCol_PlotHistogram,
|
||||||
|
pct > 0.5f ? ImVec4(0.8f, 0.2f, 0.2f, 1.0f) :
|
||||||
|
pct > 0.2f ? ImVec4(0.9f, 0.5f, 0.1f, 1.0f) :
|
||||||
|
ImVec4(1.0f, 0.8f, 0.1f, 1.0f));
|
||||||
|
char label[32];
|
||||||
|
std::snprintf(label, sizeof(label), "%u / %u", hp, maxHp);
|
||||||
|
ImGui::ProgressBar(pct, ImVec2(-1, 14), label);
|
||||||
|
ImGui::PopStyleColor();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Boss cast bar — shown when the boss is casting (critical for interrupt)
|
||||||
|
if (auto* cs = gameHandler.getUnitCastState(bs.guid)) {
|
||||||
|
float castPct = (cs->timeTotal > 0.0f)
|
||||||
|
? (cs->timeTotal - cs->timeRemaining) / cs->timeTotal : 0.0f;
|
||||||
|
uint32_t bspell = cs->spellId;
|
||||||
|
const std::string& bcastName = (bspell != 0)
|
||||||
|
? gameHandler.getSpellName(bspell) : "";
|
||||||
|
ImGui::PushStyleColor(ImGuiCol_PlotHistogram, ImVec4(0.9f, 0.3f, 0.2f, 1.0f));
|
||||||
|
char bcastLabel[72];
|
||||||
|
if (!bcastName.empty())
|
||||||
|
snprintf(bcastLabel, sizeof(bcastLabel), "%s (%.1fs)",
|
||||||
|
bcastName.c_str(), cs->timeRemaining);
|
||||||
|
else
|
||||||
|
snprintf(bcastLabel, sizeof(bcastLabel), "Casting... (%.1fs)", cs->timeRemaining);
|
||||||
|
ImGui::ProgressBar(castPct, ImVec2(-1, 12), bcastLabel);
|
||||||
|
ImGui::PopStyleColor();
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::PopID();
|
||||||
|
ImGui::Spacing();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ImGui::End();
|
||||||
|
|
||||||
|
ImGui::PopStyleColor();
|
||||||
|
ImGui::PopStyleVar();
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Group Invite Popup (Phase 4)
|
// Group Invite Popup (Phase 4)
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
@ -6935,6 +7095,34 @@ void GameScreen::renderDeathScreen(game::GameHandler& gameHandler) {
|
||||||
ImGui::PopStyleVar();
|
ImGui::PopStyleVar();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void GameScreen::renderReclaimCorpseButton(game::GameHandler& gameHandler) {
|
||||||
|
if (!gameHandler.isPlayerGhost() || !gameHandler.canReclaimCorpse()) return;
|
||||||
|
|
||||||
|
auto* window = core::Application::getInstance().getWindow();
|
||||||
|
float screenW = window ? static_cast<float>(window->getWidth()) : 1280.0f;
|
||||||
|
float screenH = window ? static_cast<float>(window->getHeight()) : 720.0f;
|
||||||
|
|
||||||
|
float btnW = 220.0f, btnH = 36.0f;
|
||||||
|
ImGui::SetNextWindowPos(ImVec2(screenW / 2 - btnW / 2, screenH * 0.72f), ImGuiCond_Always);
|
||||||
|
ImGui::SetNextWindowSize(ImVec2(btnW + 16.0f, btnH + 16.0f), ImGuiCond_Always);
|
||||||
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 6.0f);
|
||||||
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(8.0f, 8.0f));
|
||||||
|
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.0f, 0.0f, 0.0f, 0.7f));
|
||||||
|
if (ImGui::Begin("##ReclaimCorpse", nullptr,
|
||||||
|
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove |
|
||||||
|
ImGuiWindowFlags_NoBringToFrontOnFocus)) {
|
||||||
|
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.15f, 0.35f, 0.15f, 1.0f));
|
||||||
|
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.25f, 0.55f, 0.25f, 1.0f));
|
||||||
|
if (ImGui::Button("Resurrect from Corpse", ImVec2(btnW, btnH))) {
|
||||||
|
gameHandler.reclaimCorpse();
|
||||||
|
}
|
||||||
|
ImGui::PopStyleColor(2);
|
||||||
|
}
|
||||||
|
ImGui::End();
|
||||||
|
ImGui::PopStyleColor();
|
||||||
|
ImGui::PopStyleVar(2);
|
||||||
|
}
|
||||||
|
|
||||||
void GameScreen::renderResurrectDialog(game::GameHandler& gameHandler) {
|
void GameScreen::renderResurrectDialog(game::GameHandler& gameHandler) {
|
||||||
if (!gameHandler.showResurrectDialog()) return;
|
if (!gameHandler.showResurrectDialog()) return;
|
||||||
|
|
||||||
|
|
@ -6956,10 +7144,13 @@ void GameScreen::renderResurrectDialog(game::GameHandler& gameHandler) {
|
||||||
ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar)) {
|
ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar)) {
|
||||||
|
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
const char* text = "Return to life?";
|
const std::string& casterName = gameHandler.getResurrectCasterName();
|
||||||
float textW = ImGui::CalcTextSize(text).x;
|
std::string text = casterName.empty()
|
||||||
ImGui::SetCursorPosX((dlgW - textW) / 2);
|
? "Return to life?"
|
||||||
ImGui::TextColored(ImVec4(0.8f, 0.9f, 1.0f, 1.0f), "%s", text);
|
: casterName + " wishes to resurrect you.";
|
||||||
|
float textW = ImGui::CalcTextSize(text.c_str()).x;
|
||||||
|
ImGui::SetCursorPosX(std::max(4.0f, (dlgW - textW) / 2));
|
||||||
|
ImGui::TextColored(ImVec4(0.8f, 0.9f, 1.0f, 1.0f), "%s", text.c_str());
|
||||||
|
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
|
|
@ -7106,9 +7297,19 @@ void GameScreen::renderSettingsWindow() {
|
||||||
saveSettings();
|
saveSettings();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (ImGui::Checkbox("Water Refraction", &pendingWaterRefraction)) {
|
{
|
||||||
if (renderer) renderer->setWaterRefractionEnabled(pendingWaterRefraction);
|
bool fsrActive = renderer && (renderer->isFSREnabled() || renderer->isFSR2Enabled());
|
||||||
saveSettings();
|
if (!fsrActive && pendingWaterRefraction) {
|
||||||
|
// FSR was disabled while refraction was on — auto-disable
|
||||||
|
pendingWaterRefraction = false;
|
||||||
|
if (renderer) renderer->setWaterRefractionEnabled(false);
|
||||||
|
}
|
||||||
|
if (!fsrActive) ImGui::BeginDisabled();
|
||||||
|
if (ImGui::Checkbox("Water Refraction (requires FSR)", &pendingWaterRefraction)) {
|
||||||
|
if (renderer) renderer->setWaterRefractionEnabled(pendingWaterRefraction);
|
||||||
|
saveSettings();
|
||||||
|
}
|
||||||
|
if (!fsrActive) ImGui::EndDisabled();
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
const char* aaLabels[] = { "Off", "2x MSAA", "4x MSAA", "8x MSAA" };
|
const char* aaLabels[] = { "Off", "2x MSAA", "4x MSAA", "8x MSAA" };
|
||||||
|
|
@ -7962,6 +8163,25 @@ void GameScreen::renderMinimapMarkers(game::GameHandler& gameHandler) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Minimap pings from party members
|
||||||
|
for (const auto& ping : gameHandler.getMinimapPings()) {
|
||||||
|
glm::vec3 pingRender = core::coords::canonicalToRender(glm::vec3(ping.wowX, ping.wowY, 0.0f));
|
||||||
|
float sx = 0.0f, sy = 0.0f;
|
||||||
|
if (!projectToMinimap(pingRender, sx, sy)) continue;
|
||||||
|
|
||||||
|
float t = ping.age / game::GameHandler::MinimapPing::LIFETIME;
|
||||||
|
float alpha = 1.0f - t;
|
||||||
|
float pulse = 1.0f + 1.5f * t; // expands outward as it fades
|
||||||
|
|
||||||
|
ImU32 col = IM_COL32(255, 220, 0, static_cast<int>(alpha * 200));
|
||||||
|
ImU32 col2 = IM_COL32(255, 150, 0, static_cast<int>(alpha * 100));
|
||||||
|
float r1 = 4.0f * pulse;
|
||||||
|
float r2 = 8.0f * pulse;
|
||||||
|
drawList->AddCircle(ImVec2(sx, sy), r1, col, 16, 2.0f);
|
||||||
|
drawList->AddCircle(ImVec2(sx, sy), r2, col2, 16, 1.0f);
|
||||||
|
drawList->AddCircleFilled(ImVec2(sx, sy), 2.5f, col);
|
||||||
|
}
|
||||||
|
|
||||||
auto applyMuteState = [&]() {
|
auto applyMuteState = [&]() {
|
||||||
auto* activeRenderer = core::Application::getInstance().getRenderer();
|
auto* activeRenderer = core::Application::getInstance().getRenderer();
|
||||||
float masterScale = soundMuted_ ? 0.0f : static_cast<float>(pendingMasterVolume) / 100.0f;
|
float masterScale = soundMuted_ ? 0.0f : static_cast<float>(pendingMasterVolume) / 100.0f;
|
||||||
|
|
@ -10157,4 +10377,122 @@ void GameScreen::renderInstanceLockouts(game::GameHandler& gameHandler) {
|
||||||
ImGui::End();
|
ImGui::End();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Battleground score frame
|
||||||
|
//
|
||||||
|
// Displays the current score for the player's battleground using world states.
|
||||||
|
// Shown in the top-centre of the screen whenever SMSG_INIT_WORLD_STATES has
|
||||||
|
// been received for a known BG map. The layout adapts per battleground:
|
||||||
|
//
|
||||||
|
// WSG 489 – Alliance / Horde flag captures (max 3)
|
||||||
|
// AB 529 – Alliance / Horde resource scores (max 1600)
|
||||||
|
// AV 30 – Alliance / Horde reinforcements
|
||||||
|
// EotS 566 – Alliance / Horde resource scores (max 1600)
|
||||||
|
// ============================================================================
|
||||||
|
void GameScreen::renderBattlegroundScore(game::GameHandler& gameHandler) {
|
||||||
|
// Only show when in a recognised battleground map
|
||||||
|
uint32_t mapId = gameHandler.getWorldStateMapId();
|
||||||
|
|
||||||
|
// World state key sets per battleground
|
||||||
|
// Keys from the WoW 3.3.5a WorldState.dbc / client source
|
||||||
|
struct BgScoreDef {
|
||||||
|
uint32_t mapId;
|
||||||
|
const char* name;
|
||||||
|
uint32_t allianceKey; // world state key for Alliance value
|
||||||
|
uint32_t hordeKey; // world state key for Horde value
|
||||||
|
uint32_t maxKey; // max score world state key (0 = use hardcoded)
|
||||||
|
uint32_t hardcodedMax; // used when maxKey == 0
|
||||||
|
const char* unit; // suffix label (e.g. "flags", "resources")
|
||||||
|
};
|
||||||
|
|
||||||
|
static constexpr BgScoreDef kBgDefs[] = {
|
||||||
|
// Warsong Gulch: 3 flag captures wins
|
||||||
|
{ 489, "Warsong Gulch", 1581, 1582, 0, 3, "flags" },
|
||||||
|
// Arathi Basin: 1600 resources wins
|
||||||
|
{ 529, "Arathi Basin", 1218, 1219, 0, 1600, "resources" },
|
||||||
|
// Alterac Valley: reinforcements count down from 600 / 800 etc.
|
||||||
|
{ 30, "Alterac Valley", 1322, 1323, 0, 600, "reinforcements" },
|
||||||
|
// Eye of the Storm: 1600 resources wins
|
||||||
|
{ 566, "Eye of the Storm", 2757, 2758, 0, 1600, "resources" },
|
||||||
|
// Strand of the Ancients (WotLK)
|
||||||
|
{ 607, "Strand of the Ancients", 3476, 3477, 0, 4, "" },
|
||||||
|
};
|
||||||
|
|
||||||
|
const BgScoreDef* def = nullptr;
|
||||||
|
for (const auto& d : kBgDefs) {
|
||||||
|
if (d.mapId == mapId) { def = &d; break; }
|
||||||
|
}
|
||||||
|
if (!def) return;
|
||||||
|
|
||||||
|
auto allianceOpt = gameHandler.getWorldState(def->allianceKey);
|
||||||
|
auto hordeOpt = gameHandler.getWorldState(def->hordeKey);
|
||||||
|
if (!allianceOpt && !hordeOpt) return;
|
||||||
|
|
||||||
|
uint32_t allianceScore = allianceOpt.value_or(0);
|
||||||
|
uint32_t hordeScore = hordeOpt.value_or(0);
|
||||||
|
uint32_t maxScore = def->hardcodedMax;
|
||||||
|
if (def->maxKey != 0) {
|
||||||
|
if (auto mv = gameHandler.getWorldState(def->maxKey)) maxScore = *mv;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto* window = core::Application::getInstance().getWindow();
|
||||||
|
float screenW = window ? static_cast<float>(window->getWidth()) : 1280.0f;
|
||||||
|
|
||||||
|
// Width scales with screen but stays reasonable
|
||||||
|
float frameW = 260.0f;
|
||||||
|
float frameH = 60.0f;
|
||||||
|
float posX = screenW / 2.0f - frameW / 2.0f;
|
||||||
|
float posY = 4.0f;
|
||||||
|
|
||||||
|
ImGui::SetNextWindowPos(ImVec2(posX, posY), ImGuiCond_Always);
|
||||||
|
ImGui::SetNextWindowSize(ImVec2(frameW, frameH), ImGuiCond_Always);
|
||||||
|
ImGui::SetNextWindowBgAlpha(0.75f);
|
||||||
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 6.0f);
|
||||||
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(6.0f, 4.0f));
|
||||||
|
|
||||||
|
if (ImGui::Begin("##BGScore", nullptr,
|
||||||
|
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove |
|
||||||
|
ImGuiWindowFlags_NoNav | ImGuiWindowFlags_NoBringToFrontOnFocus |
|
||||||
|
ImGuiWindowFlags_NoSavedSettings)) {
|
||||||
|
|
||||||
|
// BG name centred at top
|
||||||
|
float nameW = ImGui::CalcTextSize(def->name).x;
|
||||||
|
ImGui::SetCursorPosX((frameW - nameW) / 2.0f);
|
||||||
|
ImGui::TextColored(ImVec4(1.0f, 0.85f, 0.0f, 1.0f), "%s", def->name);
|
||||||
|
|
||||||
|
// Alliance score | separator | Horde score
|
||||||
|
float innerW = frameW - 12.0f;
|
||||||
|
float halfW = innerW / 2.0f - 4.0f;
|
||||||
|
|
||||||
|
ImGui::SetCursorPosX(6.0f);
|
||||||
|
ImGui::BeginGroup();
|
||||||
|
{
|
||||||
|
// Alliance (blue)
|
||||||
|
char aBuf[32];
|
||||||
|
if (maxScore > 0 && strlen(def->unit) > 0)
|
||||||
|
snprintf(aBuf, sizeof(aBuf), "\xF0\x9F\x94\xB5 %u / %u", allianceScore, maxScore);
|
||||||
|
else
|
||||||
|
snprintf(aBuf, sizeof(aBuf), "\xF0\x9F\x94\xB5 %u", allianceScore);
|
||||||
|
ImGui::TextColored(ImVec4(0.4f, 0.6f, 1.0f, 1.0f), "%s", aBuf);
|
||||||
|
}
|
||||||
|
ImGui::EndGroup();
|
||||||
|
|
||||||
|
ImGui::SameLine(halfW + 16.0f);
|
||||||
|
|
||||||
|
ImGui::BeginGroup();
|
||||||
|
{
|
||||||
|
// Horde (red)
|
||||||
|
char hBuf[32];
|
||||||
|
if (maxScore > 0 && strlen(def->unit) > 0)
|
||||||
|
snprintf(hBuf, sizeof(hBuf), "\xF0\x9F\x94\xB4 %u / %u", hordeScore, maxScore);
|
||||||
|
else
|
||||||
|
snprintf(hBuf, sizeof(hBuf), "\xF0\x9F\x94\xB4 %u", hordeScore);
|
||||||
|
ImGui::TextColored(ImVec4(1.0f, 0.35f, 0.35f, 1.0f), "%s", hBuf);
|
||||||
|
}
|
||||||
|
ImGui::EndGroup();
|
||||||
|
}
|
||||||
|
ImGui::End();
|
||||||
|
ImGui::PopStyleVar(2);
|
||||||
|
}
|
||||||
|
|
||||||
}} // namespace wowee::ui
|
}} // namespace wowee::ui
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue