diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 00000000..6a65ab41 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +github: Kelsidavis diff --git a/Data/expansions/classic/opcodes.json b/Data/expansions/classic/opcodes.json index fb0a72eb..b549d0e3 100644 --- a/Data/expansions/classic/opcodes.json +++ b/Data/expansions/classic/opcodes.json @@ -94,6 +94,7 @@ "CMSG_BINDER_ACTIVATE": "0x1B5", "SMSG_LOG_XPGAIN": "0x1D0", "SMSG_MONSTER_MOVE": "0x0DD", + "SMSG_COMPRESSED_MOVES": "0x06B", "CMSG_ATTACKSWING": "0x141", "CMSG_ATTACKSTOP": "0x142", "SMSG_ATTACKSTART": "0x143", diff --git a/Data/expansions/turtle/opcodes.json b/Data/expansions/turtle/opcodes.json index 8dbd776c..ebadec31 100644 --- a/Data/expansions/turtle/opcodes.json +++ b/Data/expansions/turtle/opcodes.json @@ -93,7 +93,8 @@ "CMSG_SET_ACTIVE_MOVER": "0x26A", "CMSG_BINDER_ACTIVATE": "0x1B5", "SMSG_LOG_XPGAIN": "0x1D0", - "SMSG_MONSTER_MOVE": "0x0DD", + "SMSG_MONSTER_MOVE": "0x2FB", + "SMSG_COMPRESSED_MOVES": "0x06B", "CMSG_ATTACKSWING": "0x141", "CMSG_ATTACKSTOP": "0x142", "SMSG_ATTACKSTART": "0x143", diff --git a/README.md b/README.md index a4eea60e..89569fb1 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,8 @@ A native C++ World of Warcraft client with a custom OpenGL renderer. +[![Sponsor](https://img.shields.io/github/sponsors/Kelsidavis?label=Sponsor&logo=GitHub)](https://github.com/sponsors/Kelsidavis) + [![Watch the video](https://img.youtube.com/vi/Pd9JuYYxu0o/maxresdefault.jpg)](https://youtu.be/Pd9JuYYxu0o) [![Watch the video](https://img.youtube.com/vi/J4NXegzqWSQ/maxresdefault.jpg)](https://youtu.be/J4NXegzqWSQ) diff --git a/include/game/entity.hpp b/include/game/entity.hpp index f8e05735..aa9986ec 100644 --- a/include/game/entity.hpp +++ b/include/game/entity.hpp @@ -105,6 +105,13 @@ public: bool isEntityMoving() const { return isMoving_; } + // Returns the latest server-authoritative position: destination if moving, current if not. + // Unlike getX/Y/Z (which only update via updateMovement), this always reflects the + // last known server position regardless of distance culling. + float getLatestX() const { return isMoving_ ? moveEndX_ : x; } + float getLatestY() const { return isMoving_ ? moveEndY_ : y; } + float getLatestZ() const { return isMoving_ ? moveEndZ_ : z; } + // Object type ObjectType getType() const { return type; } void setType(ObjectType t) { type = t; } diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index e5e5f460..b8b99fa7 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -1049,6 +1049,7 @@ private: // ---- Creature movement handler ---- void handleMonsterMove(network::Packet& packet); + void handleCompressedMoves(network::Packet& packet); void handleMonsterMoveTransport(network::Packet& packet); // ---- Other player movement (MSG_MOVE_* from server) ---- diff --git a/include/game/opcode_table.hpp b/include/game/opcode_table.hpp index 8875ede0..79d87b4b 100644 --- a/include/game/opcode_table.hpp +++ b/include/game/opcode_table.hpp @@ -157,6 +157,7 @@ enum class LogicalOpcode : uint16_t { // ---- Creature Movement ---- SMSG_MONSTER_MOVE, + SMSG_COMPRESSED_MOVES, // Vanilla/Classic batch movement packet (0x6B) // ---- Phase 2: Combat Core ---- CMSG_ATTACKSWING, diff --git a/include/game/world_packets.hpp b/include/game/world_packets.hpp index ecb84c73..6503ee95 100644 --- a/include/game/world_packets.hpp +++ b/include/game/world_packets.hpp @@ -1450,7 +1450,11 @@ struct MonsterMoveData { class MonsterMoveParser { public: + // WotLK 3.3.5a format: PackedGUID + uint8 unk + float[3] + uint32 splineId + uint8 moveType + ... static bool parse(network::Packet& packet, MonsterMoveData& data); + // Vanilla 1.12 format: PackedGUID + float[3] + uint32 timeInMs + uint8 moveType + ... + // Used for Classic/TBC/Turtle WoW servers (no splineId, timeInMs before moveType) + static bool parseVanilla(network::Packet& packet, MonsterMoveData& data); }; /** SMSG_ATTACKSTART data */ diff --git a/src/core/application.cpp b/src/core/application.cpp index 3573efba..d1eaeee2 100644 --- a/src/core/application.cpp +++ b/src/core/application.cpp @@ -3419,6 +3419,20 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x } } + // Use the entity's latest server-authoritative position rather than the stale spawn + // position. Movement packets (SMSG_MONSTER_MOVE) can arrive while a creature is still + // queued in pendingCreatureSpawns_ and get silently dropped. getLatestX/Y/Z returns + // the movement destination if the entity is mid-move, which is always up-to-date + // regardless of distance culling (unlike getX/Y/Z which requires updateMovement). + if (gameHandler) { + if (auto entity = gameHandler->getEntityManager().getEntity(guid)) { + x = entity->getLatestX(); + y = entity->getLatestY(); + z = entity->getLatestZ(); + orientation = entity->getOrientation(); + } + } + // Convert canonical → render coordinates glm::vec3 renderPos = core::coords::canonicalToRender(glm::vec3(x, y, z)); diff --git a/src/game/opcode_table.cpp b/src/game/opcode_table.cpp index 22acd50b..017cf616 100644 --- a/src/game/opcode_table.cpp +++ b/src/game/opcode_table.cpp @@ -126,6 +126,7 @@ static const OpcodeNameEntry kOpcodeNames[] = { {"CMSG_BINDER_ACTIVATE", LogicalOpcode::CMSG_BINDER_ACTIVATE}, {"SMSG_LOG_XPGAIN", LogicalOpcode::SMSG_LOG_XPGAIN}, {"SMSG_MONSTER_MOVE", LogicalOpcode::SMSG_MONSTER_MOVE}, + {"SMSG_COMPRESSED_MOVES", LogicalOpcode::SMSG_COMPRESSED_MOVES}, {"CMSG_ATTACKSWING", LogicalOpcode::CMSG_ATTACKSWING}, {"CMSG_ATTACKSTOP", LogicalOpcode::CMSG_ATTACKSTOP}, {"SMSG_ATTACKSTART", LogicalOpcode::SMSG_ATTACKSTART}, diff --git a/src/rendering/m2_renderer.cpp b/src/rendering/m2_renderer.cpp index d372ea50..fe689a18 100644 --- a/src/rendering/m2_renderer.cpp +++ b/src/rendering/m2_renderer.cpp @@ -1175,22 +1175,15 @@ bool M2Renderer::loadModel(const pipeline::M2Model& model, uint32_t modelId) { } bgpu.texture = tex; bgpu.hasAlpha = (tex != 0 && tex != whiteTexture); - bgpu.textureUnit = static_cast(batch.textureUnit & 0x1); + // textureCoordIndex is an index into a texture coord combo table, not directly + // a UV set selector. Most batches have index=0 (UV set 0). We always use UV set 0 + // since we don't have the full combo table — dual-UV effects are rare edge cases. + bgpu.textureUnit = 0; - // Resolve opacity: texture weight track × color animation alpha - // Batches whose texture failed to load are hidden (avoid white shell artifacts) + // Batch is hidden only when its named texture failed to load (avoids white shell artifacts). + // Do NOT bake transparency/color animation tracks here — they animate over time and + // baking the first keyframe value causes legitimate meshes to become invisible. bgpu.batchOpacity = texFailed ? 0.0f : 1.0f; - // Texture weight track (via transparency lookup) - if (batch.transparencyIndex < model.textureTransformLookup.size()) { - uint16_t trackIdx = model.textureTransformLookup[batch.transparencyIndex]; - if (trackIdx < model.textureWeights.size()) { - bgpu.batchOpacity *= model.textureWeights[trackIdx]; - } - } - // Color animation alpha (M2Color.alpha, indexed directly by colorIndex) - if (batch.colorIndex < model.colorAlphas.size()) { - bgpu.batchOpacity *= model.colorAlphas[batch.colorIndex]; - } // Compute batch center and radius for glow sprite positioning if (bgpu.blendMode >= 3 && batch.indexCount > 0) {