mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-03-26 16:50:15 +00:00
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:
parent
cbb3035313
commit
bf31da8c13
14 changed files with 556 additions and 55 deletions
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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(); }
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -205,9 +205,10 @@ bool Application::initialize() {
|
|||
renderer->getCharacterRenderer()->setAssetManager(assetManager.get());
|
||||
}
|
||||
|
||||
// Load transport paths from TransportAnimation.dbc
|
||||
// Load transport paths from TransportAnimation.dbc and TaxiPathNode.dbc
|
||||
if (gameHandler && gameHandler->getTransportManager()) {
|
||||
gameHandler->getTransportManager()->loadTransportAnimationDBC(assetManager.get());
|
||||
gameHandler->getTransportManager()->loadTaxiPathNodeDBC(assetManager.get());
|
||||
}
|
||||
|
||||
// Initialize HD texture packs
|
||||
|
|
@ -1487,7 +1488,7 @@ void Application::setupUICallbacks() {
|
|||
}
|
||||
|
||||
// Register the transport with spawn position (prevents rendering at origin until server update)
|
||||
transportManager->registerTransport(guid, wmoInstanceId, pathId, canonicalSpawnPos);
|
||||
transportManager->registerTransport(guid, wmoInstanceId, pathId, canonicalSpawnPos, entry);
|
||||
|
||||
// Server-authoritative movement - set initial position from spawn data
|
||||
glm::vec3 canonicalPos(x, y, z);
|
||||
|
|
@ -1502,6 +1503,20 @@ void Application::setupUICallbacks() {
|
|||
" pos=(", pending.x, ", ", pending.y, ", ", pending.z, ") orientation=", pending.orientation);
|
||||
pendingTransportMoves_.erase(pendingIt);
|
||||
}
|
||||
|
||||
// For MO_TRANSPORT at (0,0,0): check if GO data is already cached with a taxiPathId
|
||||
if (glm::length(canonicalSpawnPos) < 1.0f && gameHandler) {
|
||||
auto goData = gameHandler->getCachedGameObjectInfo(entry);
|
||||
if (goData && goData->type == 15 && goData->hasData && goData->data[0] != 0) {
|
||||
uint32_t taxiPathId = goData->data[0];
|
||||
if (transportManager->hasTaxiPath(taxiPathId)) {
|
||||
transportManager->assignTaxiPathToTransport(entry, taxiPathId);
|
||||
LOG_INFO("Assigned cached TaxiPathNode path for MO_TRANSPORT entry=", entry,
|
||||
" taxiPathId=", taxiPathId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (auto* tr = transportManager->getTransport(guid); tr) {
|
||||
LOG_INFO("Transport registered: guid=0x", std::hex, guid, std::dec,
|
||||
" entry=", entry, " displayId=", displayId,
|
||||
|
|
@ -1598,7 +1613,7 @@ void Application::setupUICallbacks() {
|
|||
" displayId=", displayId, " wmoInstance=", wmoInstanceId);
|
||||
}
|
||||
|
||||
transportManager->registerTransport(guid, wmoInstanceId, pathId, canonicalSpawnPos);
|
||||
transportManager->registerTransport(guid, wmoInstanceId, pathId, canonicalSpawnPos, entry);
|
||||
} else {
|
||||
pendingTransportMoves_[guid] = PendingTransportMove{x, y, z, orientation};
|
||||
LOG_WARNING("Cannot auto-spawn transport 0x", std::hex, guid, std::dec,
|
||||
|
|
|
|||
|
|
@ -5392,7 +5392,9 @@ void GameHandler::handleCreatureQueryResponse(network::Packet& packet) {
|
|||
|
||||
void GameHandler::handleGameObjectQueryResponse(network::Packet& packet) {
|
||||
GameObjectQueryResponseData data;
|
||||
if (!GameObjectQueryResponseParser::parse(packet, data)) return;
|
||||
bool ok = packetParsers_ ? packetParsers_->parseGameObjectQueryResponse(packet, data)
|
||||
: GameObjectQueryResponseParser::parse(packet, data);
|
||||
if (!ok) return;
|
||||
|
||||
pendingGameObjectQueries_.erase(data.entry);
|
||||
|
||||
|
|
@ -5407,6 +5409,19 @@ void GameHandler::handleGameObjectQueryResponse(network::Packet& packet) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MO_TRANSPORT (type 15): assign TaxiPathNode path if available
|
||||
if (data.type == 15 && data.hasData && data.data[0] != 0 && transportManager_) {
|
||||
uint32_t taxiPathId = data.data[0];
|
||||
if (transportManager_->hasTaxiPath(taxiPathId)) {
|
||||
if (transportManager_->assignTaxiPathToTransport(data.entry, taxiPathId)) {
|
||||
LOG_INFO("MO_TRANSPORT entry=", data.entry, " assigned TaxiPathNode path ", taxiPathId);
|
||||
}
|
||||
} else {
|
||||
LOG_INFO("MO_TRANSPORT entry=", data.entry, " taxiPathId=", taxiPathId,
|
||||
" not found in TaxiPathNode.dbc");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -580,6 +580,49 @@ bool ClassicPacketParsers::parseGuildQueryResponse(network::Packet& packet, Guil
|
|||
return true;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// GameObject Query — Classic has no extra strings before data[]
|
||||
// WotLK has iconName + castBarCaption + unk1 between names and data[].
|
||||
// Vanilla: entry, type, displayId, name[4], data[24]
|
||||
// ============================================================================
|
||||
|
||||
bool ClassicPacketParsers::parseGameObjectQueryResponse(network::Packet& packet, GameObjectQueryResponseData& data) {
|
||||
data.entry = packet.readUInt32();
|
||||
|
||||
// High bit set means gameobject not found
|
||||
if (data.entry & 0x80000000) {
|
||||
data.entry &= ~0x80000000;
|
||||
data.name = "";
|
||||
return true;
|
||||
}
|
||||
|
||||
data.type = packet.readUInt32();
|
||||
data.displayId = packet.readUInt32();
|
||||
// 4 name strings
|
||||
data.name = packet.readString();
|
||||
packet.readString();
|
||||
packet.readString();
|
||||
packet.readString();
|
||||
|
||||
// Classic: data[24] comes immediately after names (no extra strings)
|
||||
size_t remaining = packet.getSize() - packet.getReadPos();
|
||||
if (remaining >= 24 * 4) {
|
||||
for (int i = 0; i < 24; i++) {
|
||||
data.data[i] = packet.readUInt32();
|
||||
}
|
||||
data.hasData = true;
|
||||
}
|
||||
|
||||
if (data.type == 15) { // MO_TRANSPORT
|
||||
LOG_INFO("Classic GO query: MO_TRANSPORT entry=", data.entry,
|
||||
" name=\"", data.name, "\" displayId=", data.displayId,
|
||||
" taxiPathId=", data.data[0], " moveSpeed=", data.data[1]);
|
||||
} else {
|
||||
LOG_DEBUG("Classic GO query: ", data.name, " type=", data.type, " entry=", data.entry);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Gossip — Classic has no menuId, and quest items lack questFlags + isRepeatable
|
||||
// ============================================================================
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ void TransportManager::update(float deltaTime) {
|
|||
}
|
||||
}
|
||||
|
||||
void TransportManager::registerTransport(uint64_t guid, uint32_t wmoInstanceId, uint32_t pathId, const glm::vec3& spawnWorldPos) {
|
||||
void TransportManager::registerTransport(uint64_t guid, uint32_t wmoInstanceId, uint32_t pathId, const glm::vec3& spawnWorldPos, uint32_t entry) {
|
||||
auto pathIt = paths_.find(pathId);
|
||||
if (pathIt == paths_.end()) {
|
||||
std::cerr << "TransportManager: Path " << pathId << " not found for transport " << guid << std::endl;
|
||||
|
|
@ -44,6 +44,7 @@ void TransportManager::registerTransport(uint64_t guid, uint32_t wmoInstanceId,
|
|||
transport.guid = guid;
|
||||
transport.wmoInstanceId = wmoInstanceId;
|
||||
transport.pathId = pathId;
|
||||
transport.entry = entry;
|
||||
transport.allowBootstrapVelocity = false;
|
||||
|
||||
// CRITICAL: Set basePosition from spawn position and t=0 offset
|
||||
|
|
@ -52,6 +53,10 @@ void TransportManager::registerTransport(uint64_t guid, uint32_t wmoInstanceId,
|
|||
// Stationary transport - no path animation
|
||||
transport.basePosition = spawnWorldPos;
|
||||
transport.position = spawnWorldPos;
|
||||
} else if (path.worldCoords) {
|
||||
// World-coordinate path (TaxiPathNode) - points are absolute world positions
|
||||
transport.basePosition = glm::vec3(0.0f);
|
||||
transport.position = evalTimedCatmullRom(path, 0);
|
||||
} else {
|
||||
// Moving transport - infer base from first path offset
|
||||
glm::vec3 offset0 = evalTimedCatmullRom(path, 0);
|
||||
|
|
@ -542,6 +547,16 @@ void TransportManager::updateServerTransport(uint64_t guid, const glm::vec3& pos
|
|||
auto pathIt = paths_.find(transport->pathId);
|
||||
const bool hasPath = (pathIt != paths_.end());
|
||||
const bool isZOnlyPath = (hasPath && pathIt->second.fromDBC && pathIt->second.zOnly && pathIt->second.durationMs > 0);
|
||||
const bool isWorldCoordPath = (hasPath && pathIt->second.worldCoords && pathIt->second.durationMs > 0);
|
||||
|
||||
// Don't let (0,0,0) server updates override a TaxiPathNode world-coordinate path
|
||||
if (isWorldCoordPath && glm::length(position) < 1.0f) {
|
||||
transport->serverUpdateCount++;
|
||||
transport->lastServerUpdate = elapsedTime_;
|
||||
transport->serverYaw = orientation;
|
||||
transport->hasServerYaw = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// Track server updates
|
||||
transport->serverUpdateCount++;
|
||||
|
|
@ -940,6 +955,181 @@ bool TransportManager::loadTransportAnimationDBC(pipeline::AssetManager* assetMg
|
|||
return pathsLoaded > 0;
|
||||
}
|
||||
|
||||
bool TransportManager::loadTaxiPathNodeDBC(pipeline::AssetManager* assetMgr) {
|
||||
LOG_INFO("Loading TaxiPathNode.dbc...");
|
||||
|
||||
if (!assetMgr) {
|
||||
LOG_ERROR("AssetManager is null");
|
||||
return false;
|
||||
}
|
||||
|
||||
auto dbcData = assetMgr->readFile("DBFilesClient\\TaxiPathNode.dbc");
|
||||
if (dbcData.empty()) {
|
||||
LOG_WARNING("TaxiPathNode.dbc not found - MO_TRANSPORT will use fallback paths");
|
||||
return false;
|
||||
}
|
||||
|
||||
pipeline::DBCFile dbc;
|
||||
if (!dbc.load(dbcData)) {
|
||||
LOG_ERROR("Failed to parse TaxiPathNode.dbc");
|
||||
return false;
|
||||
}
|
||||
|
||||
LOG_INFO("TaxiPathNode.dbc: ", dbc.getRecordCount(), " records, ",
|
||||
dbc.getFieldCount(), " fields per record");
|
||||
|
||||
// Group nodes by PathID, storing (NodeIndex, MapID, X, Y, Z)
|
||||
struct TaxiNode {
|
||||
uint32_t nodeIndex;
|
||||
uint32_t mapId;
|
||||
float x, y, z;
|
||||
};
|
||||
std::map<uint32_t, std::vector<TaxiNode>> nodesByPath;
|
||||
|
||||
for (uint32_t i = 0; i < dbc.getRecordCount(); i++) {
|
||||
uint32_t pathId = dbc.getUInt32(i, 1); // PathID
|
||||
uint32_t nodeIdx = dbc.getUInt32(i, 2); // NodeIndex
|
||||
uint32_t mapId = dbc.getUInt32(i, 3); // MapID
|
||||
float posX = dbc.getFloat(i, 4); // X (server coords)
|
||||
float posY = dbc.getFloat(i, 5); // Y (server coords)
|
||||
float posZ = dbc.getFloat(i, 6); // Z (server coords)
|
||||
|
||||
nodesByPath[pathId].push_back({nodeIdx, mapId, posX, posY, posZ});
|
||||
}
|
||||
|
||||
// Build world-coordinate transport paths
|
||||
int pathsLoaded = 0;
|
||||
for (auto& [pathId, nodes] : nodesByPath) {
|
||||
if (nodes.size() < 2) continue;
|
||||
|
||||
// Sort by NodeIndex
|
||||
std::sort(nodes.begin(), nodes.end(),
|
||||
[](const TaxiNode& a, const TaxiNode& b) { return a.nodeIndex < b.nodeIndex; });
|
||||
|
||||
// Skip flight-master paths (nodes on different maps are map teleports)
|
||||
// Transport paths stay on the same map
|
||||
bool sameMap = true;
|
||||
uint32_t firstMap = nodes[0].mapId;
|
||||
for (const auto& node : nodes) {
|
||||
if (node.mapId != firstMap) { sameMap = false; break; }
|
||||
}
|
||||
|
||||
// Calculate total path distance to identify transport routes (long water crossings)
|
||||
float totalDist = 0.0f;
|
||||
for (size_t i = 1; i < nodes.size(); i++) {
|
||||
float dx = nodes[i].x - nodes[i-1].x;
|
||||
float dy = nodes[i].y - nodes[i-1].y;
|
||||
float dz = nodes[i].z - nodes[i-1].z;
|
||||
totalDist += std::sqrt(dx*dx + dy*dy + dz*dz);
|
||||
}
|
||||
|
||||
// Transport routes are typically >500 units long and stay on same map
|
||||
// Flight paths can also be long, but we'll store all same-map paths
|
||||
// and let the caller choose the right one by pathId
|
||||
if (!sameMap) continue;
|
||||
|
||||
// Build timed points using distance-based timing (28 units/sec default boat speed)
|
||||
const float transportSpeed = 28.0f; // units per second
|
||||
std::vector<TimedPoint> timedPoints;
|
||||
timedPoints.reserve(nodes.size() + 1);
|
||||
|
||||
uint32_t cumulativeMs = 0;
|
||||
for (size_t i = 0; i < nodes.size(); i++) {
|
||||
// Convert server coords to canonical
|
||||
glm::vec3 serverPos(nodes[i].x, nodes[i].y, nodes[i].z);
|
||||
glm::vec3 canonical = core::coords::serverToCanonical(serverPos);
|
||||
|
||||
timedPoints.push_back({cumulativeMs, canonical});
|
||||
|
||||
if (i + 1 < nodes.size()) {
|
||||
float dx = nodes[i+1].x - nodes[i].x;
|
||||
float dy = nodes[i+1].y - nodes[i].y;
|
||||
float dz = nodes[i+1].z - nodes[i].z;
|
||||
float segDist = std::sqrt(dx*dx + dy*dy + dz*dz);
|
||||
uint32_t segMs = static_cast<uint32_t>((segDist / transportSpeed) * 1000.0f);
|
||||
if (segMs < 100) segMs = 100; // Minimum 100ms per segment
|
||||
cumulativeMs += segMs;
|
||||
}
|
||||
}
|
||||
|
||||
// Add wrap point (return to start) for looping
|
||||
float wrapDx = nodes.front().x - nodes.back().x;
|
||||
float wrapDy = nodes.front().y - nodes.back().y;
|
||||
float wrapDz = nodes.front().z - nodes.back().z;
|
||||
float wrapDist = std::sqrt(wrapDx*wrapDx + wrapDy*wrapDy + wrapDz*wrapDz);
|
||||
uint32_t wrapMs = static_cast<uint32_t>((wrapDist / transportSpeed) * 1000.0f);
|
||||
if (wrapMs < 100) wrapMs = 100;
|
||||
cumulativeMs += wrapMs;
|
||||
timedPoints.push_back({cumulativeMs, timedPoints[0].pos});
|
||||
|
||||
TransportPath path;
|
||||
path.pathId = pathId;
|
||||
path.points = timedPoints;
|
||||
path.looping = false; // Explicit wrap point added
|
||||
path.durationMs = cumulativeMs;
|
||||
path.zOnly = false;
|
||||
path.fromDBC = true;
|
||||
path.worldCoords = true; // TaxiPathNode uses absolute world coordinates
|
||||
|
||||
taxiPaths_[pathId] = path;
|
||||
pathsLoaded++;
|
||||
}
|
||||
|
||||
LOG_INFO("Loaded ", pathsLoaded, " TaxiPathNode transport paths (", nodesByPath.size(), " total taxi paths)");
|
||||
return pathsLoaded > 0;
|
||||
}
|
||||
|
||||
bool TransportManager::hasTaxiPath(uint32_t taxiPathId) const {
|
||||
return taxiPaths_.find(taxiPathId) != taxiPaths_.end();
|
||||
}
|
||||
|
||||
bool TransportManager::assignTaxiPathToTransport(uint32_t entry, uint32_t taxiPathId) {
|
||||
auto taxiIt = taxiPaths_.find(taxiPathId);
|
||||
if (taxiIt == taxiPaths_.end()) {
|
||||
LOG_WARNING("No TaxiPathNode path for taxiPathId=", taxiPathId);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Find transport(s) with matching entry that are at (0,0,0)
|
||||
for (auto& [guid, transport] : transports_) {
|
||||
if (transport.entry != entry) continue;
|
||||
if (glm::length(transport.position) > 1.0f) continue; // Already has real position
|
||||
|
||||
// Copy the taxi path into the main paths_ map (indexed by entry for this transport)
|
||||
TransportPath path = taxiIt->second;
|
||||
path.pathId = entry; // Index by GO entry
|
||||
paths_[entry] = path;
|
||||
|
||||
// Update transport to use the new path
|
||||
transport.pathId = entry;
|
||||
transport.basePosition = glm::vec3(0.0f); // World-coordinate path, no base offset
|
||||
if (!path.points.empty()) {
|
||||
transport.position = evalTimedCatmullRom(path, 0);
|
||||
}
|
||||
transport.useClientAnimation = true; // Server won't send position updates
|
||||
|
||||
// Seed local clock to a deterministic phase
|
||||
if (path.durationMs > 0) {
|
||||
transport.localClockMs = static_cast<uint32_t>(elapsedTime_ * 1000.0f) % path.durationMs;
|
||||
}
|
||||
|
||||
updateTransformMatrices(transport);
|
||||
if (wmoRenderer_) {
|
||||
wmoRenderer_->setInstanceTransform(transport.wmoInstanceId, transport.transform);
|
||||
}
|
||||
|
||||
LOG_INFO("Assigned TaxiPathNode path to transport 0x", std::hex, guid, std::dec,
|
||||
" entry=", entry, " taxiPathId=", taxiPathId,
|
||||
" waypoints=", path.points.size(),
|
||||
" duration=", path.durationMs, "ms",
|
||||
" startPos=(", transport.position.x, ", ", transport.position.y, ", ", transport.position.z, ")");
|
||||
return true;
|
||||
}
|
||||
|
||||
LOG_DEBUG("No transport at (0,0,0) found for entry=", entry, " taxiPathId=", taxiPathId);
|
||||
return false;
|
||||
}
|
||||
|
||||
bool TransportManager::hasPathForEntry(uint32_t entry) const {
|
||||
auto it = paths_.find(entry);
|
||||
return it != paths_.end() && it->second.fromDBC;
|
||||
|
|
|
|||
|
|
@ -1962,7 +1962,7 @@ bool GameObjectQueryResponseParser::parse(network::Packet& packet, GameObjectQue
|
|||
}
|
||||
|
||||
data.type = packet.readUInt32(); // GameObjectType
|
||||
/*uint32_t displayId =*/ packet.readUInt32();
|
||||
data.displayId = packet.readUInt32();
|
||||
// 4 name strings (only first is usually populated)
|
||||
data.name = packet.readString();
|
||||
// name2, name3, name4
|
||||
|
|
@ -1970,6 +1970,20 @@ bool GameObjectQueryResponseParser::parse(network::Packet& packet, GameObjectQue
|
|||
packet.readString();
|
||||
packet.readString();
|
||||
|
||||
// WotLK: 3 extra strings before data[] (iconName, castBarCaption, unk1)
|
||||
packet.readString(); // iconName
|
||||
packet.readString(); // castBarCaption
|
||||
packet.readString(); // unk1
|
||||
|
||||
// Read 24 type-specific data fields
|
||||
size_t remaining = packet.getSize() - packet.getReadPos();
|
||||
if (remaining >= 24 * 4) {
|
||||
for (int i = 0; i < 24; i++) {
|
||||
data.data[i] = packet.readUInt32();
|
||||
}
|
||||
data.hasData = true;
|
||||
}
|
||||
|
||||
LOG_DEBUG("GameObject query response: ", data.name, " (type=", data.type, " entry=", data.entry, ")");
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -374,6 +374,17 @@ void ADTLoader::parseMCNK(const uint8_t* data, size_t size, int chunkIndex, ADTT
|
|||
uint32_t skip = (possibleMagic == MCAL) ? 8 : 0;
|
||||
parseMCAL(data + ofsAlpha + skip, sizeAlpha - skip, chunk);
|
||||
}
|
||||
|
||||
// Liquid (MCLQ) - vanilla/TBC per-chunk water (no MH2O in these expansions)
|
||||
// ofsLiquid at MCNK header offset 0x60, sizeLiquid at 0x64
|
||||
uint32_t ofsLiquid = readUInt32(data, 0x60);
|
||||
uint32_t sizeLiquid = readUInt32(data, 0x64);
|
||||
if (ofsLiquid > 0 && sizeLiquid > 8 && ofsLiquid + sizeLiquid <= size) {
|
||||
uint32_t possibleMagic = readUInt32(data, ofsLiquid);
|
||||
uint32_t skip = (possibleMagic == MCLQ) ? 8 : 0;
|
||||
parseMCLQ(data + ofsLiquid + skip, sizeLiquid - skip,
|
||||
chunkIndex, chunk.flags, terrain);
|
||||
}
|
||||
}
|
||||
|
||||
void ADTLoader::parseMCVT(const uint8_t* data, size_t size, MapChunk& chunk) {
|
||||
|
|
@ -453,6 +464,100 @@ void ADTLoader::parseMCAL(const uint8_t* data, size_t size, MapChunk& chunk) {
|
|||
std::memcpy(chunk.alphaMap.data(), data, size);
|
||||
}
|
||||
|
||||
void ADTLoader::parseMCLQ(const uint8_t* data, size_t size, int chunkIndex,
|
||||
uint32_t mcnkFlags, ADTTerrain& terrain) {
|
||||
// MCLQ: Vanilla/TBC per-chunk liquid data (inside MCNK)
|
||||
// Layout:
|
||||
// float minHeight, maxHeight (8 bytes)
|
||||
// SLiquidVertex[9*9] (81 * 8 = 648 bytes)
|
||||
// water: uint8 depth, flow0, flow1, filler, float height
|
||||
// magma: uint16 s, uint16 t, float height
|
||||
// uint8 tiles[8*8] (64 bytes)
|
||||
// Total minimum: 720 bytes
|
||||
|
||||
if (size < 720) {
|
||||
return; // Not enough data for a valid MCLQ
|
||||
}
|
||||
|
||||
float minHeight = readFloat(data, 0);
|
||||
float maxHeight = readFloat(data, 4);
|
||||
|
||||
// Determine liquid type from MCNK flags
|
||||
// 0x04 = has liquid (river/lake), 0x08 = ocean, 0x10 = magma, 0x20 = slime
|
||||
uint16_t liquidType = 0; // water
|
||||
if (mcnkFlags & 0x08) liquidType = 1; // ocean
|
||||
else if (mcnkFlags & 0x10) liquidType = 2; // magma
|
||||
else if (mcnkFlags & 0x20) liquidType = 3; // slime
|
||||
|
||||
// Read 9x9 height values (skip depth/flow bytes, just read the float height)
|
||||
const uint8_t* vertData = data + 8;
|
||||
std::vector<float> heights(81);
|
||||
for (int i = 0; i < 81; i++) {
|
||||
heights[i] = readFloat(vertData, i * 8 + 4); // float at offset 4 within each 8-byte vertex
|
||||
}
|
||||
|
||||
// Read 8x8 tile flags
|
||||
const uint8_t* tileData = data + 8 + 648;
|
||||
std::vector<uint8_t> tileMask(64);
|
||||
bool anyVisible = false;
|
||||
for (int i = 0; i < 64; i++) {
|
||||
uint8_t tileFlag = tileData[i];
|
||||
// Bit 0x0F = liquid type, bit 0x40 = fatigue, bit 0x80 = hidden
|
||||
// A tile is visible if NOT hidden (0x80 not set) and type is non-zero or has base flag
|
||||
bool hidden = (tileFlag & 0x80) != 0;
|
||||
tileMask[i] = hidden ? 0 : 1;
|
||||
if (!hidden) anyVisible = true;
|
||||
}
|
||||
|
||||
if (!anyVisible) {
|
||||
return; // All tiles hidden, no visible water
|
||||
}
|
||||
|
||||
// Validate heights - if all heights are 0 or unreasonable, skip
|
||||
bool validHeights = false;
|
||||
for (float h : heights) {
|
||||
if (h != 0.0f && std::isfinite(h)) {
|
||||
validHeights = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// If heights are all zero, use maxHeight as flat water level
|
||||
if (!validHeights) {
|
||||
for (float& h : heights) h = maxHeight;
|
||||
}
|
||||
|
||||
// Build a WaterLayer matching the MH2O format
|
||||
ADTTerrain::WaterLayer layer;
|
||||
layer.liquidType = liquidType;
|
||||
layer.flags = 0;
|
||||
layer.minHeight = minHeight;
|
||||
layer.maxHeight = maxHeight;
|
||||
layer.x = 0;
|
||||
layer.y = 0;
|
||||
layer.width = 8; // 8 tiles = 9 vertices per axis
|
||||
layer.height = 8;
|
||||
layer.heights = std::move(heights);
|
||||
layer.mask.resize(8); // 8 bytes = 64 bits for 8x8 tiles
|
||||
for (int row = 0; row < 8; row++) {
|
||||
uint8_t rowBits = 0;
|
||||
for (int col = 0; col < 8; col++) {
|
||||
if (tileMask[row * 8 + col]) {
|
||||
rowBits |= (1 << col);
|
||||
}
|
||||
}
|
||||
layer.mask[row] = rowBits;
|
||||
}
|
||||
|
||||
terrain.waterData[chunkIndex].layers.push_back(std::move(layer));
|
||||
|
||||
static int mclqLogCount = 0;
|
||||
if (mclqLogCount < 5) {
|
||||
LOG_INFO("MCLQ[", chunkIndex, "]: type=", liquidType,
|
||||
" height=[", minHeight, ",", maxHeight, "]");
|
||||
mclqLogCount++;
|
||||
}
|
||||
}
|
||||
|
||||
void ADTLoader::parseMH2O(const uint8_t* data, size_t size, ADTTerrain& terrain) {
|
||||
// MH2O contains water/liquid data for all 256 map chunks
|
||||
// Structure: 256 SMLiquidChunk headers followed by instance data
|
||||
|
|
|
|||
|
|
@ -1050,26 +1050,34 @@ M2Model M2Loader::load(const std::vector<uint8_t>& m2Data) {
|
|||
model.attachmentLookup = readArray<uint16_t>(m2Data, header.ofsAttachmentLookup, header.nAttachmentLookup);
|
||||
}
|
||||
|
||||
// Parse particle emitters (WotLK M2ParticleOld: 0x1DC = 476 bytes per emitter)
|
||||
// Skip for vanilla — emitter struct size differs
|
||||
static constexpr uint32_t EMITTER_STRUCT_SIZE = 0x1DC;
|
||||
if (header.version >= 264 &&
|
||||
header.nParticleEmitters > 0 && header.ofsParticleEmitters > 0 &&
|
||||
header.nParticleEmitters < 256 &&
|
||||
static_cast<size_t>(header.ofsParticleEmitters) +
|
||||
static_cast<size_t>(header.nParticleEmitters) * EMITTER_STRUCT_SIZE <= m2Data.size()) {
|
||||
// Parse particle emitters — struct size differs between versions:
|
||||
// WotLK (version >= 264): M2ParticleOld = 0x1DC (476) bytes, M2TrackDisk (20 bytes), FBlocks
|
||||
// Vanilla (version < 264): 0x1F8 (504) bytes, M2TrackDiskVanilla (28 bytes), static lifecycle arrays
|
||||
if (header.nParticleEmitters > 0 && header.ofsParticleEmitters > 0 &&
|
||||
header.nParticleEmitters < 256) {
|
||||
|
||||
// Build sequence flags for parseAnimTrack
|
||||
const bool isVanilla = (header.version < 264);
|
||||
static constexpr uint32_t EMITTER_SIZE_WOTLK = 0x1DC; // 476
|
||||
static constexpr uint32_t EMITTER_SIZE_VANILLA = 0x1F8; // 504
|
||||
const uint32_t emitterSize = isVanilla ? EMITTER_SIZE_VANILLA : EMITTER_SIZE_WOTLK;
|
||||
|
||||
if (static_cast<size_t>(header.ofsParticleEmitters) +
|
||||
static_cast<size_t>(header.nParticleEmitters) * emitterSize <= m2Data.size()) {
|
||||
|
||||
// Build sequence flags for parseAnimTrack (WotLK only)
|
||||
std::vector<uint32_t> emSeqFlags;
|
||||
emSeqFlags.reserve(model.sequences.size());
|
||||
for (const auto& seq : model.sequences) {
|
||||
emSeqFlags.push_back(seq.flags);
|
||||
if (!isVanilla) {
|
||||
emSeqFlags.reserve(model.sequences.size());
|
||||
for (const auto& seq : model.sequences) {
|
||||
emSeqFlags.push_back(seq.flags);
|
||||
}
|
||||
}
|
||||
|
||||
for (uint32_t ei = 0; ei < header.nParticleEmitters; ei++) {
|
||||
uint32_t base = header.ofsParticleEmitters + ei * EMITTER_STRUCT_SIZE;
|
||||
uint32_t base = header.ofsParticleEmitters + ei * emitterSize;
|
||||
|
||||
M2ParticleEmitter em;
|
||||
// Header fields (0x00-0x33) are the same for both versions
|
||||
em.particleId = readValue<int32_t>(m2Data, base + 0x00);
|
||||
em.flags = readValue<uint32_t>(m2Data, base + 0x04);
|
||||
em.position.x = readValue<float>(m2Data, base + 0x08);
|
||||
|
|
@ -1085,32 +1093,97 @@ M2Model M2Loader::load(const std::vector<uint8_t>& m2Data) {
|
|||
if (em.textureRows == 0) em.textureRows = 1;
|
||||
if (em.textureCols == 0) em.textureCols = 1;
|
||||
|
||||
// Parse animated tracks (M2TrackDisk at known offsets)
|
||||
auto parseTrack = [&](uint32_t off, M2AnimationTrack& track) {
|
||||
if (base + off + sizeof(M2TrackDisk) <= m2Data.size()) {
|
||||
M2TrackDisk disk = readValue<M2TrackDisk>(m2Data, base + off);
|
||||
parseAnimTrack(m2Data, disk, track, TrackType::FLOAT, emSeqFlags);
|
||||
}
|
||||
};
|
||||
parseTrack(0x34, em.emissionSpeed);
|
||||
parseTrack(0x48, em.speedVariation);
|
||||
parseTrack(0x5C, em.verticalRange);
|
||||
parseTrack(0x70, em.horizontalRange);
|
||||
parseTrack(0x84, em.gravity);
|
||||
parseTrack(0x98, em.lifespan);
|
||||
parseTrack(0xB0, em.emissionRate);
|
||||
parseTrack(0xC8, em.emissionAreaLength);
|
||||
parseTrack(0xDC, em.emissionAreaWidth);
|
||||
parseTrack(0xF0, em.deceleration);
|
||||
if (isVanilla) {
|
||||
// Vanilla: 10 contiguous M2TrackDiskVanilla tracks (28 bytes each) at 0x34
|
||||
auto parseTrackV = [&](uint32_t off, M2AnimationTrack& track) {
|
||||
if (base + off + sizeof(M2TrackDiskVanilla) <= m2Data.size()) {
|
||||
M2TrackDiskVanilla disk = readValue<M2TrackDiskVanilla>(m2Data, base + off);
|
||||
parseAnimTrackVanilla(m2Data, disk, track, TrackType::FLOAT);
|
||||
}
|
||||
};
|
||||
parseTrackV(0x34, em.emissionSpeed); // +28 = 0x50
|
||||
parseTrackV(0x50, em.speedVariation); // +28 = 0x6C
|
||||
parseTrackV(0x6C, em.verticalRange); // +28 = 0x88
|
||||
parseTrackV(0x88, em.horizontalRange); // +28 = 0xA4
|
||||
parseTrackV(0xA4, em.gravity); // +28 = 0xC0
|
||||
parseTrackV(0xC0, em.lifespan); // +28 = 0xDC
|
||||
parseTrackV(0xDC, em.emissionRate); // +28 = 0xF8
|
||||
parseTrackV(0xF8, em.emissionAreaLength); // +28 = 0x114
|
||||
parseTrackV(0x114, em.emissionAreaWidth); // +28 = 0x130
|
||||
parseTrackV(0x130, em.deceleration); // +28 = 0x14C
|
||||
|
||||
// Parse FBlocks (color, alpha, scale) — FBlocks are 16 bytes each
|
||||
parseFBlock(m2Data, base + 0x104, em.particleColor, 0);
|
||||
parseFBlock(m2Data, base + 0x114, em.particleAlpha, 1);
|
||||
parseFBlock(m2Data, base + 0x124, em.particleScale, 2);
|
||||
// Vanilla: NO FBlocks — color/alpha/scale are static inline values
|
||||
// Layout (empirically confirmed from real vanilla M2 files):
|
||||
// +0x14C: float midpoint (lifecycle split: 0→mid→1)
|
||||
// +0x150: uint32 colorValues[3] (BGRA, A channel = opacity)
|
||||
// +0x15C: float scaleValues[3] (1D particle scale)
|
||||
float midpoint = readValue<float>(m2Data, base + 0x14C);
|
||||
if (midpoint < 0.0f || midpoint > 1.0f) midpoint = 0.5f;
|
||||
|
||||
// Synthesize color FBlock from static BGRA values
|
||||
{
|
||||
em.particleColor.timestamps = {0.0f, midpoint, 1.0f};
|
||||
em.particleColor.vec3Values.resize(3);
|
||||
em.particleAlpha.timestamps = {0.0f, midpoint, 1.0f};
|
||||
em.particleAlpha.floatValues.resize(3);
|
||||
for (int c = 0; c < 3; c++) {
|
||||
uint32_t bgra = readValue<uint32_t>(m2Data, base + 0x150 + c * 4);
|
||||
float b = ((bgra >> 0) & 0xFF) / 255.0f;
|
||||
float g = ((bgra >> 8) & 0xFF) / 255.0f;
|
||||
float r = ((bgra >> 16) & 0xFF) / 255.0f;
|
||||
float a = ((bgra >> 24) & 0xFF) / 255.0f;
|
||||
em.particleColor.vec3Values[c] = glm::vec3(r, g, b);
|
||||
em.particleAlpha.floatValues[c] = a;
|
||||
}
|
||||
// If all alpha zero, use sensible default (fade out)
|
||||
bool allZero = true;
|
||||
for (auto v : em.particleAlpha.floatValues) {
|
||||
if (v > 0.01f) { allZero = false; break; }
|
||||
}
|
||||
if (allZero) {
|
||||
em.particleAlpha.floatValues = {1.0f, 1.0f, 0.0f};
|
||||
}
|
||||
}
|
||||
|
||||
// Synthesize scale FBlock from static float values
|
||||
{
|
||||
em.particleScale.timestamps = {0.0f, midpoint, 1.0f};
|
||||
em.particleScale.floatValues.resize(3);
|
||||
for (int s = 0; s < 3; s++) {
|
||||
float scale = readValue<float>(m2Data, base + 0x15C + s * 4);
|
||||
if (scale < 0.001f || scale > 100.0f) scale = 1.0f;
|
||||
em.particleScale.floatValues[s] = scale;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// WotLK: M2TrackDisk (20 bytes) at known offsets with vary floats interspersed
|
||||
auto parseTrack = [&](uint32_t off, M2AnimationTrack& track) {
|
||||
if (base + off + sizeof(M2TrackDisk) <= m2Data.size()) {
|
||||
M2TrackDisk disk = readValue<M2TrackDisk>(m2Data, base + off);
|
||||
parseAnimTrack(m2Data, disk, track, TrackType::FLOAT, emSeqFlags);
|
||||
}
|
||||
};
|
||||
parseTrack(0x34, em.emissionSpeed);
|
||||
parseTrack(0x48, em.speedVariation);
|
||||
parseTrack(0x5C, em.verticalRange);
|
||||
parseTrack(0x70, em.horizontalRange);
|
||||
parseTrack(0x84, em.gravity);
|
||||
parseTrack(0x98, em.lifespan);
|
||||
parseTrack(0xB0, em.emissionRate);
|
||||
parseTrack(0xC8, em.emissionAreaLength);
|
||||
parseTrack(0xDC, em.emissionAreaWidth);
|
||||
parseTrack(0xF0, em.deceleration);
|
||||
|
||||
// Parse FBlocks (color, alpha, scale) — FBlocks are 16 bytes each
|
||||
parseFBlock(m2Data, base + 0x104, em.particleColor, 0);
|
||||
parseFBlock(m2Data, base + 0x114, em.particleAlpha, 1);
|
||||
parseFBlock(m2Data, base + 0x124, em.particleScale, 2);
|
||||
}
|
||||
|
||||
model.particleEmitters.push_back(std::move(em));
|
||||
}
|
||||
core::Logger::getInstance().debug(" Particle emitters: ", model.particleEmitters.size());
|
||||
} // end size check
|
||||
}
|
||||
|
||||
// Read collision mesh (bounding triangles/vertices/normals)
|
||||
|
|
|
|||
|
|
@ -223,11 +223,20 @@ bool CharacterPreview::loadCharacter(game::Race race, game::Gender gender,
|
|||
foundUnderwear = true;
|
||||
}
|
||||
}
|
||||
|
||||
LOG_INFO("CharSections lookup: skin=", foundSkin ? bodySkinPath_ : "(not found)",
|
||||
" face=", foundFace ? (faceLowerPath.empty() ? "(empty)" : faceLowerPath) : "(not found)",
|
||||
" hair=", foundHair ? (hairScalpPath.empty() ? "(empty)" : hairScalpPath) : "(not found)",
|
||||
" underwear=", foundUnderwear, " (", underwearPaths.size(), " textures)");
|
||||
} else {
|
||||
LOG_WARNING("CharSections.dbc not loaded — no character textures");
|
||||
}
|
||||
|
||||
// Assign texture filenames on model before GPU upload
|
||||
for (size_t ti = 0; ti < model.textures.size(); ti++) {
|
||||
auto& tex = model.textures[ti];
|
||||
LOG_INFO(" Model texture[", ti, "]: type=", tex.type,
|
||||
" filename='", tex.filename, "'");
|
||||
if (tex.type == 1 && tex.filename.empty() && !bodySkinPath_.empty()) {
|
||||
tex.filename = bodySkinPath_;
|
||||
} else if (tex.type == 6 && tex.filename.empty() && !hairScalpPath.empty()) {
|
||||
|
|
|
|||
|
|
@ -59,18 +59,6 @@ void CharacterScreen::render(game::GameHandler& gameHandler) {
|
|||
// Get character list
|
||||
const auto& characters = gameHandler.getCharacters();
|
||||
|
||||
// Handle disconnected state (e.g. Warden kicked us)
|
||||
if (gameHandler.getState() == game::WorldState::DISCONNECTED ||
|
||||
gameHandler.getState() == game::WorldState::FAILED) {
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.4f, 1.0f), "Disconnected from server.");
|
||||
ImGui::TextWrapped("The server closed the connection. This may be caused by "
|
||||
"anti-cheat (Warden) verification failure.");
|
||||
ImGui::Spacing();
|
||||
if (ImGui::Button("Back", ImVec2(120, 36))) { if (onBack) onBack(); }
|
||||
ImGui::End();
|
||||
return;
|
||||
}
|
||||
|
||||
// Request character list if not available.
|
||||
// Also show a loading state while CHAR_LIST_REQUESTED is in-flight (characters may be cleared to avoid stale UI).
|
||||
if (characters.empty() &&
|
||||
|
|
@ -84,6 +72,18 @@ void CharacterScreen::render(game::GameHandler& gameHandler) {
|
|||
return;
|
||||
}
|
||||
|
||||
// Handle disconnected state with no characters received
|
||||
if (characters.empty() &&
|
||||
(gameHandler.getState() == game::WorldState::DISCONNECTED ||
|
||||
gameHandler.getState() == game::WorldState::FAILED)) {
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.4f, 1.0f), "Disconnected from server.");
|
||||
ImGui::TextWrapped("The server closed the connection before sending the character list.");
|
||||
ImGui::Spacing();
|
||||
if (ImGui::Button("Back", ImVec2(120, 36))) { if (onBack) onBack(); }
|
||||
ImGui::End();
|
||||
return;
|
||||
}
|
||||
|
||||
if (characters.empty()) {
|
||||
ImGui::Text("No characters available.");
|
||||
// Bottom buttons even when empty
|
||||
|
|
@ -343,6 +343,12 @@ void CharacterScreen::render(game::GameHandler& gameHandler) {
|
|||
|
||||
// Enter World button — full width
|
||||
float btnW = ImGui::GetContentRegionAvail().x;
|
||||
bool disconnected = (gameHandler.getState() == game::WorldState::DISCONNECTED ||
|
||||
gameHandler.getState() == game::WorldState::FAILED);
|
||||
if (disconnected) {
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.6f, 0.3f, 1.0f), "Connection lost — click Back to reconnect");
|
||||
}
|
||||
if (disconnected) ImGui::BeginDisabled();
|
||||
if (ImGui::Button("Enter World", ImVec2(btnW, 44))) {
|
||||
characterSelected = true;
|
||||
saveLastCharacter(character.guid);
|
||||
|
|
@ -352,6 +358,7 @@ void CharacterScreen::render(game::GameHandler& gameHandler) {
|
|||
gameHandler.selectCharacter(character.guid);
|
||||
if (onCharacterSelected) onCharacterSelected(character.guid);
|
||||
}
|
||||
if (disconnected) ImGui::EndDisabled();
|
||||
|
||||
ImGui::EndChild();
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue