Add taxi system, fix WMO interior lighting, ramp collision, and /unstuck

- Implement flight path system: SMSG_SHOWTAXINODES parser, CMSG_ACTIVATETAXIEXPRESS builder, BFS multi-hop pathfinding through TaxiNodes/TaxiPath DBC, taxi destination UI, movement blocking during flight
- Fix WMO interiors too dark by boosting vertex color lighting multiplier
- Dim M2 objects inside WMO interiors (rugs, furniture) via per-instance interior detection
- Fix ramp/stair clipping by lowering wall collision normal threshold from 0.85 to 0.55
- Restore 5-sample cardinal footprint for ground detection to fix rug slipping
- Fix /unstuck command to reset player Z to WMO/terrain floor height
- Handle MSG_MOVE_TELEPORT_ACK and SMSG_TRANSFER_PENDING for hearthstone teleports
- Fix spawning under Stormwind with online-mode camera controller reset
This commit is contained in:
Kelsi 2026-02-07 16:59:20 -08:00
parent c5a1fe927b
commit 3c2a728ec4
15 changed files with 691 additions and 108 deletions

View file

@ -364,6 +364,11 @@ public:
using WorldEntryCallback = std::function<void(uint32_t mapId, float x, float y, float z)>;
void setWorldEntryCallback(WorldEntryCallback cb) { worldEntryCallback_ = std::move(cb); }
// Unstuck callback (resets player Z to floor height)
using UnstuckCallback = std::function<void()>;
void setUnstuckCallback(UnstuckCallback cb) { unstuckCallback_ = std::move(cb); }
void unstuck();
// Creature spawn callback (online mode - triggered when creature enters view)
// Parameters: guid, displayId, x, y, z (canonical), orientation
using CreatureSpawnCallback = std::function<void(uint64_t guid, uint32_t displayId, float x, float y, float z, float orientation)>;
@ -450,6 +455,27 @@ public:
}
const std::unordered_map<uint64_t, QuestGiverStatus>& getNpcQuestStatuses() const { return npcQuestStatus_; }
// Taxi / Flight Paths
bool isTaxiWindowOpen() const { return taxiWindowOpen_; }
void closeTaxi();
void activateTaxi(uint32_t destNodeId);
bool isOnTaxiFlight() const { return onTaxiFlight_; }
const ShowTaxiNodesData& getTaxiData() const { return currentTaxiData_; }
uint32_t getTaxiCurrentNode() const { return currentTaxiData_.nearestNode; }
struct TaxiNode {
uint32_t id = 0;
uint32_t mapId = 0;
float x = 0, y = 0, z = 0;
std::string name;
};
struct TaxiPathEdge {
uint32_t pathId = 0;
uint32_t fromNode = 0, toNode = 0;
uint32_t cost = 0;
};
const std::unordered_map<uint32_t, TaxiNode>& getTaxiNodes() const { return taxiNodes_; }
// Vendor
void openVendor(uint64_t npcGuid);
void closeVendor();
@ -601,6 +627,14 @@ private:
void handleListInventory(network::Packet& packet);
void addMoneyCopper(uint32_t amount);
// ---- Teleport handler ----
void handleTeleportAck(network::Packet& packet);
// ---- Taxi handlers ----
void handleShowTaxiNodes(network::Packet& packet);
void handleActivateTaxiReply(network::Packet& packet);
void loadTaxiDbc();
// ---- Server info handlers ----
void handleQueryTimeResponse(network::Packet& packet);
void handlePlayedTime(network::Packet& packet);
@ -686,8 +720,9 @@ private:
float pingInterval = 30.0f; // Ping interval (30 seconds)
uint32_t lastLatency = 0; // Last measured latency (milliseconds)
// Player GUID
// Player GUID and map
uint64_t playerGuid = 0;
uint32_t currentMapId_ = 0;
// ---- Phase 1: Name caches ----
std::unordered_map<uint64_t, std::string> playerNameCache;
@ -742,6 +777,7 @@ private:
// ---- Phase 3: Spells ----
WorldEntryCallback worldEntryCallback_;
UnstuckCallback unstuckCallback_;
CreatureSpawnCallback creatureSpawnCallback_;
CreatureDespawnCallback creatureDespawnCallback_;
CreatureMoveCallback creatureMoveCallback_;
@ -800,6 +836,15 @@ private:
return it != factionHostileMap_.end() ? it->second : true; // default hostile if unknown
}
// Taxi / Flight Paths
std::unordered_map<uint32_t, TaxiNode> taxiNodes_;
std::vector<TaxiPathEdge> taxiPathEdges_;
bool taxiDbcLoaded_ = false;
bool taxiWindowOpen_ = false;
ShowTaxiNodesData currentTaxiData_;
uint64_t taxiNpcGuid_ = 0;
bool onTaxiFlight_ = false;
// Vendor
bool vendorWindowOpen = false;
ListInventoryData currentVendorItems;

View file

@ -236,6 +236,15 @@ enum class Opcode : uint16_t {
// ---- Death/Respawn ----
CMSG_REPOP_REQUEST = 0x015A,
CMSG_SPIRIT_HEALER_ACTIVATE = 0x0176,
// ---- Teleport / Transfer ----
MSG_MOVE_TELEPORT_ACK = 0x0C7,
SMSG_TRANSFER_PENDING = 0x003F,
// ---- Taxi / Flight Paths ----
SMSG_SHOWTAXINODES = 0x01A9,
SMSG_ACTIVATETAXIREPLY = 0x01AE,
CMSG_ACTIVATETAXIEXPRESS = 0x0312,
};
} // namespace game

View file

@ -1652,6 +1652,48 @@ public:
static bool parse(network::Packet& packet, ListInventoryData& data);
};
// ============================================================
// Taxi / Flight Paths
// ============================================================
static constexpr uint32_t TLK_TAXI_MASK_SIZE = 12;
/** SMSG_SHOWTAXINODES data */
struct ShowTaxiNodesData {
uint32_t windowInfo = 0; // 1 = show window
uint64_t npcGuid = 0;
uint32_t nearestNode = 0; // Taxi node player is at
uint32_t nodeMask[TLK_TAXI_MASK_SIZE] = {};
bool isNodeKnown(uint32_t nodeId) const {
uint32_t idx = nodeId / 32;
uint32_t bit = nodeId % 32;
return idx < TLK_TAXI_MASK_SIZE && (nodeMask[idx] & (1u << bit));
}
};
/** SMSG_SHOWTAXINODES parser */
class ShowTaxiNodesParser {
public:
static bool parse(network::Packet& packet, ShowTaxiNodesData& data);
};
/** SMSG_ACTIVATETAXIREPLY data */
struct ActivateTaxiReplyData {
uint32_t result = 0; // 0 = OK
};
/** SMSG_ACTIVATETAXIREPLY parser */
class ActivateTaxiReplyParser {
public:
static bool parse(network::Packet& packet, ActivateTaxiReplyData& data);
};
/** CMSG_ACTIVATETAXIEXPRESS packet builder */
class ActivateTaxiExpressPacket {
public:
static network::Packet build(uint64_t npcGuid, const std::vector<uint32_t>& pathNodes);
};
/** CMSG_REPOP_REQUEST packet builder */
class RepopRequestPacket {
public:

View file

@ -42,6 +42,7 @@ public:
}
void reset();
void setOnlineMode(bool online) { onlineMode = online; }
void startIntroPan(float durationSec = 2.8f, float orbitDegrees = 140.0f);
bool isIntroActive() const { return introActive; }
@ -63,6 +64,7 @@ public:
bool isSitting() const { return sitting; }
bool isSwimming() const { return swimming; }
const glm::vec3* getFollowTarget() const { return followTarget; }
glm::vec3* getFollowTargetMutable() { return followTarget; }
// Movement callback for sending opcodes to server
using MovementCallback = std::function<void(uint32_t opcode)>;
@ -134,10 +136,6 @@ private:
static constexpr float JUMP_BUFFER_TIME = 0.15f; // 150ms input buffer
static constexpr float COYOTE_TIME = 0.10f; // 100ms grace after leaving ground
// Cached floor height between frames (skip expensive probes when barely moving)
std::optional<float> cachedFloorHeight;
glm::vec2 cachedFloorPos = glm::vec2(0.0f);
// Cached isInsideWMO result (throttled to avoid per-frame cost)
bool cachedInsideWMO = false;
int insideWMOCheckCounter = 0;
@ -188,6 +186,9 @@ private:
static constexpr float WOW_GRAVITY = -19.29f;
static constexpr float WOW_JUMP_VELOCITY = 7.96f;
// Online mode: trust server position, don't prefer outdoors over WMO floors
bool onlineMode = false;
// Default spawn position (Goldshire Inn)
glm::vec3 defaultPosition = glm::vec3(-9464.0f, 62.0f, 200.0f);
float defaultYaw = 0.0f;

View file

@ -22,6 +22,7 @@ namespace rendering {
class Shader;
class Camera;
class WMORenderer;
/**
* GPU representation of an M2 model
@ -276,7 +277,10 @@ public:
}
void clearShadowMap() { shadowEnabled = false; }
void setWMORenderer(WMORenderer* wmo) { wmoRenderer = wmo; }
private:
WMORenderer* wmoRenderer = nullptr;
pipeline::AssetManager* assetManager = nullptr;
std::unique_ptr<Shader> shader;

View file

@ -203,6 +203,12 @@ public:
*/
bool isInsideWMO(float glX, float glY, float glZ, uint32_t* outModelId = nullptr) const;
/**
* Check if a position is inside an interior WMO group (flag 0x2000).
* Used to dim M2 lighting for doodads placed indoors.
*/
bool isInsideInteriorWMO(float glX, float glY, float glZ) const;
/**
* Raycast against WMO bounding boxes for camera collision
* @param origin Ray origin (e.g., character head position)

View file

@ -143,6 +143,7 @@ private:
void renderQuestRequestItemsWindow(game::GameHandler& gameHandler);
void renderQuestOfferRewardWindow(game::GameHandler& gameHandler);
void renderVendorWindow(game::GameHandler& gameHandler);
void renderTaxiWindow(game::GameHandler& gameHandler);
void renderDeathScreen(game::GameHandler& gameHandler);
void renderEscapeMenu();
void renderSettingsWindow();