Add MCLQ water, TaxiPathNode transports, and vanilla M2 particles

- Parse MCLQ sub-chunks in vanilla ADTs for water rendering (WotLK uses MH2O)
- Load TaxiPathNode.dbc for MO_TRANSPORT world-coordinate paths (vanilla boats)
- Parse data[] from SMSG_GAMEOBJECT_QUERY_RESPONSE (taxiPathId for transports)
- Support vanilla M2 particle emitters (504-byte struct, different from WotLK 476)
- Add character preview texture diagnostic logging
- Fix disconnect handling on character screen (show error only when no chars)
This commit is contained in:
Kelsi 2026-02-14 20:20:43 -08:00
parent cbb3035313
commit bf31da8c13
14 changed files with 556 additions and 55 deletions

View file

@ -389,6 +389,10 @@ public:
void queryPlayerName(uint64_t guid);
void queryCreatureInfo(uint32_t entry, uint64_t guid);
void queryGameObjectInfo(uint32_t entry, uint64_t guid);
const GameObjectQueryResponseData* getCachedGameObjectInfo(uint32_t entry) const {
auto it = gameObjectInfoCache_.find(entry);
return (it != gameObjectInfoCache_.end()) ? &it->second : nullptr;
}
std::string getCachedPlayerName(uint64_t guid) const;
std::string getCachedCreatureName(uint32_t entry) const;

View file

@ -117,6 +117,13 @@ public:
return NameQueryResponseParser::parse(packet, data);
}
// --- GameObject Query ---
/** Parse SMSG_GAMEOBJECT_QUERY_RESPONSE */
virtual bool parseGameObjectQueryResponse(network::Packet& packet, GameObjectQueryResponseData& data) {
return GameObjectQueryResponseParser::parse(packet, data);
}
// --- Gossip ---
/** Parse SMSG_GOSSIP_MESSAGE */
@ -233,6 +240,7 @@ public:
network::Packet buildCastSpell(uint32_t spellId, uint64_t targetGuid, uint8_t castCount) override;
bool parseCastFailed(network::Packet& packet, CastFailedData& data) override;
bool parseMessageChat(network::Packet& packet, MessageChatData& data) override;
bool parseGameObjectQueryResponse(network::Packet& packet, GameObjectQueryResponseData& data) override;
bool parseGossipMessage(network::Packet& packet, GossipMessageData& data) override;
bool parseGuildRoster(network::Packet& packet, GuildRosterData& data) override;
bool parseGuildQueryResponse(network::Packet& packet, GuildQueryResponseData& data) override;

View file

@ -29,12 +29,14 @@ struct TransportPath {
uint32_t durationMs; // Total loop duration in ms (includes wrap segment if added)
bool zOnly; // True if path only has Z movement (elevator/bobbing), false if real XY travel
bool fromDBC; // True if loaded from TransportAnimation.dbc, false for runtime fallback/custom paths
bool worldCoords = false; // True if points are absolute world coords (TaxiPathNode), not local offsets
};
struct ActiveTransport {
uint64_t guid; // Entity GUID
uint32_t wmoInstanceId; // WMO renderer instance ID
uint32_t pathId; // Current path
uint32_t entry = 0; // GameObject entry (for MO_TRANSPORT path updates)
glm::vec3 basePosition; // Spawn position (base offset for path)
glm::vec3 position; // Current world position
glm::quat rotation; // Current world rotation
@ -79,7 +81,7 @@ public:
void setWMORenderer(rendering::WMORenderer* renderer) { wmoRenderer_ = renderer; }
void update(float deltaTime);
void registerTransport(uint64_t guid, uint32_t wmoInstanceId, uint32_t pathId, const glm::vec3& spawnWorldPos);
void registerTransport(uint64_t guid, uint32_t wmoInstanceId, uint32_t pathId, const glm::vec3& spawnWorldPos, uint32_t entry = 0);
void unregisterTransport(uint64_t guid);
ActiveTransport* getTransport(uint64_t guid);
@ -92,6 +94,16 @@ public:
// Load transport paths from TransportAnimation.dbc
bool loadTransportAnimationDBC(pipeline::AssetManager* assetMgr);
// Load transport paths from TaxiPathNode.dbc (world-coordinate paths for MO_TRANSPORT)
bool loadTaxiPathNodeDBC(pipeline::AssetManager* assetMgr);
// Check if a TaxiPathNode path exists for a given taxiPathId
bool hasTaxiPath(uint32_t taxiPathId) const;
// Assign a TaxiPathNode path to an existing transport (called when GO query response arrives)
// Returns true if the transport was updated
bool assignTaxiPathToTransport(uint32_t entry, uint32_t taxiPathId);
// Check if a path exists for a given GameObject entry
bool hasPathForEntry(uint32_t entry) const;
// Check if a path has meaningful XY travel (used to reject near-stationary false positives).
@ -126,7 +138,8 @@ private:
void updateTransformMatrices(ActiveTransport& transport);
std::unordered_map<uint64_t, ActiveTransport> transports_;
std::unordered_map<uint32_t, TransportPath> paths_; // Indexed by transportEntry (pathId from TransportAnimation.dbc)
std::unordered_map<uint32_t, TransportPath> paths_; // Indexed by transportEntry (pathId from TransportAnimation.dbc)
std::unordered_map<uint32_t, TransportPath> taxiPaths_; // Indexed by TaxiPath.dbc ID (world-coord paths for MO_TRANSPORT)
rendering::WMORenderer* wmoRenderer_ = nullptr;
bool clientSideAnimation_ = false; // DISABLED - use server positions instead of client prediction
float elapsedTime_ = 0.0f; // Total elapsed time (seconds)

View file

@ -1356,7 +1356,10 @@ public:
struct GameObjectQueryResponseData {
uint32_t entry = 0;
std::string name;
uint32_t type = 0; // GameObjectType (e.g. 3=chest, 2=questgiver)
uint32_t type = 0; // GameObjectType (e.g. 3=chest, 2=questgiver, 15=MO_TRANSPORT)
uint32_t displayId = 0;
uint32_t data[24] = {}; // Type-specific data fields (e.g. data[0]=taxiPathId for MO_TRANSPORT)
bool hasData = false; // Whether data[] was parsed
bool isValid() const { return entry != 0 && !name.empty(); }
};

View file

@ -206,6 +206,8 @@ private:
static void parseMCLY(const uint8_t* data, size_t size, MapChunk& chunk);
static void parseMCAL(const uint8_t* data, size_t size, MapChunk& chunk);
static void parseMH2O(const uint8_t* data, size_t size, ADTTerrain& terrain);
static void parseMCLQ(const uint8_t* data, size_t size, int chunkIndex,
uint32_t mcnkFlags, ADTTerrain& terrain);
};
} // namespace pipeline