mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-05-02 15:53:51 +00:00
Compare commits
82 commits
a90f2acd26
...
a87d62abf8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a87d62abf8 | ||
|
|
a7261a0d15 | ||
|
|
bba2f20588 | ||
|
|
9aa4b223dc | ||
|
|
214c1a9ff8 | ||
|
|
284b98d93a | ||
|
|
81b95b4af7 | ||
|
|
cd01d07a91 | ||
|
|
eafd09aca0 | ||
|
|
a97941f062 | ||
|
|
5883654e1e | ||
|
|
218d68e275 | ||
|
|
2f479c6230 | ||
|
|
6957ba97ea | ||
|
|
6df8c72cf7 | ||
|
|
882cb1bae3 | ||
|
|
b7c1aa39a9 | ||
|
|
fbcec9e7bf | ||
|
|
806744c483 | ||
|
|
925d15713c | ||
|
|
6e95709b68 | ||
|
|
819a690c33 | ||
|
|
42d66bc876 | ||
|
|
129fa84fe3 | ||
|
|
59fc7cebaf | ||
|
|
b52e9c29c6 | ||
|
|
3f340ca235 | ||
|
|
c3afe543c6 | ||
|
|
5216582f15 | ||
|
|
77879769d3 | ||
|
|
98ad71df0d | ||
|
|
9e4c3d67d9 | ||
|
|
78ad20f95d | ||
|
|
c503bc9432 | ||
|
|
39fc6a645e | ||
|
|
bff690ea53 | ||
|
|
e2f36f6ac5 | ||
|
|
6dc630c1d8 | ||
|
|
6645845d05 | ||
|
|
c0c750a76e | ||
|
|
6044739661 | ||
|
|
61412ae06d | ||
|
|
d682ec4ca7 | ||
|
|
2f234af43b | ||
|
|
6d21a8cb8d | ||
|
|
b44ff09b63 | ||
|
|
950a4e2991 | ||
|
|
65f19b2d53 | ||
|
|
d34f505eea | ||
|
|
9494b1e607 | ||
|
|
d2db0b46ff | ||
|
|
1165aa6e74 | ||
|
|
8cb0f1d0ef | ||
|
|
a03ee33f8c | ||
|
|
fb843026ad | ||
|
|
e2b2425230 | ||
|
|
f39ba56390 | ||
|
|
e6f48dd822 | ||
|
|
1e76df7c98 | ||
|
|
3d1b187986 | ||
|
|
3665723622 | ||
|
|
abfb6ecdb5 | ||
|
|
8d7391d73e | ||
|
|
c76ac579cb | ||
|
|
9336b2943c | ||
|
|
d46feee4fc | ||
|
|
2268f7ac34 | ||
|
|
6ffc0cec3d | ||
|
|
fe4fc714c3 | ||
|
|
0ffcf001a5 | ||
|
|
d7c4bdcd57 | ||
|
|
3ea1b96681 | ||
|
|
a10139284d | ||
|
|
ed2b50af26 | ||
|
|
8921c2ddf4 | ||
|
|
d44f5e6560 | ||
|
|
bc0d98adae | ||
|
|
a56b50df2b | ||
|
|
48f12d9ca8 | ||
|
|
d48e4fb7c3 | ||
|
|
60794c6e0f | ||
|
|
0a2cd213dc |
25 changed files with 4567 additions and 214 deletions
|
|
@ -31,6 +31,7 @@
|
|||
"ReputationBase0": 10, "ReputationBase1": 11,
|
||||
"ReputationBase2": 12, "ReputationBase3": 13
|
||||
},
|
||||
"CharTitles": { "ID": 0, "Title": 2, "TitleBit": 20 },
|
||||
"AreaTable": { "ID": 0, "MapID": 1, "ParentAreaNum": 2, "ExploreFlag": 3 },
|
||||
"CreatureDisplayInfoExtra": {
|
||||
"ID": 0, "RaceID": 1, "SexID": 2, "SkinID": 3, "FaceID": 4,
|
||||
|
|
@ -97,5 +98,18 @@
|
|||
"ID": 0, "MapID": 1, "AreaID": 2, "AreaName": 3,
|
||||
"LocLeft": 4, "LocRight": 5, "LocTop": 6, "LocBottom": 7,
|
||||
"DisplayMapID": 8, "ParentWorldMapID": 10
|
||||
},
|
||||
"SpellItemEnchantment": {
|
||||
"ID": 0, "Name": 8
|
||||
},
|
||||
"ItemSet": {
|
||||
"ID": 0, "Name": 1,
|
||||
"Item0": 18, "Item1": 19, "Item2": 20, "Item3": 21, "Item4": 22,
|
||||
"Item5": 23, "Item6": 24, "Item7": 25, "Item8": 26, "Item9": 27,
|
||||
"Spell0": 28, "Spell1": 29, "Spell2": 30, "Spell3": 31, "Spell4": 32,
|
||||
"Spell5": 33, "Spell6": 34, "Spell7": 35, "Spell8": 36, "Spell9": 37,
|
||||
"Threshold0": 38, "Threshold1": 39, "Threshold2": 40, "Threshold3": 41,
|
||||
"Threshold4": 42, "Threshold5": 43, "Threshold6": 44, "Threshold7": 45,
|
||||
"Threshold8": 46, "Threshold9": 47
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -95,5 +95,18 @@
|
|||
"ID": 0, "MapID": 1, "AreaID": 2, "AreaName": 3,
|
||||
"LocLeft": 4, "LocRight": 5, "LocTop": 6, "LocBottom": 7,
|
||||
"DisplayMapID": 8, "ParentWorldMapID": 10
|
||||
},
|
||||
"SpellItemEnchantment": {
|
||||
"ID": 0, "Name": 8
|
||||
},
|
||||
"ItemSet": {
|
||||
"ID": 0, "Name": 1,
|
||||
"Item0": 10, "Item1": 11, "Item2": 12, "Item3": 13, "Item4": 14,
|
||||
"Item5": 15, "Item6": 16, "Item7": 17, "Item8": 18, "Item9": 19,
|
||||
"Spell0": 20, "Spell1": 21, "Spell2": 22, "Spell3": 23, "Spell4": 24,
|
||||
"Spell5": 25, "Spell6": 26, "Spell7": 27, "Spell8": 28, "Spell9": 29,
|
||||
"Threshold0": 30, "Threshold1": 31, "Threshold2": 32, "Threshold3": 33,
|
||||
"Threshold4": 34, "Threshold5": 35, "Threshold6": 36, "Threshold7": 37,
|
||||
"Threshold8": 38, "Threshold9": 39
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,7 +31,9 @@
|
|||
"ReputationBase0": 10, "ReputationBase1": 11,
|
||||
"ReputationBase2": 12, "ReputationBase3": 13
|
||||
},
|
||||
"Achievement": { "ID": 0, "Title": 4, "Description": 21 },
|
||||
"CharTitles": { "ID": 0, "Title": 2, "TitleBit": 36 },
|
||||
"Achievement": { "ID": 0, "Title": 4, "Description": 21, "Points": 39 },
|
||||
"AchievementCriteria": { "ID": 0, "AchievementID": 1, "Quantity": 4, "Description": 9 },
|
||||
"AreaTable": { "ID": 0, "MapID": 1, "ParentAreaNum": 2, "ExploreFlag": 3 },
|
||||
"CreatureDisplayInfoExtra": {
|
||||
"ID": 0, "RaceID": 1, "SexID": 2, "SkinID": 3, "FaceID": 4,
|
||||
|
|
@ -98,5 +100,18 @@
|
|||
"ID": 0, "MapID": 1, "AreaID": 2, "AreaName": 3,
|
||||
"LocLeft": 4, "LocRight": 5, "LocTop": 6, "LocBottom": 7,
|
||||
"DisplayMapID": 8, "ParentWorldMapID": 10
|
||||
},
|
||||
"SpellItemEnchantment": {
|
||||
"ID": 0, "Name": 8
|
||||
},
|
||||
"ItemSet": {
|
||||
"ID": 0, "Name": 1,
|
||||
"Item0": 18, "Item1": 19, "Item2": 20, "Item3": 21, "Item4": 22,
|
||||
"Item5": 23, "Item6": 24, "Item7": 25, "Item8": 26, "Item9": 27,
|
||||
"Spell0": 28, "Spell1": 29, "Spell2": 30, "Spell3": 31, "Spell4": 32,
|
||||
"Spell5": 33, "Spell6": 34, "Spell7": 35, "Spell8": 36, "Spell9": 37,
|
||||
"Threshold0": 38, "Threshold1": 39, "Threshold2": 40, "Threshold3": 41,
|
||||
"Threshold4": 42, "Threshold5": 43, "Threshold6": 44, "Threshold7": 45,
|
||||
"Threshold8": 46, "Threshold9": 47
|
||||
}
|
||||
}
|
||||
|
|
|
|||
132
assets/shaders/fxaa.frag.glsl
Normal file
132
assets/shaders/fxaa.frag.glsl
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
#version 450
|
||||
|
||||
// FXAA 3.11 — Fast Approximate Anti-Aliasing post-process pass.
|
||||
// Reads the resolved scene color and outputs a smoothed result.
|
||||
// Push constant: rcpFrame = vec2(1/width, 1/height).
|
||||
|
||||
layout(set = 0, binding = 0) uniform sampler2D uScene;
|
||||
|
||||
layout(location = 0) in vec2 TexCoord;
|
||||
layout(location = 0) out vec4 outColor;
|
||||
|
||||
layout(push_constant) uniform PC {
|
||||
vec2 rcpFrame;
|
||||
} pc;
|
||||
|
||||
// Quality tuning
|
||||
#define FXAA_EDGE_THRESHOLD (1.0/8.0) // minimum edge contrast to process
|
||||
#define FXAA_EDGE_THRESHOLD_MIN (1.0/24.0) // ignore very dark regions
|
||||
#define FXAA_SEARCH_STEPS 12
|
||||
#define FXAA_SEARCH_THRESHOLD (1.0/4.0)
|
||||
#define FXAA_SUBPIX 0.75
|
||||
#define FXAA_SUBPIX_TRIM (1.0/4.0)
|
||||
#define FXAA_SUBPIX_TRIM_SCALE (1.0/(1.0 - FXAA_SUBPIX_TRIM))
|
||||
#define FXAA_SUBPIX_CAP (3.0/4.0)
|
||||
|
||||
float luma(vec3 c) {
|
||||
return dot(c, vec3(0.299, 0.587, 0.114));
|
||||
}
|
||||
|
||||
void main() {
|
||||
vec2 uv = TexCoord;
|
||||
vec2 rcp = pc.rcpFrame;
|
||||
|
||||
// --- Centre and cardinal neighbours ---
|
||||
vec3 rgbM = texture(uScene, uv).rgb;
|
||||
vec3 rgbN = texture(uScene, uv + vec2( 0.0, -1.0) * rcp).rgb;
|
||||
vec3 rgbS = texture(uScene, uv + vec2( 0.0, 1.0) * rcp).rgb;
|
||||
vec3 rgbE = texture(uScene, uv + vec2( 1.0, 0.0) * rcp).rgb;
|
||||
vec3 rgbW = texture(uScene, uv + vec2(-1.0, 0.0) * rcp).rgb;
|
||||
|
||||
float lumaN = luma(rgbN);
|
||||
float lumaS = luma(rgbS);
|
||||
float lumaE = luma(rgbE);
|
||||
float lumaW = luma(rgbW);
|
||||
float lumaM = luma(rgbM);
|
||||
|
||||
float lumaMin = min(lumaM, min(min(lumaN, lumaS), min(lumaE, lumaW)));
|
||||
float lumaMax = max(lumaM, max(max(lumaN, lumaS), max(lumaE, lumaW)));
|
||||
float range = lumaMax - lumaMin;
|
||||
|
||||
// Early exit on smooth regions
|
||||
if (range < max(FXAA_EDGE_THRESHOLD_MIN, lumaMax * FXAA_EDGE_THRESHOLD)) {
|
||||
outColor = vec4(rgbM, 1.0);
|
||||
return;
|
||||
}
|
||||
|
||||
// --- Diagonal neighbours ---
|
||||
vec3 rgbNW = texture(uScene, uv + vec2(-1.0, -1.0) * rcp).rgb;
|
||||
vec3 rgbNE = texture(uScene, uv + vec2( 1.0, -1.0) * rcp).rgb;
|
||||
vec3 rgbSW = texture(uScene, uv + vec2(-1.0, 1.0) * rcp).rgb;
|
||||
vec3 rgbSE = texture(uScene, uv + vec2( 1.0, 1.0) * rcp).rgb;
|
||||
|
||||
float lumaNW = luma(rgbNW);
|
||||
float lumaNE = luma(rgbNE);
|
||||
float lumaSW = luma(rgbSW);
|
||||
float lumaSE = luma(rgbSE);
|
||||
|
||||
// --- Sub-pixel blend factor ---
|
||||
float lumaL = (lumaN + lumaS + lumaE + lumaW) * 0.25;
|
||||
float rangeL = abs(lumaL - lumaM);
|
||||
float blendL = max(0.0, (rangeL / range) - FXAA_SUBPIX_TRIM) * FXAA_SUBPIX_TRIM_SCALE;
|
||||
blendL = min(FXAA_SUBPIX_CAP, blendL) * FXAA_SUBPIX;
|
||||
|
||||
// --- Edge orientation (horizontal vs. vertical) ---
|
||||
float edgeHorz =
|
||||
abs(-2.0*lumaW + lumaNW + lumaSW)
|
||||
+ 2.0*abs(-2.0*lumaM + lumaN + lumaS)
|
||||
+ abs(-2.0*lumaE + lumaNE + lumaSE);
|
||||
float edgeVert =
|
||||
abs(-2.0*lumaS + lumaSW + lumaSE)
|
||||
+ 2.0*abs(-2.0*lumaM + lumaW + lumaE)
|
||||
+ abs(-2.0*lumaN + lumaNW + lumaNE);
|
||||
|
||||
bool horzSpan = (edgeHorz >= edgeVert);
|
||||
float lengthSign = horzSpan ? rcp.y : rcp.x;
|
||||
|
||||
float luma1 = horzSpan ? lumaN : lumaW;
|
||||
float luma2 = horzSpan ? lumaS : lumaE;
|
||||
float grad1 = abs(luma1 - lumaM);
|
||||
float grad2 = abs(luma2 - lumaM);
|
||||
lengthSign = (grad1 >= grad2) ? -lengthSign : lengthSign;
|
||||
|
||||
// --- Edge search ---
|
||||
vec2 posB = uv;
|
||||
vec2 offNP = horzSpan ? vec2(rcp.x, 0.0) : vec2(0.0, rcp.y);
|
||||
if (!horzSpan) posB.x += lengthSign * 0.5;
|
||||
if ( horzSpan) posB.y += lengthSign * 0.5;
|
||||
|
||||
float lumaMLSS = lumaM - (luma1 + luma2) * 0.5;
|
||||
float gradientScaled = max(grad1, grad2) * 0.25;
|
||||
|
||||
vec2 posN = posB - offNP;
|
||||
vec2 posP = posB + offNP;
|
||||
bool done1 = false, done2 = false;
|
||||
float lumaEnd1 = 0.0, lumaEnd2 = 0.0;
|
||||
|
||||
for (int i = 0; i < FXAA_SEARCH_STEPS; ++i) {
|
||||
if (!done1) lumaEnd1 = luma(texture(uScene, posN).rgb) - lumaMLSS;
|
||||
if (!done2) lumaEnd2 = luma(texture(uScene, posP).rgb) - lumaMLSS;
|
||||
done1 = done1 || (abs(lumaEnd1) >= gradientScaled * FXAA_SEARCH_THRESHOLD);
|
||||
done2 = done2 || (abs(lumaEnd2) >= gradientScaled * FXAA_SEARCH_THRESHOLD);
|
||||
if (done1 && done2) break;
|
||||
if (!done1) posN -= offNP;
|
||||
if (!done2) posP += offNP;
|
||||
}
|
||||
|
||||
float dstN = horzSpan ? (uv.x - posN.x) : (uv.y - posN.y);
|
||||
float dstP = horzSpan ? (posP.x - uv.x) : (posP.y - uv.y);
|
||||
bool dirN = (dstN < dstP);
|
||||
float lumaEndFinal = dirN ? lumaEnd1 : lumaEnd2;
|
||||
|
||||
float spanLength = dstN + dstP;
|
||||
float pixelOffset = (dirN ? dstN : dstP) / spanLength;
|
||||
bool goodSpan = ((lumaEndFinal < 0.0) != (lumaMLSS < 0.0));
|
||||
float pixelOffsetFinal = max(goodSpan ? pixelOffset : 0.0, blendL);
|
||||
|
||||
vec2 finalUV = uv;
|
||||
if ( horzSpan) finalUV.y += pixelOffsetFinal * lengthSign;
|
||||
if (!horzSpan) finalUV.x += pixelOffsetFinal * lengthSign;
|
||||
|
||||
outColor = vec4(texture(uScene, finalUV).rgb, 1.0);
|
||||
}
|
||||
|
|
@ -295,6 +295,13 @@ public:
|
|||
// Server-authoritative armor (UNIT_FIELD_RESISTANCES[0])
|
||||
int32_t getArmorRating() const { return playerArmorRating_; }
|
||||
|
||||
// Server-authoritative elemental resistances (UNIT_FIELD_RESISTANCES[1-6]).
|
||||
// school: 1=Holy, 2=Fire, 3=Nature, 4=Frost, 5=Shadow, 6=Arcane. Returns 0 if not received.
|
||||
int32_t getResistance(int school) const {
|
||||
if (school < 1 || school > 6) return 0;
|
||||
return playerResistances_[school - 1];
|
||||
}
|
||||
|
||||
// Server-authoritative primary stats (UNIT_FIELD_STAT0-4: STR, AGI, STA, INT, SPI).
|
||||
// Returns -1 if the server hasn't sent the value yet.
|
||||
int32_t getPlayerStat(int idx) const {
|
||||
|
|
@ -541,6 +548,17 @@ public:
|
|||
}
|
||||
std::string getCachedPlayerName(uint64_t guid) const;
|
||||
std::string getCachedCreatureName(uint32_t entry) const;
|
||||
// Returns the creature subname/title (e.g. "<Warchief of the Horde>"), empty if not cached
|
||||
std::string getCachedCreatureSubName(uint32_t entry) const {
|
||||
auto it = creatureInfoCache.find(entry);
|
||||
return (it != creatureInfoCache.end()) ? it->second.subName : "";
|
||||
}
|
||||
// Returns the creature rank (0=Normal,1=Elite,2=RareElite,3=Boss,4=Rare)
|
||||
// or -1 if not cached yet
|
||||
int getCreatureRank(uint32_t entry) const {
|
||||
auto it = creatureInfoCache.find(entry);
|
||||
return (it != creatureInfoCache.end()) ? static_cast<int>(it->second.rank) : -1;
|
||||
}
|
||||
|
||||
// ---- Phase 2: Combat ----
|
||||
void startAutoAttack(uint64_t targetGuid);
|
||||
|
|
@ -555,6 +573,9 @@ public:
|
|||
}
|
||||
uint64_t getAutoAttackTargetGuid() const { return autoAttackTarget; }
|
||||
bool isAggressiveTowardPlayer(uint64_t guid) const { return hostileAttackers_.count(guid) > 0; }
|
||||
// Timestamp (ms since epoch) of the most recent player melee auto-attack.
|
||||
// Zero if no swing has occurred this session.
|
||||
uint64_t getLastMeleeSwingMs() const { return lastMeleeSwingMs_; }
|
||||
const std::vector<CombatTextEntry>& getCombatText() const { return combatText; }
|
||||
void updateCombatText(float deltaTime);
|
||||
|
||||
|
|
@ -591,6 +612,7 @@ public:
|
|||
void cancelCast();
|
||||
void cancelAura(uint32_t spellId);
|
||||
void dismissPet();
|
||||
void renamePet(const std::string& newName);
|
||||
bool hasPet() const { return petGuid_ != 0; }
|
||||
uint64_t getPetGuid() const { return petGuid_; }
|
||||
|
||||
|
|
@ -616,6 +638,24 @@ public:
|
|||
void sendPetAction(uint32_t action, uint64_t targetGuid = 0);
|
||||
const std::unordered_set<uint32_t>& getKnownSpells() const { return knownSpells; }
|
||||
|
||||
// ---- Pet Stable ----
|
||||
struct StabledPet {
|
||||
uint32_t petNumber = 0; // server-side pet number (used for unstable/swap)
|
||||
uint32_t entry = 0; // creature entry ID
|
||||
uint32_t level = 0;
|
||||
std::string name;
|
||||
uint32_t displayId = 0;
|
||||
bool isActive = false; // true = currently summoned/active slot
|
||||
};
|
||||
bool isStableWindowOpen() const { return stableWindowOpen_; }
|
||||
void closeStableWindow() { stableWindowOpen_ = false; }
|
||||
uint64_t getStableMasterGuid() const { return stableMasterGuid_; }
|
||||
uint8_t getStableSlots() const { return stableNumSlots_; }
|
||||
const std::vector<StabledPet>& getStabledPets() const { return stabledPets_; }
|
||||
void requestStabledPetList(); // CMSG MSG_LIST_STABLED_PETS
|
||||
void stablePet(uint8_t slot); // CMSG_STABLE_PET (store active pet in slot)
|
||||
void unstablePet(uint32_t petNumber); // CMSG_UNSTABLE_PET (retrieve to active)
|
||||
|
||||
// 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)
|
||||
|
|
@ -688,6 +728,14 @@ public:
|
|||
static std::unordered_map<uint32_t, uint8_t> empty;
|
||||
return spec < 2 ? learnedTalents_[spec] : empty;
|
||||
}
|
||||
|
||||
// Glyphs (WotLK): up to 6 glyph slots per spec (3 major + 3 minor)
|
||||
static constexpr uint8_t MAX_GLYPH_SLOTS = 6;
|
||||
const std::array<uint16_t, MAX_GLYPH_SLOTS>& getGlyphs() const { return learnedGlyphs_[activeTalentSpec_]; }
|
||||
const std::array<uint16_t, MAX_GLYPH_SLOTS>& getGlyphs(uint8_t spec) const {
|
||||
static std::array<uint16_t, MAX_GLYPH_SLOTS> empty{};
|
||||
return spec < 2 ? learnedGlyphs_[spec] : empty;
|
||||
}
|
||||
uint8_t getTalentRank(uint32_t talentId) const {
|
||||
auto it = learnedTalents_[activeTalentSpec_].find(talentId);
|
||||
return (it != learnedTalents_[activeTalentSpec_].end()) ? it->second : 0;
|
||||
|
|
@ -838,6 +886,11 @@ public:
|
|||
using KnockBackCallback = std::function<void(float vcos, float vsin, float hspeed, float vspeed)>;
|
||||
void setKnockBackCallback(KnockBackCallback cb) { knockBackCallback_ = std::move(cb); }
|
||||
|
||||
// Camera shake callback: called when server sends SMSG_CAMERA_SHAKE.
|
||||
// Parameters: magnitude (world units), frequency (Hz), duration (seconds).
|
||||
using CameraShakeCallback = std::function<void(float magnitude, float frequency, float duration)>;
|
||||
void setCameraShakeCallback(CameraShakeCallback cb) { cameraShakeCallback_ = std::move(cb); }
|
||||
|
||||
// Unstuck callback (resets player Z to floor height)
|
||||
using UnstuckCallback = std::function<void()>;
|
||||
void setUnstuckCallback(UnstuckCallback cb) { unstuckCallback_ = std::move(cb); }
|
||||
|
|
@ -965,6 +1018,7 @@ public:
|
|||
|
||||
// Cooldowns
|
||||
float getSpellCooldown(uint32_t spellId) const;
|
||||
const std::unordered_map<uint32_t, float>& getSpellCooldowns() const { return spellCooldowns; }
|
||||
|
||||
// Player GUID
|
||||
uint64_t getPlayerGuid() const { return playerGuid; }
|
||||
|
|
@ -1203,6 +1257,11 @@ public:
|
|||
void setAutoLoot(bool enabled) { autoLoot_ = enabled; }
|
||||
bool isAutoLoot() const { return autoLoot_; }
|
||||
|
||||
// Master loot candidates (from SMSG_LOOT_MASTER_LIST)
|
||||
const std::vector<uint64_t>& getMasterLootCandidates() const { return masterLootCandidates_; }
|
||||
bool hasMasterLootCandidates() const { return !masterLootCandidates_.empty(); }
|
||||
void lootMasterGive(uint8_t lootSlot, uint64_t targetGuid);
|
||||
|
||||
// Group loot roll
|
||||
struct LootRollEntry {
|
||||
uint64_t objectGuid = 0;
|
||||
|
|
@ -1225,6 +1284,16 @@ public:
|
|||
void sendLootRoll(uint64_t objectGuid, uint32_t slot, uint8_t rollType);
|
||||
// rollType: 0=need, 1=greed, 2=disenchant, 96=pass
|
||||
|
||||
// Equipment Sets (WotLK): saved gear loadouts
|
||||
struct EquipmentSetInfo {
|
||||
uint64_t setGuid = 0;
|
||||
uint32_t setId = 0;
|
||||
std::string name;
|
||||
std::string iconName;
|
||||
};
|
||||
const std::vector<EquipmentSetInfo>& getEquipmentSets() const { return equipmentSetInfo_; }
|
||||
void useEquipmentSet(uint32_t setId);
|
||||
|
||||
// NPC Gossip
|
||||
void interactWithNpc(uint64_t guid);
|
||||
void interactWithGameObject(uint64_t guid);
|
||||
|
|
@ -1401,6 +1470,32 @@ public:
|
|||
using LevelUpCallback = std::function<void(uint32_t newLevel)>;
|
||||
void setLevelUpCallback(LevelUpCallback cb) { levelUpCallback_ = std::move(cb); }
|
||||
|
||||
// Stat deltas from the last SMSG_LEVELUP_INFO (valid until next level-up)
|
||||
struct LevelUpDeltas {
|
||||
uint32_t hp = 0;
|
||||
uint32_t mana = 0;
|
||||
uint32_t str = 0, agi = 0, sta = 0, intel = 0, spi = 0;
|
||||
};
|
||||
const LevelUpDeltas& getLastLevelUpDeltas() const { return lastLevelUpDeltas_; }
|
||||
|
||||
// Temporary weapon enchant timers (from SMSG_ITEM_ENCHANT_TIME_UPDATE)
|
||||
// Slot: 0=main-hand, 1=off-hand, 2=ranged. Value: expire time (steady_clock ms).
|
||||
struct TempEnchantTimer {
|
||||
uint32_t slot = 0;
|
||||
uint64_t expireMs = 0; // std::chrono::steady_clock ms timestamp when it expires
|
||||
};
|
||||
const std::vector<TempEnchantTimer>& getTempEnchantTimers() const { return tempEnchantTimers_; }
|
||||
// Returns remaining ms for a given slot, or 0 if absent/expired.
|
||||
uint32_t getTempEnchantRemainingMs(uint32_t slot) const;
|
||||
static constexpr const char* kTempEnchantSlotNames[] = { "Main Hand", "Off Hand", "Ranged" };
|
||||
|
||||
// ---- Readable text (books / scrolls / notes) ----
|
||||
// Populated by handlePageTextQueryResponse(); multi-page items chain via nextPageId.
|
||||
struct BookPage { uint32_t pageId = 0; std::string text; };
|
||||
const std::vector<BookPage>& getBookPages() const { return bookPages_; }
|
||||
bool hasBookOpen() const { return !bookPages_.empty(); }
|
||||
void clearBook() { bookPages_.clear(); }
|
||||
|
||||
// Other player level-up callback — fires when another player gains a level
|
||||
using OtherPlayerLevelUpCallback = std::function<void(uint64_t guid, uint32_t newLevel)>;
|
||||
void setOtherPlayerLevelUpCallback(OtherPlayerLevelUpCallback cb) { otherPlayerLevelUpCallback_ = std::move(cb); }
|
||||
|
|
@ -1409,6 +1504,17 @@ public:
|
|||
using AchievementEarnedCallback = std::function<void(uint32_t achievementId, const std::string& name)>;
|
||||
void setAchievementEarnedCallback(AchievementEarnedCallback cb) { achievementEarnedCallback_ = std::move(cb); }
|
||||
const std::unordered_set<uint32_t>& getEarnedAchievements() const { return earnedAchievements_; }
|
||||
|
||||
// Area discovery callback — fires when SMSG_EXPLORATION_EXPERIENCE is received
|
||||
using AreaDiscoveryCallback = std::function<void(const std::string& areaName, uint32_t xpGained)>;
|
||||
void setAreaDiscoveryCallback(AreaDiscoveryCallback cb) { areaDiscoveryCallback_ = std::move(cb); }
|
||||
|
||||
// Quest objective progress callback — fires on SMSG_QUESTUPDATE_ADD_KILL / ADD_ITEM
|
||||
// questTitle: name of the quest; objectiveName: creature/item name; current/required counts
|
||||
using QuestProgressCallback = std::function<void(const std::string& questTitle,
|
||||
const std::string& objectiveName,
|
||||
uint32_t current, uint32_t required)>;
|
||||
void setQuestProgressCallback(QuestProgressCallback cb) { questProgressCallback_ = std::move(cb); }
|
||||
const std::unordered_map<uint32_t, uint64_t>& getCriteriaProgress() const { return criteriaProgress_; }
|
||||
/// Returns the WoW PackedTime earn date for an achievement, or 0 if unknown.
|
||||
uint32_t getAchievementDate(uint32_t id) const {
|
||||
|
|
@ -1422,6 +1528,18 @@ public:
|
|||
static const std::string kEmpty;
|
||||
return kEmpty;
|
||||
}
|
||||
/// Returns the description of an achievement by ID, or empty string if unknown.
|
||||
const std::string& getAchievementDescription(uint32_t id) const {
|
||||
auto it = achievementDescCache_.find(id);
|
||||
if (it != achievementDescCache_.end()) return it->second;
|
||||
static const std::string kEmpty;
|
||||
return kEmpty;
|
||||
}
|
||||
/// Returns the point value of an achievement by ID, or 0 if unknown.
|
||||
uint32_t getAchievementPoints(uint32_t id) const {
|
||||
auto it = achievementPointsCache_.find(id);
|
||||
return (it != achievementPointsCache_.end()) ? it->second : 0u;
|
||||
}
|
||||
|
||||
// Server-triggered music callback — fires when SMSG_PLAY_MUSIC is received.
|
||||
// The soundId corresponds to a SoundEntries.dbc record. The receiver is
|
||||
|
|
@ -1448,6 +1566,14 @@ public:
|
|||
using RepChangeCallback = std::function<void(const std::string& factionName, int32_t delta, int32_t standing)>;
|
||||
void setRepChangeCallback(RepChangeCallback cb) { repChangeCallback_ = std::move(cb); }
|
||||
|
||||
// PvP honor credit callback (honorable kill or BG reward)
|
||||
using PvpHonorCallback = std::function<void(uint32_t honorAmount, uint64_t victimGuid, uint32_t victimRank)>;
|
||||
void setPvpHonorCallback(PvpHonorCallback cb) { pvpHonorCallback_ = std::move(cb); }
|
||||
|
||||
// Item looted / received callback (SMSG_ITEM_PUSH_RESULT when showInChat is set)
|
||||
using ItemLootCallback = std::function<void(uint32_t itemId, uint32_t count, uint32_t quality, const std::string& name)>;
|
||||
void setItemLootCallback(ItemLootCallback cb) { itemLootCallback_ = std::move(cb); }
|
||||
|
||||
// Quest turn-in completion callback
|
||||
using QuestCompleteCallback = std::function<void(uint32_t questId, const std::string& questTitle)>;
|
||||
void setQuestCompleteCallback(QuestCompleteCallback cb) { questCompleteCallback_ = std::move(cb); }
|
||||
|
|
@ -1544,6 +1670,11 @@ public:
|
|||
return it != taxiNpcHasRoutes_.end() && it->second;
|
||||
}
|
||||
|
||||
// Vehicle (WotLK)
|
||||
bool isInVehicle() const { return vehicleId_ != 0; }
|
||||
uint32_t getVehicleId() const { return vehicleId_; }
|
||||
void sendRequestVehicleExit();
|
||||
|
||||
// Vendor
|
||||
void openVendor(uint64_t npcGuid);
|
||||
void closeVendor();
|
||||
|
|
@ -1660,6 +1791,8 @@ public:
|
|||
void closeTrainer();
|
||||
const std::string& getSpellName(uint32_t spellId) const;
|
||||
const std::string& getSpellRank(uint32_t spellId) const;
|
||||
/// Returns the tooltip/description text from Spell.dbc (empty if unknown or has no text).
|
||||
const std::string& getSpellDescription(uint32_t spellId) const;
|
||||
const std::string& getSkillLineName(uint32_t spellId) const;
|
||||
/// Returns the DispelType for a spell (0=none,1=magic,2=curse,3=disease,4=poison,5+=other)
|
||||
uint8_t getSpellDispelType(uint32_t spellId) const;
|
||||
|
|
@ -2199,6 +2332,7 @@ private:
|
|||
// ---- Phase 3: Spells ----
|
||||
WorldEntryCallback worldEntryCallback_;
|
||||
KnockBackCallback knockBackCallback_;
|
||||
CameraShakeCallback cameraShakeCallback_;
|
||||
UnstuckCallback unstuckCallback_;
|
||||
UnstuckCallback unstuckGyCallback_;
|
||||
UnstuckCallback unstuckHearthCallback_;
|
||||
|
|
@ -2251,6 +2385,7 @@ private:
|
|||
uint8_t activeTalentSpec_ = 0; // Currently active spec (0 or 1)
|
||||
uint8_t unspentTalentPoints_[2] = {0, 0}; // Unspent points per spec
|
||||
std::unordered_map<uint32_t, uint8_t> learnedTalents_[2]; // Learned talents per spec
|
||||
std::array<std::array<uint16_t, MAX_GLYPH_SLOTS>, 2> learnedGlyphs_{}; // Glyphs per spec
|
||||
std::unordered_map<uint32_t, TalentEntry> talentCache_; // talentId -> entry
|
||||
std::unordered_map<uint32_t, TalentTabEntry> talentTabCache_; // tabId -> entry
|
||||
bool talentDbcLoaded_ = false;
|
||||
|
|
@ -2283,6 +2418,13 @@ private:
|
|||
std::vector<uint32_t> petSpellList_; // known pet spells
|
||||
std::unordered_set<uint32_t> petAutocastSpells_; // spells with autocast on
|
||||
|
||||
// ---- Pet Stable ----
|
||||
bool stableWindowOpen_ = false;
|
||||
uint64_t stableMasterGuid_ = 0;
|
||||
uint8_t stableNumSlots_ = 0;
|
||||
std::vector<StabledPet> stabledPets_;
|
||||
void handleListStabledPets(network::Packet& packet);
|
||||
|
||||
// ---- Battleground queue state ----
|
||||
std::array<BgQueueSlot, 3> bgQueues_{};
|
||||
|
||||
|
|
@ -2409,6 +2551,7 @@ private:
|
|||
bool lootWindowOpen = false;
|
||||
bool autoLoot_ = false;
|
||||
LootResponseData currentLoot;
|
||||
std::vector<uint64_t> masterLootCandidates_; // from SMSG_LOOT_MASTER_LIST
|
||||
|
||||
// Group loot roll state
|
||||
bool pendingLootRollActive_ = false;
|
||||
|
|
@ -2436,6 +2579,7 @@ private:
|
|||
std::unordered_map<uint64_t, float> recentLootMoneyAnnounceCooldowns_;
|
||||
uint64_t playerMoneyCopper_ = 0;
|
||||
int32_t playerArmorRating_ = 0;
|
||||
int32_t playerResistances_[6] = {}; // [0]=Holy,[1]=Fire,[2]=Nature,[3]=Frost,[4]=Shadow,[5]=Arcane
|
||||
// Server-authoritative primary stats: [0]=STR [1]=AGI [2]=STA [3]=INT [4]=SPI; -1 = not received yet
|
||||
int32_t playerStats_[5] = {-1, -1, -1, -1, -1};
|
||||
// Some servers/custom clients shift update field indices. We can auto-detect coinage by correlating
|
||||
|
|
@ -2483,6 +2627,9 @@ private:
|
|||
return it != factionHostileMap_.end() ? it->second : true; // default hostile if unknown
|
||||
}
|
||||
|
||||
// Vehicle (WotLK): non-zero when player is seated in a vehicle
|
||||
uint32_t vehicleId_ = 0;
|
||||
|
||||
// Taxi / Flight Paths
|
||||
std::unordered_map<uint64_t, bool> taxiNpcHasRoutes_; // guid -> has new/available routes
|
||||
std::unordered_map<uint32_t, TaxiNode> taxiNodes_;
|
||||
|
|
@ -2578,12 +2725,20 @@ private:
|
|||
// Trainer
|
||||
bool trainerWindowOpen_ = false;
|
||||
TrainerListData currentTrainerList_;
|
||||
struct SpellNameEntry { std::string name; std::string rank; uint32_t schoolMask = 0; uint8_t dispelType = 0; };
|
||||
struct SpellNameEntry { std::string name; std::string rank; std::string description; uint32_t schoolMask = 0; uint8_t dispelType = 0; };
|
||||
std::unordered_map<uint32_t, SpellNameEntry> spellNameCache_;
|
||||
bool spellNameCacheLoaded_ = false;
|
||||
|
||||
// Achievement name cache (lazy-loaded from Achievement.dbc on first earned event)
|
||||
// Title cache: maps titleBit → title string (lazy-loaded from CharTitles.dbc)
|
||||
// The strings use "%s" as a player-name placeholder (e.g. "Commander %s", "%s the Explorer").
|
||||
std::unordered_map<uint32_t, std::string> titleNameCache_;
|
||||
bool titleNameCacheLoaded_ = false;
|
||||
void loadTitleNameCache();
|
||||
|
||||
// Achievement caches (lazy-loaded from Achievement.dbc on first earned event)
|
||||
std::unordered_map<uint32_t, std::string> achievementNameCache_;
|
||||
std::unordered_map<uint32_t, std::string> achievementDescCache_;
|
||||
std::unordered_map<uint32_t, uint32_t> achievementPointsCache_;
|
||||
bool achievementNameCacheLoaded_ = false;
|
||||
void loadAchievementNameCache();
|
||||
// Set of achievement IDs earned by the player (populated from SMSG_ALL_ACHIEVEMENT_DATA)
|
||||
|
|
@ -2702,6 +2857,7 @@ private:
|
|||
StandStateCallback standStateCallback_;
|
||||
GhostStateCallback ghostStateCallback_;
|
||||
MeleeSwingCallback meleeSwingCallback_;
|
||||
uint64_t lastMeleeSwingMs_ = 0; // system_clock ms at last player auto-attack swing
|
||||
SpellCastAnimCallback spellCastAnimCallback_;
|
||||
UnitAnimHintCallback unitAnimHintCallback_;
|
||||
UnitMoveFlagsCallback unitMoveFlagsCallback_;
|
||||
|
|
@ -2711,8 +2867,13 @@ private:
|
|||
NpcVendorCallback npcVendorCallback_;
|
||||
ChargeCallback chargeCallback_;
|
||||
LevelUpCallback levelUpCallback_;
|
||||
LevelUpDeltas lastLevelUpDeltas_;
|
||||
std::vector<TempEnchantTimer> tempEnchantTimers_;
|
||||
std::vector<BookPage> bookPages_; // pages collected for the current readable item
|
||||
OtherPlayerLevelUpCallback otherPlayerLevelUpCallback_;
|
||||
AchievementEarnedCallback achievementEarnedCallback_;
|
||||
AreaDiscoveryCallback areaDiscoveryCallback_;
|
||||
QuestProgressCallback questProgressCallback_;
|
||||
MountCallback mountCallback_;
|
||||
TaxiPrecacheCallback taxiPrecacheCallback_;
|
||||
TaxiOrientationCallback taxiOrientationCallback_;
|
||||
|
|
@ -2766,6 +2927,7 @@ private:
|
|||
std::array<uint64_t, 19> itemGuids{};
|
||||
};
|
||||
std::vector<EquipmentSet> equipmentSets_;
|
||||
std::vector<EquipmentSetInfo> equipmentSetInfo_; // public-facing copy
|
||||
|
||||
// ---- Forced faction reactions (SMSG_SET_FORCED_REACTIONS) ----
|
||||
std::unordered_map<uint32_t, uint8_t> forcedReactions_; // factionId -> reaction tier
|
||||
|
|
@ -2782,6 +2944,12 @@ private:
|
|||
RepChangeCallback repChangeCallback_;
|
||||
uint32_t watchedFactionId_ = 0; // auto-set to most recently changed faction
|
||||
|
||||
// ---- PvP honor credit callback ----
|
||||
PvpHonorCallback pvpHonorCallback_;
|
||||
|
||||
// ---- Item loot callback ----
|
||||
ItemLootCallback itemLootCallback_;
|
||||
|
||||
// ---- Quest completion callback ----
|
||||
QuestCompleteCallback questCompleteCallback_;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1594,6 +1594,10 @@ struct ItemQueryResponseData {
|
|||
struct ExtraStat { uint32_t statType = 0; int32_t statValue = 0; };
|
||||
std::vector<ExtraStat> extraStats;
|
||||
uint32_t startQuestId = 0; // Non-zero: item begins a quest
|
||||
// Gem socket slots (WotLK/TBC): 0=no socket; color mask: 1=Meta,2=Red,4=Yellow,8=Blue
|
||||
std::array<uint32_t, 3> socketColor{};
|
||||
uint32_t socketBonus = 0; // enchantmentId of socket bonus; 0=none
|
||||
uint32_t itemSetId = 0; // ItemSet.dbc entry; 0=not part of a set
|
||||
bool valid = false;
|
||||
};
|
||||
|
||||
|
|
@ -2695,5 +2699,33 @@ public:
|
|||
static bool parse(network::Packet& packet, AuctionCommandResult& data);
|
||||
};
|
||||
|
||||
/** Pet Stable packet builders */
|
||||
class ListStabledPetsPacket {
|
||||
public:
|
||||
/** MSG_LIST_STABLED_PETS (CMSG): request list from stable master */
|
||||
static network::Packet build(uint64_t stableMasterGuid);
|
||||
};
|
||||
|
||||
class StablePetPacket {
|
||||
public:
|
||||
/** CMSG_STABLE_PET: store active pet in the given stable slot (1-based) */
|
||||
static network::Packet build(uint64_t stableMasterGuid, uint8_t slot);
|
||||
};
|
||||
|
||||
class UnstablePetPacket {
|
||||
public:
|
||||
/** CMSG_UNSTABLE_PET: retrieve a stabled pet by its server-side petNumber */
|
||||
static network::Packet build(uint64_t stableMasterGuid, uint32_t petNumber);
|
||||
};
|
||||
|
||||
class PetRenamePacket {
|
||||
public:
|
||||
/** CMSG_PET_RENAME: rename the player's active pet.
|
||||
* petGuid: the pet's object GUID (from GameHandler::getPetGuid())
|
||||
* name: new name (max 12 chars; server validates and may reject)
|
||||
* isDeclined: 0 for non-Cyrillic locales (no declined name forms) */
|
||||
static network::Packet build(uint64_t petGuid, const std::string& name, uint8_t isDeclined = 0);
|
||||
};
|
||||
|
||||
} // namespace game
|
||||
} // namespace wowee
|
||||
|
|
|
|||
|
|
@ -129,6 +129,12 @@ public:
|
|||
// vspeed: raw packet vspeed field (server sends negative for upward launch)
|
||||
void applyKnockBack(float vcos, float vsin, float hspeed, float vspeed);
|
||||
|
||||
// Trigger a camera shake effect (e.g. from SMSG_CAMERA_SHAKE).
|
||||
// magnitude: peak positional offset in world units
|
||||
// frequency: oscillation frequency in Hz
|
||||
// duration: shake duration in seconds
|
||||
void triggerShake(float magnitude, float frequency, float duration);
|
||||
|
||||
// For first-person player hiding
|
||||
void setCharacterRenderer(class CharacterRenderer* cr, uint32_t playerId) {
|
||||
characterRenderer = cr;
|
||||
|
|
@ -369,6 +375,12 @@ private:
|
|||
glm::vec2 knockbackHorizVel_ = glm::vec2(0.0f); // render-space horizontal velocity (units/s)
|
||||
// Horizontal velocity decays via WoW-like drag so the player doesn't slide forever.
|
||||
static constexpr float KNOCKBACK_HORIZ_DRAG = 4.5f; // exponential decay rate (1/s)
|
||||
|
||||
// Camera shake state (SMSG_CAMERA_SHAKE)
|
||||
float shakeElapsed_ = 0.0f;
|
||||
float shakeDuration_ = 0.0f;
|
||||
float shakeMagnitude_ = 0.0f;
|
||||
float shakeFrequency_ = 0.0f;
|
||||
};
|
||||
|
||||
} // namespace rendering
|
||||
|
|
|
|||
|
|
@ -271,6 +271,10 @@ public:
|
|||
float getShadowDistance() const { return shadowDistance_; }
|
||||
void setMsaaSamples(VkSampleCountFlagBits samples);
|
||||
|
||||
// FXAA post-process anti-aliasing (combinable with MSAA)
|
||||
void setFXAAEnabled(bool enabled);
|
||||
bool isFXAAEnabled() const { return fxaa_.enabled; }
|
||||
|
||||
// FSR (FidelityFX Super Resolution) upscaling
|
||||
void setFSREnabled(bool enabled);
|
||||
bool isFSREnabled() const { return fsr_.enabled; }
|
||||
|
|
@ -398,6 +402,31 @@ private:
|
|||
void destroyFSRResources();
|
||||
void renderFSRUpscale();
|
||||
|
||||
// FXAA post-process state
|
||||
struct FXAAState {
|
||||
bool enabled = false;
|
||||
bool needsRecreate = false;
|
||||
|
||||
// Off-screen scene target (same resolution as swapchain — no scaling)
|
||||
AllocatedImage sceneColor{}; // 1x resolved color target
|
||||
AllocatedImage sceneDepth{}; // Depth (matches MSAA sample count)
|
||||
AllocatedImage sceneMsaaColor{}; // MSAA color target (when MSAA > 1x)
|
||||
AllocatedImage sceneDepthResolve{}; // Depth resolve (MSAA + depth resolve)
|
||||
VkFramebuffer sceneFramebuffer = VK_NULL_HANDLE;
|
||||
VkSampler sceneSampler = VK_NULL_HANDLE;
|
||||
|
||||
// FXAA fullscreen pipeline
|
||||
VkPipeline pipeline = VK_NULL_HANDLE;
|
||||
VkPipelineLayout pipelineLayout = VK_NULL_HANDLE;
|
||||
VkDescriptorSetLayout descSetLayout = VK_NULL_HANDLE;
|
||||
VkDescriptorPool descPool = VK_NULL_HANDLE;
|
||||
VkDescriptorSet descSet = VK_NULL_HANDLE;
|
||||
};
|
||||
FXAAState fxaa_;
|
||||
bool initFXAAResources();
|
||||
void destroyFXAAResources();
|
||||
void renderFXAAPass();
|
||||
|
||||
// FSR 2.2 temporal upscaling state
|
||||
struct FSR2State {
|
||||
bool enabled = false;
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
#include <vulkan/vulkan.h>
|
||||
#include <vk_mem_alloc.h>
|
||||
#include <glm/glm.hpp>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
|
@ -17,6 +18,13 @@ class VkContext;
|
|||
class VkTexture;
|
||||
class VkRenderTarget;
|
||||
|
||||
/// Party member dot passed in from the UI layer for world map overlay.
|
||||
struct WorldMapPartyDot {
|
||||
glm::vec3 renderPos; ///< Position in render-space coordinates
|
||||
uint32_t color; ///< RGBA packed color (IM_COL32 format)
|
||||
std::string name; ///< Member name (shown as tooltip on hover)
|
||||
};
|
||||
|
||||
struct WorldMapZone {
|
||||
uint32_t wmaID = 0;
|
||||
uint32_t areaID = 0; // 0 = continent level
|
||||
|
|
@ -47,6 +55,7 @@ public:
|
|||
|
||||
void setMapName(const std::string& name);
|
||||
void setServerExplorationMask(const std::vector<uint32_t>& masks, bool hasData);
|
||||
void setPartyDots(std::vector<WorldMapPartyDot> dots) { partyDots_ = std::move(dots); }
|
||||
bool isOpen() const { return open; }
|
||||
void close() { open = false; }
|
||||
|
||||
|
|
@ -113,6 +122,9 @@ private:
|
|||
// Texture storage (owns all VkTexture objects for zone tiles)
|
||||
std::vector<std::unique_ptr<VkTexture>> zoneTextures;
|
||||
|
||||
// Party member dots (set each frame from the UI layer)
|
||||
std::vector<WorldMapPartyDot> partyDots_;
|
||||
|
||||
// Exploration / fog of war
|
||||
std::vector<uint32_t> serverExplorationMask;
|
||||
bool hasServerExplorationMask = false;
|
||||
|
|
|
|||
|
|
@ -157,6 +157,8 @@ private:
|
|||
bool chatWindowLocked = true;
|
||||
ImVec2 chatWindowPos_ = ImVec2(0.0f, 0.0f);
|
||||
bool chatWindowPosInit_ = false;
|
||||
ImVec2 questTrackerPos_ = ImVec2(-1.0f, -1.0f); // <0 = use default
|
||||
bool questTrackerPosInit_ = false;
|
||||
bool showEscapeMenu = false;
|
||||
bool showEscapeSettingsNotice = false;
|
||||
bool showSettingsWindow = false;
|
||||
|
|
@ -204,6 +206,7 @@ private:
|
|||
float pendingLeftBarOffsetY = 0.0f; // Vertical offset from screen center
|
||||
int pendingGroundClutterDensity = 100;
|
||||
int pendingAntiAliasing = 0; // 0=Off, 1=2x, 2=4x, 3=8x
|
||||
bool pendingFXAA = false; // FXAA post-process (combinable with MSAA)
|
||||
bool pendingNormalMapping = true; // on by default
|
||||
float pendingNormalMapStrength = 0.8f; // 0.0-2.0
|
||||
bool pendingPOM = true; // on by default
|
||||
|
|
@ -238,6 +241,7 @@ private:
|
|||
bool minimapSettingsApplied_ = false;
|
||||
bool volumeSettingsApplied_ = false; // True once saved volume settings applied to audio managers
|
||||
bool msaaSettingsApplied_ = false; // True once saved MSAA setting applied to renderer
|
||||
bool fxaaSettingsApplied_ = false; // True once saved FXAA setting applied to renderer
|
||||
bool waterRefractionApplied_ = false;
|
||||
bool normalMapSettingsApplied_ = false; // True once saved normal map/POM settings applied
|
||||
|
||||
|
|
@ -314,6 +318,7 @@ private:
|
|||
void renderRepBar(game::GameHandler& gameHandler);
|
||||
void renderCastBar(game::GameHandler& gameHandler);
|
||||
void renderMirrorTimers(game::GameHandler& gameHandler);
|
||||
void renderCooldownTracker(game::GameHandler& gameHandler);
|
||||
void renderCombatText(game::GameHandler& gameHandler);
|
||||
void renderRaidWarningOverlay(game::GameHandler& gameHandler);
|
||||
void renderPartyFrames(game::GameHandler& gameHandler);
|
||||
|
|
@ -339,6 +344,7 @@ private:
|
|||
void renderQuestOfferRewardWindow(game::GameHandler& gameHandler);
|
||||
void renderVendorWindow(game::GameHandler& gameHandler);
|
||||
void renderTrainerWindow(game::GameHandler& gameHandler);
|
||||
void renderStableWindow(game::GameHandler& gameHandler);
|
||||
void renderTaxiWindow(game::GameHandler& gameHandler);
|
||||
void renderDeathScreen(game::GameHandler& gameHandler);
|
||||
void renderReclaimCorpseButton(game::GameHandler& gameHandler);
|
||||
|
|
@ -368,6 +374,7 @@ private:
|
|||
void renderNameplates(game::GameHandler& gameHandler);
|
||||
void renderBattlegroundScore(game::GameHandler& gameHandler);
|
||||
void renderDPSMeter(game::GameHandler& gameHandler);
|
||||
void renderDurabilityWarning(game::GameHandler& gameHandler);
|
||||
|
||||
/**
|
||||
* Inventory screen
|
||||
|
|
@ -427,10 +434,19 @@ private:
|
|||
char gmTicketBuf_[2048] = {};
|
||||
void renderGmTicketWindow(game::GameHandler& gameHandler);
|
||||
|
||||
// Pet rename modal (triggered from pet frame context menu)
|
||||
bool petRenameOpen_ = false;
|
||||
char petRenameBuf_[16] = {};
|
||||
|
||||
// Inspect window
|
||||
bool showInspectWindow_ = false;
|
||||
void renderInspectWindow(game::GameHandler& gameHandler);
|
||||
|
||||
// Readable text window (books / scrolls / notes)
|
||||
bool showBookWindow_ = false;
|
||||
int bookCurrentPage_ = 0;
|
||||
void renderBookWindow(game::GameHandler& gameHandler);
|
||||
|
||||
// Threat window
|
||||
bool showThreatWindow_ = false;
|
||||
void renderThreatWindow(game::GameHandler& gameHandler);
|
||||
|
|
@ -505,9 +521,12 @@ private:
|
|||
bool leftClickWasPress_ = false;
|
||||
|
||||
// Level-up ding animation
|
||||
static constexpr float DING_DURATION = 3.0f;
|
||||
static constexpr float DING_DURATION = 4.0f;
|
||||
float dingTimer_ = 0.0f;
|
||||
uint32_t dingLevel_ = 0;
|
||||
uint32_t dingHpDelta_ = 0;
|
||||
uint32_t dingManaDelta_ = 0;
|
||||
uint32_t dingStats_[5] = {}; // str/agi/sta/int/spi deltas
|
||||
void renderDingEffect();
|
||||
|
||||
// Achievement toast banner
|
||||
|
|
@ -517,6 +536,80 @@ private:
|
|||
std::string achievementToastName_;
|
||||
void renderAchievementToast();
|
||||
|
||||
// Area discovery toast ("Discovered! <AreaName> +XP XP")
|
||||
static constexpr float DISCOVERY_TOAST_DURATION = 4.0f;
|
||||
float discoveryToastTimer_ = 0.0f;
|
||||
std::string discoveryToastName_;
|
||||
uint32_t discoveryToastXP_ = 0;
|
||||
bool areaDiscoveryCallbackSet_ = false;
|
||||
void renderDiscoveryToast();
|
||||
|
||||
// Whisper toast — brief overlay at screen top when a whisper arrives while chat is not focused
|
||||
struct WhisperToastEntry {
|
||||
std::string sender;
|
||||
std::string preview; // first ~60 chars of message
|
||||
float age = 0.0f;
|
||||
};
|
||||
static constexpr float WHISPER_TOAST_DURATION = 5.0f;
|
||||
std::vector<WhisperToastEntry> whisperToasts_;
|
||||
size_t whisperSeenCount_ = 0; // how many chat entries have been scanned for whispers
|
||||
void renderWhisperToasts();
|
||||
|
||||
// Quest objective progress toast ("Quest: <ObjectiveName> X/Y")
|
||||
struct QuestProgressToastEntry {
|
||||
std::string questTitle;
|
||||
std::string objectiveName;
|
||||
uint32_t current = 0;
|
||||
uint32_t required = 0;
|
||||
float age = 0.0f;
|
||||
};
|
||||
static constexpr float QUEST_TOAST_DURATION = 4.0f;
|
||||
std::vector<QuestProgressToastEntry> questToasts_;
|
||||
bool questProgressCallbackSet_ = false;
|
||||
void renderQuestProgressToasts();
|
||||
|
||||
// Nearby player level-up toast ("<Name> is now level X!")
|
||||
struct PlayerLevelUpToastEntry {
|
||||
uint64_t guid = 0;
|
||||
std::string playerName; // resolved lazily at render time
|
||||
uint32_t newLevel = 0;
|
||||
float age = 0.0f;
|
||||
};
|
||||
static constexpr float PLAYER_LEVELUP_TOAST_DURATION = 4.0f;
|
||||
std::vector<PlayerLevelUpToastEntry> playerLevelUpToasts_;
|
||||
bool otherPlayerLevelUpCallbackSet_ = false;
|
||||
void renderPlayerLevelUpToasts(game::GameHandler& gameHandler);
|
||||
|
||||
// PvP honor credit toast ("+N Honor" shown when an honorable kill is credited)
|
||||
struct PvpHonorToastEntry {
|
||||
uint32_t honor = 0;
|
||||
uint32_t victimRank = 0; // 0 = unranked / not available
|
||||
float age = 0.0f;
|
||||
};
|
||||
static constexpr float PVP_HONOR_TOAST_DURATION = 3.5f;
|
||||
std::vector<PvpHonorToastEntry> pvpHonorToasts_;
|
||||
bool pvpHonorCallbackSet_ = false;
|
||||
void renderPvpHonorToasts();
|
||||
|
||||
// Item loot toast — quality-coloured popup when an item is received
|
||||
struct ItemLootToastEntry {
|
||||
uint32_t itemId = 0;
|
||||
uint32_t count = 0;
|
||||
uint32_t quality = 1; // 0=grey,1=white,2=green,3=blue,4=purple,5=orange
|
||||
std::string name;
|
||||
float age = 0.0f;
|
||||
};
|
||||
static constexpr float ITEM_LOOT_TOAST_DURATION = 3.0f;
|
||||
std::vector<ItemLootToastEntry> itemLootToasts_;
|
||||
bool itemLootCallbackSet_ = false;
|
||||
void renderItemLootToasts();
|
||||
|
||||
// Resurrection flash: brief "You have been resurrected!" overlay on ghost→alive transition
|
||||
float resurrectFlashTimer_ = 0.0f;
|
||||
static constexpr float kResurrectFlashDuration = 3.0f;
|
||||
bool ghostStateCallbackSet_ = false;
|
||||
void renderResurrectFlash();
|
||||
|
||||
// Zone discovery text ("Entering: <ZoneName>")
|
||||
static constexpr float ZONE_TEXT_DURATION = 5.0f;
|
||||
float zoneTextTimer_ = 0.0f;
|
||||
|
|
@ -524,6 +617,9 @@ private:
|
|||
std::string lastKnownZoneName_;
|
||||
void renderZoneText();
|
||||
|
||||
// Cooldown tracker
|
||||
bool showCooldownTracker_ = false;
|
||||
|
||||
// DPS / HPS meter
|
||||
bool showDPSMeter_ = false;
|
||||
float dpsCombatAge_ = 0.0f; // seconds in current combat (for accurate early-combat DPS)
|
||||
|
|
@ -533,7 +629,9 @@ private:
|
|||
size_t dpsLogSeenCount_ = 0; // log entries already scanned
|
||||
|
||||
public:
|
||||
void triggerDing(uint32_t newLevel);
|
||||
void triggerDing(uint32_t newLevel, uint32_t hpDelta = 0, uint32_t manaDelta = 0,
|
||||
uint32_t str = 0, uint32_t agi = 0, uint32_t sta = 0,
|
||||
uint32_t intel = 0, uint32_t spi = 0);
|
||||
void triggerAchievementToast(uint32_t achievementId, std::string name = {});
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -149,7 +149,7 @@ private:
|
|||
void renderEquipmentPanel(game::Inventory& inventory);
|
||||
void renderBackpackPanel(game::Inventory& inventory, bool collapseEmptySections = false);
|
||||
void renderStatsPanel(game::Inventory& inventory, uint32_t playerLevel, int32_t serverArmor = 0,
|
||||
const int32_t* serverStats = nullptr);
|
||||
const int32_t* serverStats = nullptr, const int32_t* serverResists = nullptr);
|
||||
void renderReputationPanel(game::GameHandler& gameHandler);
|
||||
|
||||
void renderItemSlot(game::Inventory& inventory, const game::ItemSlot& slot,
|
||||
|
|
|
|||
|
|
@ -28,6 +28,8 @@ private:
|
|||
|
||||
void loadSpellDBC(pipeline::AssetManager* assetManager);
|
||||
void loadSpellIconDBC(pipeline::AssetManager* assetManager);
|
||||
void loadGlyphPropertiesDBC(pipeline::AssetManager* assetManager);
|
||||
void renderGlyphs(game::GameHandler& gameHandler);
|
||||
VkDescriptorSet getSpellIcon(uint32_t iconId, pipeline::AssetManager* assetManager);
|
||||
|
||||
bool open = false;
|
||||
|
|
@ -36,11 +38,16 @@ private:
|
|||
// DBC caches
|
||||
bool spellDbcLoaded = false;
|
||||
bool iconDbcLoaded = false;
|
||||
bool glyphDbcLoaded = false;
|
||||
std::unordered_map<uint32_t, uint32_t> spellIconIds; // spellId -> iconId
|
||||
std::unordered_map<uint32_t, std::string> spellIconPaths; // iconId -> path
|
||||
std::unordered_map<uint32_t, VkDescriptorSet> spellIconCache; // iconId -> texture
|
||||
std::unordered_map<uint32_t, std::string> spellTooltips; // spellId -> description
|
||||
std::unordered_map<uint32_t, VkDescriptorSet> bgTextureCache_; // tabId -> bg texture
|
||||
|
||||
// GlyphProperties.dbc cache: glyphId -> { spellId, isMajor }
|
||||
struct GlyphInfo { uint32_t spellId = 0; bool isMajor = false; };
|
||||
std::unordered_map<uint32_t, GlyphInfo> glyphProperties_; // glyphId -> info
|
||||
};
|
||||
|
||||
} // namespace ui
|
||||
|
|
|
|||
|
|
@ -646,6 +646,11 @@ void Application::setState(AppState newState) {
|
|||
renderer->getCameraController()->applyKnockBack(vcos, vsin, hspeed, vspeed);
|
||||
}
|
||||
});
|
||||
gameHandler->setCameraShakeCallback([this](float magnitude, float frequency, float duration) {
|
||||
if (renderer && renderer->getCameraController()) {
|
||||
renderer->getCameraController()->triggerShake(magnitude, frequency, duration);
|
||||
}
|
||||
});
|
||||
}
|
||||
// Load quest marker models
|
||||
loadQuestMarkerModels();
|
||||
|
|
|
|||
|
|
@ -1701,12 +1701,17 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
queryItemInfo(itemId, 0);
|
||||
if (showInChat) {
|
||||
std::string itemName = "item #" + std::to_string(itemId);
|
||||
uint32_t quality = 1; // white default
|
||||
if (const ItemQueryResponseData* info = getItemInfo(itemId)) {
|
||||
if (!info->name.empty()) itemName = info->name;
|
||||
quality = info->quality;
|
||||
}
|
||||
std::string msg = "Received: " + itemName;
|
||||
if (count > 1) msg += " x" + std::to_string(count);
|
||||
addSystemChatMessage(msg);
|
||||
if (itemLootCallback_) {
|
||||
itemLootCallback_(itemId, count, quality, itemName);
|
||||
}
|
||||
}
|
||||
LOG_INFO("Item push: itemId=", itemId, " count=", count,
|
||||
" showInChat=", static_cast<int>(showInChat));
|
||||
|
|
@ -1767,6 +1772,8 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
}
|
||||
addSystemChatMessage(msg);
|
||||
// XP is updated via PLAYER_XP update fields from the server.
|
||||
if (areaDiscoveryCallback_)
|
||||
areaDiscoveryCallback_(areaName, xpGained);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
|
@ -1787,8 +1794,23 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
break;
|
||||
}
|
||||
case Opcode::SMSG_PET_ACTION_FEEDBACK: {
|
||||
// uint8 action + uint8 flags
|
||||
packet.setReadPos(packet.getSize()); // Consume; no UI for pet feedback yet.
|
||||
// uint8 msg: 1=dead, 2=nothing_to_attack, 3=cant_attack_target,
|
||||
// 4=target_too_far, 5=no_path, 6=cant_attack_immune
|
||||
if (packet.getSize() - packet.getReadPos() < 1) break;
|
||||
uint8_t msg = packet.readUInt8();
|
||||
static const char* kPetFeedback[] = {
|
||||
nullptr,
|
||||
"Your pet is dead.",
|
||||
"Your pet has nothing to attack.",
|
||||
"Your pet cannot attack that target.",
|
||||
"That target is too far away.",
|
||||
"Your pet cannot find a path to the target.",
|
||||
"Your pet cannot attack an immune target.",
|
||||
};
|
||||
if (msg > 0 && msg < 7 && kPetFeedback[msg]) {
|
||||
addSystemChatMessage(kPetFeedback[msg]);
|
||||
}
|
||||
packet.setReadPos(packet.getSize());
|
||||
break;
|
||||
}
|
||||
case Opcode::SMSG_PET_NAME_QUERY_RESPONSE: {
|
||||
|
|
@ -1880,6 +1902,9 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
std::dec, " rank=", rank);
|
||||
std::string msg = "You gain " + std::to_string(honor) + " honor points.";
|
||||
addSystemChatMessage(msg);
|
||||
if (pvpHonorCallback_) {
|
||||
pvpHonorCallback_(honor, victimGuid, rank);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
|
@ -2045,6 +2070,11 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
break;
|
||||
}
|
||||
|
||||
// ---- Pet stable list ----
|
||||
case Opcode::MSG_LIST_STABLED_PETS:
|
||||
if (state == WorldState::IN_WORLD) handleListStabledPets(packet);
|
||||
break;
|
||||
|
||||
// ---- Pet stable result ----
|
||||
case Opcode::SMSG_STABLE_RESULT: {
|
||||
// uint8 result
|
||||
|
|
@ -2061,6 +2091,11 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
}
|
||||
if (msg) addSystemChatMessage(msg);
|
||||
LOG_INFO("SMSG_STABLE_RESULT: result=", static_cast<int>(result));
|
||||
// Refresh the stable list after a result to reflect the new state
|
||||
if (stableWindowOpen_ && stableMasterGuid_ != 0 && socket && result <= 0x08) {
|
||||
auto refreshPkt = ListStabledPetsPacket::build(stableMasterGuid_);
|
||||
socket->send(refreshPkt);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
|
|
@ -2070,12 +2105,40 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
if (packet.getSize() - packet.getReadPos() < 8) break;
|
||||
uint32_t titleBit = packet.readUInt32();
|
||||
uint32_t isLost = packet.readUInt32();
|
||||
char buf[128];
|
||||
std::snprintf(buf, sizeof(buf),
|
||||
isLost ? "Title removed (ID %u)." : "Title earned (ID %u)!",
|
||||
titleBit);
|
||||
addSystemChatMessage(buf);
|
||||
LOG_INFO("SMSG_TITLE_EARNED: id=", titleBit, " lost=", isLost);
|
||||
loadTitleNameCache();
|
||||
|
||||
// Format the title string using the player's own name
|
||||
std::string titleStr;
|
||||
auto tit = titleNameCache_.find(titleBit);
|
||||
if (tit != titleNameCache_.end() && !tit->second.empty()) {
|
||||
// Title strings contain "%s" as a player-name placeholder.
|
||||
// Replace it with the local player's name if known.
|
||||
auto nameIt = playerNameCache.find(playerGuid);
|
||||
const std::string& pName = (nameIt != playerNameCache.end())
|
||||
? nameIt->second : "you";
|
||||
const std::string& fmt = tit->second;
|
||||
size_t pos = fmt.find("%s");
|
||||
if (pos != std::string::npos) {
|
||||
titleStr = fmt.substr(0, pos) + pName + fmt.substr(pos + 2);
|
||||
} else {
|
||||
titleStr = fmt;
|
||||
}
|
||||
}
|
||||
|
||||
std::string msg;
|
||||
if (!titleStr.empty()) {
|
||||
msg = isLost ? ("Title removed: " + titleStr + ".")
|
||||
: ("Title earned: " + titleStr + "!");
|
||||
} else {
|
||||
char buf[64];
|
||||
std::snprintf(buf, sizeof(buf),
|
||||
isLost ? "Title removed (bit %u)." : "Title earned (bit %u)!",
|
||||
titleBit);
|
||||
msg = buf;
|
||||
}
|
||||
addSystemChatMessage(msg);
|
||||
LOG_INFO("SMSG_TITLE_EARNED: bit=", titleBit, " lost=", isLost,
|
||||
" title='", titleStr, "'");
|
||||
break;
|
||||
}
|
||||
|
||||
|
|
@ -2697,6 +2760,25 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
handleMoveKnockBack(packet);
|
||||
break;
|
||||
|
||||
case Opcode::SMSG_CAMERA_SHAKE: {
|
||||
// uint32 shakeID (CameraShakes.dbc), uint32 shakeType
|
||||
// We don't parse CameraShakes.dbc; apply a hardcoded moderate shake.
|
||||
if (packet.getSize() - packet.getReadPos() >= 8) {
|
||||
uint32_t shakeId = packet.readUInt32();
|
||||
uint32_t shakeType = packet.readUInt32();
|
||||
(void)shakeType;
|
||||
// Map shakeId ranges to approximate magnitudes:
|
||||
// IDs < 50: minor environmental (0.04), others: larger boss effects (0.08)
|
||||
float magnitude = (shakeId < 50) ? 0.04f : 0.08f;
|
||||
if (cameraShakeCallback_) {
|
||||
cameraShakeCallback_(magnitude, 18.0f, 0.5f);
|
||||
}
|
||||
LOG_DEBUG("SMSG_CAMERA_SHAKE: id=", shakeId, " type=", shakeType,
|
||||
" magnitude=", magnitude);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case Opcode::SMSG_CLIENT_CONTROL_UPDATE: {
|
||||
// Minimal parse: PackedGuid + uint8 allowMovement.
|
||||
if (packet.getSize() - packet.getReadPos() < 2) {
|
||||
|
|
@ -3332,10 +3414,19 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
case Opcode::SMSG_LOOT_ROLL_WON:
|
||||
handleLootRollWon(packet);
|
||||
break;
|
||||
case Opcode::SMSG_LOOT_MASTER_LIST:
|
||||
// Master looter list — no UI yet; consume to avoid unhandled warning.
|
||||
packet.setReadPos(packet.getSize());
|
||||
case Opcode::SMSG_LOOT_MASTER_LIST: {
|
||||
// uint8 count + count * uint64 guid — eligible recipients for master looter
|
||||
masterLootCandidates_.clear();
|
||||
if (packet.getSize() - packet.getReadPos() < 1) break;
|
||||
uint8_t mlCount = packet.readUInt8();
|
||||
masterLootCandidates_.reserve(mlCount);
|
||||
for (uint8_t i = 0; i < mlCount; ++i) {
|
||||
if (packet.getSize() - packet.getReadPos() < 8) break;
|
||||
masterLootCandidates_.push_back(packet.readUInt64());
|
||||
}
|
||||
LOG_INFO("SMSG_LOOT_MASTER_LIST: ", (int)masterLootCandidates_.size(), " candidates");
|
||||
break;
|
||||
}
|
||||
case Opcode::SMSG_GOSSIP_MESSAGE:
|
||||
handleGossipMessage(packet);
|
||||
break;
|
||||
|
|
@ -3813,10 +3904,21 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
case Opcode::SMSG_LEVELUP_INFO:
|
||||
case Opcode::SMSG_LEVELUP_INFO_ALT: {
|
||||
// Server-authoritative level-up event.
|
||||
// First field is always the new level in Classic/TBC/WotLK-era layouts.
|
||||
// WotLK layout: uint32 newLevel + uint32 hpDelta + uint32 manaDelta + 5x uint32 statDeltas
|
||||
if (packet.getSize() - packet.getReadPos() >= 4) {
|
||||
uint32_t newLevel = packet.readUInt32();
|
||||
if (newLevel > 0) {
|
||||
// Parse stat deltas (WotLK layout has 7 more uint32s)
|
||||
lastLevelUpDeltas_ = {};
|
||||
if (packet.getSize() - packet.getReadPos() >= 28) {
|
||||
lastLevelUpDeltas_.hp = packet.readUInt32();
|
||||
lastLevelUpDeltas_.mana = packet.readUInt32();
|
||||
lastLevelUpDeltas_.str = packet.readUInt32();
|
||||
lastLevelUpDeltas_.agi = packet.readUInt32();
|
||||
lastLevelUpDeltas_.sta = packet.readUInt32();
|
||||
lastLevelUpDeltas_.intel = packet.readUInt32();
|
||||
lastLevelUpDeltas_.spi = packet.readUInt32();
|
||||
}
|
||||
uint32_t oldLevel = serverPlayerLevel_;
|
||||
serverPlayerLevel_ = std::max(serverPlayerLevel_, newLevel);
|
||||
for (auto& ch : characters) {
|
||||
|
|
@ -3830,7 +3932,6 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
}
|
||||
}
|
||||
}
|
||||
// Remaining payload (hp/mana/stat deltas) is optional for our client.
|
||||
packet.setReadPos(packet.getSize());
|
||||
break;
|
||||
}
|
||||
|
|
@ -4232,6 +4333,11 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
weatherIntensity_ = wIntensity;
|
||||
const char* typeName = (wType == 1) ? "Rain" : (wType == 2) ? "Snow" : (wType == 3) ? "Storm" : "Clear";
|
||||
LOG_INFO("Weather changed: type=", wType, " (", typeName, "), intensity=", wIntensity);
|
||||
// Storm transition: trigger a low-frequency thunder rumble shake
|
||||
if (wType == 3 && wIntensity > 0.3f && cameraShakeCallback_) {
|
||||
float mag = 0.03f + wIntensity * 0.04f; // 0.03–0.07 units
|
||||
cameraShakeCallback_(mag, 6.0f, 0.6f);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
|
@ -4482,6 +4588,10 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
progressMsg += std::to_string(count) + "/" + std::to_string(reqCount);
|
||||
addSystemChatMessage(progressMsg);
|
||||
|
||||
if (questProgressCallback_) {
|
||||
questProgressCallback_(quest.title, creatureName, count, reqCount);
|
||||
}
|
||||
|
||||
LOG_INFO("Updated kill count for quest ", questId, ": ",
|
||||
count, "/", reqCount);
|
||||
break;
|
||||
|
|
@ -4536,6 +4646,26 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
updatedAny = true;
|
||||
}
|
||||
addSystemChatMessage("Quest item: " + itemLabel + " (" + std::to_string(count) + ")");
|
||||
|
||||
if (questProgressCallback_ && updatedAny) {
|
||||
// Find the quest that tracks this item to get title and required count
|
||||
for (const auto& quest : questLog_) {
|
||||
if (quest.complete) continue;
|
||||
if (quest.itemCounts.count(itemId) == 0) continue;
|
||||
uint32_t required = 0;
|
||||
auto rIt = quest.requiredItemCounts.find(itemId);
|
||||
if (rIt != quest.requiredItemCounts.end()) required = rIt->second;
|
||||
if (required == 0) {
|
||||
for (const auto& obj : quest.itemObjectives) {
|
||||
if (obj.itemId == itemId) { required = obj.required; break; }
|
||||
}
|
||||
}
|
||||
if (required == 0) required = count;
|
||||
questProgressCallback_(quest.title, itemLabel, count, required);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
LOG_INFO("Quest item update: itemId=", itemId, " count=", count,
|
||||
" trackedQuestsUpdated=", updatedAny);
|
||||
}
|
||||
|
|
@ -5177,10 +5307,19 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
// GM ticket status (new/updated); no ticket UI yet
|
||||
packet.setReadPos(packet.getSize());
|
||||
break;
|
||||
case Opcode::SMSG_PLAYER_VEHICLE_DATA:
|
||||
// Vehicle data update for player in vehicle; no vehicle UI yet
|
||||
packet.setReadPos(packet.getSize());
|
||||
case Opcode::SMSG_PLAYER_VEHICLE_DATA: {
|
||||
// PackedGuid (player guid) + uint32 vehicleId
|
||||
// vehicleId == 0 means the player left the vehicle
|
||||
if (packet.getSize() - packet.getReadPos() >= 1) {
|
||||
(void)UpdateObjectParser::readPackedGuid(packet); // player guid (unused)
|
||||
}
|
||||
if (packet.getSize() - packet.getReadPos() >= 4) {
|
||||
vehicleId_ = packet.readUInt32();
|
||||
} else {
|
||||
vehicleId_ = 0;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case Opcode::SMSG_SET_EXTRA_AURA_INFO_NEED_UPDATE:
|
||||
packet.setReadPos(packet.getSize());
|
||||
break;
|
||||
|
|
@ -5636,9 +5775,56 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
}
|
||||
|
||||
// ---- Misc consume ----
|
||||
case Opcode::SMSG_ITEM_ENCHANT_TIME_UPDATE: {
|
||||
// Format: uint64 itemGuid + uint32 slot + uint32 durationSec + uint64 playerGuid
|
||||
// slot: 0=main-hand, 1=off-hand, 2=ranged
|
||||
if (packet.getSize() - packet.getReadPos() < 24) {
|
||||
packet.setReadPos(packet.getSize()); break;
|
||||
}
|
||||
/*uint64_t itemGuid =*/ packet.readUInt64();
|
||||
uint32_t enchSlot = packet.readUInt32();
|
||||
uint32_t durationSec = packet.readUInt32();
|
||||
/*uint64_t playerGuid =*/ packet.readUInt64();
|
||||
|
||||
// Clamp to known slots (0-2)
|
||||
if (enchSlot > 2) { break; }
|
||||
|
||||
uint64_t nowMs = static_cast<uint64_t>(
|
||||
std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::steady_clock::now().time_since_epoch()).count());
|
||||
|
||||
if (durationSec == 0) {
|
||||
// Enchant expired / removed — erase the slot entry
|
||||
tempEnchantTimers_.erase(
|
||||
std::remove_if(tempEnchantTimers_.begin(), tempEnchantTimers_.end(),
|
||||
[enchSlot](const TempEnchantTimer& t) { return t.slot == enchSlot; }),
|
||||
tempEnchantTimers_.end());
|
||||
} else {
|
||||
uint64_t expireMs = nowMs + static_cast<uint64_t>(durationSec) * 1000u;
|
||||
bool found = false;
|
||||
for (auto& t : tempEnchantTimers_) {
|
||||
if (t.slot == enchSlot) { t.expireMs = expireMs; found = true; break; }
|
||||
}
|
||||
if (!found) tempEnchantTimers_.push_back({enchSlot, expireMs});
|
||||
|
||||
// Warn at important thresholds
|
||||
if (durationSec <= 60 && durationSec > 55) {
|
||||
const char* slotName = (enchSlot < 3) ? kTempEnchantSlotNames[enchSlot] : "weapon";
|
||||
char buf[80];
|
||||
std::snprintf(buf, sizeof(buf), "Weapon enchant (%s) expires in 1 minute!", slotName);
|
||||
addSystemChatMessage(buf);
|
||||
} else if (durationSec <= 300 && durationSec > 295) {
|
||||
const char* slotName = (enchSlot < 3) ? kTempEnchantSlotNames[enchSlot] : "weapon";
|
||||
char buf[80];
|
||||
std::snprintf(buf, sizeof(buf), "Weapon enchant (%s) expires in 5 minutes.", slotName);
|
||||
addSystemChatMessage(buf);
|
||||
}
|
||||
}
|
||||
LOG_DEBUG("SMSG_ITEM_ENCHANT_TIME_UPDATE: slot=", enchSlot, " dur=", durationSec, "s");
|
||||
break;
|
||||
}
|
||||
case Opcode::SMSG_COMPLAIN_RESULT:
|
||||
case Opcode::SMSG_ITEM_REFUND_INFO_RESPONSE:
|
||||
case Opcode::SMSG_ITEM_ENCHANT_TIME_UPDATE:
|
||||
case Opcode::SMSG_LOOT_LIST:
|
||||
// Consume — not yet processed
|
||||
packet.setReadPos(packet.getSize());
|
||||
|
|
@ -5989,7 +6175,7 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
|
||||
// ---- Read item results ----
|
||||
case Opcode::SMSG_READ_ITEM_OK:
|
||||
addSystemChatMessage("You read the item.");
|
||||
bookPages_.clear(); // fresh book for this item read
|
||||
packet.setReadPos(packet.getSize());
|
||||
break;
|
||||
case Opcode::SMSG_READ_ITEM_FAILED:
|
||||
|
|
@ -6150,10 +6336,58 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
packet.setReadPos(packet.getSize());
|
||||
break;
|
||||
|
||||
// ---- Inspect (full character inspection) ----
|
||||
case Opcode::SMSG_INSPECT:
|
||||
packet.setReadPos(packet.getSize());
|
||||
// ---- Inspect (Classic 1.12 gear inspection) ----
|
||||
case Opcode::SMSG_INSPECT: {
|
||||
// Classic 1.12: PackedGUID + 19×uint32 itemEntries (EQUIPMENT_SLOT_END=19)
|
||||
// This opcode is only reachable on Classic servers; TBC/WotLK wire 0x115 maps to
|
||||
// SMSG_INSPECT_RESULTS_UPDATE which is handled separately.
|
||||
if (packet.getSize() - packet.getReadPos() < 2) {
|
||||
packet.setReadPos(packet.getSize()); break;
|
||||
}
|
||||
uint64_t guid = UpdateObjectParser::readPackedGuid(packet);
|
||||
if (guid == 0) { packet.setReadPos(packet.getSize()); break; }
|
||||
|
||||
constexpr int kGearSlots = 19;
|
||||
size_t needed = kGearSlots * sizeof(uint32_t);
|
||||
if (packet.getSize() - packet.getReadPos() < needed) {
|
||||
packet.setReadPos(packet.getSize()); break;
|
||||
}
|
||||
|
||||
std::array<uint32_t, 19> items{};
|
||||
for (int s = 0; s < kGearSlots; ++s)
|
||||
items[s] = packet.readUInt32();
|
||||
|
||||
// Resolve player name
|
||||
auto ent = entityManager.getEntity(guid);
|
||||
std::string playerName = "Target";
|
||||
if (ent) {
|
||||
auto pl = std::dynamic_pointer_cast<Player>(ent);
|
||||
if (pl && !pl->getName().empty()) playerName = pl->getName();
|
||||
}
|
||||
|
||||
// Populate inspect result immediately (no talent data in Classic SMSG_INSPECT)
|
||||
inspectResult_.guid = guid;
|
||||
inspectResult_.playerName = playerName;
|
||||
inspectResult_.totalTalents = 0;
|
||||
inspectResult_.unspentTalents = 0;
|
||||
inspectResult_.talentGroups = 0;
|
||||
inspectResult_.activeTalentGroup = 0;
|
||||
inspectResult_.itemEntries = items;
|
||||
inspectResult_.enchantIds = {};
|
||||
|
||||
// Also cache for future talent-inspect cross-reference
|
||||
inspectedPlayerItemEntries_[guid] = items;
|
||||
|
||||
// Trigger item queries for non-empty slots
|
||||
for (int s = 0; s < kGearSlots; ++s) {
|
||||
if (items[s] != 0) queryItemInfo(items[s], 0);
|
||||
}
|
||||
|
||||
LOG_INFO("SMSG_INSPECT (Classic): ", playerName, " has gear in ",
|
||||
std::count_if(items.begin(), items.end(),
|
||||
[](uint32_t e) { return e != 0; }), "/19 slots");
|
||||
break;
|
||||
}
|
||||
|
||||
// ---- Multiple aggregated packets/moves ----
|
||||
case Opcode::SMSG_MULTIPLE_MOVES:
|
||||
|
|
@ -6209,6 +6443,9 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
handleQuestPoiQueryResponse(packet);
|
||||
break;
|
||||
case Opcode::SMSG_ON_CANCEL_EXPECTED_RIDE_VEHICLE_AURA:
|
||||
vehicleId_ = 0; // Vehicle ride cancelled; clear UI
|
||||
packet.setReadPos(packet.getSize());
|
||||
break;
|
||||
case Opcode::SMSG_RESET_RANGED_COMBAT_TIMER:
|
||||
case Opcode::SMSG_PROFILEDATA_RESPONSE:
|
||||
packet.setReadPos(packet.getSize());
|
||||
|
|
@ -6703,6 +6940,7 @@ void GameHandler::selectCharacter(uint64_t characterGuid) {
|
|||
onlineEquipDirty_ = false;
|
||||
playerMoneyCopper_ = 0;
|
||||
playerArmorRating_ = 0;
|
||||
std::fill(std::begin(playerResistances_), std::end(playerResistances_), 0);
|
||||
std::fill(std::begin(playerStats_), std::end(playerStats_), -1);
|
||||
knownSpells.clear();
|
||||
spellCooldowns.clear();
|
||||
|
|
@ -6712,6 +6950,10 @@ void GameHandler::selectCharacter(uint64_t characterGuid) {
|
|||
unitAurasCache_.clear();
|
||||
unitCastStates_.clear();
|
||||
petGuid_ = 0;
|
||||
stableWindowOpen_ = false;
|
||||
stableMasterGuid_ = 0;
|
||||
stableNumSlots_ = 0;
|
||||
stabledPets_.clear();
|
||||
playerXp_ = 0;
|
||||
playerNextLevelXp_ = 0;
|
||||
serverPlayerLevel_ = 1;
|
||||
|
|
@ -6833,6 +7075,7 @@ void GameHandler::handleLoginVerifyWorld(network::Packet& packet) {
|
|||
taxiStartGrace_ = 0.0f;
|
||||
currentMountDisplayId_ = 0;
|
||||
taxiMountDisplayId_ = 0;
|
||||
vehicleId_ = 0;
|
||||
if (mountCallback_) {
|
||||
mountCallback_(0);
|
||||
}
|
||||
|
|
@ -6846,6 +7089,8 @@ void GameHandler::handleLoginVerifyWorld(network::Packet& packet) {
|
|||
talentsInitialized_ = false;
|
||||
learnedTalents_[0].clear();
|
||||
learnedTalents_[1].clear();
|
||||
learnedGlyphs_[0].fill(0);
|
||||
learnedGlyphs_[1].fill(0);
|
||||
unspentTalentPoints_[0] = 0;
|
||||
unspentTalentPoints_[1] = 0;
|
||||
activeTalentSpec_ = 0;
|
||||
|
|
@ -7856,6 +8101,22 @@ void GameHandler::sendPing() {
|
|||
socket->send(packet);
|
||||
}
|
||||
|
||||
void GameHandler::sendRequestVehicleExit() {
|
||||
if (state != WorldState::IN_WORLD || vehicleId_ == 0) return;
|
||||
// CMSG_REQUEST_VEHICLE_EXIT has no payload — opcode only
|
||||
network::Packet pkt(wireOpcode(Opcode::CMSG_REQUEST_VEHICLE_EXIT));
|
||||
socket->send(pkt);
|
||||
vehicleId_ = 0; // Optimistically clear; server will confirm via SMSG_PLAYER_VEHICLE_DATA(0)
|
||||
}
|
||||
|
||||
void GameHandler::useEquipmentSet(uint32_t setId) {
|
||||
if (state != WorldState::IN_WORLD) return;
|
||||
// CMSG_EQUIPMENT_SET_USE: uint32 setId
|
||||
network::Packet pkt(wireOpcode(Opcode::CMSG_EQUIPMENT_SET_USE));
|
||||
pkt.writeUInt32(setId);
|
||||
socket->send(pkt);
|
||||
}
|
||||
|
||||
void GameHandler::sendMinimapPing(float wowX, float wowY) {
|
||||
if (state != WorldState::IN_WORLD) return;
|
||||
|
||||
|
|
@ -8167,6 +8428,7 @@ void GameHandler::forceClearTaxiAndMovementState() {
|
|||
taxiMountActive_ = false;
|
||||
taxiMountDisplayId_ = 0;
|
||||
currentMountDisplayId_ = 0;
|
||||
vehicleId_ = 0;
|
||||
resurrectPending_ = false;
|
||||
resurrectRequestPending_ = false;
|
||||
playerDead_ = false;
|
||||
|
|
@ -8807,6 +9069,9 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
|
|||
playerArmorRating_ = static_cast<int32_t>(val);
|
||||
LOG_DEBUG("Armor rating from update fields: ", playerArmorRating_);
|
||||
}
|
||||
else if (ufArmor != 0xFFFF && key > ufArmor && key <= ufArmor + 6) {
|
||||
playerResistances_[key - ufArmor - 1] = static_cast<int32_t>(val);
|
||||
}
|
||||
else if (ufPBytes2 != 0xFFFF && key == ufPBytes2) {
|
||||
uint8_t bankBagSlots = static_cast<uint8_t>((val >> 16) & 0xFF);
|
||||
LOG_WARNING("PLAYER_BYTES_2 (CREATE): raw=0x", std::hex, val, std::dec,
|
||||
|
|
@ -9147,6 +9412,9 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
|
|||
else if (ufArmor != 0xFFFF && key == ufArmor) {
|
||||
playerArmorRating_ = static_cast<int32_t>(val);
|
||||
}
|
||||
else if (ufArmor != 0xFFFF && key > ufArmor && key <= ufArmor + 6) {
|
||||
playerResistances_[key - ufArmor - 1] = static_cast<int32_t>(val);
|
||||
}
|
||||
else if (ufPBytes2v != 0xFFFF && key == ufPBytes2v) {
|
||||
uint8_t bankBagSlots = static_cast<uint8_t>((val >> 16) & 0xFF);
|
||||
LOG_WARNING("PLAYER_BYTES_2 (VALUES): raw=0x", std::hex, val, std::dec,
|
||||
|
|
@ -11140,6 +11408,7 @@ void GameHandler::handleGameObjectPageText(network::Packet& packet) {
|
|||
else if (info.type == 10) pageId = info.data[7];
|
||||
|
||||
if (pageId != 0 && socket && state == WorldState::IN_WORLD) {
|
||||
bookPages_.clear(); // start a fresh book for this interaction
|
||||
auto req = PageTextQueryPacket::build(pageId, guid);
|
||||
socket->send(req);
|
||||
return;
|
||||
|
|
@ -11154,19 +11423,31 @@ void GameHandler::handlePageTextQueryResponse(network::Packet& packet) {
|
|||
PageTextQueryResponseData data;
|
||||
if (!PageTextQueryResponseParser::parse(packet, data)) return;
|
||||
|
||||
if (!data.text.empty()) {
|
||||
std::istringstream iss(data.text);
|
||||
std::string line;
|
||||
bool wrote = false;
|
||||
while (std::getline(iss, line)) {
|
||||
if (line.empty()) continue;
|
||||
addSystemChatMessage(line);
|
||||
wrote = true;
|
||||
if (!data.isValid()) return;
|
||||
|
||||
// Append page if not already collected
|
||||
bool alreadyHave = false;
|
||||
for (const auto& bp : bookPages_) {
|
||||
if (bp.pageId == data.pageId) { alreadyHave = true; break; }
|
||||
}
|
||||
if (!alreadyHave) {
|
||||
bookPages_.push_back({data.pageId, data.text});
|
||||
}
|
||||
|
||||
// Follow the chain: if there's a next page we haven't fetched yet, request it
|
||||
if (data.nextPageId != 0) {
|
||||
bool nextHave = false;
|
||||
for (const auto& bp : bookPages_) {
|
||||
if (bp.pageId == data.nextPageId) { nextHave = true; break; }
|
||||
}
|
||||
if (!wrote) {
|
||||
addSystemChatMessage(data.text);
|
||||
if (!nextHave && socket && state == WorldState::IN_WORLD) {
|
||||
auto req = PageTextQueryPacket::build(data.nextPageId, playerGuid);
|
||||
socket->send(req);
|
||||
}
|
||||
}
|
||||
LOG_DEBUG("handlePageTextQueryResponse: pageId=", data.pageId,
|
||||
" nextPage=", data.nextPageId,
|
||||
" totalPages=", bookPages_.size());
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
|
|
@ -11278,10 +11559,12 @@ void GameHandler::handleInspectResults(network::Packet& packet) {
|
|||
learnedTalents_[g][talentId] = rank;
|
||||
}
|
||||
if (packet.getSize() - packet.getReadPos() < 1) break;
|
||||
learnedGlyphs_[g].fill(0);
|
||||
uint8_t glyphCount = packet.readUInt8();
|
||||
for (uint8_t gl = 0; gl < glyphCount; ++gl) {
|
||||
if (packet.getSize() - packet.getReadPos() < 2) break;
|
||||
packet.readUInt16(); // glyphId (skip)
|
||||
uint16_t glyphId = packet.readUInt16();
|
||||
if (gl < MAX_GLYPH_SLOTS) learnedGlyphs_[g][gl] = glyphId;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -14045,8 +14328,11 @@ void GameHandler::handleAttackerStateUpdate(network::Packet& packet) {
|
|||
bool isPlayerTarget = (data.targetGuid == playerGuid);
|
||||
if (!isPlayerAttacker && !isPlayerTarget) return; // Not our combat
|
||||
|
||||
if (isPlayerAttacker && meleeSwingCallback_) {
|
||||
meleeSwingCallback_();
|
||||
if (isPlayerAttacker) {
|
||||
lastMeleeSwingMs_ = static_cast<uint64_t>(
|
||||
std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::system_clock::now().time_since_epoch()).count());
|
||||
if (meleeSwingCallback_) meleeSwingCallback_();
|
||||
}
|
||||
if (!isPlayerAttacker && npcSwingCallback_) {
|
||||
npcSwingCallback_(data.attackerGuid);
|
||||
|
|
@ -14289,6 +14575,19 @@ void GameHandler::cancelAura(uint32_t spellId) {
|
|||
socket->send(packet);
|
||||
}
|
||||
|
||||
uint32_t GameHandler::getTempEnchantRemainingMs(uint32_t slot) const {
|
||||
uint64_t nowMs = static_cast<uint64_t>(
|
||||
std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::steady_clock::now().time_since_epoch()).count());
|
||||
for (const auto& t : tempEnchantTimers_) {
|
||||
if (t.slot == slot) {
|
||||
return (t.expireMs > nowMs)
|
||||
? static_cast<uint32_t>(t.expireMs - nowMs) : 0u;
|
||||
}
|
||||
}
|
||||
return 0u;
|
||||
}
|
||||
|
||||
void GameHandler::handlePetSpells(network::Packet& packet) {
|
||||
const size_t remaining = packet.getSize() - packet.getReadPos();
|
||||
if (remaining < 8) {
|
||||
|
|
@ -14364,6 +14663,86 @@ void GameHandler::dismissPet() {
|
|||
socket->send(packet);
|
||||
}
|
||||
|
||||
void GameHandler::renamePet(const std::string& newName) {
|
||||
if (petGuid_ == 0 || state != WorldState::IN_WORLD || !socket) return;
|
||||
if (newName.empty() || newName.size() > 12) return; // Server enforces max 12 chars
|
||||
auto packet = PetRenamePacket::build(petGuid_, newName, 0);
|
||||
socket->send(packet);
|
||||
LOG_INFO("Sent CMSG_PET_RENAME: petGuid=0x", std::hex, petGuid_, std::dec, " name='", newName, "'");
|
||||
}
|
||||
|
||||
void GameHandler::requestStabledPetList() {
|
||||
if (state != WorldState::IN_WORLD || !socket || stableMasterGuid_ == 0) return;
|
||||
auto pkt = ListStabledPetsPacket::build(stableMasterGuid_);
|
||||
socket->send(pkt);
|
||||
LOG_INFO("Sent MSG_LIST_STABLED_PETS to npc=0x", std::hex, stableMasterGuid_, std::dec);
|
||||
}
|
||||
|
||||
void GameHandler::stablePet(uint8_t slot) {
|
||||
if (state != WorldState::IN_WORLD || !socket || stableMasterGuid_ == 0) return;
|
||||
if (petGuid_ == 0) {
|
||||
addSystemChatMessage("You do not have an active pet to stable.");
|
||||
return;
|
||||
}
|
||||
auto pkt = StablePetPacket::build(stableMasterGuid_, slot);
|
||||
socket->send(pkt);
|
||||
LOG_INFO("Sent CMSG_STABLE_PET: slot=", static_cast<int>(slot));
|
||||
}
|
||||
|
||||
void GameHandler::unstablePet(uint32_t petNumber) {
|
||||
if (state != WorldState::IN_WORLD || !socket || stableMasterGuid_ == 0 || petNumber == 0) return;
|
||||
auto pkt = UnstablePetPacket::build(stableMasterGuid_, petNumber);
|
||||
socket->send(pkt);
|
||||
LOG_INFO("Sent CMSG_UNSTABLE_PET: petNumber=", petNumber);
|
||||
}
|
||||
|
||||
void GameHandler::handleListStabledPets(network::Packet& packet) {
|
||||
// SMSG MSG_LIST_STABLED_PETS:
|
||||
// uint64 stableMasterGuid
|
||||
// uint8 petCount
|
||||
// uint8 numSlots
|
||||
// per pet:
|
||||
// uint32 petNumber
|
||||
// uint32 entry
|
||||
// uint32 level
|
||||
// string name (null-terminated)
|
||||
// uint32 displayId
|
||||
// uint8 isActive (1 = active/summoned, 0 = stabled)
|
||||
constexpr size_t kMinHeader = 8 + 1 + 1;
|
||||
if (packet.getSize() - packet.getReadPos() < kMinHeader) {
|
||||
LOG_WARNING("MSG_LIST_STABLED_PETS: packet too short (", packet.getSize(), ")");
|
||||
return;
|
||||
}
|
||||
stableMasterGuid_ = packet.readUInt64();
|
||||
uint8_t petCount = packet.readUInt8();
|
||||
stableNumSlots_ = packet.readUInt8();
|
||||
|
||||
stabledPets_.clear();
|
||||
stabledPets_.reserve(petCount);
|
||||
|
||||
for (uint8_t i = 0; i < petCount; ++i) {
|
||||
if (packet.getSize() - packet.getReadPos() < 4 + 4 + 4) break;
|
||||
StabledPet pet;
|
||||
pet.petNumber = packet.readUInt32();
|
||||
pet.entry = packet.readUInt32();
|
||||
pet.level = packet.readUInt32();
|
||||
pet.name = packet.readString();
|
||||
if (packet.getSize() - packet.getReadPos() < 4 + 1) break;
|
||||
pet.displayId = packet.readUInt32();
|
||||
pet.isActive = (packet.readUInt8() != 0);
|
||||
stabledPets_.push_back(std::move(pet));
|
||||
}
|
||||
|
||||
stableWindowOpen_ = true;
|
||||
LOG_INFO("MSG_LIST_STABLED_PETS: stableMasterGuid=0x", std::hex, stableMasterGuid_, std::dec,
|
||||
" petCount=", (int)petCount, " numSlots=", (int)stableNumSlots_);
|
||||
for (const auto& p : stabledPets_) {
|
||||
LOG_DEBUG(" Pet: number=", p.petNumber, " entry=", p.entry,
|
||||
" level=", p.level, " name='", p.name, "' displayId=", p.displayId,
|
||||
" active=", p.isActive);
|
||||
}
|
||||
}
|
||||
|
||||
void GameHandler::setActionBarSlot(int slot, ActionBarSlot::Type type, uint32_t id) {
|
||||
if (slot < 0 || slot >= ACTION_BAR_SLOTS) return;
|
||||
actionBar[slot].type = type;
|
||||
|
|
@ -15500,6 +15879,7 @@ void GameHandler::lootItem(uint8_t slotIndex) {
|
|||
void GameHandler::closeLoot() {
|
||||
if (!lootWindowOpen) return;
|
||||
lootWindowOpen = false;
|
||||
masterLootCandidates_.clear();
|
||||
if (currentLoot.lootGuid != 0 && targetGuid == currentLoot.lootGuid) {
|
||||
clearTarget();
|
||||
}
|
||||
|
|
@ -15510,6 +15890,16 @@ void GameHandler::closeLoot() {
|
|||
currentLoot = LootResponseData{};
|
||||
}
|
||||
|
||||
void GameHandler::lootMasterGive(uint8_t lootSlot, uint64_t targetGuid) {
|
||||
if (state != WorldState::IN_WORLD || !socket) return;
|
||||
// CMSG_LOOT_MASTER_GIVE: uint64 lootGuid + uint8 slotIndex + uint64 targetGuid
|
||||
network::Packet pkt(wireOpcode(Opcode::CMSG_LOOT_MASTER_GIVE));
|
||||
pkt.writeUInt64(currentLoot.lootGuid);
|
||||
pkt.writeUInt8(lootSlot);
|
||||
pkt.writeUInt64(targetGuid);
|
||||
socket->send(pkt);
|
||||
}
|
||||
|
||||
void GameHandler::interactWithNpc(uint64_t guid) {
|
||||
if (state != WorldState::IN_WORLD || !socket) return;
|
||||
auto packet = GossipHelloPacket::build(guid);
|
||||
|
|
@ -15689,6 +16079,18 @@ void GameHandler::selectGossipOption(uint32_t optionId) {
|
|||
socket->send(bindPkt);
|
||||
LOG_INFO("Sent CMSG_BINDER_ACTIVATE for npc=0x", std::hex, currentGossip.npcGuid, std::dec);
|
||||
}
|
||||
|
||||
// Stable master detection: GOSSIP_OPTION_STABLE or text keywords
|
||||
if (text == "GOSSIP_OPTION_STABLE" ||
|
||||
textLower.find("stable") != std::string::npos ||
|
||||
textLower.find("my pet") != std::string::npos) {
|
||||
stableMasterGuid_ = currentGossip.npcGuid;
|
||||
stableWindowOpen_ = false; // will open when list arrives
|
||||
auto listPkt = ListStabledPetsPacket::build(currentGossip.npcGuid);
|
||||
socket->send(listPkt);
|
||||
LOG_INFO("Sent MSG_LIST_STABLED_PETS (gossip) to npc=0x",
|
||||
std::hex, currentGossip.npcGuid, std::dec);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
@ -17154,6 +17556,13 @@ void GameHandler::loadSpellNameCache() {
|
|||
if (f != 0xFFFFFFFF && f < dbc->getFieldCount()) { dispelField = f; hasDispelField = true; }
|
||||
}
|
||||
|
||||
// Tooltip/description field
|
||||
uint32_t tooltipField = 0xFFFFFFFF;
|
||||
if (spellL) {
|
||||
uint32_t f = spellL->field("Tooltip");
|
||||
if (f != 0xFFFFFFFF && f < dbc->getFieldCount()) tooltipField = f;
|
||||
}
|
||||
|
||||
uint32_t count = dbc->getRecordCount();
|
||||
for (uint32_t i = 0; i < count; ++i) {
|
||||
uint32_t id = dbc->getUInt32(i, spellL ? (*spellL)["ID"] : 0);
|
||||
|
|
@ -17161,7 +17570,10 @@ void GameHandler::loadSpellNameCache() {
|
|||
std::string name = dbc->getString(i, spellL ? (*spellL)["Name"] : 136);
|
||||
std::string rank = dbc->getString(i, spellL ? (*spellL)["Rank"] : 153);
|
||||
if (!name.empty()) {
|
||||
SpellNameEntry entry{std::move(name), std::move(rank), 0, 0};
|
||||
SpellNameEntry entry{std::move(name), std::move(rank), {}, 0, 0};
|
||||
if (tooltipField != 0xFFFFFFFF) {
|
||||
entry.description = dbc->getString(i, tooltipField);
|
||||
}
|
||||
if (hasSchoolMask) {
|
||||
entry.schoolMask = dbc->getUInt32(i, schoolMaskField);
|
||||
} else if (hasSchoolEnum) {
|
||||
|
|
@ -17366,6 +17778,12 @@ const std::string& GameHandler::getSpellRank(uint32_t spellId) const {
|
|||
return (it != spellNameCache_.end()) ? it->second.rank : EMPTY_STRING;
|
||||
}
|
||||
|
||||
const std::string& GameHandler::getSpellDescription(uint32_t spellId) const {
|
||||
const_cast<GameHandler*>(this)->loadSpellNameCache();
|
||||
auto it = spellNameCache_.find(spellId);
|
||||
return (it != spellNameCache_.end()) ? it->second.description : EMPTY_STRING;
|
||||
}
|
||||
|
||||
uint8_t GameHandler::getSpellDispelType(uint32_t spellId) const {
|
||||
const_cast<GameHandler*>(this)->loadSpellNameCache();
|
||||
auto it = spellNameCache_.find(spellId);
|
||||
|
|
@ -20282,6 +20700,33 @@ void GameHandler::sendLootRoll(uint64_t objectGuid, uint32_t slot, uint8_t rollT
|
|||
// PackedTime date — uint32 bitfield (seconds since epoch)
|
||||
// uint32 realmFirst — how many on realm also got it (0 = realm first)
|
||||
// ---------------------------------------------------------------------------
|
||||
void GameHandler::loadTitleNameCache() {
|
||||
if (titleNameCacheLoaded_) return;
|
||||
titleNameCacheLoaded_ = true;
|
||||
|
||||
auto* am = core::Application::getInstance().getAssetManager();
|
||||
if (!am || !am->isInitialized()) return;
|
||||
|
||||
auto dbc = am->loadDBC("CharTitles.dbc");
|
||||
if (!dbc || !dbc->isLoaded() || dbc->getFieldCount() < 5) return;
|
||||
|
||||
const auto* layout = pipeline::getActiveDBCLayout()
|
||||
? pipeline::getActiveDBCLayout()->getLayout("CharTitles") : nullptr;
|
||||
|
||||
uint32_t titleField = layout ? layout->field("Title") : 2;
|
||||
uint32_t bitField = layout ? layout->field("TitleBit") : 36;
|
||||
if (titleField == 0xFFFFFFFF) titleField = 2;
|
||||
if (bitField == 0xFFFFFFFF) bitField = static_cast<uint32_t>(dbc->getFieldCount() - 1);
|
||||
|
||||
for (uint32_t i = 0; i < dbc->getRecordCount(); ++i) {
|
||||
uint32_t bit = dbc->getUInt32(i, bitField);
|
||||
if (bit == 0) continue;
|
||||
std::string name = dbc->getString(i, titleField);
|
||||
if (!name.empty()) titleNameCache_[bit] = std::move(name);
|
||||
}
|
||||
LOG_INFO("CharTitles: loaded ", titleNameCache_.size(), " title names from DBC");
|
||||
}
|
||||
|
||||
void GameHandler::loadAchievementNameCache() {
|
||||
if (achievementNameCacheLoaded_) return;
|
||||
achievementNameCacheLoaded_ = true;
|
||||
|
|
@ -20296,12 +20741,23 @@ void GameHandler::loadAchievementNameCache() {
|
|||
? pipeline::getActiveDBCLayout()->getLayout("Achievement") : nullptr;
|
||||
uint32_t titleField = achL ? achL->field("Title") : 4;
|
||||
if (titleField == 0xFFFFFFFF) titleField = 4;
|
||||
uint32_t descField = achL ? achL->field("Description") : 0xFFFFFFFF;
|
||||
uint32_t ptsField = achL ? achL->field("Points") : 0xFFFFFFFF;
|
||||
|
||||
uint32_t fieldCount = dbc->getFieldCount();
|
||||
for (uint32_t i = 0; i < dbc->getRecordCount(); ++i) {
|
||||
uint32_t id = dbc->getUInt32(i, 0);
|
||||
if (id == 0) continue;
|
||||
std::string title = dbc->getString(i, titleField);
|
||||
if (!title.empty()) achievementNameCache_[id] = std::move(title);
|
||||
if (descField != 0xFFFFFFFF && descField < fieldCount) {
|
||||
std::string desc = dbc->getString(i, descField);
|
||||
if (!desc.empty()) achievementDescCache_[id] = std::move(desc);
|
||||
}
|
||||
if (ptsField != 0xFFFFFFFF && ptsField < fieldCount) {
|
||||
uint32_t pts = dbc->getUInt32(i, ptsField);
|
||||
if (pts > 0) achievementPointsCache_[id] = pts;
|
||||
}
|
||||
}
|
||||
LOG_INFO("Achievement: loaded ", achievementNameCache_.size(), " names from Achievement.dbc");
|
||||
}
|
||||
|
|
@ -20539,6 +20995,17 @@ void GameHandler::handleEquipmentSetList(network::Packet& packet) {
|
|||
}
|
||||
equipmentSets_.push_back(std::move(es));
|
||||
}
|
||||
// Populate public-facing info
|
||||
equipmentSetInfo_.clear();
|
||||
equipmentSetInfo_.reserve(equipmentSets_.size());
|
||||
for (const auto& es : equipmentSets_) {
|
||||
EquipmentSetInfo info;
|
||||
info.setGuid = es.setGuid;
|
||||
info.setId = es.setId;
|
||||
info.name = es.name;
|
||||
info.iconName = es.iconName;
|
||||
equipmentSetInfo_.push_back(std::move(info));
|
||||
}
|
||||
LOG_INFO("SMSG_EQUIPMENT_SET_LIST: ", equipmentSets_.size(), " equipment sets received");
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2930,6 +2930,29 @@ bool ItemQueryResponseParser::parse(network::Packet& packet, ItemQueryResponseDa
|
|||
data.startQuestId = packet.readUInt32(); // StartQuest
|
||||
}
|
||||
|
||||
// WotLK 3.3.5a: additional fields after StartQuest (read up to socket data)
|
||||
// LockID(4), Material(4), Sheath(4), RandomProperty(4), RandomSuffix(4),
|
||||
// Block(4), ItemSet(4), MaxDurability(4), Area(4), Map(4), BagFamily(4),
|
||||
// TotemCategory(4) = 48 bytes before sockets
|
||||
constexpr size_t kPreSocketSkip = 48;
|
||||
if (packet.getReadPos() + kPreSocketSkip + 28 <= packet.getSize()) {
|
||||
// LockID(0), Material(1), Sheath(2), RandomProperty(3), RandomSuffix(4), Block(5)
|
||||
for (size_t i = 0; i < 6; ++i) packet.readUInt32();
|
||||
data.itemSetId = packet.readUInt32(); // ItemSet(6)
|
||||
// MaxDurability(7), Area(8), Map(9), BagFamily(10), TotemCategory(11)
|
||||
for (size_t i = 0; i < 5; ++i) packet.readUInt32();
|
||||
// 3 socket slots: socketColor (4 bytes each)
|
||||
data.socketColor[0] = packet.readUInt32();
|
||||
data.socketColor[1] = packet.readUInt32();
|
||||
data.socketColor[2] = packet.readUInt32();
|
||||
// 3 socket content (gem enchantment IDs — skip, not currently displayed)
|
||||
packet.readUInt32();
|
||||
packet.readUInt32();
|
||||
packet.readUInt32();
|
||||
// socketBonus (enchantmentId)
|
||||
data.socketBonus = packet.readUInt32();
|
||||
}
|
||||
|
||||
data.valid = !data.name.empty();
|
||||
return true;
|
||||
}
|
||||
|
|
@ -5374,5 +5397,37 @@ bool AuctionCommandResultParser::parse(network::Packet& packet, AuctionCommandRe
|
|||
return true;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Pet Stable System
|
||||
// ============================================================
|
||||
|
||||
network::Packet ListStabledPetsPacket::build(uint64_t stableMasterGuid) {
|
||||
network::Packet p(wireOpcode(Opcode::MSG_LIST_STABLED_PETS));
|
||||
p.writeUInt64(stableMasterGuid);
|
||||
return p;
|
||||
}
|
||||
|
||||
network::Packet StablePetPacket::build(uint64_t stableMasterGuid, uint8_t slot) {
|
||||
network::Packet p(wireOpcode(Opcode::CMSG_STABLE_PET));
|
||||
p.writeUInt64(stableMasterGuid);
|
||||
p.writeUInt8(slot);
|
||||
return p;
|
||||
}
|
||||
|
||||
network::Packet UnstablePetPacket::build(uint64_t stableMasterGuid, uint32_t petNumber) {
|
||||
network::Packet p(wireOpcode(Opcode::CMSG_UNSTABLE_PET));
|
||||
p.writeUInt64(stableMasterGuid);
|
||||
p.writeUInt32(petNumber);
|
||||
return p;
|
||||
}
|
||||
|
||||
network::Packet PetRenamePacket::build(uint64_t petGuid, const std::string& name, uint8_t isDeclined) {
|
||||
network::Packet p(wireOpcode(Opcode::CMSG_PET_RENAME));
|
||||
p.writeUInt64(petGuid);
|
||||
p.writeString(name); // null-terminated
|
||||
p.writeUInt8(isDeclined);
|
||||
return p;
|
||||
}
|
||||
|
||||
} // namespace game
|
||||
} // namespace wowee
|
||||
|
|
|
|||
|
|
@ -140,6 +140,17 @@ std::optional<float> CameraController::getCachedFloorHeight(float x, float y, fl
|
|||
return result;
|
||||
}
|
||||
|
||||
void CameraController::triggerShake(float magnitude, float frequency, float duration) {
|
||||
// Allow stronger shake to override weaker; don't allow zero magnitude.
|
||||
if (magnitude <= 0.0f || duration <= 0.0f) return;
|
||||
if (magnitude > shakeMagnitude_ || shakeElapsed_ >= shakeDuration_) {
|
||||
shakeMagnitude_ = magnitude;
|
||||
shakeFrequency_ = frequency;
|
||||
shakeDuration_ = duration;
|
||||
shakeElapsed_ = 0.0f;
|
||||
}
|
||||
}
|
||||
|
||||
void CameraController::update(float deltaTime) {
|
||||
if (!enabled || !camera) {
|
||||
return;
|
||||
|
|
@ -1859,6 +1870,23 @@ void CameraController::update(float deltaTime) {
|
|||
wasFalling = !grounded && verticalVelocity <= 0.0f;
|
||||
|
||||
// R key is now handled above with chat safeguard (WantTextInput check)
|
||||
|
||||
// Camera shake (SMSG_CAMERA_SHAKE): apply sinusoidal offset to final camera position.
|
||||
if (shakeElapsed_ < shakeDuration_) {
|
||||
shakeElapsed_ += deltaTime;
|
||||
float t = shakeElapsed_ / shakeDuration_;
|
||||
// Envelope: fade out over the last 30% of shake duration
|
||||
float envelope = (t < 0.7f) ? 1.0f : (1.0f - (t - 0.7f) / 0.3f);
|
||||
float theta = shakeElapsed_ * shakeFrequency_ * 2.0f * 3.14159265f;
|
||||
glm::vec3 offset(
|
||||
shakeMagnitude_ * envelope * std::sin(theta),
|
||||
shakeMagnitude_ * envelope * std::cos(theta * 1.3f),
|
||||
shakeMagnitude_ * envelope * std::sin(theta * 0.7f) * 0.5f
|
||||
);
|
||||
if (camera) {
|
||||
camera->setPosition(camera->getPosition() + offset);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void CameraController::processMouseMotion(const SDL_MouseMotionEvent& event) {
|
||||
|
|
|
|||
|
|
@ -3905,7 +3905,14 @@ void M2Renderer::cleanupUnusedModels() {
|
|||
}
|
||||
}
|
||||
|
||||
// Delete GPU resources and remove from map
|
||||
// Delete GPU resources and remove from map.
|
||||
// Wait for the GPU to finish all in-flight frames before destroying any
|
||||
// buffers — the previous frame's command buffer may still be referencing
|
||||
// vertex/index buffers that are about to be freed. Without this wait,
|
||||
// the GPU reads freed memory, which can cause VK_ERROR_DEVICE_LOST.
|
||||
if (!toRemove.empty() && vkCtx_) {
|
||||
vkDeviceWaitIdle(vkCtx_->getDevice());
|
||||
}
|
||||
for (uint32_t id : toRemove) {
|
||||
auto it = models.find(id);
|
||||
if (it != models.end()) {
|
||||
|
|
|
|||
|
|
@ -219,6 +219,13 @@ void PerformanceHUD::render(const Renderer* renderer, const Camera* camera) {
|
|||
ImGui::Text(" Upscale Dispatches: %zu", renderer->getAmdFsr3UpscaleDispatchCount());
|
||||
ImGui::Text(" FG Fallbacks: %zu", renderer->getAmdFsr3FallbackCount());
|
||||
}
|
||||
if (renderer->isFXAAEnabled()) {
|
||||
if (renderer->isFSR2Enabled()) {
|
||||
ImGui::TextColored(ImVec4(0.6f, 1.0f, 0.8f, 1.0f), "FXAA: ON (FSR3+FXAA combined)");
|
||||
} else {
|
||||
ImGui::TextColored(ImVec4(0.8f, 1.0f, 0.6f, 1.0f), "FXAA: ON");
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::Spacing();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -858,6 +858,7 @@ void Renderer::shutdown() {
|
|||
|
||||
destroyFSRResources();
|
||||
destroyFSR2Resources();
|
||||
destroyFXAAResources();
|
||||
destroyPerFrameResources();
|
||||
|
||||
zoneManager.reset();
|
||||
|
|
@ -960,8 +961,9 @@ void Renderer::applyMsaaChange() {
|
|||
VkDevice device = vkCtx->getDevice();
|
||||
if (selCirclePipeline) { vkDestroyPipeline(device, selCirclePipeline, nullptr); selCirclePipeline = VK_NULL_HANDLE; }
|
||||
if (overlayPipeline) { vkDestroyPipeline(device, overlayPipeline, nullptr); overlayPipeline = VK_NULL_HANDLE; }
|
||||
if (fsr_.sceneFramebuffer) destroyFSRResources(); // Will be lazily recreated in beginFrame()
|
||||
if (fsr_.sceneFramebuffer) destroyFSRResources(); // Will be lazily recreated in beginFrame()
|
||||
if (fsr2_.sceneFramebuffer) destroyFSR2Resources();
|
||||
if (fxaa_.sceneFramebuffer) destroyFXAAResources(); // Will be lazily recreated in beginFrame()
|
||||
|
||||
// Reinitialize ImGui Vulkan backend with new MSAA sample count
|
||||
ImGui_ImplVulkan_Shutdown();
|
||||
|
|
@ -1017,6 +1019,21 @@ void Renderer::beginFrame() {
|
|||
}
|
||||
}
|
||||
|
||||
// FXAA resource management — FXAA can coexist with FSR1 and FSR3.
|
||||
// When both FXAA and FSR3 are enabled, FXAA runs as a post-FSR3 pass.
|
||||
// When both FXAA and FSR1 are enabled, FXAA takes priority (native res render).
|
||||
if (fxaa_.needsRecreate && fxaa_.sceneFramebuffer) {
|
||||
destroyFXAAResources();
|
||||
fxaa_.needsRecreate = false;
|
||||
if (!fxaa_.enabled) LOG_INFO("FXAA: disabled");
|
||||
}
|
||||
if (fxaa_.enabled && !fxaa_.sceneFramebuffer) {
|
||||
if (!initFXAAResources()) {
|
||||
LOG_ERROR("FXAA: initialization failed, disabling");
|
||||
fxaa_.enabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle swapchain recreation if needed
|
||||
if (vkCtx->isSwapchainDirty()) {
|
||||
vkCtx->recreateSwapchain(window->getWidth(), window->getHeight());
|
||||
|
|
@ -1033,6 +1050,12 @@ void Renderer::beginFrame() {
|
|||
destroyFSR2Resources();
|
||||
initFSR2Resources();
|
||||
}
|
||||
// Recreate FXAA resources for new swapchain dimensions
|
||||
// FXAA can coexist with FSR1 and FSR3 simultaneously.
|
||||
if (fxaa_.enabled) {
|
||||
destroyFXAAResources();
|
||||
initFXAAResources();
|
||||
}
|
||||
}
|
||||
|
||||
// Acquire swapchain image and begin command buffer
|
||||
|
|
@ -1119,6 +1142,11 @@ void Renderer::beginFrame() {
|
|||
if (fsr2_.enabled && fsr2_.sceneFramebuffer) {
|
||||
rpInfo.framebuffer = fsr2_.sceneFramebuffer;
|
||||
renderExtent = { fsr2_.internalWidth, fsr2_.internalHeight };
|
||||
} else if (fxaa_.enabled && fxaa_.sceneFramebuffer) {
|
||||
// FXAA takes priority over FSR1: renders at native res with AA post-process.
|
||||
// When both FSR1 and FXAA are enabled, FXAA wins (native res, no downscale).
|
||||
rpInfo.framebuffer = fxaa_.sceneFramebuffer;
|
||||
renderExtent = vkCtx->getSwapchainExtent(); // native resolution — no downscaling
|
||||
} else if (fsr_.enabled && fsr_.sceneFramebuffer) {
|
||||
rpInfo.framebuffer = fsr_.sceneFramebuffer;
|
||||
renderExtent = { fsr_.internalWidth, fsr_.internalHeight };
|
||||
|
|
@ -1208,6 +1236,35 @@ void Renderer::endFrame() {
|
|||
VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT);
|
||||
}
|
||||
|
||||
// FSR3+FXAA combined: re-point FXAA's descriptor to the FSR3 temporal output
|
||||
// so renderFXAAPass() applies spatial AA on the temporally-stabilized frame.
|
||||
// This must happen outside the render pass (descriptor updates are CPU-side).
|
||||
if (fxaa_.enabled && fxaa_.descSet && fxaa_.sceneSampler) {
|
||||
VkImageView fsr3OutputView = VK_NULL_HANDLE;
|
||||
if (fsr2_.useAmdBackend) {
|
||||
if (fsr2_.amdFsr3FramegenRuntimeActive && fsr2_.framegenOutput.image)
|
||||
fsr3OutputView = fsr2_.framegenOutput.imageView;
|
||||
else if (fsr2_.history[fsr2_.currentHistory].image)
|
||||
fsr3OutputView = fsr2_.history[fsr2_.currentHistory].imageView;
|
||||
} else if (fsr2_.history[fsr2_.currentHistory].image) {
|
||||
fsr3OutputView = fsr2_.history[fsr2_.currentHistory].imageView;
|
||||
}
|
||||
if (fsr3OutputView) {
|
||||
VkDescriptorImageInfo imgInfo{};
|
||||
imgInfo.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
|
||||
imgInfo.imageView = fsr3OutputView;
|
||||
imgInfo.sampler = fxaa_.sceneSampler;
|
||||
VkWriteDescriptorSet write{};
|
||||
write.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
|
||||
write.dstSet = fxaa_.descSet;
|
||||
write.dstBinding = 0;
|
||||
write.descriptorCount = 1;
|
||||
write.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
|
||||
write.pImageInfo = &imgInfo;
|
||||
vkUpdateDescriptorSets(vkCtx->getDevice(), 1, &write, 0, nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
// Begin swapchain render pass at full resolution for sharpening + ImGui
|
||||
VkRenderPassBeginInfo rpInfo{};
|
||||
rpInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO;
|
||||
|
|
@ -1237,8 +1294,33 @@ void Renderer::endFrame() {
|
|||
sc.extent = ext;
|
||||
vkCmdSetScissor(currentCmd, 0, 1, &sc);
|
||||
|
||||
// Draw RCAS sharpening from accumulated history buffer
|
||||
renderFSR2Sharpen();
|
||||
// When FXAA is also enabled: apply FXAA on the FSR3 temporal output instead
|
||||
// of RCAS sharpening. FXAA descriptor is temporarily pointed to the FSR3
|
||||
// history buffer (which is already in SHADER_READ_ONLY_OPTIMAL). This gives
|
||||
// FSR3 temporal stability + FXAA spatial edge smoothing ("ultra quality native").
|
||||
if (fxaa_.enabled && fxaa_.pipeline && fxaa_.descSet) {
|
||||
renderFXAAPass();
|
||||
} else {
|
||||
// Draw RCAS sharpening from accumulated history buffer
|
||||
renderFSR2Sharpen();
|
||||
}
|
||||
|
||||
// Restore FXAA descriptor to its normal scene color source so standalone
|
||||
// FXAA frames are not affected by the FSR3 history pointer set above.
|
||||
if (fxaa_.enabled && fxaa_.descSet && fxaa_.sceneSampler && fxaa_.sceneColor.imageView) {
|
||||
VkDescriptorImageInfo restoreInfo{};
|
||||
restoreInfo.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
|
||||
restoreInfo.imageView = fxaa_.sceneColor.imageView;
|
||||
restoreInfo.sampler = fxaa_.sceneSampler;
|
||||
VkWriteDescriptorSet restoreWrite{};
|
||||
restoreWrite.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
|
||||
restoreWrite.dstSet = fxaa_.descSet;
|
||||
restoreWrite.dstBinding = 0;
|
||||
restoreWrite.descriptorCount = 1;
|
||||
restoreWrite.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
|
||||
restoreWrite.pImageInfo = &restoreInfo;
|
||||
vkUpdateDescriptorSets(vkCtx->getDevice(), 1, &restoreWrite, 0, nullptr);
|
||||
}
|
||||
|
||||
// Maintain frame bookkeeping
|
||||
fsr2_.prevViewProjection = camera->getViewProjectionMatrix();
|
||||
|
|
@ -1249,43 +1331,33 @@ void Renderer::endFrame() {
|
|||
}
|
||||
fsr2_.frameIndex = (fsr2_.frameIndex + 1) % 256; // Wrap to keep Halton values well-distributed
|
||||
|
||||
} else if (fsr_.enabled && fsr_.sceneFramebuffer) {
|
||||
} else if (fxaa_.enabled && fxaa_.sceneFramebuffer) {
|
||||
// End the off-screen scene render pass
|
||||
vkCmdEndRenderPass(currentCmd);
|
||||
|
||||
// Transition scene color (1x resolve/color target): PRESENT_SRC_KHR → SHADER_READ_ONLY
|
||||
// The render pass finalLayout puts the resolve/color attachment in PRESENT_SRC_KHR
|
||||
transitionImageLayout(currentCmd, fsr_.sceneColor.image,
|
||||
// Transition resolved scene color: PRESENT_SRC_KHR → SHADER_READ_ONLY
|
||||
transitionImageLayout(currentCmd, fxaa_.sceneColor.image,
|
||||
VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,
|
||||
VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT,
|
||||
VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT);
|
||||
|
||||
// Begin swapchain render pass at full resolution
|
||||
// Begin swapchain render pass (1x — no MSAA on the output pass)
|
||||
VkRenderPassBeginInfo rpInfo{};
|
||||
rpInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO;
|
||||
rpInfo.renderPass = vkCtx->getImGuiRenderPass();
|
||||
rpInfo.framebuffer = vkCtx->getSwapchainFramebuffers()[currentImageIndex];
|
||||
rpInfo.renderArea.offset = {0, 0};
|
||||
rpInfo.renderArea.extent = vkCtx->getSwapchainExtent();
|
||||
|
||||
// Clear values must match the render pass attachment count
|
||||
bool msaaOn = (vkCtx->getMsaaSamples() > VK_SAMPLE_COUNT_1_BIT);
|
||||
VkClearValue clearValues[4]{};
|
||||
clearValues[0].color = {{0.0f, 0.0f, 0.0f, 1.0f}};
|
||||
clearValues[1].depthStencil = {1.0f, 0};
|
||||
clearValues[2].color = {{0.0f, 0.0f, 0.0f, 1.0f}};
|
||||
clearValues[3].depthStencil = {1.0f, 0};
|
||||
if (msaaOn) {
|
||||
bool depthRes = (vkCtx->getDepthResolveImageView() != VK_NULL_HANDLE);
|
||||
rpInfo.clearValueCount = depthRes ? 4 : 3;
|
||||
} else {
|
||||
rpInfo.clearValueCount = 2;
|
||||
}
|
||||
rpInfo.pClearValues = clearValues;
|
||||
// The swapchain render pass always has 2 attachments when MSAA is off;
|
||||
// FXAA output goes to the non-MSAA swapchain directly.
|
||||
VkClearValue fxaaClear[2]{};
|
||||
fxaaClear[0].color = {{0.0f, 0.0f, 0.0f, 1.0f}};
|
||||
fxaaClear[1].depthStencil = {1.0f, 0};
|
||||
rpInfo.clearValueCount = 2;
|
||||
rpInfo.pClearValues = fxaaClear;
|
||||
|
||||
vkCmdBeginRenderPass(currentCmd, &rpInfo, VK_SUBPASS_CONTENTS_INLINE);
|
||||
|
||||
// Set full-resolution viewport and scissor
|
||||
VkExtent2D ext = vkCtx->getSwapchainExtent();
|
||||
VkViewport vp{};
|
||||
vp.width = static_cast<float>(ext.width);
|
||||
|
|
@ -1296,12 +1368,60 @@ void Renderer::endFrame() {
|
|||
sc.extent = ext;
|
||||
vkCmdSetScissor(currentCmd, 0, 1, &sc);
|
||||
|
||||
// Draw FSR upscale fullscreen quad
|
||||
// Draw FXAA pass
|
||||
renderFXAAPass();
|
||||
|
||||
} else if (fsr_.enabled && fsr_.sceneFramebuffer) {
|
||||
// FSR1 upscale path — only runs when FXAA is not active.
|
||||
// When both FSR1 and FXAA are enabled, FXAA took priority above.
|
||||
vkCmdEndRenderPass(currentCmd);
|
||||
|
||||
// Transition scene color (1x resolve/color target): PRESENT_SRC_KHR → SHADER_READ_ONLY
|
||||
transitionImageLayout(currentCmd, fsr_.sceneColor.image,
|
||||
VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,
|
||||
VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT,
|
||||
VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT);
|
||||
|
||||
// Begin swapchain render pass at full resolution
|
||||
VkRenderPassBeginInfo fsrRpInfo{};
|
||||
fsrRpInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO;
|
||||
fsrRpInfo.renderPass = vkCtx->getImGuiRenderPass();
|
||||
fsrRpInfo.framebuffer = vkCtx->getSwapchainFramebuffers()[currentImageIndex];
|
||||
fsrRpInfo.renderArea.offset = {0, 0};
|
||||
fsrRpInfo.renderArea.extent = vkCtx->getSwapchainExtent();
|
||||
|
||||
bool fsrMsaaOn = (vkCtx->getMsaaSamples() > VK_SAMPLE_COUNT_1_BIT);
|
||||
VkClearValue fsrClearValues[4]{};
|
||||
fsrClearValues[0].color = {{0.0f, 0.0f, 0.0f, 1.0f}};
|
||||
fsrClearValues[1].depthStencil = {1.0f, 0};
|
||||
fsrClearValues[2].color = {{0.0f, 0.0f, 0.0f, 1.0f}};
|
||||
fsrClearValues[3].depthStencil = {1.0f, 0};
|
||||
if (fsrMsaaOn) {
|
||||
bool depthRes = (vkCtx->getDepthResolveImageView() != VK_NULL_HANDLE);
|
||||
fsrRpInfo.clearValueCount = depthRes ? 4 : 3;
|
||||
} else {
|
||||
fsrRpInfo.clearValueCount = 2;
|
||||
}
|
||||
fsrRpInfo.pClearValues = fsrClearValues;
|
||||
|
||||
vkCmdBeginRenderPass(currentCmd, &fsrRpInfo, VK_SUBPASS_CONTENTS_INLINE);
|
||||
|
||||
VkExtent2D fsrExt = vkCtx->getSwapchainExtent();
|
||||
VkViewport fsrVp{};
|
||||
fsrVp.width = static_cast<float>(fsrExt.width);
|
||||
fsrVp.height = static_cast<float>(fsrExt.height);
|
||||
fsrVp.maxDepth = 1.0f;
|
||||
vkCmdSetViewport(currentCmd, 0, 1, &fsrVp);
|
||||
VkRect2D fsrSc{};
|
||||
fsrSc.extent = fsrExt;
|
||||
vkCmdSetScissor(currentCmd, 0, 1, &fsrSc);
|
||||
|
||||
renderFSRUpscale();
|
||||
}
|
||||
|
||||
// ImGui rendering — must respect subpass contents mode
|
||||
if (!fsr_.enabled && !fsr2_.enabled && parallelRecordingEnabled_) {
|
||||
// Parallel recording only applies when no post-process pass is active.
|
||||
if (!fsr_.enabled && !fsr2_.enabled && !fxaa_.enabled && parallelRecordingEnabled_) {
|
||||
// Scene pass was begun with VK_SUBPASS_CONTENTS_SECONDARY_COMMAND_BUFFERS,
|
||||
// so ImGui must be recorded into a secondary command buffer.
|
||||
VkCommandBuffer imguiCmd = beginSecondary(SEC_IMGUI);
|
||||
|
|
@ -4698,6 +4818,247 @@ void Renderer::setAmdFsr3FramegenEnabled(bool enabled) {
|
|||
|
||||
// ========================= End FSR 2.2 =========================
|
||||
|
||||
// ========================= FXAA Post-Process =========================
|
||||
|
||||
bool Renderer::initFXAAResources() {
|
||||
if (!vkCtx) return false;
|
||||
|
||||
VkDevice device = vkCtx->getDevice();
|
||||
VmaAllocator alloc = vkCtx->getAllocator();
|
||||
VkExtent2D ext = vkCtx->getSwapchainExtent();
|
||||
VkSampleCountFlagBits msaa = vkCtx->getMsaaSamples();
|
||||
bool useMsaa = (msaa > VK_SAMPLE_COUNT_1_BIT);
|
||||
bool useDepthResolve = (vkCtx->getDepthResolveImageView() != VK_NULL_HANDLE);
|
||||
|
||||
LOG_INFO("FXAA: initializing at ", ext.width, "x", ext.height,
|
||||
" (MSAA=", static_cast<int>(msaa), "x)");
|
||||
|
||||
VkFormat colorFmt = vkCtx->getSwapchainFormat();
|
||||
VkFormat depthFmt = vkCtx->getDepthFormat();
|
||||
|
||||
// sceneColor: 1x resolved color target — FXAA reads from here
|
||||
fxaa_.sceneColor = createImage(device, alloc, ext.width, ext.height,
|
||||
colorFmt, VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT);
|
||||
if (!fxaa_.sceneColor.image) {
|
||||
LOG_ERROR("FXAA: failed to create scene color image");
|
||||
return false;
|
||||
}
|
||||
|
||||
// sceneDepth: depth buffer at current MSAA sample count
|
||||
fxaa_.sceneDepth = createImage(device, alloc, ext.width, ext.height,
|
||||
depthFmt, VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT, msaa);
|
||||
if (!fxaa_.sceneDepth.image) {
|
||||
LOG_ERROR("FXAA: failed to create scene depth image");
|
||||
destroyFXAAResources();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (useMsaa) {
|
||||
fxaa_.sceneMsaaColor = createImage(device, alloc, ext.width, ext.height,
|
||||
colorFmt, VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT, msaa);
|
||||
if (!fxaa_.sceneMsaaColor.image) {
|
||||
LOG_ERROR("FXAA: failed to create MSAA color image");
|
||||
destroyFXAAResources();
|
||||
return false;
|
||||
}
|
||||
if (useDepthResolve) {
|
||||
fxaa_.sceneDepthResolve = createImage(device, alloc, ext.width, ext.height,
|
||||
depthFmt, VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT);
|
||||
if (!fxaa_.sceneDepthResolve.image) {
|
||||
LOG_ERROR("FXAA: failed to create depth resolve image");
|
||||
destroyFXAAResources();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Framebuffer — same attachment layout as main render pass
|
||||
VkImageView fbAttachments[4]{};
|
||||
uint32_t fbCount;
|
||||
if (useMsaa) {
|
||||
fbAttachments[0] = fxaa_.sceneMsaaColor.imageView;
|
||||
fbAttachments[1] = fxaa_.sceneDepth.imageView;
|
||||
fbAttachments[2] = fxaa_.sceneColor.imageView; // resolve target
|
||||
fbCount = 3;
|
||||
if (useDepthResolve) {
|
||||
fbAttachments[3] = fxaa_.sceneDepthResolve.imageView;
|
||||
fbCount = 4;
|
||||
}
|
||||
} else {
|
||||
fbAttachments[0] = fxaa_.sceneColor.imageView;
|
||||
fbAttachments[1] = fxaa_.sceneDepth.imageView;
|
||||
fbCount = 2;
|
||||
}
|
||||
|
||||
VkFramebufferCreateInfo fbInfo{};
|
||||
fbInfo.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO;
|
||||
fbInfo.renderPass = vkCtx->getImGuiRenderPass();
|
||||
fbInfo.attachmentCount = fbCount;
|
||||
fbInfo.pAttachments = fbAttachments;
|
||||
fbInfo.width = ext.width;
|
||||
fbInfo.height = ext.height;
|
||||
fbInfo.layers = 1;
|
||||
if (vkCreateFramebuffer(device, &fbInfo, nullptr, &fxaa_.sceneFramebuffer) != VK_SUCCESS) {
|
||||
LOG_ERROR("FXAA: failed to create scene framebuffer");
|
||||
destroyFXAAResources();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Sampler
|
||||
VkSamplerCreateInfo samplerInfo{};
|
||||
samplerInfo.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO;
|
||||
samplerInfo.minFilter = VK_FILTER_LINEAR;
|
||||
samplerInfo.magFilter = VK_FILTER_LINEAR;
|
||||
samplerInfo.addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
|
||||
samplerInfo.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
|
||||
samplerInfo.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
|
||||
samplerInfo.mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR;
|
||||
if (vkCreateSampler(device, &samplerInfo, nullptr, &fxaa_.sceneSampler) != VK_SUCCESS) {
|
||||
LOG_ERROR("FXAA: failed to create sampler");
|
||||
destroyFXAAResources();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Descriptor set layout: binding 0 = combined image sampler
|
||||
VkDescriptorSetLayoutBinding binding{};
|
||||
binding.binding = 0;
|
||||
binding.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
|
||||
binding.descriptorCount = 1;
|
||||
binding.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT;
|
||||
VkDescriptorSetLayoutCreateInfo layoutInfo{};
|
||||
layoutInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO;
|
||||
layoutInfo.bindingCount = 1;
|
||||
layoutInfo.pBindings = &binding;
|
||||
vkCreateDescriptorSetLayout(device, &layoutInfo, nullptr, &fxaa_.descSetLayout);
|
||||
|
||||
VkDescriptorPoolSize poolSize{};
|
||||
poolSize.type = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
|
||||
poolSize.descriptorCount = 1;
|
||||
VkDescriptorPoolCreateInfo poolInfo{};
|
||||
poolInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO;
|
||||
poolInfo.maxSets = 1;
|
||||
poolInfo.poolSizeCount = 1;
|
||||
poolInfo.pPoolSizes = &poolSize;
|
||||
vkCreateDescriptorPool(device, &poolInfo, nullptr, &fxaa_.descPool);
|
||||
|
||||
VkDescriptorSetAllocateInfo dsAllocInfo{};
|
||||
dsAllocInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO;
|
||||
dsAllocInfo.descriptorPool = fxaa_.descPool;
|
||||
dsAllocInfo.descriptorSetCount = 1;
|
||||
dsAllocInfo.pSetLayouts = &fxaa_.descSetLayout;
|
||||
vkAllocateDescriptorSets(device, &dsAllocInfo, &fxaa_.descSet);
|
||||
|
||||
// Bind the resolved 1x sceneColor
|
||||
VkDescriptorImageInfo imgInfo{};
|
||||
imgInfo.sampler = fxaa_.sceneSampler;
|
||||
imgInfo.imageView = fxaa_.sceneColor.imageView;
|
||||
imgInfo.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
|
||||
VkWriteDescriptorSet write{};
|
||||
write.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
|
||||
write.dstSet = fxaa_.descSet;
|
||||
write.dstBinding = 0;
|
||||
write.descriptorCount = 1;
|
||||
write.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
|
||||
write.pImageInfo = &imgInfo;
|
||||
vkUpdateDescriptorSets(device, 1, &write, 0, nullptr);
|
||||
|
||||
// Pipeline layout — push constant holds vec2 rcpFrame
|
||||
VkPushConstantRange pc{};
|
||||
pc.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT;
|
||||
pc.offset = 0;
|
||||
pc.size = 8; // vec2
|
||||
VkPipelineLayoutCreateInfo plCI{};
|
||||
plCI.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
|
||||
plCI.setLayoutCount = 1;
|
||||
plCI.pSetLayouts = &fxaa_.descSetLayout;
|
||||
plCI.pushConstantRangeCount = 1;
|
||||
plCI.pPushConstantRanges = &pc;
|
||||
vkCreatePipelineLayout(device, &plCI, nullptr, &fxaa_.pipelineLayout);
|
||||
|
||||
// FXAA pipeline — fullscreen triangle into the swapchain render pass
|
||||
// Uses VK_SAMPLE_COUNT_1_BIT: it always runs after MSAA resolve.
|
||||
VkShaderModule vertMod, fragMod;
|
||||
if (!vertMod.loadFromFile(device, "assets/shaders/postprocess.vert.spv") ||
|
||||
!fragMod.loadFromFile(device, "assets/shaders/fxaa.frag.spv")) {
|
||||
LOG_ERROR("FXAA: failed to load shaders");
|
||||
destroyFXAAResources();
|
||||
return false;
|
||||
}
|
||||
|
||||
fxaa_.pipeline = PipelineBuilder()
|
||||
.setShaders(vertMod.stageInfo(VK_SHADER_STAGE_VERTEX_BIT),
|
||||
fragMod.stageInfo(VK_SHADER_STAGE_FRAGMENT_BIT))
|
||||
.setVertexInput({}, {})
|
||||
.setTopology(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST)
|
||||
.setRasterization(VK_POLYGON_MODE_FILL, VK_CULL_MODE_NONE)
|
||||
.setNoDepthTest()
|
||||
.setColorBlendAttachment(PipelineBuilder::blendDisabled())
|
||||
.setMultisample(VK_SAMPLE_COUNT_1_BIT) // swapchain pass is always 1x
|
||||
.setLayout(fxaa_.pipelineLayout)
|
||||
.setRenderPass(vkCtx->getImGuiRenderPass())
|
||||
.setDynamicStates({VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR})
|
||||
.build(device);
|
||||
|
||||
vertMod.destroy();
|
||||
fragMod.destroy();
|
||||
|
||||
if (!fxaa_.pipeline) {
|
||||
LOG_ERROR("FXAA: failed to create pipeline");
|
||||
destroyFXAAResources();
|
||||
return false;
|
||||
}
|
||||
|
||||
LOG_INFO("FXAA: initialized successfully");
|
||||
return true;
|
||||
}
|
||||
|
||||
void Renderer::destroyFXAAResources() {
|
||||
if (!vkCtx) return;
|
||||
VkDevice device = vkCtx->getDevice();
|
||||
VmaAllocator alloc = vkCtx->getAllocator();
|
||||
vkDeviceWaitIdle(device);
|
||||
|
||||
if (fxaa_.pipeline) { vkDestroyPipeline(device, fxaa_.pipeline, nullptr); fxaa_.pipeline = VK_NULL_HANDLE; }
|
||||
if (fxaa_.pipelineLayout) { vkDestroyPipelineLayout(device, fxaa_.pipelineLayout, nullptr); fxaa_.pipelineLayout = VK_NULL_HANDLE; }
|
||||
if (fxaa_.descPool) { vkDestroyDescriptorPool(device, fxaa_.descPool, nullptr); fxaa_.descPool = VK_NULL_HANDLE; fxaa_.descSet = VK_NULL_HANDLE; }
|
||||
if (fxaa_.descSetLayout) { vkDestroyDescriptorSetLayout(device, fxaa_.descSetLayout, nullptr); fxaa_.descSetLayout = VK_NULL_HANDLE; }
|
||||
if (fxaa_.sceneFramebuffer) { vkDestroyFramebuffer(device, fxaa_.sceneFramebuffer, nullptr); fxaa_.sceneFramebuffer = VK_NULL_HANDLE; }
|
||||
if (fxaa_.sceneSampler) { vkDestroySampler(device, fxaa_.sceneSampler, nullptr); fxaa_.sceneSampler = VK_NULL_HANDLE; }
|
||||
destroyImage(device, alloc, fxaa_.sceneDepthResolve);
|
||||
destroyImage(device, alloc, fxaa_.sceneMsaaColor);
|
||||
destroyImage(device, alloc, fxaa_.sceneDepth);
|
||||
destroyImage(device, alloc, fxaa_.sceneColor);
|
||||
}
|
||||
|
||||
void Renderer::renderFXAAPass() {
|
||||
if (!fxaa_.pipeline || currentCmd == VK_NULL_HANDLE) return;
|
||||
VkExtent2D ext = vkCtx->getSwapchainExtent();
|
||||
|
||||
vkCmdBindPipeline(currentCmd, VK_PIPELINE_BIND_POINT_GRAPHICS, fxaa_.pipeline);
|
||||
vkCmdBindDescriptorSets(currentCmd, VK_PIPELINE_BIND_POINT_GRAPHICS,
|
||||
fxaa_.pipelineLayout, 0, 1, &fxaa_.descSet, 0, nullptr);
|
||||
|
||||
// Push rcpFrame = vec2(1/width, 1/height)
|
||||
float rcpFrame[2] = {
|
||||
1.0f / static_cast<float>(ext.width),
|
||||
1.0f / static_cast<float>(ext.height)
|
||||
};
|
||||
vkCmdPushConstants(currentCmd, fxaa_.pipelineLayout,
|
||||
VK_SHADER_STAGE_FRAGMENT_BIT, 0, 8, rcpFrame);
|
||||
|
||||
vkCmdDraw(currentCmd, 3, 1, 0, 0); // fullscreen triangle
|
||||
}
|
||||
|
||||
void Renderer::setFXAAEnabled(bool enabled) {
|
||||
if (fxaa_.enabled == enabled) return;
|
||||
fxaa_.enabled = enabled;
|
||||
if (!enabled) {
|
||||
fxaa_.needsRecreate = true; // defer destruction to next beginFrame()
|
||||
}
|
||||
}
|
||||
|
||||
// ========================= End FXAA =========================
|
||||
|
||||
void Renderer::renderWorld(game::World* world, game::GameHandler* gameHandler) {
|
||||
(void)world;
|
||||
|
||||
|
|
|
|||
|
|
@ -835,7 +835,12 @@ void WMORenderer::cleanupUnusedModels() {
|
|||
}
|
||||
}
|
||||
|
||||
// Delete GPU resources and remove from map
|
||||
// Delete GPU resources and remove from map.
|
||||
// Ensure all in-flight frames are complete before freeing vertex/index buffers —
|
||||
// the GPU may still be reading them from the previous frame's command buffer.
|
||||
if (!toRemove.empty() && vkCtx_) {
|
||||
vkDeviceWaitIdle(vkCtx_->getDevice());
|
||||
}
|
||||
for (uint32_t id : toRemove) {
|
||||
unloadModel(id);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1017,6 +1017,33 @@ void WorldMap::renderImGuiOverlay(const glm::vec3& playerRenderPos, int screenWi
|
|||
}
|
||||
}
|
||||
|
||||
// Party member dots
|
||||
if (currentIdx >= 0 && viewLevel != ViewLevel::WORLD) {
|
||||
ImFont* font = ImGui::GetFont();
|
||||
for (const auto& dot : partyDots_) {
|
||||
glm::vec2 uv = renderPosToMapUV(dot.renderPos, currentIdx);
|
||||
if (uv.x < 0.0f || uv.x > 1.0f || uv.y < 0.0f || uv.y > 1.0f) continue;
|
||||
float px = imgMin.x + uv.x * displayW;
|
||||
float py = imgMin.y + uv.y * displayH;
|
||||
drawList->AddCircleFilled(ImVec2(px, py), 5.0f, dot.color);
|
||||
drawList->AddCircle(ImVec2(px, py), 5.0f, IM_COL32(0, 0, 0, 200), 0, 1.5f);
|
||||
// Name tooltip on hover
|
||||
if (!dot.name.empty()) {
|
||||
ImVec2 mp = ImGui::GetMousePos();
|
||||
float dx = mp.x - px, dy = mp.y - py;
|
||||
if (dx * dx + dy * dy <= 49.0f) { // radius 7 px hit area
|
||||
ImGui::SetTooltip("%s", dot.name.c_str());
|
||||
}
|
||||
// Draw name label above the dot
|
||||
ImVec2 nameSz = font->CalcTextSizeA(ImGui::GetFontSize(), FLT_MAX, 0.0f, dot.name.c_str());
|
||||
float tx = px - nameSz.x * 0.5f;
|
||||
float ty = py - nameSz.y - 7.0f;
|
||||
drawList->AddText(ImVec2(tx + 1.0f, ty + 1.0f), IM_COL32(0, 0, 0, 180), dot.name.c_str());
|
||||
drawList->AddText(ImVec2(tx, ty), IM_COL32(255, 255, 255, 220), dot.name.c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Hover coordinate display — show WoW coordinates under cursor
|
||||
if (currentIdx >= 0 && viewLevel != ViewLevel::WORLD) {
|
||||
auto& io = ImGui::GetIO();
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1159,7 +1159,9 @@ void InventoryScreen::renderCharacterScreen(game::GameHandler& gameHandler) {
|
|||
int32_t stats[5];
|
||||
for (int i = 0; i < 5; ++i) stats[i] = gameHandler.getPlayerStat(i);
|
||||
const int32_t* serverStats = (stats[0] >= 0) ? stats : nullptr;
|
||||
renderStatsPanel(inventory, gameHandler.getPlayerLevel(), gameHandler.getArmorRating(), serverStats);
|
||||
int32_t resists[6];
|
||||
for (int i = 0; i < 6; ++i) resists[i] = gameHandler.getResistance(i + 1);
|
||||
renderStatsPanel(inventory, gameHandler.getPlayerLevel(), gameHandler.getArmorRating(), serverStats, resists);
|
||||
|
||||
// Played time (shown if available, fetched on character screen open)
|
||||
uint32_t totalSec = gameHandler.getTotalTimePlayed();
|
||||
|
|
@ -1340,6 +1342,34 @@ void InventoryScreen::renderCharacterScreen(game::GameHandler& gameHandler) {
|
|||
ImGui::EndTabItem();
|
||||
}
|
||||
|
||||
// Equipment Sets tab (WotLK only)
|
||||
const auto& eqSets = gameHandler.getEquipmentSets();
|
||||
if (!eqSets.empty()) {
|
||||
if (ImGui::BeginTabItem("Outfits")) {
|
||||
ImGui::Spacing();
|
||||
ImGui::TextDisabled("Saved Equipment Sets");
|
||||
ImGui::Separator();
|
||||
ImGui::BeginChild("##EqSetsList", ImVec2(0, 0), false);
|
||||
for (const auto& es : eqSets) {
|
||||
ImGui::PushID(static_cast<int>(es.setId));
|
||||
// Icon placeholder or name
|
||||
const char* displayName = es.name.empty() ? "(Unnamed)" : es.name.c_str();
|
||||
ImGui::Text("%s", displayName);
|
||||
if (!es.iconName.empty()) {
|
||||
ImGui::SameLine();
|
||||
ImGui::TextDisabled("(%s)", es.iconName.c_str());
|
||||
}
|
||||
ImGui::SameLine(ImGui::GetContentRegionAvail().x - 60.0f);
|
||||
if (ImGui::SmallButton("Equip")) {
|
||||
gameHandler.useEquipmentSet(es.setId);
|
||||
}
|
||||
ImGui::PopID();
|
||||
}
|
||||
ImGui::EndChild();
|
||||
ImGui::EndTabItem();
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::EndTabBar();
|
||||
}
|
||||
|
||||
|
|
@ -1557,7 +1587,8 @@ void InventoryScreen::renderEquipmentPanel(game::Inventory& inventory) {
|
|||
// ============================================================
|
||||
|
||||
void InventoryScreen::renderStatsPanel(game::Inventory& inventory, uint32_t playerLevel,
|
||||
int32_t serverArmor, const int32_t* serverStats) {
|
||||
int32_t serverArmor, const int32_t* serverStats,
|
||||
const int32_t* serverResists) {
|
||||
// Sum equipment stats for item-query bonus display
|
||||
int32_t itemStr = 0, itemAgi = 0, itemSta = 0, itemInt = 0, itemSpi = 0;
|
||||
// Secondary stat sums from extraStats
|
||||
|
|
@ -1596,6 +1627,28 @@ void InventoryScreen::renderStatsPanel(game::Inventory& inventory, uint32_t play
|
|||
}
|
||||
int32_t totalArmor = (serverArmor > 0) ? serverArmor : itemQueryArmor;
|
||||
|
||||
// Average item level (exclude shirt/tabard as WoW convention)
|
||||
{
|
||||
uint32_t iLvlSum = 0;
|
||||
int iLvlCount = 0;
|
||||
for (int s = 0; s < game::Inventory::NUM_EQUIP_SLOTS; s++) {
|
||||
auto eslot = static_cast<game::EquipSlot>(s);
|
||||
if (eslot == game::EquipSlot::SHIRT || eslot == game::EquipSlot::TABARD) continue;
|
||||
const auto& slot = inventory.getEquipSlot(eslot);
|
||||
if (!slot.empty() && slot.item.itemLevel > 0) {
|
||||
iLvlSum += slot.item.itemLevel;
|
||||
++iLvlCount;
|
||||
}
|
||||
}
|
||||
if (iLvlCount > 0) {
|
||||
float avg = static_cast<float>(iLvlSum) / static_cast<float>(iLvlCount);
|
||||
ImGui::TextColored(ImVec4(0.7f, 0.9f, 1.0f, 1.0f),
|
||||
"Average Item Level: %.1f (%d/%d slots)", avg, iLvlCount,
|
||||
game::Inventory::NUM_EQUIP_SLOTS - 2); // -2 for shirt/tabard
|
||||
}
|
||||
ImGui::Separator();
|
||||
}
|
||||
|
||||
ImVec4 green(0.0f, 1.0f, 0.0f, 1.0f);
|
||||
ImVec4 white(1.0f, 1.0f, 1.0f, 1.0f);
|
||||
ImVec4 gold(1.0f, 0.84f, 0.0f, 1.0f);
|
||||
|
|
@ -1665,6 +1718,28 @@ void InventoryScreen::renderStatsPanel(game::Inventory& inventory, uint32_t play
|
|||
renderSecondary("Mana per 5 sec", itemMp5);
|
||||
renderSecondary("Health per 5 sec",itemHp5);
|
||||
}
|
||||
|
||||
// Elemental resistances from server update fields
|
||||
if (serverResists) {
|
||||
static const char* kResistNames[6] = {
|
||||
"Holy Resistance", "Fire Resistance", "Nature Resistance",
|
||||
"Frost Resistance", "Shadow Resistance", "Arcane Resistance"
|
||||
};
|
||||
bool hasResist = false;
|
||||
for (int i = 0; i < 6; ++i) {
|
||||
if (serverResists[i] > 0) { hasResist = true; break; }
|
||||
}
|
||||
if (hasResist) {
|
||||
ImGui::Spacing();
|
||||
ImGui::Separator();
|
||||
for (int i = 0; i < 6; ++i) {
|
||||
if (serverResists[i] > 0) {
|
||||
ImGui::TextColored(ImVec4(0.7f, 0.85f, 1.0f, 1.0f),
|
||||
"%s: %d", kResistNames[i], serverResists[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void InventoryScreen::renderBackpackPanel(game::Inventory& inventory, bool collapseEmptySections) {
|
||||
|
|
@ -2186,10 +2261,13 @@ void InventoryScreen::renderItemTooltip(const game::ItemDef& item, const game::I
|
|||
default: break;
|
||||
}
|
||||
if (!trigger) continue;
|
||||
const std::string& spName = gameHandler_->getSpellName(sp.spellId);
|
||||
if (!spName.empty()) {
|
||||
const std::string& spDesc = gameHandler_->getSpellDescription(sp.spellId);
|
||||
const std::string& spText = spDesc.empty() ? gameHandler_->getSpellName(sp.spellId) : spDesc;
|
||||
if (!spText.empty()) {
|
||||
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + 320.0f);
|
||||
ImGui::TextColored(ImVec4(0.0f, 0.8f, 1.0f, 1.0f),
|
||||
"%s: %s", trigger, spName.c_str());
|
||||
"%s: %s", trigger, spText.c_str());
|
||||
ImGui::PopTextWrapPos();
|
||||
} else {
|
||||
ImGui::TextColored(ImVec4(0.0f, 0.8f, 1.0f, 1.0f),
|
||||
"%s: Spell #%u", trigger, sp.spellId);
|
||||
|
|
@ -2474,11 +2552,161 @@ void InventoryScreen::renderItemTooltip(const game::ItemQueryResponseData& info,
|
|||
}
|
||||
if (!trigger) continue;
|
||||
if (gameHandler_) {
|
||||
const std::string& spName = gameHandler_->getSpellName(sp.spellId);
|
||||
if (!spName.empty())
|
||||
// Prefer the spell's tooltip text (the actual effect description).
|
||||
// Fall back to the spell name if the description is empty.
|
||||
const std::string& spDesc = gameHandler_->getSpellDescription(sp.spellId);
|
||||
const std::string& spName = spDesc.empty() ? gameHandler_->getSpellName(sp.spellId) : spDesc;
|
||||
if (!spName.empty()) {
|
||||
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + 320.0f);
|
||||
ImGui::TextColored(ImVec4(0.0f, 0.8f, 1.0f, 1.0f), "%s: %s", trigger, spName.c_str());
|
||||
else
|
||||
ImGui::PopTextWrapPos();
|
||||
} else {
|
||||
ImGui::TextColored(ImVec4(0.0f, 0.8f, 1.0f, 1.0f), "%s: Spell #%u", trigger, sp.spellId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Gem socket slots
|
||||
{
|
||||
static const struct { uint32_t mask; const char* label; ImVec4 col; } kSocketTypes[] = {
|
||||
{ 1, "Meta Socket", { 0.7f, 0.7f, 0.9f, 1.0f } },
|
||||
{ 2, "Red Socket", { 1.0f, 0.3f, 0.3f, 1.0f } },
|
||||
{ 4, "Yellow Socket", { 1.0f, 0.9f, 0.3f, 1.0f } },
|
||||
{ 8, "Blue Socket", { 0.3f, 0.6f, 1.0f, 1.0f } },
|
||||
};
|
||||
bool hasSocket = false;
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
if (info.socketColor[i] == 0) continue;
|
||||
if (!hasSocket) { ImGui::Spacing(); hasSocket = true; }
|
||||
for (const auto& st : kSocketTypes) {
|
||||
if (info.socketColor[i] & st.mask) {
|
||||
ImGui::TextColored(st.col, "%s", st.label);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (hasSocket && info.socketBonus != 0) {
|
||||
// Socket bonus is a SpellItemEnchantment ID — look up via SpellItemEnchantment.dbc
|
||||
static std::unordered_map<uint32_t, std::string> s_enchantNames;
|
||||
static bool s_enchantNamesLoaded = false;
|
||||
if (!s_enchantNamesLoaded && assetManager_) {
|
||||
s_enchantNamesLoaded = true;
|
||||
auto dbc = assetManager_->loadDBC("SpellItemEnchantment.dbc");
|
||||
if (dbc && dbc->isLoaded()) {
|
||||
const auto* lay = pipeline::getActiveDBCLayout()
|
||||
? pipeline::getActiveDBCLayout()->getLayout("SpellItemEnchantment") : nullptr;
|
||||
uint32_t nameField = lay ? lay->field("Name") : 8u;
|
||||
if (nameField == 0xFFFFFFFF) nameField = 8;
|
||||
uint32_t fc = dbc->getFieldCount();
|
||||
for (uint32_t r = 0; r < dbc->getRecordCount(); ++r) {
|
||||
uint32_t eid = dbc->getUInt32(r, 0);
|
||||
if (eid == 0 || nameField >= fc) continue;
|
||||
std::string ename = dbc->getString(r, nameField);
|
||||
if (!ename.empty()) s_enchantNames[eid] = std::move(ename);
|
||||
}
|
||||
}
|
||||
}
|
||||
auto enchIt = s_enchantNames.find(info.socketBonus);
|
||||
if (enchIt != s_enchantNames.end())
|
||||
ImGui::TextColored(ImVec4(0.5f, 0.8f, 0.5f, 1.0f), "Socket Bonus: %s", enchIt->second.c_str());
|
||||
else
|
||||
ImGui::TextColored(ImVec4(0.5f, 0.8f, 0.5f, 1.0f), "Socket Bonus: (id %u)", info.socketBonus);
|
||||
}
|
||||
}
|
||||
|
||||
// Item set membership
|
||||
if (info.itemSetId != 0) {
|
||||
// Lazy-load full ItemSet.dbc data (name + item IDs + bonus spells/thresholds)
|
||||
struct SetEntry {
|
||||
std::string name;
|
||||
std::array<uint32_t, 10> itemIds{};
|
||||
std::array<uint32_t, 10> spellIds{};
|
||||
std::array<uint32_t, 10> thresholds{};
|
||||
};
|
||||
static std::unordered_map<uint32_t, SetEntry> s_setData;
|
||||
static bool s_setDataLoaded = false;
|
||||
if (!s_setDataLoaded && assetManager_) {
|
||||
s_setDataLoaded = true;
|
||||
auto dbc = assetManager_->loadDBC("ItemSet.dbc");
|
||||
if (dbc && dbc->isLoaded()) {
|
||||
const auto* layout = pipeline::getActiveDBCLayout()
|
||||
? pipeline::getActiveDBCLayout()->getLayout("ItemSet") : nullptr;
|
||||
auto lf = [&](const char* k, uint32_t def) -> uint32_t {
|
||||
return layout ? (*layout)[k] : def;
|
||||
};
|
||||
uint32_t idF = lf("ID", 0), nameF = lf("Name", 1);
|
||||
static const char* itemKeys[10] = {
|
||||
"Item0","Item1","Item2","Item3","Item4",
|
||||
"Item5","Item6","Item7","Item8","Item9"
|
||||
};
|
||||
static const char* spellKeys[10] = {
|
||||
"Spell0","Spell1","Spell2","Spell3","Spell4",
|
||||
"Spell5","Spell6","Spell7","Spell8","Spell9"
|
||||
};
|
||||
static const char* thrKeys[10] = {
|
||||
"Threshold0","Threshold1","Threshold2","Threshold3","Threshold4",
|
||||
"Threshold5","Threshold6","Threshold7","Threshold8","Threshold9"
|
||||
};
|
||||
uint32_t itemFallback[10], spellFallback[10], thrFallback[10];
|
||||
for (int i = 0; i < 10; ++i) {
|
||||
itemFallback[i] = 18 + i;
|
||||
spellFallback[i] = 28 + i;
|
||||
thrFallback[i] = 38 + i;
|
||||
}
|
||||
for (uint32_t r = 0; r < dbc->getRecordCount(); ++r) {
|
||||
uint32_t id = dbc->getUInt32(r, idF);
|
||||
if (!id) continue;
|
||||
SetEntry e;
|
||||
e.name = dbc->getString(r, nameF);
|
||||
for (int i = 0; i < 10; ++i) {
|
||||
e.itemIds[i] = dbc->getUInt32(r, layout ? (*layout)[itemKeys[i]] : itemFallback[i]);
|
||||
e.spellIds[i] = dbc->getUInt32(r, layout ? (*layout)[spellKeys[i]] : spellFallback[i]);
|
||||
e.thresholds[i] = dbc->getUInt32(r, layout ? (*layout)[thrKeys[i]] : thrFallback[i]);
|
||||
}
|
||||
s_setData[id] = std::move(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto setIt = s_setData.find(info.itemSetId);
|
||||
ImGui::Spacing();
|
||||
if (setIt != s_setData.end()) {
|
||||
const SetEntry& se = setIt->second;
|
||||
// Count equipped pieces
|
||||
int equipped = 0, total = 0;
|
||||
for (int i = 0; i < 10; ++i) {
|
||||
if (se.itemIds[i] == 0) continue;
|
||||
++total;
|
||||
if (inventory) {
|
||||
for (int s = 0; s < game::Inventory::NUM_EQUIP_SLOTS; s++) {
|
||||
const auto& eSlot = inventory->getEquipSlot(static_cast<game::EquipSlot>(s));
|
||||
if (!eSlot.empty() && eSlot.item.itemId == se.itemIds[i]) { ++equipped; break; }
|
||||
}
|
||||
}
|
||||
}
|
||||
if (total > 0) {
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.82f, 0.0f, 1.0f),
|
||||
"%s (%d/%d)", se.name.empty() ? "Set" : se.name.c_str(), equipped, total);
|
||||
} else {
|
||||
if (!se.name.empty())
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.82f, 0.0f, 1.0f), "%s", se.name.c_str());
|
||||
}
|
||||
// Show set bonuses: gray if not reached, green if active
|
||||
if (gameHandler_) {
|
||||
for (int i = 0; i < 10; ++i) {
|
||||
if (se.spellIds[i] == 0 || se.thresholds[i] == 0) continue;
|
||||
const std::string& bname = gameHandler_->getSpellName(se.spellIds[i]);
|
||||
bool active = (equipped >= static_cast<int>(se.thresholds[i]));
|
||||
ImVec4 col = active ? ImVec4(0.5f, 1.0f, 0.5f, 1.0f)
|
||||
: ImVec4(0.55f, 0.55f, 0.55f, 1.0f);
|
||||
if (!bname.empty())
|
||||
ImGui::TextColored(col, "(%u) %s", se.thresholds[i], bname.c_str());
|
||||
else
|
||||
ImGui::TextColored(col, "(%u) Set Bonus", se.thresholds[i]);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.82f, 0.0f, 1.0f), "Set (id %u)", info.itemSetId);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -485,12 +485,28 @@ void QuestLogScreen::render(game::GameHandler& gameHandler, InventoryScreen& inv
|
|||
auto reqIt = sel.requiredItemCounts.find(itemId);
|
||||
if (reqIt != sel.requiredItemCounts.end()) required = reqIt->second;
|
||||
VkDescriptorSet iconTex = dispId ? invScreen.getItemIcon(dispId) : VK_NULL_HANDLE;
|
||||
const auto* objInfo = gameHandler.getItemInfo(itemId);
|
||||
if (iconTex) {
|
||||
ImGui::Image((ImTextureID)(uintptr_t)iconTex, ImVec2(14, 14));
|
||||
if (objInfo && objInfo->valid && ImGui::IsItemHovered()) {
|
||||
ImGui::BeginTooltip();
|
||||
invScreen.renderItemTooltip(*objInfo);
|
||||
ImGui::EndTooltip();
|
||||
}
|
||||
ImGui::SameLine();
|
||||
ImGui::Text("%s: %u/%u", itemLabel.c_str(), count, required);
|
||||
if (objInfo && objInfo->valid && ImGui::IsItemHovered()) {
|
||||
ImGui::BeginTooltip();
|
||||
invScreen.renderItemTooltip(*objInfo);
|
||||
ImGui::EndTooltip();
|
||||
}
|
||||
} else {
|
||||
ImGui::BulletText("%s: %u/%u", itemLabel.c_str(), count, required);
|
||||
if (objInfo && objInfo->valid && ImGui::IsItemHovered()) {
|
||||
ImGui::BeginTooltip();
|
||||
invScreen.renderItemTooltip(*objInfo);
|
||||
ImGui::EndTooltip();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -534,6 +550,11 @@ void QuestLogScreen::render(game::GameHandler& gameHandler, InventoryScreen& inv
|
|||
ImGui::Text("%s x%u", name.c_str(), ri.count);
|
||||
else
|
||||
ImGui::Text("%s", name.c_str());
|
||||
if (info && info->valid && ImGui::IsItemHovered()) {
|
||||
ImGui::BeginTooltip();
|
||||
invScreen.renderItemTooltip(*info);
|
||||
ImGui::EndTooltip();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -560,6 +581,11 @@ void QuestLogScreen::render(game::GameHandler& gameHandler, InventoryScreen& inv
|
|||
ImGui::Text("%s x%u", name.c_str(), ri.count);
|
||||
else
|
||||
ImGui::Text("%s", name.c_str());
|
||||
if (info && info->valid && ImGui::IsItemHovered()) {
|
||||
ImGui::BeginTooltip();
|
||||
invScreen.renderItemTooltip(*info);
|
||||
ImGui::EndTooltip();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -76,6 +76,7 @@ void TalentScreen::renderTalentTrees(game::GameHandler& gameHandler) {
|
|||
gameHandler.loadTalentDbc();
|
||||
loadSpellDBC(assetManager);
|
||||
loadSpellIconDBC(assetManager);
|
||||
loadGlyphPropertiesDBC(assetManager);
|
||||
}
|
||||
|
||||
uint8_t playerClass = gameHandler.getPlayerClass();
|
||||
|
|
@ -161,6 +162,18 @@ void TalentScreen::renderTalentTrees(game::GameHandler& gameHandler) {
|
|||
ImGui::EndTabItem();
|
||||
}
|
||||
}
|
||||
|
||||
// Glyphs tab (WotLK only — visible when any glyph slot is populated or DBC data loaded)
|
||||
if (!glyphProperties_.empty() || [&]() {
|
||||
const auto& g = gameHandler.getGlyphs();
|
||||
for (auto id : g) if (id != 0) return true;
|
||||
return false; }()) {
|
||||
if (ImGui::BeginTabItem("Glyphs")) {
|
||||
renderGlyphs(gameHandler);
|
||||
ImGui::EndTabItem();
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::EndTabBar();
|
||||
}
|
||||
}
|
||||
|
|
@ -616,6 +629,99 @@ void TalentScreen::loadSpellIconDBC(pipeline::AssetManager* assetManager) {
|
|||
}
|
||||
}
|
||||
|
||||
void TalentScreen::loadGlyphPropertiesDBC(pipeline::AssetManager* assetManager) {
|
||||
if (glyphDbcLoaded) return;
|
||||
glyphDbcLoaded = true;
|
||||
|
||||
if (!assetManager || !assetManager->isInitialized()) return;
|
||||
|
||||
auto dbc = assetManager->loadDBC("GlyphProperties.dbc");
|
||||
if (!dbc || !dbc->isLoaded()) return;
|
||||
|
||||
// GlyphProperties.dbc: field 0=ID, field 1=SpellID, field 2=GlyphSlotFlags (1=minor), field 3=SpellIconID
|
||||
for (uint32_t i = 0; i < dbc->getRecordCount(); i++) {
|
||||
uint32_t id = dbc->getUInt32(i, 0);
|
||||
uint32_t spellId = dbc->getUInt32(i, 1);
|
||||
uint32_t flags = dbc->getUInt32(i, 2);
|
||||
if (id == 0) continue;
|
||||
GlyphInfo info;
|
||||
info.spellId = spellId;
|
||||
info.isMajor = (flags == 0); // flag 0 = major, flag 1 = minor
|
||||
glyphProperties_[id] = info;
|
||||
}
|
||||
}
|
||||
|
||||
void TalentScreen::renderGlyphs(game::GameHandler& gameHandler) {
|
||||
auto* assetManager = core::Application::getInstance().getAssetManager();
|
||||
const auto& glyphs = gameHandler.getGlyphs();
|
||||
|
||||
ImGui::Spacing();
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.85f, 0.0f, 1.0f), "Major Glyphs");
|
||||
ImGui::Separator();
|
||||
|
||||
// WotLK: 6 glyph slots total. Slots 0,2,4 are major by convention from the server,
|
||||
// but we check GlyphProperties.dbc flags when available.
|
||||
// Display all 6 slots grouped: show major (non-minor) first, then minor.
|
||||
std::vector<std::pair<int, bool>> majorSlots, minorSlots;
|
||||
for (int i = 0; i < game::GameHandler::MAX_GLYPH_SLOTS; i++) {
|
||||
uint16_t glyphId = glyphs[i];
|
||||
bool isMajor = true;
|
||||
if (glyphId != 0) {
|
||||
auto git = glyphProperties_.find(glyphId);
|
||||
if (git != glyphProperties_.end()) isMajor = git->second.isMajor;
|
||||
else isMajor = (i % 2 == 0); // fallback: even slots = major
|
||||
} else {
|
||||
isMajor = (i % 2 == 0); // empty slots follow same pattern
|
||||
}
|
||||
if (isMajor) majorSlots.push_back({i, true});
|
||||
else minorSlots.push_back({i, false});
|
||||
}
|
||||
|
||||
auto renderGlyphSlot = [&](int slotIdx) {
|
||||
uint16_t glyphId = glyphs[slotIdx];
|
||||
char label[64];
|
||||
if (glyphId == 0) {
|
||||
snprintf(label, sizeof(label), "Slot %d [Empty]", slotIdx + 1);
|
||||
ImGui::TextDisabled("%s", label);
|
||||
return;
|
||||
}
|
||||
|
||||
uint32_t spellId = 0;
|
||||
uint32_t iconId = 0;
|
||||
auto git = glyphProperties_.find(glyphId);
|
||||
if (git != glyphProperties_.end()) {
|
||||
spellId = git->second.spellId;
|
||||
auto iit = spellIconIds.find(spellId);
|
||||
if (iit != spellIconIds.end()) iconId = iit->second;
|
||||
}
|
||||
|
||||
// Icon (24x24)
|
||||
VkDescriptorSet icon = getSpellIcon(iconId, assetManager);
|
||||
if (icon != VK_NULL_HANDLE) {
|
||||
ImGui::Image((ImTextureID)(uintptr_t)icon, ImVec2(24, 24));
|
||||
ImGui::SameLine(0, 6);
|
||||
} else {
|
||||
ImGui::Dummy(ImVec2(24, 24));
|
||||
ImGui::SameLine(0, 6);
|
||||
}
|
||||
|
||||
// Spell name
|
||||
const std::string& name = spellId ? gameHandler.getSpellName(spellId) : "";
|
||||
if (!name.empty()) {
|
||||
ImGui::TextColored(ImVec4(0.9f, 0.9f, 0.9f, 1.0f), "%s", name.c_str());
|
||||
} else {
|
||||
ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "Glyph #%u", (uint32_t)glyphId);
|
||||
}
|
||||
};
|
||||
|
||||
for (auto& [idx, major] : majorSlots) renderGlyphSlot(idx);
|
||||
|
||||
ImGui::Spacing();
|
||||
ImGui::TextColored(ImVec4(0.6f, 0.8f, 1.0f, 1.0f), "Minor Glyphs");
|
||||
ImGui::Separator();
|
||||
for (auto& [idx, major] : minorSlots) renderGlyphSlot(idx);
|
||||
}
|
||||
|
||||
VkDescriptorSet TalentScreen::getSpellIcon(uint32_t iconId, pipeline::AssetManager* assetManager) {
|
||||
if (iconId == 0 || !assetManager) return VK_NULL_HANDLE;
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue