mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-05-03 08:03:50 +00:00
Merge master into chore/god-object-decomposition-2nd
Resolve conflicts: - audio_callback_handler.cpp: keep PR's animation_controller include - movement_handler.cpp: use PR accessors with master's transportResolved logic - world_packets.cpp: keep PR's decomposed version (functions moved to split files) Apply overkill field fix to world_packets_entity.cpp (WotLK SMSG_ATTACKERSTATEUPDATE missing uint32 overkill between damage and subDamageCount).
This commit is contained in:
commit
e32f4fbff9
9 changed files with 148 additions and 35 deletions
|
|
@ -191,7 +191,7 @@ void ChatHandler::handleMessageChat(network::Packet& packet) {
|
||||||
LOG_WARNING("Failed to parse SMSG_MESSAGECHAT, size=", packet.getSize());
|
LOG_WARNING("Failed to parse SMSG_MESSAGECHAT, size=", packet.getSize());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
LOG_INFO("INCOMING CHAT: type=", static_cast<int>(data.type),
|
LOG_WARNING("INCOMING CHAT: type=", static_cast<int>(data.type),
|
||||||
" (", getChatTypeString(data.type), ") sender=0x", std::hex, data.senderGuid, std::dec,
|
" (", getChatTypeString(data.type), ") sender=0x", std::hex, data.senderGuid, std::dec,
|
||||||
" '", data.senderName, "' msg='", data.message.substr(0, 60), "'");
|
" '", data.senderName, "' msg='", data.message.substr(0, 60), "'");
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1342,7 +1342,11 @@ void GameHandler::mailMarkAsRead(uint32_t mailId) {
|
||||||
|
|
||||||
glm::vec3 GameHandler::getComposedWorldPosition() {
|
glm::vec3 GameHandler::getComposedWorldPosition() {
|
||||||
if (playerTransportGuid_ != 0 && transportManager_) {
|
if (playerTransportGuid_ != 0 && transportManager_) {
|
||||||
return transportManager_->getPlayerWorldPosition(playerTransportGuid_, playerTransportOffset_);
|
auto* tr = transportManager_->getTransport(playerTransportGuid_);
|
||||||
|
if (tr) {
|
||||||
|
return transportManager_->getPlayerWorldPosition(playerTransportGuid_, playerTransportOffset_);
|
||||||
|
}
|
||||||
|
// Transport not tracked — fall through to normal position
|
||||||
}
|
}
|
||||||
// Not on transport, return normal movement position
|
// Not on transport, return normal movement position
|
||||||
return glm::vec3(movementInfo.x, movementInfo.y, movementInfo.z);
|
return glm::vec3(movementInfo.x, movementInfo.y, movementInfo.z);
|
||||||
|
|
|
||||||
|
|
@ -515,12 +515,28 @@ void MovementHandler::sendMovement(Opcode opcode) {
|
||||||
|
|
||||||
// Add transport data if player is on a server-recognized transport
|
// Add transport data if player is on a server-recognized transport
|
||||||
if (includeTransportInWire) {
|
if (includeTransportInWire) {
|
||||||
|
bool transportResolved = false;
|
||||||
if (owner_.getTransportManager()) {
|
if (owner_.getTransportManager()) {
|
||||||
glm::vec3 composed = owner_.getTransportManager()->getPlayerWorldPosition(owner_.playerTransportGuidRef(), owner_.playerTransportOffsetRef());
|
auto* tr = owner_.getTransportManager()->getTransport(owner_.playerTransportGuidRef());
|
||||||
movementInfo.x = composed.x;
|
if (tr) {
|
||||||
movementInfo.y = composed.y;
|
transportResolved = true;
|
||||||
movementInfo.z = composed.z;
|
glm::vec3 composed = owner_.getTransportManager()->getPlayerWorldPosition(owner_.playerTransportGuidRef(), owner_.playerTransportOffsetRef());
|
||||||
|
movementInfo.x = composed.x;
|
||||||
|
movementInfo.y = composed.y;
|
||||||
|
movementInfo.z = composed.z;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
if (!transportResolved) {
|
||||||
|
// Transport not tracked — don't send ONTRANSPORT to the server.
|
||||||
|
// Sending stale transport GUID + local offset causes the server to
|
||||||
|
// compute a bad world position and teleport us to map origin.
|
||||||
|
LOG_WARNING("sendMovement: transport 0x", std::hex, owner_.playerTransportGuidRef(),
|
||||||
|
std::dec, " not found — clearing transport state");
|
||||||
|
includeTransportInWire = false;
|
||||||
|
owner_.clearPlayerTransport();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (includeTransportInWire) {
|
||||||
movementInfo.flags |= static_cast<uint32_t>(MovementFlags::ONTRANSPORT);
|
movementInfo.flags |= static_cast<uint32_t>(MovementFlags::ONTRANSPORT);
|
||||||
movementInfo.transportGuid = owner_.playerTransportGuidRef();
|
movementInfo.transportGuid = owner_.playerTransportGuidRef();
|
||||||
movementInfo.transportX = owner_.playerTransportOffsetRef().x;
|
movementInfo.transportX = owner_.playerTransportOffsetRef().x;
|
||||||
|
|
@ -596,6 +612,17 @@ void MovementHandler::sendMovement(Opcode opcode) {
|
||||||
wireOpcode(opcode), std::dec,
|
wireOpcode(opcode), std::dec,
|
||||||
(includeTransportInWire ? " ONTRANSPORT" : ""));
|
(includeTransportInWire ? " ONTRANSPORT" : ""));
|
||||||
|
|
||||||
|
// Detect near-origin position on Eastern Kingdoms (map 0) — this would place
|
||||||
|
// the player near Alterac Mountains and is almost certainly a bug.
|
||||||
|
if (owner_.getCurrentMapId() == 0 &&
|
||||||
|
std::abs(movementInfo.x) < 500.0f && std::abs(movementInfo.y) < 500.0f) {
|
||||||
|
LOG_WARNING("sendMovement: position near map origin! canonical=(",
|
||||||
|
movementInfo.x, ", ", movementInfo.y, ", ", movementInfo.z,
|
||||||
|
") onTransport=", owner_.isOnTransport(),
|
||||||
|
" transportGuid=0x", std::hex, owner_.playerTransportGuidRef(), std::dec,
|
||||||
|
" flags=0x", std::hex, movementInfo.flags, std::dec);
|
||||||
|
}
|
||||||
|
|
||||||
// Convert canonical → server coordinates for the wire
|
// Convert canonical → server coordinates for the wire
|
||||||
MovementInfo wireInfo = movementInfo;
|
MovementInfo wireInfo = movementInfo;
|
||||||
glm::vec3 serverPos = core::coords::canonicalToServer(glm::vec3(wireInfo.x, wireInfo.y, wireInfo.z));
|
glm::vec3 serverPos = core::coords::canonicalToServer(glm::vec3(wireInfo.x, wireInfo.y, wireInfo.z));
|
||||||
|
|
@ -603,10 +630,12 @@ void MovementHandler::sendMovement(Opcode opcode) {
|
||||||
wireInfo.y = serverPos.y;
|
wireInfo.y = serverPos.y;
|
||||||
wireInfo.z = serverPos.z;
|
wireInfo.z = serverPos.z;
|
||||||
|
|
||||||
// Periodic position audit — DEBUG to avoid flooding production logs.
|
// Periodic position audit — log every ~60 heartbeats (~30s) to trace position drift.
|
||||||
if (opcode == Opcode::MSG_MOVE_HEARTBEAT && ++heartbeatLogCount_ % 30 == 0) {
|
if (opcode == Opcode::MSG_MOVE_HEARTBEAT && ++heartbeatLogCount_ % 60 == 0) {
|
||||||
LOG_DEBUG("HEARTBEAT pos canonical=(", movementInfo.x, ",", movementInfo.y, ",", movementInfo.z,
|
LOG_WARNING("HEARTBEAT #", heartbeatLogCount_, " canonical=(",
|
||||||
") wire=(", wireInfo.x, ",", wireInfo.y, ",", wireInfo.z, ")");
|
movementInfo.x, ",", movementInfo.y, ",", movementInfo.z,
|
||||||
|
") server=(", wireInfo.x, ",", wireInfo.y, ",", wireInfo.z,
|
||||||
|
") flags=0x", std::hex, movementInfo.flags, std::dec);
|
||||||
}
|
}
|
||||||
|
|
||||||
wireInfo.orientation = core::coords::canonicalToServerYaw(wireInfo.orientation);
|
wireInfo.orientation = core::coords::canonicalToServerYaw(wireInfo.orientation);
|
||||||
|
|
@ -1644,9 +1673,10 @@ void MovementHandler::handleTeleportAck(network::Packet& packet) {
|
||||||
float serverZ = packet.readFloat();
|
float serverZ = packet.readFloat();
|
||||||
float orientation = packet.readFloat();
|
float orientation = packet.readFloat();
|
||||||
|
|
||||||
LOG_INFO("MSG_MOVE_TELEPORT_ACK: guid=0x", std::hex, guid, std::dec,
|
LOG_WARNING("MSG_MOVE_TELEPORT_ACK: guid=0x", std::hex, guid, std::dec,
|
||||||
" counter=", counter,
|
" counter=", counter,
|
||||||
" pos=(", serverX, ", ", serverY, ", ", serverZ, ")");
|
" pos=(", serverX, ", ", serverY, ", ", serverZ, ")",
|
||||||
|
" currentPos=(", movementInfo.x, ", ", movementInfo.y, ", ", movementInfo.z, ")");
|
||||||
|
|
||||||
glm::vec3 canonical = core::coords::serverToCanonical(glm::vec3(serverX, serverY, serverZ));
|
glm::vec3 canonical = core::coords::serverToCanonical(glm::vec3(serverX, serverY, serverZ));
|
||||||
movementInfo.x = canonical.x;
|
movementInfo.x = canonical.x;
|
||||||
|
|
@ -2535,6 +2565,17 @@ void MovementHandler::checkAreaTriggers() {
|
||||||
const float py = movementInfo.y;
|
const float py = movementInfo.y;
|
||||||
const float pz = movementInfo.z;
|
const float pz = movementInfo.z;
|
||||||
|
|
||||||
|
// Sanity: if position is near map origin on Eastern Kingdoms (map 0),
|
||||||
|
// something has corrupted movementInfo — skip area trigger check to
|
||||||
|
// avoid firing Alterac/Hillsbrad triggers and causing a rogue teleport.
|
||||||
|
if (owner_.getCurrentMapId() == 0 && std::abs(px) < 500.0f && std::abs(py) < 500.0f) {
|
||||||
|
LOG_WARNING("checkAreaTriggers: position near map origin (", px, ", ", py, ", ", pz,
|
||||||
|
") on map 0 — skipping to avoid rogue teleport. onTransport=",
|
||||||
|
owner_.isOnTransport(), " transportGuid=0x", std::hex,
|
||||||
|
owner_.playerTransportGuidRef(), std::dec);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// On first check after map transfer, just mark which triggers we're inside
|
// On first check after map transfer, just mark which triggers we're inside
|
||||||
// without firing them — prevents exit portal from immediately sending us back
|
// without firing them — prevents exit portal from immediately sending us back
|
||||||
bool suppressFirst = owner_.areaTriggerSuppressFirstRef();
|
bool suppressFirst = owner_.areaTriggerSuppressFirstRef();
|
||||||
|
|
|
||||||
|
|
@ -141,7 +141,9 @@ ActiveTransport* TransportManager::getTransport(uint64_t guid) {
|
||||||
glm::vec3 TransportManager::getPlayerWorldPosition(uint64_t transportGuid, const glm::vec3& localOffset) {
|
glm::vec3 TransportManager::getPlayerWorldPosition(uint64_t transportGuid, const glm::vec3& localOffset) {
|
||||||
auto* transport = getTransport(transportGuid);
|
auto* transport = getTransport(transportGuid);
|
||||||
if (!transport) {
|
if (!transport) {
|
||||||
return localOffset; // Fallback
|
LOG_WARNING("getPlayerWorldPosition: transport 0x", std::hex, transportGuid, std::dec,
|
||||||
|
" not found — returning localOffset as-is (callers should guard)");
|
||||||
|
return localOffset;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (transport->isM2) {
|
if (transport->isM2) {
|
||||||
|
|
|
||||||
|
|
@ -794,8 +794,8 @@ bool AttackStopParser::parse(network::Packet& packet, AttackStopData& data) {
|
||||||
}
|
}
|
||||||
|
|
||||||
bool AttackerStateUpdateParser::parse(network::Packet& packet, AttackerStateUpdateData& data) {
|
bool AttackerStateUpdateParser::parse(network::Packet& packet, AttackerStateUpdateData& data) {
|
||||||
// Upfront validation: hitInfo(4) + packed GUIDs(1-8 each) + totalDamage(4) + subDamageCount(1) = 13 bytes minimum
|
// Upfront validation: hitInfo(4) + packed GUIDs(1-8 each) + totalDamage(4) + overkill(4) + subDamageCount(1) = 17 bytes minimum
|
||||||
if (!packet.hasRemaining(13)) return false;
|
if (!packet.hasRemaining(17)) return false;
|
||||||
|
|
||||||
size_t startPos = packet.getReadPos();
|
size_t startPos = packet.getReadPos();
|
||||||
data.hitInfo = packet.readUInt32();
|
data.hitInfo = packet.readUInt32();
|
||||||
|
|
@ -810,13 +810,15 @@ bool AttackerStateUpdateParser::parse(network::Packet& packet, AttackerStateUpda
|
||||||
}
|
}
|
||||||
data.targetGuid = packet.readPackedGuid();
|
data.targetGuid = packet.readPackedGuid();
|
||||||
|
|
||||||
// Validate totalDamage + subDamageCount can be read (5 bytes)
|
// Validate totalDamage + overkill + subDamageCount can be read (9 bytes)
|
||||||
if (!packet.hasRemaining(5)) {
|
// WotLK (AzerothCore) sends: damage(4) + overkill(4) + subDamageCount(1)
|
||||||
|
if (!packet.hasRemaining(9)) {
|
||||||
packet.setReadPos(startPos);
|
packet.setReadPos(startPos);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
data.totalDamage = static_cast<int32_t>(packet.readUInt32());
|
data.totalDamage = static_cast<int32_t>(packet.readUInt32());
|
||||||
|
data.overkill = static_cast<int32_t>(packet.readUInt32());
|
||||||
data.subDamageCount = packet.readUInt8();
|
data.subDamageCount = packet.readUInt8();
|
||||||
|
|
||||||
// Cap subDamageCount: each entry is 20 bytes. If the claimed count
|
// Cap subDamageCount: each entry is 20 bytes. If the claimed count
|
||||||
|
|
@ -853,17 +855,14 @@ bool AttackerStateUpdateParser::parse(network::Packet& packet, AttackerStateUpda
|
||||||
// Validate victimState + overkill fields (8 bytes)
|
// Validate victimState + overkill fields (8 bytes)
|
||||||
if (!packet.hasRemaining(8)) {
|
if (!packet.hasRemaining(8)) {
|
||||||
data.victimState = 0;
|
data.victimState = 0;
|
||||||
data.overkill = 0;
|
|
||||||
return !data.subDamages.empty();
|
return !data.subDamages.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
data.victimState = packet.readUInt32();
|
data.victimState = packet.readUInt32();
|
||||||
// WotLK (AzerothCore): two unknown uint32 fields follow victimState before overkill.
|
// WotLK: attackerState(4) + meleeSpellId(4) follow victimState
|
||||||
// Older parsers omitted these, reading overkill from the wrong offset.
|
|
||||||
auto rem = [&]() { return packet.getRemainingSize(); };
|
auto rem = [&]() { return packet.getRemainingSize(); };
|
||||||
if (rem() >= 4) packet.readUInt32(); // unk1 (always 0)
|
if (rem() >= 4) packet.readUInt32(); // attackerState (always 0)
|
||||||
if (rem() >= 4) packet.readUInt32(); // unk2 (melee spell ID, 0 for auto-attack)
|
if (rem() >= 4) packet.readUInt32(); // meleeSpellId (0 for auto-attack)
|
||||||
data.overkill = (rem() >= 4) ? static_cast<int32_t>(packet.readUInt32()) : -1;
|
|
||||||
|
|
||||||
// hitInfo-conditional fields: HITINFO_BLOCK(0x2000), RAGE_GAIN(0x20000), FAKE_DAMAGE(0x40)
|
// hitInfo-conditional fields: HITINFO_BLOCK(0x2000), RAGE_GAIN(0x20000), FAKE_DAMAGE(0x40)
|
||||||
if ((data.hitInfo & 0x2000) && rem() >= 4) data.blocked = packet.readUInt32();
|
if ((data.hitInfo & 0x2000) && rem() >= 4) data.blocked = packet.readUInt32();
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
#include "rendering/animation/combat_fsm.hpp"
|
#include "rendering/animation/combat_fsm.hpp"
|
||||||
#include "rendering/animation/animation_ids.hpp"
|
#include "rendering/animation/animation_ids.hpp"
|
||||||
#include "game/inventory.hpp"
|
#include "game/inventory.hpp"
|
||||||
|
#include "core/logger.hpp"
|
||||||
|
|
||||||
namespace wowee {
|
namespace wowee {
|
||||||
namespace rendering {
|
namespace rendering {
|
||||||
|
|
@ -378,7 +379,10 @@ AnimOutput CombatFSM::resolve(const Input& in, const AnimCapabilitySet& caps,
|
||||||
animId = caps.resolvedMelee1H;
|
animId = caps.resolvedMelee1H;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (animId == 0) animId = anim::STAND; // Melee must play something
|
if (animId == 0) {
|
||||||
|
LOG_DEBUG("CombatFSM: MELEE_SWING resolved animId=0, falling back to STAND");
|
||||||
|
animId = anim::STAND;
|
||||||
|
}
|
||||||
loop = false;
|
loop = false;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ static std::vector<std::string> parseEmoteCommands(const std::string& raw) {
|
||||||
|
|
||||||
static bool isLoopingEmote(const std::string& command) {
|
static bool isLoopingEmote(const std::string& command) {
|
||||||
static const std::unordered_set<std::string> kLooping = {
|
static const std::unordered_set<std::string> kLooping = {
|
||||||
"dance", "train", "dead", "eat", "work",
|
"dance", "train", "dead", "eat", "work", "sleep",
|
||||||
};
|
};
|
||||||
return kLooping.find(command) != kLooping.end();
|
return kLooping.find(command) != kLooping.end();
|
||||||
}
|
}
|
||||||
|
|
@ -117,6 +117,9 @@ void EmoteRegistry::loadFromDbc() {
|
||||||
uint32_t animId = emotesDbc->getUInt32(r, emL ? (*emL)["AnimID"] : 2);
|
uint32_t animId = emotesDbc->getUInt32(r, emL ? (*emL)["AnimID"] : 2);
|
||||||
if (animId != 0) emoteIdToAnim[emoteId] = animId;
|
if (animId != 0) emoteIdToAnim[emoteId] = animId;
|
||||||
}
|
}
|
||||||
|
LOG_WARNING("Emotes: loaded ", emoteIdToAnim.size(), " anim mappings from Emotes.dbc");
|
||||||
|
} else {
|
||||||
|
LOG_WARNING("Emotes: Emotes.dbc failed to load — all emotes will use fallback animations");
|
||||||
}
|
}
|
||||||
|
|
||||||
emoteTable_.clear();
|
emoteTable_.clear();
|
||||||
|
|
@ -128,11 +131,13 @@ void EmoteRegistry::loadFromDbc() {
|
||||||
|
|
||||||
uint32_t emoteRef = emotesTextDbc->getUInt32(r, etL ? (*etL)["EmoteRef"] : 2);
|
uint32_t emoteRef = emotesTextDbc->getUInt32(r, etL ? (*etL)["EmoteRef"] : 2);
|
||||||
uint32_t animId = 0;
|
uint32_t animId = 0;
|
||||||
auto animIt = emoteIdToAnim.find(emoteRef);
|
if (emoteRef != 0) {
|
||||||
if (animIt != emoteIdToAnim.end()) {
|
auto animIt = emoteIdToAnim.find(emoteRef);
|
||||||
animId = animIt->second;
|
if (animIt != emoteIdToAnim.end()) {
|
||||||
} else {
|
animId = animIt->second;
|
||||||
animId = emoteRef;
|
}
|
||||||
|
// If Emotes.dbc has AnimID=0 for this ref, leave animId=0 (text-only).
|
||||||
|
// Previously fell back to using emoteRef as animId which is wrong.
|
||||||
}
|
}
|
||||||
|
|
||||||
uint32_t senderTargetTextId = emotesTextDbc->getUInt32(r, etL ? (*etL)["SenderTargetTextID"] : 5);
|
uint32_t senderTargetTextId = emotesTextDbc->getUInt32(r, etL ? (*etL)["SenderTargetTextID"] : 5);
|
||||||
|
|
@ -161,11 +166,33 @@ void EmoteRegistry::loadFromDbc() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Override emotes whose DBC chain yields animId=0.
|
||||||
|
// /sleep uses the stand-state system in WoW rather than Emotes.dbc AnimID.
|
||||||
|
// /laugh and /flirt should resolve from Emotes.dbc (70 and 83), but these
|
||||||
|
// serve as backup if Emotes.dbc failed to load.
|
||||||
|
// /fart and /stink have EmoteRef=0 in EmotesText.dbc — no Emotes.dbc link.
|
||||||
|
static const std::unordered_map<std::string, uint32_t> kAnimOverrides = {
|
||||||
|
{"sleep", anim::EMOTE_SLEEP}, // 71 — stand-state emote
|
||||||
|
{"laugh", anim::EMOTE_LAUGH}, // 70 — backup
|
||||||
|
{"flirt", anim::EMOTE_SHY}, // 83 — DBC calls it SHY; it's the flirt animation
|
||||||
|
{"fart", anim::EMOTE_TALK}, // 60 — generic gesture (WoW has no dedicated anim)
|
||||||
|
{"stink", anim::EMOTE_TALK}, // 60 — generic gesture (WoW has no dedicated anim)
|
||||||
|
};
|
||||||
|
for (auto& [cmd, info] : emoteTable_) {
|
||||||
|
if (info.animId == 0) {
|
||||||
|
auto ov = kAnimOverrides.find(cmd);
|
||||||
|
if (ov != kAnimOverrides.end()) {
|
||||||
|
LOG_WARNING("Emotes: override /", cmd, " → animId=", ov->second);
|
||||||
|
info.animId = ov->second;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (emoteTable_.empty()) {
|
if (emoteTable_.empty()) {
|
||||||
LOG_WARNING("Emotes: DBC loaded but no commands parsed, using fallback list");
|
LOG_WARNING("Emotes: DBC loaded but no commands parsed, using fallback list");
|
||||||
loadFallbackEmotes();
|
loadFallbackEmotes();
|
||||||
} else {
|
} else {
|
||||||
LOG_INFO("Emotes: loaded ", emoteTable_.size(), " commands from DBC");
|
LOG_WARNING("Emotes: loaded ", emoteTable_.size(), " commands from DBC");
|
||||||
}
|
}
|
||||||
|
|
||||||
buildDbcIdIndex();
|
buildDbcIdIndex();
|
||||||
|
|
|
||||||
|
|
@ -242,6 +242,7 @@ void AnimationController::triggerMeleeSwing() {
|
||||||
if (durationSec < 0.25f) durationSec = 0.25f;
|
if (durationSec < 0.25f) durationSec = 0.25f;
|
||||||
if (durationSec > 1.0f) durationSec = 1.0f;
|
if (durationSec > 1.0f) durationSec = 1.0f;
|
||||||
meleeSwingTimer_ = durationSec;
|
meleeSwingTimer_ = durationSec;
|
||||||
|
|
||||||
if (renderer_->getAudioCoordinator()->getActivitySoundManager()) {
|
if (renderer_->getAudioCoordinator()->getActivitySoundManager()) {
|
||||||
renderer_->getAudioCoordinator()->getActivitySoundManager()->playMeleeSwing();
|
renderer_->getAudioCoordinator()->getActivitySoundManager()->playMeleeSwing();
|
||||||
}
|
}
|
||||||
|
|
@ -1040,9 +1041,16 @@ void AnimationController::updateCharacterAnimation() {
|
||||||
auto* cameraController = renderer_->getCameraController();
|
auto* cameraController = renderer_->getCameraController();
|
||||||
uint32_t characterInstanceId = renderer_->getCharacterInstanceId();
|
uint32_t characterInstanceId = renderer_->getCharacterInstanceId();
|
||||||
|
|
||||||
// Lazy probe: populate capability set once per model
|
// Lazy probe: populate capability set once per model.
|
||||||
if (!capabilitiesProbed_ && characterRenderer && characterInstanceId != 0) {
|
// Re-probe if melee capabilities are missing (model may not have been fully
|
||||||
probeCapabilities();
|
// loaded on the first probe attempt).
|
||||||
|
if (characterRenderer && characterInstanceId != 0) {
|
||||||
|
if (!capabilitiesProbed_) {
|
||||||
|
probeCapabilities();
|
||||||
|
} else if (meleeSwingTimer_ > 0.0f && !characterAnimator_.getCapabilities().hasMelee) {
|
||||||
|
capabilitiesProbed_ = false;
|
||||||
|
probeCapabilities();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// When mounted, delegate to MountFSM and handle positioning
|
// When mounted, delegate to MountFSM and handle positioning
|
||||||
|
|
|
||||||
|
|
@ -1418,6 +1418,17 @@ void WMORenderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, const
|
||||||
// Portal-based visibility — reuse member scratch buffers (avoid per-frame alloc)
|
// Portal-based visibility — reuse member scratch buffers (avoid per-frame alloc)
|
||||||
portalVisibleGroups_.clear();
|
portalVisibleGroups_.clear();
|
||||||
bool usePortalCulling = doPortalCull && !model.portals.empty() && !model.portalRefs.empty();
|
bool usePortalCulling = doPortalCull && !model.portals.empty() && !model.portalRefs.empty();
|
||||||
|
if (usePortalCulling) {
|
||||||
|
// If the actual camera is outside all groups, skip portal culling.
|
||||||
|
// The character position (portalViewerPos) may fall inside a group's
|
||||||
|
// loose AABB while visually outside the WMO, causing the BFS to start
|
||||||
|
// from an interior group whose portals aren't in the frustum — hiding
|
||||||
|
// the entire WMO.
|
||||||
|
glm::vec3 localRealCam = glm::vec3(instance.invModelMatrix * glm::vec4(camPos, 1.0f));
|
||||||
|
if (findContainingGroup(model, localRealCam) < 0) {
|
||||||
|
usePortalCulling = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
if (usePortalCulling) {
|
if (usePortalCulling) {
|
||||||
portalVisibleGroupSet_.clear();
|
portalVisibleGroupSet_.clear();
|
||||||
glm::vec4 localCamPos = instance.invModelMatrix * glm::vec4(portalViewerPos, 1.0f);
|
glm::vec4 localCamPos = instance.invModelMatrix * glm::vec4(portalViewerPos, 1.0f);
|
||||||
|
|
@ -2135,6 +2146,23 @@ void WMORenderer::getVisibleGroupsViaPortals(const ModelData& model,
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// Best-fit group is indoor-only, but the position might also be inside an
|
||||||
|
// outdoor group's AABB (e.g., standing on a street near a building whose
|
||||||
|
// indoor AABB extends outward). If any outdoor group also contains the
|
||||||
|
// position, treat this as an outdoor location and show all groups.
|
||||||
|
for (size_t gi = 0; gi < model.groups.size(); gi++) {
|
||||||
|
if (static_cast<int>(gi) == cameraGroup) continue;
|
||||||
|
const auto& g = model.groups[gi];
|
||||||
|
if (!(g.groupFlags & WMO_GROUP_FLAG_OUTDOOR)) continue;
|
||||||
|
if (cameraLocalPos.x >= g.boundingBoxMin.x && cameraLocalPos.x <= g.boundingBoxMax.x &&
|
||||||
|
cameraLocalPos.y >= g.boundingBoxMin.y && cameraLocalPos.y <= g.boundingBoxMax.y &&
|
||||||
|
cameraLocalPos.z >= g.boundingBoxMin.z && cameraLocalPos.z <= g.boundingBoxMax.z) {
|
||||||
|
for (size_t gj = 0; gj < model.groups.size(); gj++) {
|
||||||
|
outVisibleGroups.insert(static_cast<uint32_t>(gj));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the camera group has no portal refs, it's a dead-end group (utility/transition group).
|
// If the camera group has no portal refs, it's a dead-end group (utility/transition group).
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue