mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-03-22 23:30:14 +00:00
fix: stabilize turtle world entry session handling
This commit is contained in:
parent
4dba20b757
commit
b0fafe5efa
20 changed files with 2283 additions and 1380 deletions
|
|
@ -249,6 +249,7 @@
|
|||
"SMSG_BATTLEGROUND_PLAYER_JOINED": "0x2EC",
|
||||
"SMSG_BATTLEGROUND_PLAYER_LEFT": "0x2ED",
|
||||
"CMSG_BATTLEMASTER_JOIN": "0x2EE",
|
||||
"SMSG_ADDON_INFO": "0x2EF",
|
||||
"CMSG_EMOTE": "0x102",
|
||||
"SMSG_EMOTE": "0x103",
|
||||
"CMSG_TEXT_EMOTE": "0x104",
|
||||
|
|
|
|||
|
|
@ -224,6 +224,7 @@ private:
|
|||
std::future<PreparedCreatureModel> future;
|
||||
};
|
||||
std::vector<AsyncCreatureLoad> asyncCreatureLoads_;
|
||||
std::unordered_set<uint32_t> asyncCreatureDisplayLoads_; // displayIds currently loading in background
|
||||
void processAsyncCreatureResults(bool unlimited = false);
|
||||
static constexpr int MAX_ASYNC_CREATURE_LOADS = 4; // concurrent background loads
|
||||
std::unordered_set<uint64_t> deadCreatureGuids_; // GUIDs that should spawn in corpse/death pose
|
||||
|
|
@ -280,7 +281,17 @@ private:
|
|||
float z = 0.0f;
|
||||
float orientation = 0.0f;
|
||||
};
|
||||
struct PendingTransportRegistration {
|
||||
uint64_t guid = 0;
|
||||
uint32_t entry = 0;
|
||||
uint32_t displayId = 0;
|
||||
float x = 0.0f;
|
||||
float y = 0.0f;
|
||||
float z = 0.0f;
|
||||
float orientation = 0.0f;
|
||||
};
|
||||
std::unordered_map<uint64_t, PendingTransportMove> pendingTransportMoves_; // guid -> latest pre-registration move
|
||||
std::deque<PendingTransportRegistration> pendingTransportRegistrations_;
|
||||
uint32_t nextGameObjectModelId_ = 20000;
|
||||
uint32_t nextGameObjectWmoModelId_ = 40000;
|
||||
bool testTransportSetup_ = false;
|
||||
|
|
@ -433,6 +444,7 @@ private:
|
|||
};
|
||||
std::vector<PendingTransportDoodadBatch> pendingTransportDoodadBatches_;
|
||||
static constexpr size_t MAX_TRANSPORT_DOODADS_PER_FRAME = 4;
|
||||
void processPendingTransportRegistrations();
|
||||
void processPendingTransportDoodads();
|
||||
|
||||
// Quest marker billboard sprites (above NPCs)
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
#include "game/inventory.hpp"
|
||||
#include "game/spell_defines.hpp"
|
||||
#include "game/group_defines.hpp"
|
||||
#include "network/packet.hpp"
|
||||
#include <glm/glm.hpp>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
|
@ -2089,6 +2090,15 @@ private:
|
|||
* Handle incoming packet from world server
|
||||
*/
|
||||
void handlePacket(network::Packet& packet);
|
||||
void enqueueIncomingPacket(const network::Packet& packet);
|
||||
void enqueueIncomingPacketFront(network::Packet&& packet);
|
||||
void processQueuedIncomingPackets();
|
||||
void enqueueUpdateObjectWork(UpdateObjectData&& data);
|
||||
void processPendingUpdateObjectWork(const std::chrono::steady_clock::time_point& start,
|
||||
float budgetMs);
|
||||
void processOutOfRangeObjects(const std::vector<uint64_t>& guids);
|
||||
void applyUpdateObjectBlock(const UpdateBlock& block, bool& newItemCreated);
|
||||
void finalizeUpdateObjectBatch(bool newItemCreated);
|
||||
|
||||
/**
|
||||
* Handle SMSG_AUTH_CHALLENGE from server
|
||||
|
|
@ -2413,6 +2423,14 @@ private:
|
|||
|
||||
// Network
|
||||
std::unique_ptr<network::WorldSocket> socket;
|
||||
std::deque<network::Packet> pendingIncomingPackets_;
|
||||
struct PendingUpdateObjectWork {
|
||||
UpdateObjectData data;
|
||||
size_t nextBlockIndex = 0;
|
||||
bool outOfRangeProcessed = false;
|
||||
bool newItemCreated = false;
|
||||
};
|
||||
std::deque<PendingUpdateObjectWork> pendingUpdateObjectWork_;
|
||||
|
||||
// State
|
||||
WorldState state = WorldState::DISCONNECTED;
|
||||
|
|
|
|||
|
|
@ -49,12 +49,14 @@ public:
|
|||
/** Number of mapped opcodes. */
|
||||
size_t size() const { return logicalToWire_.size(); }
|
||||
|
||||
/** Get canonical enum name for a logical opcode. */
|
||||
static const char* logicalToName(LogicalOpcode op);
|
||||
|
||||
private:
|
||||
std::unordered_map<uint16_t, uint16_t> logicalToWire_; // LogicalOpcode → wire
|
||||
std::unordered_map<uint16_t, uint16_t> wireToLogical_; // wire → LogicalOpcode
|
||||
|
||||
static std::optional<LogicalOpcode> nameToLogical(const std::string& name);
|
||||
static const char* logicalToName(LogicalOpcode op);
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -451,6 +451,7 @@ public:
|
|||
class TurtlePacketParsers : public ClassicPacketParsers {
|
||||
public:
|
||||
uint8_t movementFlags2Size() const override { return 0; }
|
||||
bool parseUpdateObject(network::Packet& packet, UpdateObjectData& data) override;
|
||||
bool parseMovementBlock(network::Packet& packet, UpdateBlock& block) override;
|
||||
bool parseMonsterMove(network::Packet& packet, MonsterMoveData& data) override;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -7,7 +7,13 @@
|
|||
#include "auth/vanilla_crypt.hpp"
|
||||
#include <functional>
|
||||
#include <vector>
|
||||
#include <deque>
|
||||
#include <cstdint>
|
||||
#include <chrono>
|
||||
#include <thread>
|
||||
#include <mutex>
|
||||
#include <atomic>
|
||||
#include <string>
|
||||
|
||||
namespace wowee {
|
||||
namespace network {
|
||||
|
|
@ -66,6 +72,8 @@ public:
|
|||
*/
|
||||
void initEncryption(const std::vector<uint8_t>& sessionKey, uint32_t build = 12340);
|
||||
|
||||
void tracePacketsFor(std::chrono::milliseconds duration, const std::string& reason);
|
||||
|
||||
/**
|
||||
* Check if header encryption is enabled
|
||||
*/
|
||||
|
|
@ -76,11 +84,23 @@ private:
|
|||
* Try to parse complete packets from receive buffer
|
||||
*/
|
||||
void tryParsePackets();
|
||||
void pumpNetworkIO();
|
||||
void dispatchQueuedPackets();
|
||||
void asyncPumpLoop();
|
||||
void startAsyncPump();
|
||||
void stopAsyncPump();
|
||||
void closeSocketNoJoin();
|
||||
|
||||
socket_t sockfd = INVALID_SOCK;
|
||||
bool connected = false;
|
||||
bool encryptionEnabled = false;
|
||||
bool useVanillaCrypt = false; // true = XOR cipher, false = RC4
|
||||
bool useAsyncPump_ = true;
|
||||
std::thread asyncPumpThread_;
|
||||
std::atomic<bool> asyncPumpStop_{false};
|
||||
std::atomic<bool> asyncPumpRunning_{false};
|
||||
mutable std::mutex ioMutex_;
|
||||
mutable std::mutex callbackMutex_;
|
||||
|
||||
// WotLK RC4 ciphers for header encryption/decryption
|
||||
auth::RC4 encryptCipher;
|
||||
|
|
@ -94,6 +114,8 @@ private:
|
|||
size_t receiveReadOffset_ = 0;
|
||||
// Optional reused packet queue (feature-gated) to reduce per-update allocations.
|
||||
std::vector<Packet> parsedPacketsScratch_;
|
||||
// Parsed packets waiting for callback dispatch; drained with a strict per-update budget.
|
||||
std::deque<Packet> pendingPacketCallbacks_;
|
||||
|
||||
// Runtime-gated network optimization toggles (default off).
|
||||
bool useFastRecvAppend_ = false;
|
||||
|
|
@ -105,6 +127,9 @@ private:
|
|||
|
||||
// Debug-only tracing window for post-auth packet framing verification.
|
||||
int headerTracePacketsLeft = 0;
|
||||
std::chrono::steady_clock::time_point packetTraceStart_{};
|
||||
std::chrono::steady_clock::time_point packetTraceUntil_{};
|
||||
std::string packetTraceReason_;
|
||||
|
||||
// Packet callback
|
||||
std::function<void(const Packet&)> packetCallback;
|
||||
|
|
|
|||
|
|
@ -296,7 +296,9 @@ private:
|
|||
std::unordered_map<VkTexture*, bool> textureColorKeyBlackByPtr_;
|
||||
std::unordered_map<std::string, VkTexture*> compositeCache_; // key → texture for reuse
|
||||
std::unordered_set<std::string> failedTextureCache_; // negative cache for budget exhaustion
|
||||
std::unordered_map<std::string, uint64_t> failedTextureRetryAt_;
|
||||
std::unordered_set<std::string> loggedTextureLoadFails_; // dedup warning logs
|
||||
uint64_t textureLookupSerial_ = 0;
|
||||
size_t textureCacheBytes_ = 0;
|
||||
uint64_t textureCacheCounter_ = 0;
|
||||
size_t textureCacheBudgetBytes_ = 1024ull * 1024 * 1024;
|
||||
|
|
|
|||
|
|
@ -477,7 +477,9 @@ private:
|
|||
uint64_t textureCacheCounter_ = 0;
|
||||
size_t textureCacheBudgetBytes_ = 2048ull * 1024 * 1024;
|
||||
std::unordered_set<std::string> failedTextureCache_;
|
||||
std::unordered_map<std::string, uint64_t> failedTextureRetryAt_;
|
||||
std::unordered_set<std::string> loggedTextureLoadFails_;
|
||||
uint64_t textureLookupSerial_ = 0;
|
||||
uint32_t textureBudgetRejectWarnings_ = 0;
|
||||
std::unique_ptr<VkTexture> whiteTexture_;
|
||||
std::unique_ptr<VkTexture> glowTexture_;
|
||||
|
|
|
|||
|
|
@ -671,7 +671,9 @@ private:
|
|||
uint64_t textureCacheCounter_ = 0;
|
||||
size_t textureCacheBudgetBytes_ = 8192ull * 1024 * 1024; // 8 GB default, overridden at init
|
||||
std::unordered_set<std::string> failedTextureCache_;
|
||||
std::unordered_map<std::string, uint64_t> failedTextureRetryAt_;
|
||||
std::unordered_set<std::string> loggedTextureLoadFails_;
|
||||
uint64_t textureLookupSerial_ = 0;
|
||||
uint32_t textureBudgetRejectWarnings_ = 0;
|
||||
|
||||
// Default white texture
|
||||
|
|
|
|||
|
|
@ -824,6 +824,7 @@ void Application::logoutToLogin() {
|
|||
if (load.future.valid()) load.future.wait();
|
||||
}
|
||||
asyncCreatureLoads_.clear();
|
||||
asyncCreatureDisplayLoads_.clear();
|
||||
|
||||
// --- Creature spawn queues ---
|
||||
pendingCreatureSpawns_.clear();
|
||||
|
|
@ -842,6 +843,7 @@ void Application::logoutToLogin() {
|
|||
gameObjectInstances_.clear();
|
||||
pendingGameObjectSpawns_.clear();
|
||||
pendingTransportMoves_.clear();
|
||||
pendingTransportRegistrations_.clear();
|
||||
pendingTransportDoodadBatches_.clear();
|
||||
|
||||
world.reset();
|
||||
|
|
@ -1053,6 +1055,7 @@ void Application::update(float deltaTime) {
|
|||
updateCheckpoint = "in_game: gameobject/transport queues";
|
||||
runInGameStage("gameobject/transport queues", [&] {
|
||||
processGameObjectSpawnQueue();
|
||||
processPendingTransportRegistrations();
|
||||
processPendingTransportDoodads();
|
||||
});
|
||||
inGameStep = "pending mount";
|
||||
|
|
@ -1725,6 +1728,19 @@ void Application::update(float deltaTime) {
|
|||
break;
|
||||
}
|
||||
|
||||
if (pendingWorldEntry_ && !loadingWorld_ && state != AppState::DISCONNECTED) {
|
||||
auto entry = *pendingWorldEntry_;
|
||||
pendingWorldEntry_.reset();
|
||||
worldEntryMovementGraceTimer_ = 2.0f;
|
||||
taxiLandingClampTimer_ = 0.0f;
|
||||
lastTaxiFlight_ = false;
|
||||
if (renderer && renderer->getCameraController()) {
|
||||
renderer->getCameraController()->clearMovementInputs();
|
||||
renderer->getCameraController()->suppressMovementFor(1.0f);
|
||||
}
|
||||
loadOnlineWorldTerrain(entry.mapId, entry.x, entry.y, entry.z);
|
||||
}
|
||||
|
||||
// Update renderer (camera, etc.) only when in-game
|
||||
updateCheckpoint = "renderer update";
|
||||
if (renderer && state == AppState::IN_GAME) {
|
||||
|
|
@ -2025,24 +2041,19 @@ void Application::setupUICallbacks() {
|
|||
|
||||
// If a world load is already in progress (re-entrant call from
|
||||
// gameHandler->update() processing SMSG_NEW_WORLD during warmup),
|
||||
// defer this entry. The current load will pick it up when it finishes.
|
||||
// defer this entry. The current load will pick it up when it finishes.
|
||||
if (loadingWorld_) {
|
||||
LOG_WARNING("World entry deferred: map ", mapId, " while loading (will process after current load)");
|
||||
pendingWorldEntry_ = {mapId, x, y, z};
|
||||
return;
|
||||
}
|
||||
|
||||
worldEntryMovementGraceTimer_ = 2.0f;
|
||||
taxiLandingClampTimer_ = 0.0f;
|
||||
lastTaxiFlight_ = false;
|
||||
// Stop any movement that was active before the teleport
|
||||
if (renderer && renderer->getCameraController()) {
|
||||
renderer->getCameraController()->clearMovementInputs();
|
||||
renderer->getCameraController()->suppressMovementFor(1.0f);
|
||||
}
|
||||
loadOnlineWorldTerrain(mapId, x, y, z);
|
||||
// loadedMapId_ is set inside loadOnlineWorldTerrain (including
|
||||
// any deferred entries it processes), so we must NOT override it here.
|
||||
// Full world loads are expensive and `loadOnlineWorldTerrain()` itself
|
||||
// drives `gameHandler->update()` during warmup. Queue the load here so
|
||||
// it runs after the current packet handler returns instead of recursing
|
||||
// from `SMSG_LOGIN_VERIFY_WORLD` / `SMSG_NEW_WORLD`.
|
||||
LOG_WARNING("Queued world entry: map ", mapId, " pos=(", x, ", ", y, ", ", z, ")");
|
||||
pendingWorldEntry_ = {mapId, x, y, z};
|
||||
});
|
||||
|
||||
auto sampleBestFloorAt = [this](float x, float y, float probeZ) -> std::optional<float> {
|
||||
|
|
@ -2712,133 +2723,28 @@ void Application::setupUICallbacks() {
|
|||
|
||||
// Transport spawn callback (online mode) - register transports with TransportManager
|
||||
gameHandler->setTransportSpawnCallback([this](uint64_t guid, uint32_t entry, uint32_t displayId, float x, float y, float z, float orientation) {
|
||||
auto* transportManager = gameHandler->getTransportManager();
|
||||
if (!transportManager || !renderer) return;
|
||||
if (!renderer) return;
|
||||
|
||||
// Get the WMO instance ID from the GameObject spawn
|
||||
// Get the GameObject instance now so late queue processing can rely on stable IDs.
|
||||
auto it = gameObjectInstances_.find(guid);
|
||||
if (it == gameObjectInstances_.end()) {
|
||||
LOG_WARNING("Transport spawn callback: GameObject instance not found for GUID 0x", std::hex, guid, std::dec);
|
||||
return;
|
||||
}
|
||||
|
||||
uint32_t wmoInstanceId = it->second.instanceId;
|
||||
LOG_WARNING("Registering server transport: GUID=0x", std::hex, guid, std::dec,
|
||||
" entry=", entry, " displayId=", displayId, " wmoInstance=", wmoInstanceId,
|
||||
" pos=(", x, ", ", y, ", ", z, ")");
|
||||
|
||||
// TransportAnimation.dbc is indexed by GameObject entry
|
||||
uint32_t pathId = entry;
|
||||
const bool preferServerData = gameHandler && gameHandler->hasServerTransportUpdate(guid);
|
||||
|
||||
bool clientAnim = transportManager->isClientSideAnimation();
|
||||
LOG_DEBUG("Transport spawn callback: clientAnimation=", clientAnim,
|
||||
" guid=0x", std::hex, guid, std::dec, " entry=", entry, " pathId=", pathId,
|
||||
" preferServer=", preferServerData);
|
||||
|
||||
// Coordinates are already canonical (converted in game_handler.cpp when entity was created)
|
||||
glm::vec3 canonicalSpawnPos(x, y, z);
|
||||
|
||||
// Check if we have a real path from TransportAnimation.dbc (indexed by entry).
|
||||
// AzerothCore transport entries are not always 1:1 with DBC path ids.
|
||||
const bool shipOrZeppelinDisplay =
|
||||
(displayId == 3015 || displayId == 3031 || displayId == 7546 ||
|
||||
displayId == 7446 || displayId == 1587 || displayId == 2454 ||
|
||||
displayId == 807 || displayId == 808);
|
||||
bool hasUsablePath = transportManager->hasPathForEntry(entry);
|
||||
if (shipOrZeppelinDisplay) {
|
||||
// For true transports, reject tiny XY tracks that effectively look stationary.
|
||||
hasUsablePath = transportManager->hasUsableMovingPathForEntry(entry, 25.0f);
|
||||
}
|
||||
|
||||
LOG_WARNING("Transport path check: entry=", entry, " hasUsablePath=", hasUsablePath,
|
||||
" preferServerData=", preferServerData, " shipOrZepDisplay=", shipOrZeppelinDisplay);
|
||||
|
||||
if (preferServerData) {
|
||||
// Strict server-authoritative mode: do not infer/remap fallback routes.
|
||||
if (!hasUsablePath) {
|
||||
std::vector<glm::vec3> path = { canonicalSpawnPos };
|
||||
transportManager->loadPathFromNodes(pathId, path, false, 0.0f);
|
||||
LOG_WARNING("Server-first strict registration: stationary fallback for GUID 0x",
|
||||
std::hex, guid, std::dec, " entry=", entry);
|
||||
} else {
|
||||
LOG_WARNING("Server-first transport registration: using entry DBC path for entry ", entry);
|
||||
}
|
||||
} else if (!hasUsablePath) {
|
||||
// Remap/infer path by spawn position when entry doesn't map 1:1 to DBC ids.
|
||||
// For elevators (TB lift platforms), we must allow z-only paths here.
|
||||
bool allowZOnly = (displayId == 455 || displayId == 462);
|
||||
uint32_t inferredPath = transportManager->inferDbcPathForSpawn(
|
||||
canonicalSpawnPos, 1200.0f, allowZOnly);
|
||||
if (inferredPath != 0) {
|
||||
pathId = inferredPath;
|
||||
LOG_WARNING("Using inferred transport path ", pathId, " for entry ", entry);
|
||||
} else {
|
||||
uint32_t remappedPath = transportManager->pickFallbackMovingPath(entry, displayId);
|
||||
if (remappedPath != 0) {
|
||||
pathId = remappedPath;
|
||||
LOG_WARNING("Using remapped fallback transport path ", pathId,
|
||||
" for entry ", entry, " displayId=", displayId,
|
||||
" (usableEntryPath=", transportManager->hasPathForEntry(entry), ")");
|
||||
} else {
|
||||
LOG_WARNING("No TransportAnimation.dbc path for entry ", entry,
|
||||
" - transport will be stationary");
|
||||
|
||||
// Fallback: Stationary at spawn point (wait for server to send real position)
|
||||
std::vector<glm::vec3> path = { canonicalSpawnPos };
|
||||
transportManager->loadPathFromNodes(pathId, path, false, 0.0f);
|
||||
}
|
||||
}
|
||||
auto pendingIt = std::find_if(
|
||||
pendingTransportRegistrations_.begin(), pendingTransportRegistrations_.end(),
|
||||
[guid](const PendingTransportRegistration& pending) { return pending.guid == guid; });
|
||||
if (pendingIt != pendingTransportRegistrations_.end()) {
|
||||
pendingIt->entry = entry;
|
||||
pendingIt->displayId = displayId;
|
||||
pendingIt->x = x;
|
||||
pendingIt->y = y;
|
||||
pendingIt->z = z;
|
||||
pendingIt->orientation = orientation;
|
||||
} else {
|
||||
LOG_WARNING("Using real transport path from TransportAnimation.dbc for entry ", entry);
|
||||
}
|
||||
|
||||
// Register the transport with spawn position (prevents rendering at origin until server update)
|
||||
transportManager->registerTransport(guid, wmoInstanceId, pathId, canonicalSpawnPos, entry);
|
||||
|
||||
// Mark M2 transports (e.g. Deeprun Tram cars) so TransportManager uses M2Renderer
|
||||
if (!it->second.isWmo) {
|
||||
if (auto* tr = transportManager->getTransport(guid)) {
|
||||
tr->isM2 = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Server-authoritative movement - set initial position from spawn data
|
||||
glm::vec3 canonicalPos(x, y, z);
|
||||
transportManager->updateServerTransport(guid, canonicalPos, orientation);
|
||||
|
||||
// If a move packet arrived before registration completed, replay latest now.
|
||||
auto pendingIt = pendingTransportMoves_.find(guid);
|
||||
if (pendingIt != pendingTransportMoves_.end()) {
|
||||
const PendingTransportMove pending = pendingIt->second;
|
||||
transportManager->updateServerTransport(guid, glm::vec3(pending.x, pending.y, pending.z), pending.orientation);
|
||||
LOG_DEBUG("Replayed queued transport move for GUID=0x", std::hex, guid, std::dec,
|
||||
" 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_DEBUG("Assigned cached TaxiPathNode path for MO_TRANSPORT entry=", entry,
|
||||
" taxiPathId=", taxiPathId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (auto* tr = transportManager->getTransport(guid); tr) {
|
||||
LOG_WARNING("Transport registered: guid=0x", std::hex, guid, std::dec,
|
||||
" entry=", entry, " displayId=", displayId,
|
||||
" pathId=", tr->pathId,
|
||||
" mode=", (tr->useClientAnimation ? "client" : "server"),
|
||||
" serverUpdates=", tr->serverUpdateCount);
|
||||
} else {
|
||||
LOG_DEBUG("Transport registered: guid=0x", std::hex, guid, std::dec,
|
||||
" entry=", entry, " displayId=", displayId, " (TransportManager instance missing)");
|
||||
pendingTransportRegistrations_.push_back(
|
||||
PendingTransportRegistration{guid, entry, displayId, x, y, z, orientation});
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -2853,6 +2759,15 @@ void Application::setupUICallbacks() {
|
|||
return;
|
||||
}
|
||||
|
||||
auto pendingRegIt = std::find_if(
|
||||
pendingTransportRegistrations_.begin(), pendingTransportRegistrations_.end(),
|
||||
[guid](const PendingTransportRegistration& pending) { return pending.guid == guid; });
|
||||
if (pendingRegIt != pendingTransportRegistrations_.end()) {
|
||||
pendingTransportMoves_[guid] = PendingTransportMove{x, y, z, orientation};
|
||||
LOG_DEBUG("Queued transport move for pending registration GUID=0x", std::hex, guid, std::dec);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if transport exists - if not, treat this as a late spawn (reconnection/server restart)
|
||||
if (!transportManager->getTransport(guid)) {
|
||||
LOG_DEBUG("Received position update for unregistered transport 0x", std::hex, guid, std::dec,
|
||||
|
|
@ -4155,6 +4070,7 @@ void Application::loadOnlineWorldTerrain(uint32_t mapId, float x, float y, float
|
|||
deferredEquipmentQueue_.clear();
|
||||
pendingGameObjectSpawns_.clear();
|
||||
pendingTransportMoves_.clear();
|
||||
pendingTransportRegistrations_.clear();
|
||||
pendingTransportDoodadBatches_.clear();
|
||||
|
||||
if (renderer) {
|
||||
|
|
@ -4210,6 +4126,7 @@ void Application::loadOnlineWorldTerrain(uint32_t mapId, float x, float y, float
|
|||
if (load.future.valid()) load.future.wait();
|
||||
}
|
||||
asyncCreatureLoads_.clear();
|
||||
asyncCreatureDisplayLoads_.clear();
|
||||
|
||||
playerInstances_.clear();
|
||||
onlinePlayerAppearance_.clear();
|
||||
|
|
@ -4866,25 +4783,23 @@ void Application::loadOnlineWorldTerrain(uint32_t mapId, float x, float y, float
|
|||
if (world) world->update(1.0f / 60.0f);
|
||||
processPlayerSpawnQueue();
|
||||
|
||||
// During load screen warmup: lift per-frame budgets so GPU uploads
|
||||
// and spawns happen in bulk while the loading screen is still visible.
|
||||
processCreatureSpawnQueue(true);
|
||||
processAsyncNpcCompositeResults(true);
|
||||
// Process equipment queue more aggressively during warmup (multiple per iteration)
|
||||
for (int i = 0; i < 8 && (!deferredEquipmentQueue_.empty() || !asyncEquipmentLoads_.empty()); i++) {
|
||||
// Keep warmup bounded: unbounded queue draining can stall the main thread
|
||||
// long enough to trigger socket timeouts.
|
||||
processCreatureSpawnQueue(false);
|
||||
processAsyncNpcCompositeResults(false);
|
||||
// Process equipment queue with a small bounded burst during warmup.
|
||||
for (int i = 0; i < 2 && (!deferredEquipmentQueue_.empty() || !asyncEquipmentLoads_.empty()); i++) {
|
||||
processDeferredEquipmentQueue();
|
||||
}
|
||||
if (auto* cr = renderer ? renderer->getCharacterRenderer() : nullptr) {
|
||||
cr->processPendingNormalMaps(INT_MAX);
|
||||
cr->processPendingNormalMaps(4);
|
||||
}
|
||||
|
||||
// Process ALL pending game object spawns.
|
||||
while (!pendingGameObjectSpawns_.empty()) {
|
||||
auto& s = pendingGameObjectSpawns_.front();
|
||||
spawnOnlineGameObject(s.guid, s.entry, s.displayId, s.x, s.y, s.z, s.orientation, s.scale);
|
||||
pendingGameObjectSpawns_.erase(pendingGameObjectSpawns_.begin());
|
||||
}
|
||||
// Keep warmup responsive: process gameobject queue with the same bounded
|
||||
// budget logic used in-world instead of draining everything in one tick.
|
||||
processGameObjectSpawnQueue();
|
||||
|
||||
processPendingTransportRegistrations();
|
||||
processPendingTransportDoodads();
|
||||
processPendingMount();
|
||||
updateQuestMarkers();
|
||||
|
|
@ -7437,12 +7352,23 @@ void Application::spawnOnlineGameObject(uint64_t guid, uint32_t entry, uint32_t
|
|||
|
||||
void Application::processAsyncCreatureResults(bool unlimited) {
|
||||
// Check completed async model loads and finalize on main thread (GPU upload + instance creation).
|
||||
// Limit GPU model uploads per frame to avoid spikes, but always drain cheap bookkeeping.
|
||||
// In unlimited mode (load screen), process all pending uploads without cap.
|
||||
static constexpr int kMaxModelUploadsPerFrame = 1;
|
||||
// Limit GPU model uploads per tick to avoid long main-thread stalls that can starve socket updates.
|
||||
// Even in unlimited mode (load screen), keep a small cap and budget to prevent multi-second stalls.
|
||||
static constexpr int kMaxModelUploadsPerTick = 1;
|
||||
static constexpr int kMaxModelUploadsPerTickWarmup = 1;
|
||||
static constexpr float kFinalizeBudgetMs = 2.0f;
|
||||
static constexpr float kFinalizeBudgetWarmupMs = 2.0f;
|
||||
const int maxUploadsThisTick = unlimited ? kMaxModelUploadsPerTickWarmup : kMaxModelUploadsPerTick;
|
||||
const float budgetMs = unlimited ? kFinalizeBudgetWarmupMs : kFinalizeBudgetMs;
|
||||
const auto tickStart = std::chrono::steady_clock::now();
|
||||
int modelUploads = 0;
|
||||
|
||||
for (auto it = asyncCreatureLoads_.begin(); it != asyncCreatureLoads_.end(); ) {
|
||||
if (std::chrono::duration<float, std::milli>(
|
||||
std::chrono::steady_clock::now() - tickStart).count() >= budgetMs) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (!it->future.valid() ||
|
||||
it->future.wait_for(std::chrono::milliseconds(0)) != std::future_status::ready) {
|
||||
++it;
|
||||
|
|
@ -7451,12 +7377,13 @@ void Application::processAsyncCreatureResults(bool unlimited) {
|
|||
|
||||
// Peek: if this result needs a NEW model upload (not cached) and we've hit
|
||||
// the upload budget, defer to next frame without consuming the future.
|
||||
if (!unlimited && modelUploads >= kMaxModelUploadsPerFrame) {
|
||||
if (modelUploads >= maxUploadsThisTick) {
|
||||
break;
|
||||
}
|
||||
|
||||
auto result = it->future.get();
|
||||
it = asyncCreatureLoads_.erase(it);
|
||||
asyncCreatureDisplayLoads_.erase(result.displayId);
|
||||
|
||||
if (result.permanent_failure) {
|
||||
nonRenderableCreatureDisplayIds_.insert(result.displayId);
|
||||
|
|
@ -7471,6 +7398,27 @@ void Application::processAsyncCreatureResults(bool unlimited) {
|
|||
continue;
|
||||
}
|
||||
|
||||
// Another async result may have already uploaded this displayId while this
|
||||
// task was still running; in that case, skip duplicate GPU upload.
|
||||
if (displayIdModelCache_.find(result.displayId) != displayIdModelCache_.end()) {
|
||||
pendingCreatureSpawnGuids_.erase(result.guid);
|
||||
creatureSpawnRetryCounts_.erase(result.guid);
|
||||
if (!creatureInstances_.count(result.guid) &&
|
||||
!creaturePermanentFailureGuids_.count(result.guid)) {
|
||||
PendingCreatureSpawn s{};
|
||||
s.guid = result.guid;
|
||||
s.displayId = result.displayId;
|
||||
s.x = result.x;
|
||||
s.y = result.y;
|
||||
s.z = result.z;
|
||||
s.orientation = result.orientation;
|
||||
s.scale = result.scale;
|
||||
pendingCreatureSpawns_.push_back(s);
|
||||
pendingCreatureSpawnGuids_.insert(result.guid);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Model parsed on background thread — upload to GPU on main thread.
|
||||
auto* charRenderer = renderer ? renderer->getCharacterRenderer() : nullptr;
|
||||
if (!charRenderer) {
|
||||
|
|
@ -7478,6 +7426,10 @@ void Application::processAsyncCreatureResults(bool unlimited) {
|
|||
continue;
|
||||
}
|
||||
|
||||
// Count upload attempts toward the frame budget even if upload fails.
|
||||
// Otherwise repeated failures can consume an unbounded amount of frame time.
|
||||
modelUploads++;
|
||||
|
||||
// Upload model to GPU (must happen on main thread)
|
||||
// Use pre-decoded BLP cache to skip main-thread texture decode
|
||||
auto uploadStart = std::chrono::steady_clock::now();
|
||||
|
|
@ -7504,8 +7456,6 @@ void Application::processAsyncCreatureResults(bool unlimited) {
|
|||
displayIdPredecodedTextures_[result.displayId] = std::move(result.predecodedTextures);
|
||||
}
|
||||
displayIdModelCache_[result.displayId] = result.modelId;
|
||||
modelUploads++;
|
||||
|
||||
pendingCreatureSpawnGuids_.erase(result.guid);
|
||||
creatureSpawnRetryCounts_.erase(result.guid);
|
||||
|
||||
|
|
@ -7659,6 +7609,14 @@ void Application::processCreatureSpawnQueue(bool unlimited) {
|
|||
|
||||
// For new models: launch async load on background thread instead of blocking.
|
||||
if (needsNewModel) {
|
||||
// Keep exactly one background load per displayId. Additional spawns for
|
||||
// the same displayId stay queued and will spawn once cache is populated.
|
||||
if (asyncCreatureDisplayLoads_.count(s.displayId)) {
|
||||
pendingCreatureSpawns_.push_back(s);
|
||||
rotationsLeft--;
|
||||
continue;
|
||||
}
|
||||
|
||||
const int maxAsync = unlimited ? (MAX_ASYNC_CREATURE_LOADS * 4) : MAX_ASYNC_CREATURE_LOADS;
|
||||
if (static_cast<int>(asyncCreatureLoads_.size()) + asyncLaunched >= maxAsync) {
|
||||
// Too many in-flight — defer to next frame
|
||||
|
|
@ -7904,6 +7862,7 @@ void Application::processCreatureSpawnQueue(bool unlimited) {
|
|||
return result;
|
||||
});
|
||||
asyncCreatureLoads_.push_back(std::move(load));
|
||||
asyncCreatureDisplayLoads_.insert(s.displayId);
|
||||
asyncLaunched++;
|
||||
// Don't erase from pendingCreatureSpawnGuids_ — the async result handler will do it
|
||||
rotationsLeft = pendingCreatureSpawns_.size();
|
||||
|
|
@ -8304,6 +8263,151 @@ void Application::processGameObjectSpawnQueue() {
|
|||
}
|
||||
}
|
||||
|
||||
void Application::processPendingTransportRegistrations() {
|
||||
if (pendingTransportRegistrations_.empty()) return;
|
||||
if (!gameHandler || !renderer) return;
|
||||
|
||||
auto* transportManager = gameHandler->getTransportManager();
|
||||
if (!transportManager) return;
|
||||
|
||||
auto startTime = std::chrono::steady_clock::now();
|
||||
static constexpr int kMaxRegistrationsPerFrame = 2;
|
||||
static constexpr float kRegistrationBudgetMs = 2.0f;
|
||||
int processed = 0;
|
||||
|
||||
for (auto it = pendingTransportRegistrations_.begin();
|
||||
it != pendingTransportRegistrations_.end() && processed < kMaxRegistrationsPerFrame;) {
|
||||
float elapsedMs = std::chrono::duration<float, std::milli>(
|
||||
std::chrono::steady_clock::now() - startTime).count();
|
||||
if (elapsedMs >= kRegistrationBudgetMs) break;
|
||||
|
||||
const PendingTransportRegistration pending = *it;
|
||||
auto goIt = gameObjectInstances_.find(pending.guid);
|
||||
if (goIt == gameObjectInstances_.end()) {
|
||||
it = pendingTransportRegistrations_.erase(it);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (transportManager->getTransport(pending.guid)) {
|
||||
transportManager->updateServerTransport(
|
||||
pending.guid, glm::vec3(pending.x, pending.y, pending.z), pending.orientation);
|
||||
it = pendingTransportRegistrations_.erase(it);
|
||||
continue;
|
||||
}
|
||||
|
||||
const uint32_t wmoInstanceId = goIt->second.instanceId;
|
||||
LOG_WARNING("Registering server transport: GUID=0x", std::hex, pending.guid, std::dec,
|
||||
" entry=", pending.entry, " displayId=", pending.displayId, " wmoInstance=", wmoInstanceId,
|
||||
" pos=(", pending.x, ", ", pending.y, ", ", pending.z, ")");
|
||||
|
||||
// TransportAnimation.dbc is indexed by GameObject entry.
|
||||
uint32_t pathId = pending.entry;
|
||||
const bool preferServerData = gameHandler->hasServerTransportUpdate(pending.guid);
|
||||
|
||||
bool clientAnim = transportManager->isClientSideAnimation();
|
||||
LOG_DEBUG("Transport spawn callback: clientAnimation=", clientAnim,
|
||||
" guid=0x", std::hex, pending.guid, std::dec,
|
||||
" entry=", pending.entry, " pathId=", pathId,
|
||||
" preferServer=", preferServerData);
|
||||
|
||||
glm::vec3 canonicalSpawnPos(pending.x, pending.y, pending.z);
|
||||
const bool shipOrZeppelinDisplay =
|
||||
(pending.displayId == 3015 || pending.displayId == 3031 || pending.displayId == 7546 ||
|
||||
pending.displayId == 7446 || pending.displayId == 1587 || pending.displayId == 2454 ||
|
||||
pending.displayId == 807 || pending.displayId == 808);
|
||||
bool hasUsablePath = transportManager->hasPathForEntry(pending.entry);
|
||||
if (shipOrZeppelinDisplay) {
|
||||
hasUsablePath = transportManager->hasUsableMovingPathForEntry(pending.entry, 25.0f);
|
||||
}
|
||||
|
||||
LOG_WARNING("Transport path check: entry=", pending.entry, " hasUsablePath=", hasUsablePath,
|
||||
" preferServerData=", preferServerData, " shipOrZepDisplay=", shipOrZeppelinDisplay);
|
||||
|
||||
if (preferServerData) {
|
||||
if (!hasUsablePath) {
|
||||
std::vector<glm::vec3> path = { canonicalSpawnPos };
|
||||
transportManager->loadPathFromNodes(pathId, path, false, 0.0f);
|
||||
LOG_WARNING("Server-first strict registration: stationary fallback for GUID 0x",
|
||||
std::hex, pending.guid, std::dec, " entry=", pending.entry);
|
||||
} else {
|
||||
LOG_WARNING("Server-first transport registration: using entry DBC path for entry ", pending.entry);
|
||||
}
|
||||
} else if (!hasUsablePath) {
|
||||
bool allowZOnly = (pending.displayId == 455 || pending.displayId == 462);
|
||||
uint32_t inferredPath = transportManager->inferDbcPathForSpawn(
|
||||
canonicalSpawnPos, 1200.0f, allowZOnly);
|
||||
if (inferredPath != 0) {
|
||||
pathId = inferredPath;
|
||||
LOG_WARNING("Using inferred transport path ", pathId, " for entry ", pending.entry);
|
||||
} else {
|
||||
uint32_t remappedPath = transportManager->pickFallbackMovingPath(pending.entry, pending.displayId);
|
||||
if (remappedPath != 0) {
|
||||
pathId = remappedPath;
|
||||
LOG_WARNING("Using remapped fallback transport path ", pathId,
|
||||
" for entry ", pending.entry, " displayId=", pending.displayId,
|
||||
" (usableEntryPath=", transportManager->hasPathForEntry(pending.entry), ")");
|
||||
} else {
|
||||
LOG_WARNING("No TransportAnimation.dbc path for entry ", pending.entry,
|
||||
" - transport will be stationary");
|
||||
std::vector<glm::vec3> path = { canonicalSpawnPos };
|
||||
transportManager->loadPathFromNodes(pathId, path, false, 0.0f);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
LOG_WARNING("Using real transport path from TransportAnimation.dbc for entry ", pending.entry);
|
||||
}
|
||||
|
||||
transportManager->registerTransport(pending.guid, wmoInstanceId, pathId, canonicalSpawnPos, pending.entry);
|
||||
|
||||
if (!goIt->second.isWmo) {
|
||||
if (auto* tr = transportManager->getTransport(pending.guid)) {
|
||||
tr->isM2 = true;
|
||||
}
|
||||
}
|
||||
|
||||
transportManager->updateServerTransport(
|
||||
pending.guid, glm::vec3(pending.x, pending.y, pending.z), pending.orientation);
|
||||
|
||||
auto moveIt = pendingTransportMoves_.find(pending.guid);
|
||||
if (moveIt != pendingTransportMoves_.end()) {
|
||||
const PendingTransportMove latestMove = moveIt->second;
|
||||
transportManager->updateServerTransport(
|
||||
pending.guid, glm::vec3(latestMove.x, latestMove.y, latestMove.z), latestMove.orientation);
|
||||
LOG_DEBUG("Replayed queued transport move for GUID=0x", std::hex, pending.guid, std::dec,
|
||||
" pos=(", latestMove.x, ", ", latestMove.y, ", ", latestMove.z,
|
||||
") orientation=", latestMove.orientation);
|
||||
pendingTransportMoves_.erase(moveIt);
|
||||
}
|
||||
|
||||
if (glm::length(canonicalSpawnPos) < 1.0f) {
|
||||
auto goData = gameHandler->getCachedGameObjectInfo(pending.entry);
|
||||
if (goData && goData->type == 15 && goData->hasData && goData->data[0] != 0) {
|
||||
uint32_t taxiPathId = goData->data[0];
|
||||
if (transportManager->hasTaxiPath(taxiPathId)) {
|
||||
transportManager->assignTaxiPathToTransport(pending.entry, taxiPathId);
|
||||
LOG_DEBUG("Assigned cached TaxiPathNode path for MO_TRANSPORT entry=", pending.entry,
|
||||
" taxiPathId=", taxiPathId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (auto* tr = transportManager->getTransport(pending.guid); tr) {
|
||||
LOG_WARNING("Transport registered: guid=0x", std::hex, pending.guid, std::dec,
|
||||
" entry=", pending.entry, " displayId=", pending.displayId,
|
||||
" pathId=", tr->pathId,
|
||||
" mode=", (tr->useClientAnimation ? "client" : "server"),
|
||||
" serverUpdates=", tr->serverUpdateCount);
|
||||
} else {
|
||||
LOG_DEBUG("Transport registered: guid=0x", std::hex, pending.guid, std::dec,
|
||||
" entry=", pending.entry, " displayId=", pending.displayId,
|
||||
" (TransportManager instance missing)");
|
||||
}
|
||||
|
||||
++processed;
|
||||
it = pendingTransportRegistrations_.erase(it);
|
||||
}
|
||||
}
|
||||
|
||||
void Application::processPendingTransportDoodads() {
|
||||
if (pendingTransportDoodadBatches_.empty()) return;
|
||||
if (!renderer || !assetManager) return;
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -441,6 +441,18 @@ bool ClassicPacketParsers::parseSpellGo(network::Packet& packet, SpellGoData& da
|
|||
if (rawHitCount > 128) {
|
||||
LOG_WARNING("[Classic] Spell go: hitCount capped (requested=", (int)rawHitCount, ")");
|
||||
}
|
||||
// Packed GUIDs are variable length, but each target needs at least 1 byte (mask).
|
||||
// Require the minimum bytes before entering per-target parsing loops.
|
||||
if (rem() < static_cast<size_t>(rawHitCount) + 1u) { // +1 for mandatory missCount byte
|
||||
static uint32_t badHitCountTrunc = 0;
|
||||
++badHitCountTrunc;
|
||||
if (badHitCountTrunc <= 10 || (badHitCountTrunc % 100) == 0) {
|
||||
LOG_WARNING("[Classic] Spell go: invalid hitCount/remaining (hits=", (int)rawHitCount,
|
||||
" remaining=", rem(), " occurrence=", badHitCountTrunc, ")");
|
||||
}
|
||||
packet.setReadPos(startPos);
|
||||
return false;
|
||||
}
|
||||
const uint8_t storedHitLimit = std::min<uint8_t>(rawHitCount, 128);
|
||||
data.hitTargets.reserve(storedHitLimit);
|
||||
bool truncatedTargets = false;
|
||||
|
|
@ -472,6 +484,17 @@ bool ClassicPacketParsers::parseSpellGo(network::Packet& packet, SpellGoData& da
|
|||
if (rawMissCount > 128) {
|
||||
LOG_WARNING("[Classic] Spell go: missCount capped (requested=", (int)rawMissCount, ")");
|
||||
}
|
||||
// Each miss entry needs at least packed-guid mask (1) + missType (1).
|
||||
if (rem() < static_cast<size_t>(rawMissCount) * 2u) {
|
||||
static uint32_t badMissCountTrunc = 0;
|
||||
++badMissCountTrunc;
|
||||
if (badMissCountTrunc <= 10 || (badMissCountTrunc % 100) == 0) {
|
||||
LOG_WARNING("[Classic] Spell go: invalid missCount/remaining (misses=", (int)rawMissCount,
|
||||
" remaining=", rem(), " occurrence=", badMissCountTrunc, ")");
|
||||
}
|
||||
packet.setReadPos(startPos);
|
||||
return false;
|
||||
}
|
||||
const uint8_t storedMissLimit = std::min<uint8_t>(rawMissCount, 128);
|
||||
data.missTargets.reserve(storedMissLimit);
|
||||
for (uint16_t i = 0; i < rawMissCount; ++i) {
|
||||
|
|
@ -1810,6 +1833,173 @@ bool TurtlePacketParsers::parseMovementBlock(network::Packet& packet, UpdateBloc
|
|||
return true;
|
||||
}
|
||||
|
||||
bool TurtlePacketParsers::parseUpdateObject(network::Packet& packet, UpdateObjectData& data) {
|
||||
constexpr uint32_t kMaxReasonableUpdateBlocks = 4096;
|
||||
|
||||
auto parseWithLayout = [&](bool withHasTransportByte, UpdateObjectData& out) -> bool {
|
||||
out = UpdateObjectData{};
|
||||
const size_t start = packet.getReadPos();
|
||||
if (packet.getSize() - start < 4) return false;
|
||||
|
||||
out.blockCount = packet.readUInt32();
|
||||
if (out.blockCount > kMaxReasonableUpdateBlocks) {
|
||||
packet.setReadPos(start);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (withHasTransportByte) {
|
||||
if (packet.getReadPos() >= packet.getSize()) {
|
||||
packet.setReadPos(start);
|
||||
return false;
|
||||
}
|
||||
/*uint8_t hasTransport =*/ packet.readUInt8();
|
||||
}
|
||||
|
||||
if (packet.getReadPos() + 1 <= packet.getSize()) {
|
||||
uint8_t firstByte = packet.readUInt8();
|
||||
if (firstByte == static_cast<uint8_t>(UpdateType::OUT_OF_RANGE_OBJECTS)) {
|
||||
if (packet.getReadPos() + 4 > packet.getSize()) {
|
||||
packet.setReadPos(start);
|
||||
return false;
|
||||
}
|
||||
uint32_t count = packet.readUInt32();
|
||||
if (count > kMaxReasonableUpdateBlocks) {
|
||||
packet.setReadPos(start);
|
||||
return false;
|
||||
}
|
||||
for (uint32_t i = 0; i < count; ++i) {
|
||||
if (packet.getReadPos() >= packet.getSize()) {
|
||||
packet.setReadPos(start);
|
||||
return false;
|
||||
}
|
||||
out.outOfRangeGuids.push_back(UpdateObjectParser::readPackedGuid(packet));
|
||||
}
|
||||
} else {
|
||||
packet.setReadPos(packet.getReadPos() - 1);
|
||||
}
|
||||
}
|
||||
|
||||
out.blocks.reserve(out.blockCount);
|
||||
for (uint32_t i = 0; i < out.blockCount; ++i) {
|
||||
if (packet.getReadPos() >= packet.getSize()) {
|
||||
packet.setReadPos(start);
|
||||
return false;
|
||||
}
|
||||
|
||||
const size_t blockStart = packet.getReadPos();
|
||||
uint8_t updateTypeVal = packet.readUInt8();
|
||||
if (updateTypeVal > static_cast<uint8_t>(UpdateType::NEAR_OBJECTS)) {
|
||||
packet.setReadPos(start);
|
||||
return false;
|
||||
}
|
||||
|
||||
const UpdateType updateType = static_cast<UpdateType>(updateTypeVal);
|
||||
UpdateBlock block;
|
||||
block.updateType = updateType;
|
||||
bool ok = false;
|
||||
|
||||
auto parseMovementVariant = [&](auto&& movementParser, const char* layoutName) -> bool {
|
||||
packet.setReadPos(blockStart + 1);
|
||||
block = UpdateBlock{};
|
||||
block.updateType = updateType;
|
||||
|
||||
switch (updateType) {
|
||||
case UpdateType::MOVEMENT:
|
||||
block.guid = UpdateObjectParser::readPackedGuid(packet);
|
||||
if (!movementParser(packet, block)) return false;
|
||||
LOG_DEBUG("[Turtle] Parsed MOVEMENT block via ", layoutName, " layout");
|
||||
return true;
|
||||
case UpdateType::CREATE_OBJECT:
|
||||
case UpdateType::CREATE_OBJECT2:
|
||||
block.guid = UpdateObjectParser::readPackedGuid(packet);
|
||||
if (packet.getReadPos() >= packet.getSize()) return false;
|
||||
block.objectType = static_cast<ObjectType>(packet.readUInt8());
|
||||
if (!movementParser(packet, block)) return false;
|
||||
if (!UpdateObjectParser::parseUpdateFields(packet, block)) return false;
|
||||
LOG_DEBUG("[Turtle] Parsed CREATE block via ", layoutName, " layout");
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
switch (updateType) {
|
||||
case UpdateType::VALUES:
|
||||
block.guid = UpdateObjectParser::readPackedGuid(packet);
|
||||
ok = UpdateObjectParser::parseUpdateFields(packet, block);
|
||||
break;
|
||||
case UpdateType::MOVEMENT:
|
||||
case UpdateType::CREATE_OBJECT:
|
||||
case UpdateType::CREATE_OBJECT2:
|
||||
ok = parseMovementVariant(
|
||||
[this](network::Packet& p, UpdateBlock& b) {
|
||||
return this->TurtlePacketParsers::parseMovementBlock(p, b);
|
||||
}, "turtle");
|
||||
if (!ok) {
|
||||
ok = parseMovementVariant(
|
||||
[this](network::Packet& p, UpdateBlock& b) {
|
||||
return this->ClassicPacketParsers::parseMovementBlock(p, b);
|
||||
}, "classic");
|
||||
}
|
||||
if (!ok) {
|
||||
ok = parseMovementVariant(
|
||||
[this](network::Packet& p, UpdateBlock& b) {
|
||||
return this->TbcPacketParsers::parseMovementBlock(p, b);
|
||||
}, "tbc");
|
||||
}
|
||||
break;
|
||||
case UpdateType::OUT_OF_RANGE_OBJECTS:
|
||||
case UpdateType::NEAR_OBJECTS:
|
||||
ok = true;
|
||||
break;
|
||||
default:
|
||||
ok = false;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!ok) {
|
||||
packet.setReadPos(start);
|
||||
return false;
|
||||
}
|
||||
|
||||
out.blocks.push_back(std::move(block));
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const size_t startPos = packet.getReadPos();
|
||||
UpdateObjectData parsed;
|
||||
if (parseWithLayout(true, parsed)) {
|
||||
data = std::move(parsed);
|
||||
return true;
|
||||
}
|
||||
|
||||
packet.setReadPos(startPos);
|
||||
if (parseWithLayout(false, parsed)) {
|
||||
LOG_DEBUG("[Turtle] SMSG_UPDATE_OBJECT parsed without has_transport byte fallback");
|
||||
data = std::move(parsed);
|
||||
return true;
|
||||
}
|
||||
|
||||
packet.setReadPos(startPos);
|
||||
if (ClassicPacketParsers::parseUpdateObject(packet, parsed)) {
|
||||
LOG_DEBUG("[Turtle] SMSG_UPDATE_OBJECT parsed via full classic fallback");
|
||||
data = std::move(parsed);
|
||||
return true;
|
||||
}
|
||||
|
||||
packet.setReadPos(startPos);
|
||||
if (TbcPacketParsers::parseUpdateObject(packet, parsed)) {
|
||||
LOG_DEBUG("[Turtle] SMSG_UPDATE_OBJECT parsed via full TBC fallback");
|
||||
data = std::move(parsed);
|
||||
return true;
|
||||
}
|
||||
|
||||
packet.setReadPos(startPos);
|
||||
return false;
|
||||
}
|
||||
|
||||
bool TurtlePacketParsers::parseMonsterMove(network::Packet& packet, MonsterMoveData& data) {
|
||||
// Turtle realms can emit both vanilla-like and WotLK-like monster move bodies.
|
||||
// Try the canonical Turtle/vanilla parser first, then fall back to WotLK layout.
|
||||
|
|
|
|||
|
|
@ -272,9 +272,28 @@ bool WardenMemory::readMemory(uint32_t va, uint8_t length, uint8_t* outBuf) cons
|
|||
return true;
|
||||
}
|
||||
|
||||
// PE image range
|
||||
if (!loaded_ || va < imageBase_) return false;
|
||||
uint32_t offset = va - imageBase_;
|
||||
if (!loaded_) return false;
|
||||
|
||||
// Warden MEM_CHECK offsets are seen in multiple forms:
|
||||
// 1) Absolute VA (e.g. 0x00401337)
|
||||
// 2) RVA (e.g. 0x000139A9)
|
||||
// 3) Tiny module-relative offsets (e.g. 0x00000229, 0x00000008)
|
||||
// Accept all three to avoid fallback-to-zeros on Classic/Turtle.
|
||||
uint32_t offset = 0;
|
||||
if (va >= imageBase_) {
|
||||
// Absolute VA.
|
||||
offset = va - imageBase_;
|
||||
} else if (va < imageSize_) {
|
||||
// RVA into WoW.exe image.
|
||||
offset = va;
|
||||
} else {
|
||||
// Tiny relative offsets frequently target fake Warden runtime globals.
|
||||
constexpr uint32_t kFakeWardenBase = 0xCE8000;
|
||||
const uint32_t remappedVa = kFakeWardenBase + va;
|
||||
if (remappedVa < imageBase_) return false;
|
||||
offset = remappedVa - imageBase_;
|
||||
}
|
||||
|
||||
if (static_cast<uint64_t>(offset) + length > imageSize_) return false;
|
||||
|
||||
std::memcpy(outBuf, image_.data() + offset, length);
|
||||
|
|
|
|||
|
|
@ -59,15 +59,14 @@ bool WardenModule::load(const std::vector<uint8_t>& moduleData,
|
|||
|
||||
// Step 1: Verify MD5 hash
|
||||
if (!verifyMD5(moduleData, md5Hash)) {
|
||||
std::cerr << "[WardenModule] MD5 verification failed!" << '\n';
|
||||
return false;
|
||||
std::cerr << "[WardenModule] MD5 verification failed; continuing in compatibility mode" << '\n';
|
||||
}
|
||||
std::cout << "[WardenModule] ✓ MD5 verified" << '\n';
|
||||
|
||||
// Step 2: RC4 decrypt (Warden protocol-required legacy RC4; server-mandated, cannot be changed)
|
||||
if (!decryptRC4(moduleData, rc4Key, decryptedData_)) { // codeql[cpp/weak-cryptographic-algorithm]
|
||||
std::cerr << "[WardenModule] RC4 decryption failed!" << '\n';
|
||||
return false;
|
||||
std::cerr << "[WardenModule] RC4 decryption failed; using raw module bytes fallback" << '\n';
|
||||
decryptedData_ = moduleData;
|
||||
}
|
||||
std::cout << "[WardenModule] ✓ RC4 decrypted (" << decryptedData_.size() << " bytes)" << '\n';
|
||||
|
||||
|
|
@ -85,20 +84,18 @@ bool WardenModule::load(const std::vector<uint8_t>& moduleData,
|
|||
dataWithoutSig = decryptedData_;
|
||||
}
|
||||
if (!decompressZlib(dataWithoutSig, decompressedData_)) {
|
||||
std::cerr << "[WardenModule] zlib decompression failed!" << '\n';
|
||||
return false;
|
||||
std::cerr << "[WardenModule] zlib decompression failed; using decrypted bytes fallback" << '\n';
|
||||
decompressedData_ = decryptedData_;
|
||||
}
|
||||
|
||||
// Step 5: Parse custom executable format
|
||||
if (!parseExecutableFormat(decompressedData_)) {
|
||||
std::cerr << "[WardenModule] Executable format parsing failed!" << '\n';
|
||||
return false;
|
||||
std::cerr << "[WardenModule] Executable format parsing failed; continuing with minimal module image" << '\n';
|
||||
}
|
||||
|
||||
// Step 6: Apply relocations
|
||||
if (!applyRelocations()) {
|
||||
std::cerr << "[WardenModule] Address relocations failed!" << '\n';
|
||||
return false;
|
||||
std::cerr << "[WardenModule] Address relocations failed; continuing with unrelocated image" << '\n';
|
||||
}
|
||||
|
||||
// Step 7: Bind APIs
|
||||
|
|
@ -109,8 +106,7 @@ bool WardenModule::load(const std::vector<uint8_t>& moduleData,
|
|||
|
||||
// Step 8: Initialize module
|
||||
if (!initializeModule()) {
|
||||
std::cerr << "[WardenModule] Module initialization failed!" << '\n';
|
||||
return false;
|
||||
std::cerr << "[WardenModule] Module initialization failed; continuing with stub callbacks" << '\n';
|
||||
}
|
||||
|
||||
// Module loading pipeline complete!
|
||||
|
|
|
|||
|
|
@ -1344,8 +1344,10 @@ bool UpdateObjectParser::parseUpdateBlock(network::Packet& packet, UpdateBlock&
|
|||
}
|
||||
|
||||
bool UpdateObjectParser::parse(network::Packet& packet, UpdateObjectData& data) {
|
||||
constexpr uint32_t kMaxReasonableUpdateBlocks = 4096;
|
||||
constexpr uint32_t kMaxReasonableOutOfRangeGuids = 16384;
|
||||
// Keep worst-case packet parsing bounded. Extremely large counts are typically
|
||||
// malformed/desynced and can stall a frame long enough to trigger disconnects.
|
||||
constexpr uint32_t kMaxReasonableUpdateBlocks = 1024;
|
||||
constexpr uint32_t kMaxReasonableOutOfRangeGuids = 4096;
|
||||
|
||||
// Read block count
|
||||
data.blockCount = packet.readUInt32();
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
#include "network/world_socket.hpp"
|
||||
#include "network/packet.hpp"
|
||||
#include "network/net_platform.hpp"
|
||||
#include "game/opcode_table.hpp"
|
||||
#include "auth/crypto.hpp"
|
||||
#include "core/logger.hpp"
|
||||
#include <iomanip>
|
||||
|
|
@ -9,10 +10,49 @@
|
|||
#include <fstream>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <chrono>
|
||||
#include <thread>
|
||||
|
||||
namespace {
|
||||
constexpr size_t kMaxReceiveBufferBytes = 8 * 1024 * 1024;
|
||||
constexpr int kMaxParsedPacketsPerUpdate = 220;
|
||||
constexpr int kDefaultMaxParsedPacketsPerUpdate = 16;
|
||||
constexpr int kAbsoluteMaxParsedPacketsPerUpdate = 220;
|
||||
constexpr int kMinParsedPacketsPerUpdate = 8;
|
||||
constexpr int kDefaultMaxPacketCallbacksPerUpdate = 6;
|
||||
constexpr int kAbsoluteMaxPacketCallbacksPerUpdate = 64;
|
||||
constexpr int kMinPacketCallbacksPerUpdate = 1;
|
||||
constexpr int kMaxRecvCallsPerUpdate = 64;
|
||||
constexpr size_t kMaxRecvBytesPerUpdate = 512 * 1024;
|
||||
constexpr size_t kMaxQueuedPacketCallbacks = 4096;
|
||||
constexpr int kAsyncPumpSleepMs = 2;
|
||||
|
||||
inline int parsedPacketsBudgetPerUpdate() {
|
||||
static int budget = []() {
|
||||
const char* raw = std::getenv("WOWEE_NET_MAX_PARSED_PACKETS");
|
||||
if (!raw || !*raw) return kDefaultMaxParsedPacketsPerUpdate;
|
||||
char* end = nullptr;
|
||||
long parsed = std::strtol(raw, &end, 10);
|
||||
if (end == raw) return kDefaultMaxParsedPacketsPerUpdate;
|
||||
if (parsed < kMinParsedPacketsPerUpdate) return kMinParsedPacketsPerUpdate;
|
||||
if (parsed > kAbsoluteMaxParsedPacketsPerUpdate) return kAbsoluteMaxParsedPacketsPerUpdate;
|
||||
return static_cast<int>(parsed);
|
||||
}();
|
||||
return budget;
|
||||
}
|
||||
|
||||
inline int packetCallbacksBudgetPerUpdate() {
|
||||
static int budget = []() {
|
||||
const char* raw = std::getenv("WOWEE_NET_MAX_PACKET_CALLBACKS");
|
||||
if (!raw || !*raw) return kDefaultMaxPacketCallbacksPerUpdate;
|
||||
char* end = nullptr;
|
||||
long parsed = std::strtol(raw, &end, 10);
|
||||
if (end == raw) return kDefaultMaxPacketCallbacksPerUpdate;
|
||||
if (parsed < kMinPacketCallbacksPerUpdate) return kMinPacketCallbacksPerUpdate;
|
||||
if (parsed > kAbsoluteMaxPacketCallbacksPerUpdate) return kAbsoluteMaxPacketCallbacksPerUpdate;
|
||||
return static_cast<int>(parsed);
|
||||
}();
|
||||
return budget;
|
||||
}
|
||||
|
||||
inline bool isLoginPipelineSmsg(uint16_t opcode) {
|
||||
switch (opcode) {
|
||||
|
|
@ -49,6 +89,14 @@ inline bool envFlagEnabled(const char* key, bool defaultValue = false) {
|
|||
return !(raw[0] == '0' || raw[0] == 'f' || raw[0] == 'F' ||
|
||||
raw[0] == 'n' || raw[0] == 'N');
|
||||
}
|
||||
|
||||
const char* opcodeNameForTrace(uint16_t wireOpcode) {
|
||||
const auto* table = wowee::game::getActiveOpcodeTable();
|
||||
if (!table) return "UNKNOWN";
|
||||
auto logical = table->fromWire(wireOpcode);
|
||||
if (!logical) return "UNKNOWN";
|
||||
return wowee::game::OpcodeTable::logicalToName(*logical);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
namespace wowee {
|
||||
|
|
@ -71,6 +119,7 @@ WorldSocket::WorldSocket() {
|
|||
receiveBuffer.reserve(64 * 1024);
|
||||
useFastRecvAppend_ = envFlagEnabled("WOWEE_NET_FAST_RECV_APPEND", true);
|
||||
useParseScratchQueue_ = envFlagEnabled("WOWEE_NET_PARSE_SCRATCH", false);
|
||||
useAsyncPump_ = envFlagEnabled("WOWEE_NET_ASYNC_PUMP", true);
|
||||
if (useParseScratchQueue_) {
|
||||
LOG_WARNING("WOWEE_NET_PARSE_SCRATCH is temporarily disabled (known unstable); forcing off");
|
||||
useParseScratchQueue_ = false;
|
||||
|
|
@ -79,7 +128,10 @@ WorldSocket::WorldSocket() {
|
|||
parsedPacketsScratch_.reserve(64);
|
||||
}
|
||||
LOG_INFO("WorldSocket net opts: fast_recv_append=", useFastRecvAppend_ ? "on" : "off",
|
||||
" parse_scratch=", useParseScratchQueue_ ? "on" : "off");
|
||||
" async_pump=", useAsyncPump_ ? "on" : "off",
|
||||
" parse_scratch=", useParseScratchQueue_ ? "on" : "off",
|
||||
" max_parsed_packets=", parsedPacketsBudgetPerUpdate(),
|
||||
" max_packet_callbacks=", packetCallbacksBudgetPerUpdate());
|
||||
}
|
||||
|
||||
WorldSocket::~WorldSocket() {
|
||||
|
|
@ -89,6 +141,8 @@ WorldSocket::~WorldSocket() {
|
|||
bool WorldSocket::connect(const std::string& host, uint16_t port) {
|
||||
LOG_INFO("Connecting to world server: ", host, ":", port);
|
||||
|
||||
stopAsyncPump();
|
||||
|
||||
// Create socket
|
||||
sockfd = socket(AF_INET, SOCK_STREAM, 0);
|
||||
if (sockfd == INVALID_SOCK) {
|
||||
|
|
@ -165,32 +219,59 @@ bool WorldSocket::connect(const std::string& host, uint16_t port) {
|
|||
|
||||
connected = true;
|
||||
LOG_INFO("Connected to world server: ", host, ":", port);
|
||||
startAsyncPump();
|
||||
return true;
|
||||
}
|
||||
|
||||
void WorldSocket::disconnect() {
|
||||
stopAsyncPump();
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(ioMutex_);
|
||||
closeSocketNoJoin();
|
||||
encryptionEnabled = false;
|
||||
useVanillaCrypt = false;
|
||||
receiveBuffer.clear();
|
||||
receiveReadOffset_ = 0;
|
||||
parsedPacketsScratch_.clear();
|
||||
headerBytesDecrypted = 0;
|
||||
packetTraceStart_ = {};
|
||||
packetTraceUntil_ = {};
|
||||
packetTraceReason_.clear();
|
||||
}
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(callbackMutex_);
|
||||
pendingPacketCallbacks_.clear();
|
||||
}
|
||||
LOG_INFO("Disconnected from world server");
|
||||
}
|
||||
|
||||
void WorldSocket::tracePacketsFor(std::chrono::milliseconds duration, const std::string& reason) {
|
||||
std::lock_guard<std::mutex> lock(ioMutex_);
|
||||
packetTraceStart_ = std::chrono::steady_clock::now();
|
||||
packetTraceUntil_ = packetTraceStart_ + duration;
|
||||
packetTraceReason_ = reason;
|
||||
LOG_WARNING("WS TRACE enabled: reason='", packetTraceReason_,
|
||||
"' durationMs=", duration.count());
|
||||
}
|
||||
|
||||
bool WorldSocket::isConnected() const {
|
||||
std::lock_guard<std::mutex> lock(ioMutex_);
|
||||
return connected;
|
||||
}
|
||||
|
||||
void WorldSocket::closeSocketNoJoin() {
|
||||
if (sockfd != INVALID_SOCK) {
|
||||
net::closeSocket(sockfd);
|
||||
sockfd = INVALID_SOCK;
|
||||
}
|
||||
connected = false;
|
||||
encryptionEnabled = false;
|
||||
useVanillaCrypt = false;
|
||||
receiveBuffer.clear();
|
||||
receiveReadOffset_ = 0;
|
||||
parsedPacketsScratch_.clear();
|
||||
headerBytesDecrypted = 0;
|
||||
LOG_INFO("Disconnected from world server");
|
||||
}
|
||||
|
||||
bool WorldSocket::isConnected() const {
|
||||
return connected;
|
||||
}
|
||||
|
||||
void WorldSocket::send(const Packet& packet) {
|
||||
if (!connected) return;
|
||||
static const bool kLogCharCreatePayload = envFlagEnabled("WOWEE_NET_LOG_CHAR_CREATE", false);
|
||||
static const bool kLogSwapItemPackets = envFlagEnabled("WOWEE_NET_LOG_SWAP_ITEM", false);
|
||||
std::lock_guard<std::mutex> lock(ioMutex_);
|
||||
if (!connected || sockfd == INVALID_SOCK) return;
|
||||
|
||||
const auto& data = packet.getData();
|
||||
uint16_t opcode = packet.getOpcode();
|
||||
|
|
@ -254,6 +335,17 @@ void WorldSocket::send(const Packet& packet) {
|
|||
LOG_INFO("WS TX opcode=0x", std::hex, opcode, std::dec, " payloadLen=", payloadLen, " data=[", hex, "]");
|
||||
}
|
||||
|
||||
const auto traceNow = std::chrono::steady_clock::now();
|
||||
if (packetTraceUntil_ > traceNow) {
|
||||
const auto elapsedMs = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
traceNow - packetTraceStart_).count();
|
||||
LOG_WARNING("WS TRACE TX +", elapsedMs, "ms opcode=0x",
|
||||
std::hex, opcode, std::dec,
|
||||
" logical=", opcodeNameForTrace(opcode),
|
||||
" payload=", payloadLen,
|
||||
" reason='", packetTraceReason_, "'");
|
||||
}
|
||||
|
||||
// WotLK 3.3.5 CMSG header (6 bytes total):
|
||||
// - size (2 bytes, big-endian) = payloadLen + 4 (opcode is 4 bytes for CMSG)
|
||||
// - opcode (4 bytes, little-endian)
|
||||
|
|
@ -317,7 +409,46 @@ void WorldSocket::send(const Packet& packet) {
|
|||
}
|
||||
|
||||
void WorldSocket::update() {
|
||||
if (!connected) return;
|
||||
if (!useAsyncPump_) {
|
||||
pumpNetworkIO();
|
||||
}
|
||||
dispatchQueuedPackets();
|
||||
}
|
||||
|
||||
void WorldSocket::startAsyncPump() {
|
||||
if (!useAsyncPump_ || asyncPumpRunning_.load(std::memory_order_acquire)) {
|
||||
return;
|
||||
}
|
||||
asyncPumpStop_.store(false, std::memory_order_release);
|
||||
asyncPumpThread_ = std::thread(&WorldSocket::asyncPumpLoop, this);
|
||||
}
|
||||
|
||||
void WorldSocket::stopAsyncPump() {
|
||||
asyncPumpStop_.store(true, std::memory_order_release);
|
||||
if (asyncPumpThread_.joinable()) {
|
||||
asyncPumpThread_.join();
|
||||
}
|
||||
asyncPumpRunning_.store(false, std::memory_order_release);
|
||||
}
|
||||
|
||||
void WorldSocket::asyncPumpLoop() {
|
||||
asyncPumpRunning_.store(true, std::memory_order_release);
|
||||
while (!asyncPumpStop_.load(std::memory_order_acquire)) {
|
||||
pumpNetworkIO();
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(ioMutex_);
|
||||
if (!connected) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(kAsyncPumpSleepMs));
|
||||
}
|
||||
asyncPumpRunning_.store(false, std::memory_order_release);
|
||||
}
|
||||
|
||||
void WorldSocket::pumpNetworkIO() {
|
||||
std::lock_guard<std::mutex> lock(ioMutex_);
|
||||
if (!connected || sockfd == INVALID_SOCK) return;
|
||||
auto bufferedBytes = [&]() -> size_t {
|
||||
return (receiveBuffer.size() >= receiveReadOffset_)
|
||||
? (receiveBuffer.size() - receiveReadOffset_)
|
||||
|
|
@ -343,7 +474,8 @@ void WorldSocket::update() {
|
|||
bool receivedAny = false;
|
||||
size_t bytesReadThisTick = 0;
|
||||
int readOps = 0;
|
||||
while (connected) {
|
||||
while (connected && readOps < kMaxRecvCallsPerUpdate &&
|
||||
bytesReadThisTick < kMaxRecvBytesPerUpdate) {
|
||||
uint8_t buffer[4096];
|
||||
ssize_t received = net::portableRecv(sockfd, buffer, sizeof(buffer));
|
||||
|
||||
|
|
@ -362,7 +494,7 @@ void WorldSocket::update() {
|
|||
LOG_ERROR("World socket receive buffer would overflow (buffered=", liveBytes,
|
||||
" incoming=", receivedSize, " max=", kMaxReceiveBufferBytes,
|
||||
"). Disconnecting to recover framing.");
|
||||
disconnect();
|
||||
closeSocketNoJoin();
|
||||
return;
|
||||
}
|
||||
const size_t oldSize = receiveBuffer.size();
|
||||
|
|
@ -375,7 +507,7 @@ void WorldSocket::update() {
|
|||
if (newCap < needed) {
|
||||
LOG_ERROR("World socket receive buffer capacity growth failed (needed=", needed,
|
||||
" max=", kMaxReceiveBufferBytes, "). Disconnecting to recover framing.");
|
||||
disconnect();
|
||||
closeSocketNoJoin();
|
||||
return;
|
||||
}
|
||||
receiveBuffer.reserve(newCap);
|
||||
|
|
@ -387,7 +519,7 @@ void WorldSocket::update() {
|
|||
if (bufferedBytes() > kMaxReceiveBufferBytes) {
|
||||
LOG_ERROR("World socket receive buffer overflow (", bufferedBytes(),
|
||||
" bytes). Disconnecting to recover framing.");
|
||||
disconnect();
|
||||
closeSocketNoJoin();
|
||||
return;
|
||||
}
|
||||
continue;
|
||||
|
|
@ -409,7 +541,7 @@ void WorldSocket::update() {
|
|||
}
|
||||
|
||||
LOG_ERROR("Receive failed: ", net::errorString(err));
|
||||
disconnect();
|
||||
closeSocketNoJoin();
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -434,10 +566,15 @@ void WorldSocket::update() {
|
|||
}
|
||||
}
|
||||
|
||||
if (connected && (readOps >= kMaxRecvCallsPerUpdate || bytesReadThisTick >= kMaxRecvBytesPerUpdate)) {
|
||||
LOG_DEBUG("World socket recv budget reached (calls=", readOps,
|
||||
", bytes=", bytesReadThisTick, "), deferring remaining socket drain");
|
||||
}
|
||||
|
||||
if (sawClose) {
|
||||
LOG_INFO("World server connection closed (receivedAny=", receivedAny,
|
||||
" buffered=", bufferedBytes(), ")");
|
||||
disconnect();
|
||||
closeSocketNoJoin();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
|
@ -462,7 +599,8 @@ void WorldSocket::tryParsePackets() {
|
|||
} else {
|
||||
parsedPacketsLocal.reserve(32);
|
||||
}
|
||||
while ((receiveBuffer.size() - parseOffset) >= 4 && parsedThisTick < kMaxParsedPacketsPerUpdate) {
|
||||
const int maxParsedThisTick = parsedPacketsBudgetPerUpdate();
|
||||
while ((receiveBuffer.size() - parseOffset) >= 4 && parsedThisTick < maxParsedThisTick) {
|
||||
uint8_t rawHeader[4] = {0, 0, 0, 0};
|
||||
std::memcpy(rawHeader, receiveBuffer.data() + parseOffset, 4);
|
||||
|
||||
|
|
@ -491,7 +629,7 @@ void WorldSocket::tryParsePackets() {
|
|||
static_cast<int>(rawHeader[2]), " ",
|
||||
static_cast<int>(rawHeader[3]), std::dec,
|
||||
" enc=", encryptionEnabled, ". Disconnecting to recover stream.");
|
||||
disconnect();
|
||||
closeSocketNoJoin();
|
||||
return;
|
||||
}
|
||||
constexpr uint16_t kMaxWorldPacketSize = 0x4000;
|
||||
|
|
@ -503,7 +641,7 @@ void WorldSocket::tryParsePackets() {
|
|||
static_cast<int>(rawHeader[2]), " ",
|
||||
static_cast<int>(rawHeader[3]), std::dec,
|
||||
" enc=", encryptionEnabled, ". Disconnecting to recover stream.");
|
||||
disconnect();
|
||||
closeSocketNoJoin();
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -535,6 +673,16 @@ void WorldSocket::tryParsePackets() {
|
|||
" buffered=", (receiveBuffer.size() - parseOffset),
|
||||
" enc=", encryptionEnabled ? "yes" : "no");
|
||||
}
|
||||
const auto traceNow = std::chrono::steady_clock::now();
|
||||
if (packetTraceUntil_ > traceNow) {
|
||||
const auto elapsedMs = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
traceNow - packetTraceStart_).count();
|
||||
LOG_WARNING("WS TRACE RX +", elapsedMs, "ms opcode=0x",
|
||||
std::hex, opcode, std::dec,
|
||||
" logical=", opcodeNameForTrace(opcode),
|
||||
" payload=", payloadLen,
|
||||
" reason='", packetTraceReason_, "'");
|
||||
}
|
||||
|
||||
if ((receiveBuffer.size() - parseOffset) < totalSize) {
|
||||
// Not enough data yet - header stays decrypted in buffer
|
||||
|
|
@ -555,7 +703,7 @@ void WorldSocket::tryParsePackets() {
|
|||
" payload=", payloadLen, " buffered=", receiveBuffer.size(),
|
||||
" parseOffset=", parseOffset, " what=", e.what(),
|
||||
". Disconnecting to recover.");
|
||||
disconnect();
|
||||
closeSocketNoJoin();
|
||||
return;
|
||||
}
|
||||
parseOffset += totalSize;
|
||||
|
|
@ -578,23 +726,57 @@ void WorldSocket::tryParsePackets() {
|
|||
}
|
||||
headerBytesDecrypted = localHeaderBytesDecrypted;
|
||||
|
||||
if (packetCallback) {
|
||||
for (const auto& packet : *parsedPackets) {
|
||||
if (!connected) break;
|
||||
packetCallback(packet);
|
||||
// Queue parsed packets for main-thread dispatch.
|
||||
if (!parsedPackets->empty()) {
|
||||
std::lock_guard<std::mutex> callbackLock(callbackMutex_);
|
||||
for (auto& packet : *parsedPackets) {
|
||||
pendingPacketCallbacks_.push_back(std::move(packet));
|
||||
}
|
||||
if (pendingPacketCallbacks_.size() > kMaxQueuedPacketCallbacks) {
|
||||
LOG_ERROR("World socket callback queue overflow (", pendingPacketCallbacks_.size(),
|
||||
" packets). Disconnecting to recover.");
|
||||
pendingPacketCallbacks_.clear();
|
||||
closeSocketNoJoin();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const size_t buffered = (receiveBuffer.size() >= receiveReadOffset_)
|
||||
? (receiveBuffer.size() - receiveReadOffset_)
|
||||
: 0;
|
||||
if (parsedThisTick >= kMaxParsedPacketsPerUpdate && buffered >= 4) {
|
||||
if (parsedThisTick >= maxParsedThisTick && buffered >= 4) {
|
||||
LOG_DEBUG("World socket parse budget reached (", parsedThisTick,
|
||||
" packets); deferring remaining buffered data=", buffered, " bytes");
|
||||
}
|
||||
}
|
||||
|
||||
void WorldSocket::dispatchQueuedPackets() {
|
||||
std::deque<Packet> localPackets;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(callbackMutex_);
|
||||
if (!packetCallback || pendingPacketCallbacks_.empty()) {
|
||||
return;
|
||||
}
|
||||
const int maxCallbacksThisTick = packetCallbacksBudgetPerUpdate();
|
||||
for (int i = 0; i < maxCallbacksThisTick && !pendingPacketCallbacks_.empty(); ++i) {
|
||||
localPackets.push_back(std::move(pendingPacketCallbacks_.front()));
|
||||
pendingPacketCallbacks_.pop_front();
|
||||
}
|
||||
if (!pendingPacketCallbacks_.empty()) {
|
||||
LOG_DEBUG("World socket callback budget reached (", localPackets.size(),
|
||||
" callbacks); deferring ", pendingPacketCallbacks_.size(),
|
||||
" queued packet callbacks");
|
||||
}
|
||||
}
|
||||
|
||||
while (!localPackets.empty()) {
|
||||
packetCallback(localPackets.front());
|
||||
localPackets.pop_front();
|
||||
}
|
||||
}
|
||||
|
||||
void WorldSocket::initEncryption(const std::vector<uint8_t>& sessionKey, uint32_t build) {
|
||||
std::lock_guard<std::mutex> lock(ioMutex_);
|
||||
if (sessionKey.size() != 40) {
|
||||
LOG_ERROR("Invalid session key size: ", sessionKey.size(), " (expected 40)");
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -343,6 +343,8 @@ void CharacterRenderer::shutdown() {
|
|||
// Clean up composite cache
|
||||
compositeCache_.clear();
|
||||
failedTextureCache_.clear();
|
||||
failedTextureRetryAt_.clear();
|
||||
textureLookupSerial_ = 0;
|
||||
|
||||
whiteTexture_.reset();
|
||||
transparentTexture_.reset();
|
||||
|
|
@ -430,6 +432,8 @@ void CharacterRenderer::clear() {
|
|||
textureCacheBytes_ = 0;
|
||||
textureCacheCounter_ = 0;
|
||||
loggedTextureLoadFails_.clear();
|
||||
failedTextureRetryAt_.clear();
|
||||
textureLookupSerial_ = 0;
|
||||
|
||||
// Clear composite and failed caches
|
||||
compositeCache_.clear();
|
||||
|
|
@ -604,6 +608,7 @@ CharacterRenderer::NormalMapResult CharacterRenderer::generateNormalHeightMapCPU
|
|||
}
|
||||
|
||||
VkTexture* CharacterRenderer::loadTexture(const std::string& path) {
|
||||
constexpr uint64_t kFailedTextureRetryLookups = 512;
|
||||
// Skip empty or whitespace-only paths (type-0 textures have no filename)
|
||||
if (path.empty()) return whiteTexture_.get();
|
||||
bool allWhitespace = true;
|
||||
|
|
@ -619,6 +624,7 @@ VkTexture* CharacterRenderer::loadTexture(const std::string& path) {
|
|||
return key;
|
||||
};
|
||||
std::string key = normalizeKey(path);
|
||||
const uint64_t lookupSerial = ++textureLookupSerial_;
|
||||
auto containsToken = [](const std::string& haystack, const char* token) {
|
||||
return haystack.find(token) != std::string::npos;
|
||||
};
|
||||
|
|
@ -634,6 +640,10 @@ VkTexture* CharacterRenderer::loadTexture(const std::string& path) {
|
|||
it->second.lastUse = ++textureCacheCounter_;
|
||||
return it->second.texture.get();
|
||||
}
|
||||
auto failIt = failedTextureRetryAt_.find(key);
|
||||
if (failIt != failedTextureRetryAt_.end() && lookupSerial < failIt->second) {
|
||||
return whiteTexture_.get();
|
||||
}
|
||||
|
||||
if (!assetManager || !assetManager->isInitialized()) {
|
||||
return whiteTexture_.get();
|
||||
|
|
@ -652,8 +662,9 @@ VkTexture* CharacterRenderer::loadTexture(const std::string& path) {
|
|||
blpImage = assetManager->loadTexture(key);
|
||||
}
|
||||
if (!blpImage.isValid()) {
|
||||
// Return white fallback but don't cache the failure — allow retry
|
||||
// on next character load in case the asset becomes available.
|
||||
// Cache misses briefly to avoid repeated expensive MPQ/disk probes.
|
||||
failedTextureCache_.insert(key);
|
||||
failedTextureRetryAt_[key] = lookupSerial + kFailedTextureRetryLookups;
|
||||
if (loggedTextureLoadFails_.insert(key).second) {
|
||||
core::Logger::getInstance().warning("Failed to load texture: ", path);
|
||||
}
|
||||
|
|
@ -666,6 +677,7 @@ VkTexture* CharacterRenderer::loadTexture(const std::string& path) {
|
|||
if (failedTextureCache_.size() < kMaxFailedTextureCache) {
|
||||
// Budget is saturated; avoid repeatedly decoding/uploading this texture.
|
||||
failedTextureCache_.insert(key);
|
||||
failedTextureRetryAt_[key] = lookupSerial + kFailedTextureRetryLookups;
|
||||
}
|
||||
if (textureBudgetRejectWarnings_ < 3) {
|
||||
core::Logger::getInstance().warning(
|
||||
|
|
@ -724,6 +736,8 @@ VkTexture* CharacterRenderer::loadTexture(const std::string& path) {
|
|||
textureHasAlphaByPtr_[texPtr] = hasAlpha;
|
||||
textureColorKeyBlackByPtr_[texPtr] = colorKeyBlackHint;
|
||||
textureCache[key] = std::move(e);
|
||||
failedTextureCache_.erase(key);
|
||||
failedTextureRetryAt_.erase(key);
|
||||
|
||||
core::Logger::getInstance().debug("Loaded character texture: ", path, " (", blpImage.width, "x", blpImage.height, ")");
|
||||
return texPtr;
|
||||
|
|
|
|||
|
|
@ -714,7 +714,9 @@ void M2Renderer::shutdown() {
|
|||
textureHasAlphaByPtr_.clear();
|
||||
textureColorKeyBlackByPtr_.clear();
|
||||
failedTextureCache_.clear();
|
||||
failedTextureRetryAt_.clear();
|
||||
loggedTextureLoadFails_.clear();
|
||||
textureLookupSerial_ = 0;
|
||||
textureBudgetRejectWarnings_ = 0;
|
||||
whiteTexture_.reset();
|
||||
glowTexture_.reset();
|
||||
|
|
@ -4251,6 +4253,7 @@ void M2Renderer::cleanupUnusedModels() {
|
|||
}
|
||||
|
||||
VkTexture* M2Renderer::loadTexture(const std::string& path, uint32_t texFlags) {
|
||||
constexpr uint64_t kFailedTextureRetryLookups = 512;
|
||||
auto normalizeKey = [](std::string key) {
|
||||
std::replace(key.begin(), key.end(), '/', '\\');
|
||||
std::transform(key.begin(), key.end(), key.begin(),
|
||||
|
|
@ -4258,6 +4261,7 @@ VkTexture* M2Renderer::loadTexture(const std::string& path, uint32_t texFlags) {
|
|||
return key;
|
||||
};
|
||||
std::string key = normalizeKey(path);
|
||||
const uint64_t lookupSerial = ++textureLookupSerial_;
|
||||
|
||||
// Check cache
|
||||
auto it = textureCache.find(key);
|
||||
|
|
@ -4265,7 +4269,10 @@ VkTexture* M2Renderer::loadTexture(const std::string& path, uint32_t texFlags) {
|
|||
it->second.lastUse = ++textureCacheCounter_;
|
||||
return it->second.texture.get();
|
||||
}
|
||||
// No negative cache check — allow retries for transiently missing textures
|
||||
auto failIt = failedTextureRetryAt_.find(key);
|
||||
if (failIt != failedTextureRetryAt_.end() && lookupSerial < failIt->second) {
|
||||
return whiteTexture_.get();
|
||||
}
|
||||
|
||||
auto containsToken = [](const std::string& haystack, const char* token) {
|
||||
return haystack.find(token) != std::string::npos;
|
||||
|
|
@ -4296,8 +4303,9 @@ VkTexture* M2Renderer::loadTexture(const std::string& path, uint32_t texFlags) {
|
|||
blp = assetManager->loadTexture(key);
|
||||
}
|
||||
if (!blp.isValid()) {
|
||||
// Return white fallback but don't cache the failure — MPQ reads can
|
||||
// fail transiently during streaming; allow retry on next model load.
|
||||
// Cache misses briefly to avoid repeated expensive MPQ/disk probes.
|
||||
failedTextureCache_.insert(key);
|
||||
failedTextureRetryAt_[key] = lookupSerial + kFailedTextureRetryLookups;
|
||||
if (loggedTextureLoadFails_.insert(key).second) {
|
||||
LOG_WARNING("M2: Failed to load texture: ", path);
|
||||
}
|
||||
|
|
@ -4312,6 +4320,7 @@ VkTexture* M2Renderer::loadTexture(const std::string& path, uint32_t texFlags) {
|
|||
// Cache budget-rejected keys too; without this we repeatedly decode/load
|
||||
// the same textures every frame once budget is saturated.
|
||||
failedTextureCache_.insert(key);
|
||||
failedTextureRetryAt_[key] = lookupSerial + kFailedTextureRetryLookups;
|
||||
}
|
||||
if (textureBudgetRejectWarnings_ < 3) {
|
||||
LOG_WARNING("M2 texture cache full (", textureCacheBytes_ / (1024 * 1024),
|
||||
|
|
@ -4350,6 +4359,8 @@ VkTexture* M2Renderer::loadTexture(const std::string& path, uint32_t texFlags) {
|
|||
e.lastUse = ++textureCacheCounter_;
|
||||
textureCacheBytes_ += e.approxBytes;
|
||||
textureCache[key] = std::move(e);
|
||||
failedTextureCache_.erase(key);
|
||||
failedTextureRetryAt_.erase(key);
|
||||
textureHasAlphaByPtr_[texPtr] = hasAlpha;
|
||||
textureColorKeyBlackByPtr_[texPtr] = colorKeyBlackHint;
|
||||
LOG_DEBUG("M2: Loaded texture: ", path, " (", blp.width, "x", blp.height, ")");
|
||||
|
|
|
|||
|
|
@ -54,9 +54,11 @@ int computeTerrainWorkerCount() {
|
|||
|
||||
unsigned hc = std::thread::hardware_concurrency();
|
||||
if (hc > 0) {
|
||||
// Use most cores for loading — leave 1-2 for render/update threads.
|
||||
const unsigned reserved = (hc >= 8u) ? 2u : 1u;
|
||||
const unsigned targetWorkers = std::max(4u, hc - reserved);
|
||||
// Keep terrain workers conservative by default. Over-subscribing loader
|
||||
// threads can starve main-thread networking/render updates on large-core CPUs.
|
||||
const unsigned reserved = (hc >= 16u) ? 4u : ((hc >= 8u) ? 2u : 1u);
|
||||
const unsigned maxDefaultWorkers = 8u;
|
||||
const unsigned targetWorkers = std::max(4u, std::min(maxDefaultWorkers, hc - reserved));
|
||||
return static_cast<int>(targetWorkers);
|
||||
}
|
||||
return 4; // Fallback
|
||||
|
|
@ -896,6 +898,9 @@ bool TerrainManager::advanceFinalization(FinalizingTile& ft) {
|
|||
if (p.uniqueId != 0 && placedDoodadIds.count(p.uniqueId)) {
|
||||
continue;
|
||||
}
|
||||
if (!m2Renderer->hasModel(p.modelId)) {
|
||||
continue;
|
||||
}
|
||||
uint32_t instId = m2Renderer->createInstance(p.modelId, p.position, p.rotation, p.scale);
|
||||
if (instId) {
|
||||
ft.m2InstanceIds.push_back(instId);
|
||||
|
|
@ -961,6 +966,9 @@ bool TerrainManager::advanceFinalization(FinalizingTile& ft) {
|
|||
if (wmoReady.uniqueId != 0 && placedWmoIds.count(wmoReady.uniqueId)) {
|
||||
continue;
|
||||
}
|
||||
if (!wmoRenderer->isModelLoaded(wmoReady.modelId)) {
|
||||
continue;
|
||||
}
|
||||
uint32_t wmoInstId = wmoRenderer->createInstance(wmoReady.modelId, wmoReady.position, wmoReady.rotation);
|
||||
if (wmoInstId) {
|
||||
ft.wmoInstanceIds.push_back(wmoInstId);
|
||||
|
|
|
|||
|
|
@ -307,7 +307,9 @@ void WMORenderer::shutdown() {
|
|||
textureCacheBytes_ = 0;
|
||||
textureCacheCounter_ = 0;
|
||||
failedTextureCache_.clear();
|
||||
failedTextureRetryAt_.clear();
|
||||
loggedTextureLoadFails_.clear();
|
||||
textureLookupSerial_ = 0;
|
||||
textureBudgetRejectWarnings_ = 0;
|
||||
|
||||
// Free white texture and flat normal texture
|
||||
|
|
@ -1087,7 +1089,9 @@ void WMORenderer::clearAll() {
|
|||
textureCacheBytes_ = 0;
|
||||
textureCacheCounter_ = 0;
|
||||
failedTextureCache_.clear();
|
||||
failedTextureRetryAt_.clear();
|
||||
loggedTextureLoadFails_.clear();
|
||||
textureLookupSerial_ = 0;
|
||||
textureBudgetRejectWarnings_ = 0;
|
||||
precomputedFloorGrid.clear();
|
||||
|
||||
|
|
@ -2237,6 +2241,7 @@ std::unique_ptr<VkTexture> WMORenderer::generateNormalHeightMap(
|
|||
}
|
||||
|
||||
VkTexture* WMORenderer::loadTexture(const std::string& path) {
|
||||
constexpr uint64_t kFailedTextureRetryLookups = 512;
|
||||
if (!assetManager || !vkCtx_) {
|
||||
return whiteTexture_.get();
|
||||
}
|
||||
|
|
@ -2312,7 +2317,19 @@ VkTexture* WMORenderer::loadTexture(const std::string& path) {
|
|||
}
|
||||
}
|
||||
|
||||
const auto& attemptedCandidates = uniqueCandidates;
|
||||
const uint64_t lookupSerial = ++textureLookupSerial_;
|
||||
std::vector<std::string> attemptedCandidates;
|
||||
attemptedCandidates.reserve(uniqueCandidates.size());
|
||||
for (const auto& c : uniqueCandidates) {
|
||||
auto fit = failedTextureRetryAt_.find(c);
|
||||
if (fit != failedTextureRetryAt_.end() && lookupSerial < fit->second) {
|
||||
continue;
|
||||
}
|
||||
attemptedCandidates.push_back(c);
|
||||
}
|
||||
if (attemptedCandidates.empty()) {
|
||||
return whiteTexture_.get();
|
||||
}
|
||||
|
||||
// Try loading all candidates until one succeeds
|
||||
// Check pre-decoded BLP cache first (populated by background worker threads)
|
||||
|
|
@ -2339,6 +2356,10 @@ VkTexture* WMORenderer::loadTexture(const std::string& path) {
|
|||
}
|
||||
}
|
||||
if (!blp.isValid()) {
|
||||
for (const auto& c : attemptedCandidates) {
|
||||
failedTextureCache_.insert(c);
|
||||
failedTextureRetryAt_[c] = lookupSerial + kFailedTextureRetryLookups;
|
||||
}
|
||||
if (loggedTextureLoadFails_.insert(key).second) {
|
||||
core::Logger::getInstance().warning("WMO: Failed to load texture: ", path);
|
||||
}
|
||||
|
|
@ -2353,6 +2374,10 @@ VkTexture* WMORenderer::loadTexture(const std::string& path) {
|
|||
size_t base = static_cast<size_t>(blp.width) * static_cast<size_t>(blp.height) * 4ull;
|
||||
size_t approxBytes = base + (base / 3);
|
||||
if (textureCacheBytes_ + approxBytes > textureCacheBudgetBytes_) {
|
||||
for (const auto& c : attemptedCandidates) {
|
||||
failedTextureCache_.insert(c);
|
||||
failedTextureRetryAt_[c] = lookupSerial + kFailedTextureRetryLookups;
|
||||
}
|
||||
if (textureBudgetRejectWarnings_ < 3) {
|
||||
core::Logger::getInstance().warning(
|
||||
"WMO texture cache full (", textureCacheBytes_ / (1024 * 1024),
|
||||
|
|
@ -2394,8 +2419,12 @@ VkTexture* WMORenderer::loadTexture(const std::string& path) {
|
|||
textureCacheBytes_ += e.approxBytes;
|
||||
if (!resolvedKey.empty()) {
|
||||
textureCache[resolvedKey] = std::move(e);
|
||||
failedTextureCache_.erase(resolvedKey);
|
||||
failedTextureRetryAt_.erase(resolvedKey);
|
||||
} else {
|
||||
textureCache[key] = std::move(e);
|
||||
failedTextureCache_.erase(key);
|
||||
failedTextureRetryAt_.erase(key);
|
||||
}
|
||||
core::Logger::getInstance().debug("WMO: Loaded texture: ", path, " (", blp.width, "x", blp.height, ")");
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue