mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-05-05 16:43:52 +00:00
Compare commits
10 commits
06979e5c5c
...
05d1c874d9
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
05d1c874d9 | ||
|
|
a1c41422cc | ||
|
|
a81a418dd2 | ||
|
|
d87b27a24a | ||
|
|
33c1a33059 | ||
|
|
d2ec0af6d9 | ||
|
|
2820463c7a | ||
|
|
e20341202a | ||
|
|
cce0b7e42b | ||
|
|
250fcd4f2e |
21 changed files with 824 additions and 373 deletions
|
|
@ -7,6 +7,7 @@
|
|||
A native C++ World of Warcraft client with a custom OpenGL renderer.
|
||||
|
||||
[](https://github.com/sponsors/Kelsidavis)
|
||||
[](https://discord.gg/SDqjA79B)
|
||||
|
||||
[](https://youtu.be/Pd9JuYYxu0o)
|
||||
|
||||
|
|
|
|||
BIN
assets/krayonload.png
Normal file
BIN
assets/krayonload.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.3 MiB |
BIN
assets/krayonsignin.png
Normal file
BIN
assets/krayonsignin.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.3 MiB |
|
|
@ -5,6 +5,8 @@
|
|||
#include <sstream>
|
||||
#include <mutex>
|
||||
#include <fstream>
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
|
||||
namespace wowee {
|
||||
namespace core {
|
||||
|
|
@ -30,29 +32,35 @@ public:
|
|||
|
||||
void log(LogLevel level, const std::string& message);
|
||||
void setLogLevel(LogLevel level);
|
||||
bool shouldLog(LogLevel level) const;
|
||||
|
||||
template<typename... Args>
|
||||
void debug(Args&&... args) {
|
||||
if (!shouldLog(LogLevel::DEBUG)) return;
|
||||
log(LogLevel::DEBUG, format(std::forward<Args>(args)...));
|
||||
}
|
||||
|
||||
template<typename... Args>
|
||||
void info(Args&&... args) {
|
||||
if (!shouldLog(LogLevel::INFO)) return;
|
||||
log(LogLevel::INFO, format(std::forward<Args>(args)...));
|
||||
}
|
||||
|
||||
template<typename... Args>
|
||||
void warning(Args&&... args) {
|
||||
if (!shouldLog(LogLevel::WARNING)) return;
|
||||
log(LogLevel::WARNING, format(std::forward<Args>(args)...));
|
||||
}
|
||||
|
||||
template<typename... Args>
|
||||
void error(Args&&... args) {
|
||||
if (!shouldLog(LogLevel::ERROR)) return;
|
||||
log(LogLevel::ERROR, format(std::forward<Args>(args)...));
|
||||
}
|
||||
|
||||
template<typename... Args>
|
||||
void fatal(Args&&... args) {
|
||||
if (!shouldLog(LogLevel::FATAL)) return;
|
||||
log(LogLevel::FATAL, format(std::forward<Args>(args)...));
|
||||
}
|
||||
|
||||
|
|
@ -69,10 +77,13 @@ private:
|
|||
return oss.str();
|
||||
}
|
||||
|
||||
LogLevel minLevel = LogLevel::INFO; // Changed from DEBUG to reduce log spam
|
||||
std::atomic<int> minLevel_{static_cast<int>(LogLevel::INFO)};
|
||||
std::mutex mutex;
|
||||
std::ofstream fileStream;
|
||||
bool fileReady = false;
|
||||
bool echoToStdout_ = true;
|
||||
std::chrono::steady_clock::time_point lastFlushTime_{};
|
||||
uint32_t flushIntervalMs_ = 250;
|
||||
void ensureFile();
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -353,6 +353,7 @@ class TurtlePacketParsers : public ClassicPacketParsers {
|
|||
public:
|
||||
uint8_t movementFlags2Size() const override { return 0; }
|
||||
bool parseMovementBlock(network::Packet& packet, UpdateBlock& block) override;
|
||||
bool parseMonsterMove(network::Packet& packet, MonsterMoveData& data) override;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ public:
|
|||
Packet() = default;
|
||||
explicit Packet(uint16_t opcode);
|
||||
Packet(uint16_t opcode, const std::vector<uint8_t>& data);
|
||||
Packet(uint16_t opcode, std::vector<uint8_t>&& data);
|
||||
|
||||
void writeUInt8(uint8_t value);
|
||||
void writeUInt16(uint16_t value);
|
||||
|
|
|
|||
|
|
@ -91,6 +91,13 @@ private:
|
|||
|
||||
// Receive buffer
|
||||
std::vector<uint8_t> receiveBuffer;
|
||||
size_t receiveReadOffset_ = 0;
|
||||
// Optional reused packet queue (feature-gated) to reduce per-update allocations.
|
||||
std::vector<Packet> parsedPacketsScratch_;
|
||||
|
||||
// Runtime-gated network optimization toggles (default off).
|
||||
bool useFastRecvAppend_ = false;
|
||||
bool useParseScratchQueue_ = false;
|
||||
|
||||
// Track how many header bytes have been decrypted (0-4)
|
||||
// This prevents re-decrypting the same header when waiting for more data
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
#pragma once
|
||||
|
||||
#include "auth/auth_handler.hpp"
|
||||
#include "rendering/video_player.hpp"
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <functional>
|
||||
|
|
@ -16,6 +16,7 @@ namespace wowee { namespace ui {
|
|||
class AuthScreen {
|
||||
public:
|
||||
AuthScreen();
|
||||
~AuthScreen();
|
||||
|
||||
/**
|
||||
* Render the UI
|
||||
|
|
@ -103,9 +104,13 @@ private:
|
|||
void upsertCurrentServerProfile(bool includePasswordHash);
|
||||
std::string currentExpansionId() const;
|
||||
|
||||
// Background video
|
||||
bool videoInitAttempted = false;
|
||||
rendering::VideoPlayer backgroundVideo;
|
||||
// Background image (OpenGL texture)
|
||||
bool bgInitAttempted = false;
|
||||
bool loadBackgroundImage();
|
||||
void destroyBackgroundImage();
|
||||
uint32_t bgTextureId_ = 0;
|
||||
int bgWidth_ = 0;
|
||||
int bgHeight_ = 0;
|
||||
|
||||
bool musicInitAttempted = false;
|
||||
bool musicPlaying = false;
|
||||
|
|
|
|||
|
|
@ -58,6 +58,14 @@
|
|||
namespace wowee {
|
||||
namespace core {
|
||||
|
||||
namespace {
|
||||
bool envFlagEnabled(const char* key, bool defaultValue = false) {
|
||||
const char* raw = std::getenv(key);
|
||||
if (!raw || !*raw) return defaultValue;
|
||||
return !(raw[0] == '0' || raw[0] == 'f' || raw[0] == 'F' ||
|
||||
raw[0] == 'n' || raw[0] == 'N');
|
||||
}
|
||||
} // namespace
|
||||
|
||||
const char* Application::mapIdToName(uint32_t mapId) {
|
||||
switch (mapId) {
|
||||
|
|
@ -221,6 +229,10 @@ bool Application::initialize() {
|
|||
|
||||
void Application::run() {
|
||||
LOG_INFO("Starting main loop");
|
||||
const bool frameProfileEnabled = envFlagEnabled("WOWEE_FRAME_PROFILE", false);
|
||||
if (frameProfileEnabled) {
|
||||
LOG_INFO("Frame timing profile enabled (WOWEE_FRAME_PROFILE=1)");
|
||||
}
|
||||
|
||||
auto lastTime = std::chrono::high_resolution_clock::now();
|
||||
|
||||
|
|
@ -336,8 +348,11 @@ void Application::run() {
|
|||
totalSwapMs += std::chrono::duration<double, std::milli>(t4 - t3).count();
|
||||
|
||||
if (++frameCount >= 60) {
|
||||
printf("[Frame] Update: %.1f ms, Render: %.1f ms, Swap: %.1f ms\n",
|
||||
totalUpdateMs / 60.0, totalRenderMs / 60.0, totalSwapMs / 60.0);
|
||||
if (frameProfileEnabled && core::Logger::getInstance().shouldLog(core::LogLevel::DEBUG)) {
|
||||
LOG_DEBUG("[Frame] Update: ", totalUpdateMs / 60.0,
|
||||
"ms Render: ", totalRenderMs / 60.0,
|
||||
"ms Swap: ", totalSwapMs / 60.0, "ms");
|
||||
}
|
||||
frameCount = 0;
|
||||
totalUpdateMs = totalRenderMs = totalSwapMs = 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
#include <iomanip>
|
||||
#include <ctime>
|
||||
#include <filesystem>
|
||||
#include <cstdlib>
|
||||
|
||||
namespace wowee {
|
||||
namespace core {
|
||||
|
|
@ -15,13 +16,26 @@ Logger& Logger::getInstance() {
|
|||
void Logger::ensureFile() {
|
||||
if (fileReady) return;
|
||||
fileReady = true;
|
||||
if (const char* logStdout = std::getenv("WOWEE_LOG_STDOUT")) {
|
||||
if (logStdout[0] == '0') {
|
||||
echoToStdout_ = false;
|
||||
}
|
||||
}
|
||||
if (const char* flushMs = std::getenv("WOWEE_LOG_FLUSH_MS")) {
|
||||
char* end = nullptr;
|
||||
unsigned long parsed = std::strtoul(flushMs, &end, 10);
|
||||
if (end != flushMs && parsed <= 10000ul) {
|
||||
flushIntervalMs_ = static_cast<uint32_t>(parsed);
|
||||
}
|
||||
}
|
||||
std::error_code ec;
|
||||
std::filesystem::create_directories("logs", ec);
|
||||
fileStream.open("logs/wowee.log", std::ios::out | std::ios::trunc);
|
||||
lastFlushTime_ = std::chrono::steady_clock::now();
|
||||
}
|
||||
|
||||
void Logger::log(LogLevel level, const std::string& message) {
|
||||
if (level < minLevel) {
|
||||
if (!shouldLog(level)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -58,15 +72,32 @@ void Logger::log(LogLevel level, const std::string& message) {
|
|||
|
||||
line << "] " << message;
|
||||
|
||||
std::cout << line.str() << '\n';
|
||||
if (echoToStdout_) {
|
||||
std::cout << line.str() << '\n';
|
||||
}
|
||||
if (fileStream.is_open()) {
|
||||
fileStream << line.str() << '\n';
|
||||
fileStream.flush();
|
||||
bool shouldFlush = (level >= LogLevel::WARNING);
|
||||
if (!shouldFlush) {
|
||||
auto nowSteady = std::chrono::steady_clock::now();
|
||||
auto elapsedMs = std::chrono::duration_cast<std::chrono::milliseconds>(nowSteady - lastFlushTime_).count();
|
||||
shouldFlush = (elapsedMs >= static_cast<long long>(flushIntervalMs_));
|
||||
if (shouldFlush) {
|
||||
lastFlushTime_ = nowSteady;
|
||||
}
|
||||
}
|
||||
if (shouldFlush) {
|
||||
fileStream.flush();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Logger::setLogLevel(LogLevel level) {
|
||||
minLevel = level;
|
||||
minLevel_.store(static_cast<int>(level), std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
bool Logger::shouldLog(LogLevel level) const {
|
||||
return static_cast<int>(level) >= minLevel_.load(std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
} // namespace core
|
||||
|
|
|
|||
|
|
@ -38,7 +38,9 @@
|
|||
#include <array>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <exception>
|
||||
#include <limits>
|
||||
#include <new>
|
||||
#include <openssl/sha.h>
|
||||
#include <openssl/hmac.h>
|
||||
|
||||
|
|
@ -93,6 +95,13 @@ bool isClassicLikeExpansion() {
|
|||
return isActiveExpansion("classic") || isActiveExpansion("turtle");
|
||||
}
|
||||
|
||||
bool envFlagEnabled(const char* key, bool defaultValue = false) {
|
||||
const char* raw = std::getenv(key);
|
||||
if (!raw || !*raw) return defaultValue;
|
||||
return !(raw[0] == '0' || raw[0] == 'f' || raw[0] == 'F' ||
|
||||
raw[0] == 'n' || raw[0] == 'N');
|
||||
}
|
||||
|
||||
std::string formatCopperAmount(uint32_t amount) {
|
||||
uint32_t gold = amount / 10000;
|
||||
uint32_t silver = (amount / 100) % 100;
|
||||
|
|
@ -1134,6 +1143,8 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
}
|
||||
|
||||
uint16_t opcode = packet.getOpcode();
|
||||
try {
|
||||
const bool allowVanillaAliases = isClassicLikeExpansion() || isActiveExpansion("tbc");
|
||||
|
||||
// Vanilla compatibility aliases:
|
||||
// - 0x006B: can be SMSG_COMPRESSED_MOVES on some vanilla-family servers
|
||||
|
|
@ -1141,7 +1152,7 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
// - 0x0103: SMSG_PLAY_MUSIC (some vanilla-family servers)
|
||||
//
|
||||
// We gate these by payload shape so expansion-native mappings remain intact.
|
||||
if (opcode == 0x006B) {
|
||||
if (allowVanillaAliases && opcode == 0x006B) {
|
||||
// Try compressed movement batch first:
|
||||
// [u8 subSize][u16 subOpcode][subPayload...] ...
|
||||
// where subOpcode is typically SMSG_MONSTER_MOVE / SMSG_MONSTER_MOVE_TRANSPORT.
|
||||
|
|
@ -1189,7 +1200,7 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
// Not weather-shaped: rewind and fall through to normal opcode table handling.
|
||||
packet.setReadPos(0);
|
||||
}
|
||||
} else if (opcode == 0x0103) {
|
||||
} else if (allowVanillaAliases && opcode == 0x0103) {
|
||||
// Expected play-music payload: uint32 sound/music id
|
||||
if (packet.getSize() - packet.getReadPos() == 4) {
|
||||
uint32_t soundId = packet.readUInt32();
|
||||
|
|
@ -1953,11 +1964,22 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
worldStateZoneId_ = packet.readUInt32();
|
||||
uint16_t count = packet.readUInt16();
|
||||
size_t needed = static_cast<size_t>(count) * 8;
|
||||
if (packet.getSize() - packet.getReadPos() < needed) {
|
||||
LOG_WARNING("SMSG_INIT_WORLD_STATES truncated: expected ", needed,
|
||||
" bytes of state pairs, got ", packet.getSize() - packet.getReadPos());
|
||||
packet.setReadPos(packet.getSize());
|
||||
break;
|
||||
size_t available = packet.getSize() - packet.getReadPos();
|
||||
if (available < needed) {
|
||||
// Be tolerant across expansion/private-core variants: if packet shape
|
||||
// still looks like N*(key,val) dwords, parse what is present.
|
||||
if ((available % 8) == 0) {
|
||||
uint16_t adjustedCount = static_cast<uint16_t>(available / 8);
|
||||
LOG_WARNING("SMSG_INIT_WORLD_STATES count mismatch: header=", count,
|
||||
" adjusted=", adjustedCount, " (available=", available, ")");
|
||||
count = adjustedCount;
|
||||
needed = available;
|
||||
} else {
|
||||
LOG_WARNING("SMSG_INIT_WORLD_STATES truncated: expected ", needed,
|
||||
" bytes of state pairs, got ", available);
|
||||
packet.setReadPos(packet.getSize());
|
||||
break;
|
||||
}
|
||||
}
|
||||
worldStates_.clear();
|
||||
worldStates_.reserve(count);
|
||||
|
|
@ -2849,6 +2871,23 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
}
|
||||
break;
|
||||
}
|
||||
} catch (const std::bad_alloc& e) {
|
||||
LOG_ERROR("OOM while handling world opcode=0x", std::hex, opcode, std::dec,
|
||||
" state=", worldStateName(state),
|
||||
" size=", packet.getSize(),
|
||||
" readPos=", packet.getReadPos(),
|
||||
" what=", e.what());
|
||||
if (socket && state == WorldState::IN_WORLD) {
|
||||
disconnect();
|
||||
fail("Out of memory while parsing world packet");
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
LOG_ERROR("Exception while handling world opcode=0x", std::hex, opcode, std::dec,
|
||||
" state=", worldStateName(state),
|
||||
" size=", packet.getSize(),
|
||||
" readPos=", packet.getReadPos(),
|
||||
" what=", e.what());
|
||||
}
|
||||
}
|
||||
|
||||
void GameHandler::handleAuthChallenge(network::Packet& packet) {
|
||||
|
|
@ -4581,6 +4620,7 @@ void GameHandler::setOrientation(float orientation) {
|
|||
}
|
||||
|
||||
void GameHandler::handleUpdateObject(network::Packet& packet) {
|
||||
static const bool kVerboseUpdateObject = envFlagEnabled("WOWEE_LOG_UPDATE_OBJECT_VERBOSE", false);
|
||||
UpdateObjectData data;
|
||||
if (!packetParsers_->parseUpdateObject(packet, data)) {
|
||||
LOG_WARNING("Failed to parse SMSG_UPDATE_OBJECT");
|
||||
|
|
@ -4710,48 +4750,46 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
|
|||
|
||||
// Process out-of-range objects first
|
||||
for (uint64_t guid : data.outOfRangeGuids) {
|
||||
if (entityManager.hasEntity(guid)) {
|
||||
const bool isKnownTransport = transportGuids_.count(guid) > 0;
|
||||
if (isKnownTransport) {
|
||||
// Keep transports alive across out-of-range flapping.
|
||||
// Boats/zeppelins are global movers and removing them here can make
|
||||
// them disappear until a later movement snapshot happens to recreate them.
|
||||
const bool playerAboardNow = (playerTransportGuid_ == guid);
|
||||
const bool stickyAboard = (playerTransportStickyGuid_ == guid && playerTransportStickyTimer_ > 0.0f);
|
||||
const bool movementSaysAboard = (movementInfo.transportGuid == guid);
|
||||
LOG_INFO("Preserving transport on out-of-range: 0x",
|
||||
std::hex, guid, std::dec,
|
||||
" now=", playerAboardNow,
|
||||
" sticky=", stickyAboard,
|
||||
" movement=", movementSaysAboard);
|
||||
continue;
|
||||
}
|
||||
auto entity = entityManager.getEntity(guid);
|
||||
if (!entity) continue;
|
||||
|
||||
LOG_DEBUG("Entity went out of range: 0x", std::hex, guid, std::dec);
|
||||
// Trigger despawn callbacks before removing entity
|
||||
auto entity = entityManager.getEntity(guid);
|
||||
if (entity) {
|
||||
if (entity->getType() == ObjectType::UNIT && creatureDespawnCallback_) {
|
||||
creatureDespawnCallback_(guid);
|
||||
} else if (entity->getType() == ObjectType::PLAYER && playerDespawnCallback_) {
|
||||
playerDespawnCallback_(guid);
|
||||
otherPlayerVisibleItemEntries_.erase(guid);
|
||||
otherPlayerVisibleDirty_.erase(guid);
|
||||
otherPlayerMoveTimeMs_.erase(guid);
|
||||
inspectedPlayerItemEntries_.erase(guid);
|
||||
pendingAutoInspect_.erase(guid);
|
||||
} else if (entity->getType() == ObjectType::GAMEOBJECT && gameObjectDespawnCallback_) {
|
||||
gameObjectDespawnCallback_(guid);
|
||||
}
|
||||
}
|
||||
transportGuids_.erase(guid);
|
||||
serverUpdatedTransportGuids_.erase(guid);
|
||||
clearTransportAttachment(guid);
|
||||
if (playerTransportGuid_ == guid) {
|
||||
clearPlayerTransport();
|
||||
}
|
||||
entityManager.removeEntity(guid);
|
||||
const bool isKnownTransport = transportGuids_.count(guid) > 0;
|
||||
if (isKnownTransport) {
|
||||
// Keep transports alive across out-of-range flapping.
|
||||
// Boats/zeppelins are global movers and removing them here can make
|
||||
// them disappear until a later movement snapshot happens to recreate them.
|
||||
const bool playerAboardNow = (playerTransportGuid_ == guid);
|
||||
const bool stickyAboard = (playerTransportStickyGuid_ == guid && playerTransportStickyTimer_ > 0.0f);
|
||||
const bool movementSaysAboard = (movementInfo.transportGuid == guid);
|
||||
LOG_INFO("Preserving transport on out-of-range: 0x",
|
||||
std::hex, guid, std::dec,
|
||||
" now=", playerAboardNow,
|
||||
" sticky=", stickyAboard,
|
||||
" movement=", movementSaysAboard);
|
||||
continue;
|
||||
}
|
||||
|
||||
LOG_DEBUG("Entity went out of range: 0x", std::hex, guid, std::dec);
|
||||
// Trigger despawn callbacks before removing entity
|
||||
if (entity->getType() == ObjectType::UNIT && creatureDespawnCallback_) {
|
||||
creatureDespawnCallback_(guid);
|
||||
} else if (entity->getType() == ObjectType::PLAYER && playerDespawnCallback_) {
|
||||
playerDespawnCallback_(guid);
|
||||
otherPlayerVisibleItemEntries_.erase(guid);
|
||||
otherPlayerVisibleDirty_.erase(guid);
|
||||
otherPlayerMoveTimeMs_.erase(guid);
|
||||
inspectedPlayerItemEntries_.erase(guid);
|
||||
pendingAutoInspect_.erase(guid);
|
||||
} else if (entity->getType() == ObjectType::GAMEOBJECT && gameObjectDespawnCallback_) {
|
||||
gameObjectDespawnCallback_(guid);
|
||||
}
|
||||
transportGuids_.erase(guid);
|
||||
serverUpdatedTransportGuids_.erase(guid);
|
||||
clearTransportAttachment(guid);
|
||||
if (playerTransportGuid_ == guid) {
|
||||
clearPlayerTransport();
|
||||
}
|
||||
entityManager.removeEntity(guid);
|
||||
}
|
||||
|
||||
// Process update blocks
|
||||
|
|
@ -5081,92 +5119,21 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
|
|||
|
||||
// Extract XP / inventory slot / skill fields for player entity
|
||||
if (block.guid == playerGuid && block.objectType == ObjectType::PLAYER) {
|
||||
// Store baseline snapshot on first update
|
||||
static bool baselineStored = false;
|
||||
static std::map<uint16_t, uint32_t> baselineFields;
|
||||
|
||||
if (!baselineStored) {
|
||||
baselineFields = block.fields;
|
||||
baselineStored = true;
|
||||
LOG_INFO("===== BASELINE PLAYER FIELDS STORED =====");
|
||||
LOG_INFO(" Total fields: ", block.fields.size());
|
||||
}
|
||||
|
||||
// Diff against baseline to find changes
|
||||
std::vector<uint16_t> changedIndices;
|
||||
std::vector<uint16_t> newIndices;
|
||||
std::vector<uint16_t> removedIndices;
|
||||
|
||||
for (const auto& [idx, val] : block.fields) {
|
||||
auto it = baselineFields.find(idx);
|
||||
if (it == baselineFields.end()) {
|
||||
newIndices.push_back(idx);
|
||||
} else if (it->second != val) {
|
||||
changedIndices.push_back(idx);
|
||||
}
|
||||
}
|
||||
|
||||
for (const auto& [idx, val] : baselineFields) {
|
||||
if (block.fields.find(idx) == block.fields.end()) {
|
||||
removedIndices.push_back(idx);
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-detect coinage index using the previous snapshot vs this full snapshot.
|
||||
maybeDetectCoinageIndex(lastPlayerFields_, block.fields);
|
||||
|
||||
lastPlayerFields_ = block.fields;
|
||||
detectInventorySlotBases(block.fields);
|
||||
|
||||
// Debug: Show field changes
|
||||
LOG_INFO("Player update with ", block.fields.size(), " fields");
|
||||
|
||||
if (!changedIndices.empty() || !newIndices.empty() || !removedIndices.empty()) {
|
||||
LOG_INFO(" ===== FIELD CHANGES DETECTED =====");
|
||||
if (!changedIndices.empty()) {
|
||||
LOG_INFO(" Changed fields (", changedIndices.size(), "):");
|
||||
std::sort(changedIndices.begin(), changedIndices.end());
|
||||
for (size_t i = 0; i < std::min(size_t(30), changedIndices.size()); ++i) {
|
||||
uint16_t idx = changedIndices[i];
|
||||
uint32_t oldVal = baselineFields[idx];
|
||||
uint32_t newVal = block.fields.at(idx);
|
||||
LOG_INFO(" [", idx, "]: ", oldVal, " -> ", newVal,
|
||||
" (0x", std::hex, oldVal, " -> 0x", newVal, std::dec, ")");
|
||||
}
|
||||
if (changedIndices.size() > 30) {
|
||||
LOG_INFO(" ... (", changedIndices.size() - 30, " more)");
|
||||
}
|
||||
}
|
||||
if (!newIndices.empty()) {
|
||||
LOG_INFO(" New fields (", newIndices.size(), "):");
|
||||
std::sort(newIndices.begin(), newIndices.end());
|
||||
for (size_t i = 0; i < std::min(size_t(20), newIndices.size()); ++i) {
|
||||
uint16_t idx = newIndices[i];
|
||||
uint32_t val = block.fields.at(idx);
|
||||
LOG_INFO(" [", idx, "]: ", val, " (0x", std::hex, val, std::dec, ")");
|
||||
}
|
||||
if (newIndices.size() > 20) {
|
||||
LOG_INFO(" ... (", newIndices.size() - 20, " more)");
|
||||
}
|
||||
}
|
||||
if (!removedIndices.empty()) {
|
||||
LOG_INFO(" Removed fields (", removedIndices.size(), "):");
|
||||
std::sort(removedIndices.begin(), removedIndices.end());
|
||||
for (size_t i = 0; i < std::min(size_t(20), removedIndices.size()); ++i) {
|
||||
uint16_t idx = removedIndices[i];
|
||||
uint32_t val = baselineFields.at(idx);
|
||||
LOG_INFO(" [", idx, "]: was ", val, " (0x", std::hex, val, std::dec, ")");
|
||||
}
|
||||
if (kVerboseUpdateObject) {
|
||||
uint16_t maxField = 0;
|
||||
for (const auto& [key, _val] : block.fields) {
|
||||
if (key > maxField) maxField = key;
|
||||
}
|
||||
LOG_INFO("Player update with ", block.fields.size(),
|
||||
" fields (max index=", maxField, ")");
|
||||
}
|
||||
|
||||
uint16_t maxField = 0;
|
||||
for (const auto& [key, val] : block.fields) {
|
||||
if (key > maxField) maxField = key;
|
||||
}
|
||||
|
||||
LOG_INFO(" Highest field index: ", maxField);
|
||||
|
||||
bool slotsChanged = false;
|
||||
const uint16_t ufPlayerXp = fieldIndex(UF::PLAYER_XP);
|
||||
const uint16_t ufPlayerNextXp = fieldIndex(UF::PLAYER_NEXT_LEVEL_XP);
|
||||
|
|
@ -5184,11 +5151,11 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
|
|||
}
|
||||
else if (key == ufCoinage) {
|
||||
playerMoneyCopper_ = val;
|
||||
LOG_INFO("Money set from update fields: ", val, " copper");
|
||||
LOG_DEBUG("Money set from update fields: ", val, " copper");
|
||||
}
|
||||
else if (ufArmor != 0xFFFF && key == ufArmor) {
|
||||
playerArmorRating_ = static_cast<int32_t>(val);
|
||||
LOG_INFO("Armor rating from update fields: ", playerArmorRating_);
|
||||
LOG_DEBUG("Armor rating from update fields: ", playerArmorRating_);
|
||||
}
|
||||
// Do not synthesize quest-log entries from raw update-field slots.
|
||||
// Slot layouts differ on some classic-family realms and can produce
|
||||
|
|
@ -5426,7 +5393,12 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
|
|||
}
|
||||
// Update XP / inventory slot / skill fields for player entity
|
||||
if (block.guid == playerGuid) {
|
||||
std::map<uint16_t, uint32_t> oldFieldsSnapshot = lastPlayerFields_;
|
||||
const bool needCoinageDetectSnapshot =
|
||||
(pendingMoneyDelta_ != 0 && pendingMoneyDeltaTimer_ > 0.0f);
|
||||
std::map<uint16_t, uint32_t> oldFieldsSnapshot;
|
||||
if (needCoinageDetectSnapshot) {
|
||||
oldFieldsSnapshot = lastPlayerFields_;
|
||||
}
|
||||
if (block.hasMovement && block.runSpeed > 0.1f && block.runSpeed < 100.0f) {
|
||||
serverRunSpeed_ = block.runSpeed;
|
||||
// Some server dismount paths update run speed without updating mount display field.
|
||||
|
|
@ -5440,10 +5412,13 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
|
|||
}
|
||||
}
|
||||
}
|
||||
auto mergeHint = lastPlayerFields_.end();
|
||||
for (const auto& [key, val] : block.fields) {
|
||||
lastPlayerFields_[key] = val;
|
||||
mergeHint = lastPlayerFields_.insert_or_assign(mergeHint, key, val);
|
||||
}
|
||||
if (needCoinageDetectSnapshot) {
|
||||
maybeDetectCoinageIndex(oldFieldsSnapshot, lastPlayerFields_);
|
||||
}
|
||||
maybeDetectCoinageIndex(oldFieldsSnapshot, lastPlayerFields_);
|
||||
maybeDetectVisibleItemLayout();
|
||||
detectInventorySlotBases(block.fields);
|
||||
bool slotsChanged = false;
|
||||
|
|
@ -5456,15 +5431,15 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
|
|||
for (const auto& [key, val] : block.fields) {
|
||||
if (key == ufPlayerXp) {
|
||||
playerXp_ = val;
|
||||
LOG_INFO("XP updated: ", val);
|
||||
LOG_DEBUG("XP updated: ", val);
|
||||
}
|
||||
else if (key == ufPlayerNextXp) {
|
||||
playerNextLevelXp_ = val;
|
||||
LOG_INFO("Next level XP updated: ", val);
|
||||
LOG_DEBUG("Next level XP updated: ", val);
|
||||
}
|
||||
else if (key == ufPlayerLevel) {
|
||||
serverPlayerLevel_ = val;
|
||||
LOG_INFO("Level updated: ", val);
|
||||
LOG_DEBUG("Level updated: ", val);
|
||||
for (auto& ch : characters) {
|
||||
if (ch.guid == playerGuid) {
|
||||
ch.level = val;
|
||||
|
|
@ -5474,7 +5449,7 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
|
|||
}
|
||||
else if (key == ufCoinage) {
|
||||
playerMoneyCopper_ = val;
|
||||
LOG_INFO("Money updated via VALUES: ", val, " copper");
|
||||
LOG_DEBUG("Money updated via VALUES: ", val, " copper");
|
||||
}
|
||||
else if (ufArmor != 0xFFFF && key == ufArmor) {
|
||||
playerArmorRating_ = static_cast<int32_t>(val);
|
||||
|
|
@ -5505,17 +5480,33 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
|
|||
|
||||
// Update item stack count for online items
|
||||
if (entity->getType() == ObjectType::ITEM || entity->getType() == ObjectType::CONTAINER) {
|
||||
bool inventoryChanged = false;
|
||||
const uint16_t itemStackField = fieldIndex(UF::ITEM_FIELD_STACK_COUNT);
|
||||
const uint16_t containerNumSlotsField = fieldIndex(UF::CONTAINER_FIELD_NUM_SLOTS);
|
||||
const uint16_t containerSlot1Field = fieldIndex(UF::CONTAINER_FIELD_SLOT_1);
|
||||
for (const auto& [key, val] : block.fields) {
|
||||
if (key == fieldIndex(UF::ITEM_FIELD_STACK_COUNT)) {
|
||||
if (key == itemStackField) {
|
||||
auto it = onlineItems_.find(block.guid);
|
||||
if (it != onlineItems_.end()) it->second.stackCount = val;
|
||||
if (it != onlineItems_.end() && it->second.stackCount != val) {
|
||||
it->second.stackCount = val;
|
||||
inventoryChanged = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Update container slot GUIDs on bag content changes
|
||||
if (entity->getType() == ObjectType::CONTAINER) {
|
||||
for (const auto& [key, _] : block.fields) {
|
||||
if ((containerNumSlotsField != 0xFFFF && key == containerNumSlotsField) ||
|
||||
(containerSlot1Field != 0xFFFF && key >= containerSlot1Field && key < containerSlot1Field + 72)) {
|
||||
inventoryChanged = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
extractContainerFields(block.guid, block.fields);
|
||||
}
|
||||
rebuildOnlineInventory();
|
||||
if (inventoryChanged) {
|
||||
rebuildOnlineInventory();
|
||||
}
|
||||
}
|
||||
if (block.hasMovement && entity->getType() == ObjectType::GAMEOBJECT) {
|
||||
if (transportGuids_.count(block.guid) && transportMoveCallback_) {
|
||||
|
|
@ -8650,10 +8641,30 @@ void GameHandler::handleCompressedMoves(network::Packet& packet) {
|
|||
|
||||
void GameHandler::handleMonsterMove(network::Packet& packet) {
|
||||
MonsterMoveData data;
|
||||
auto logMonsterMoveParseFailure = [&](const std::string& msg) {
|
||||
static uint32_t failCount = 0;
|
||||
++failCount;
|
||||
if (failCount <= 10 || (failCount % 100) == 0) {
|
||||
LOG_WARNING(msg, " (occurrence=", failCount, ")");
|
||||
}
|
||||
};
|
||||
auto stripWrappedSubpacket = [&](const std::vector<uint8_t>& bytes, std::vector<uint8_t>& stripped) -> bool {
|
||||
if (bytes.size() < 3) return false;
|
||||
uint8_t subSize = bytes[0];
|
||||
if (subSize < 2) return false;
|
||||
size_t wrappedLen = static_cast<size_t>(subSize) + 1; // size byte + body
|
||||
if (wrappedLen != bytes.size()) return false;
|
||||
size_t payloadLen = static_cast<size_t>(subSize) - 2; // opcode(2) stripped
|
||||
if (3 + payloadLen > bytes.size()) return false;
|
||||
stripped.assign(bytes.begin() + 3, bytes.begin() + 3 + payloadLen);
|
||||
return true;
|
||||
};
|
||||
// Turtle WoW (1.17+) compresses each SMSG_MONSTER_MOVE individually:
|
||||
// format: uint32 decompressedSize + zlib data (zlib magic = 0x78 ??)
|
||||
const auto& rawData = packet.getData();
|
||||
bool isCompressed = rawData.size() >= 6 &&
|
||||
const bool allowTurtleMoveCompression = isActiveExpansion("turtle");
|
||||
bool isCompressed = allowTurtleMoveCompression &&
|
||||
rawData.size() >= 6 &&
|
||||
rawData[4] == 0x78 &&
|
||||
(rawData[5] == 0x01 || rawData[5] == 0x9C ||
|
||||
rawData[5] == 0xDA || rawData[5] == 0x5E);
|
||||
|
|
@ -8685,36 +8696,42 @@ void GameHandler::handleMonsterMove(network::Packet& packet) {
|
|||
}
|
||||
LOG_INFO("MonsterMove decomp[", destLen, "]: ", hex);
|
||||
}
|
||||
// Some Turtle WoW compressed move payloads include an inner
|
||||
// sub-packet wrapper: uint8 size + uint16 opcode + payload.
|
||||
// Do not key this on expansion opcode mappings; strip by structure.
|
||||
std::vector<uint8_t> parseBytes = decompressed;
|
||||
if (destLen >= 3) {
|
||||
uint8_t subSize = decompressed[0];
|
||||
size_t wrappedLen = static_cast<size_t>(subSize) + 1; // size byte + subSize bytes
|
||||
uint16_t innerOpcode = static_cast<uint16_t>(decompressed[1]) |
|
||||
(static_cast<uint16_t>(decompressed[2]) << 8);
|
||||
uint16_t monsterMoveWire = wireOpcode(Opcode::SMSG_MONSTER_MOVE);
|
||||
bool looksLikeMonsterMoveWrapper =
|
||||
(innerOpcode == 0x00DD) || (innerOpcode == monsterMoveWire);
|
||||
// Strict case: one exact wrapped sub-packet in this decompressed blob.
|
||||
if (subSize >= 2 && wrappedLen == destLen && looksLikeMonsterMoveWrapper) {
|
||||
size_t payloadStart = 3;
|
||||
size_t payloadLen = static_cast<size_t>(subSize) - 2;
|
||||
parseBytes.assign(decompressed.begin() + payloadStart,
|
||||
decompressed.begin() + payloadStart + payloadLen);
|
||||
}
|
||||
}
|
||||
std::vector<uint8_t> stripped;
|
||||
bool hasWrappedForm = stripWrappedSubpacket(decompressed, stripped);
|
||||
|
||||
network::Packet decompPacket(packet.getOpcode(), parseBytes);
|
||||
// Try unwrapped payload first (common form), then wrapped-subpacket fallback.
|
||||
network::Packet decompPacket(packet.getOpcode(), decompressed);
|
||||
if (!packetParsers_->parseMonsterMove(decompPacket, data)) {
|
||||
LOG_WARNING("Failed to parse vanilla SMSG_MONSTER_MOVE (decompressed ",
|
||||
destLen, " bytes, parseBytes ", parseBytes.size(), " bytes)");
|
||||
return;
|
||||
if (!hasWrappedForm) {
|
||||
logMonsterMoveParseFailure("Failed to parse SMSG_MONSTER_MOVE (decompressed " +
|
||||
std::to_string(destLen) + " bytes)");
|
||||
return;
|
||||
}
|
||||
network::Packet wrappedPacket(packet.getOpcode(), stripped);
|
||||
if (!packetParsers_->parseMonsterMove(wrappedPacket, data)) {
|
||||
logMonsterMoveParseFailure("Failed to parse SMSG_MONSTER_MOVE (decompressed " +
|
||||
std::to_string(destLen) + " bytes, wrapped payload " +
|
||||
std::to_string(stripped.size()) + " bytes)");
|
||||
return;
|
||||
}
|
||||
LOG_WARNING("SMSG_MONSTER_MOVE parsed via wrapped-subpacket fallback");
|
||||
}
|
||||
} else if (!packetParsers_->parseMonsterMove(packet, data)) {
|
||||
LOG_WARNING("Failed to parse SMSG_MONSTER_MOVE");
|
||||
return;
|
||||
// Some realms occasionally embed an extra [size|opcode] wrapper even when the
|
||||
// outer packet wasn't zlib-compressed. Retry with wrapper stripped by structure.
|
||||
std::vector<uint8_t> stripped;
|
||||
if (stripWrappedSubpacket(rawData, stripped)) {
|
||||
network::Packet wrappedPacket(packet.getOpcode(), stripped);
|
||||
if (packetParsers_->parseMonsterMove(wrappedPacket, data)) {
|
||||
LOG_WARNING("SMSG_MONSTER_MOVE parsed via uncompressed wrapped-subpacket fallback");
|
||||
} else {
|
||||
logMonsterMoveParseFailure("Failed to parse SMSG_MONSTER_MOVE");
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
logMonsterMoveParseFailure("Failed to parse SMSG_MONSTER_MOVE");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Update entity position in entity manager
|
||||
|
|
|
|||
|
|
@ -1192,6 +1192,24 @@ bool TurtlePacketParsers::parseMovementBlock(network::Packet& packet, UpdateBloc
|
|||
return true;
|
||||
}
|
||||
|
||||
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.
|
||||
size_t start = packet.getReadPos();
|
||||
if (MonsterMoveParser::parseVanilla(packet, data)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
packet.setReadPos(start);
|
||||
if (MonsterMoveParser::parse(packet, data)) {
|
||||
LOG_WARNING("[Turtle] SMSG_MONSTER_MOVE parsed via WotLK fallback layout");
|
||||
return true;
|
||||
}
|
||||
|
||||
packet.setReadPos(start);
|
||||
return false;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Classic/Vanilla quest giver status
|
||||
//
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
#include "game/packet_parsers.hpp"
|
||||
#include "core/logger.hpp"
|
||||
#include <utility>
|
||||
|
||||
namespace wowee {
|
||||
namespace game {
|
||||
|
|
@ -376,83 +377,125 @@ bool TbcPacketParsers::parseCharEnum(network::Packet& packet, CharEnumResponse&
|
|||
// (WotLK removed this field)
|
||||
// ============================================================================
|
||||
bool TbcPacketParsers::parseUpdateObject(network::Packet& packet, UpdateObjectData& data) {
|
||||
// Read block count
|
||||
data.blockCount = packet.readUInt32();
|
||||
constexpr uint32_t kMaxReasonableUpdateBlocks = 4096;
|
||||
auto parseWithLayout = [&](bool withHasTransportByte, UpdateObjectData& out) -> bool {
|
||||
out = UpdateObjectData{};
|
||||
size_t start = packet.getReadPos();
|
||||
if (packet.getSize() - start < 4) return false;
|
||||
|
||||
// TBC/Classic: has_transport byte (WotLK removed this)
|
||||
/*uint8_t hasTransport =*/ packet.readUInt8();
|
||||
|
||||
LOG_DEBUG("[TBC] SMSG_UPDATE_OBJECT: objectCount=", data.blockCount);
|
||||
|
||||
// Check for out-of-range objects first
|
||||
if (packet.getReadPos() + 1 <= packet.getSize()) {
|
||||
uint8_t firstByte = packet.readUInt8();
|
||||
|
||||
if (firstByte == static_cast<uint8_t>(UpdateType::OUT_OF_RANGE_OBJECTS)) {
|
||||
uint32_t count = packet.readUInt32();
|
||||
for (uint32_t i = 0; i < count; ++i) {
|
||||
uint64_t guid = UpdateObjectParser::readPackedGuid(packet);
|
||||
data.outOfRangeGuids.push_back(guid);
|
||||
LOG_DEBUG(" Out of range: 0x", std::hex, guid, std::dec);
|
||||
}
|
||||
} else {
|
||||
packet.setReadPos(packet.getReadPos() - 1);
|
||||
out.blockCount = packet.readUInt32();
|
||||
if (out.blockCount > kMaxReasonableUpdateBlocks) {
|
||||
packet.setReadPos(start);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Parse update blocks — dispatching movement via virtual parseMovementBlock()
|
||||
data.blocks.reserve(data.blockCount);
|
||||
for (uint32_t i = 0; i < data.blockCount; ++i) {
|
||||
LOG_DEBUG("Parsing block ", i + 1, " / ", data.blockCount);
|
||||
UpdateBlock block;
|
||||
|
||||
// Read update type
|
||||
uint8_t updateTypeVal = packet.readUInt8();
|
||||
block.updateType = static_cast<UpdateType>(updateTypeVal);
|
||||
LOG_DEBUG("Update block: type=", (int)updateTypeVal);
|
||||
|
||||
bool ok = false;
|
||||
switch (block.updateType) {
|
||||
case UpdateType::VALUES: {
|
||||
block.guid = UpdateObjectParser::readPackedGuid(packet);
|
||||
ok = UpdateObjectParser::parseUpdateFields(packet, block);
|
||||
break;
|
||||
if (withHasTransportByte) {
|
||||
if (packet.getReadPos() >= packet.getSize()) {
|
||||
packet.setReadPos(start);
|
||||
return false;
|
||||
}
|
||||
case UpdateType::MOVEMENT: {
|
||||
block.guid = UpdateObjectParser::readPackedGuid(packet);
|
||||
ok = this->parseMovementBlock(packet, block);
|
||||
break;
|
||||
}
|
||||
case UpdateType::CREATE_OBJECT:
|
||||
case UpdateType::CREATE_OBJECT2: {
|
||||
block.guid = UpdateObjectParser::readPackedGuid(packet);
|
||||
uint8_t objectTypeVal = packet.readUInt8();
|
||||
block.objectType = static_cast<ObjectType>(objectTypeVal);
|
||||
ok = this->parseMovementBlock(packet, block);
|
||||
if (ok) {
|
||||
ok = UpdateObjectParser::parseUpdateFields(packet, block);
|
||||
/*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;
|
||||
}
|
||||
break;
|
||||
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;
|
||||
}
|
||||
uint64_t guid = UpdateObjectParser::readPackedGuid(packet);
|
||||
out.outOfRangeGuids.push_back(guid);
|
||||
}
|
||||
} else {
|
||||
packet.setReadPos(packet.getReadPos() - 1);
|
||||
}
|
||||
case UpdateType::OUT_OF_RANGE_OBJECTS:
|
||||
case UpdateType::NEAR_OBJECTS:
|
||||
ok = true;
|
||||
break;
|
||||
default:
|
||||
LOG_WARNING("Unknown update type: ", (int)updateTypeVal);
|
||||
ok = false;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!ok) {
|
||||
LOG_WARNING("Failed to parse update block ", i + 1, " of ", data.blockCount,
|
||||
" — keeping ", data.blocks.size(), " parsed blocks");
|
||||
break;
|
||||
out.blocks.reserve(out.blockCount);
|
||||
for (uint32_t i = 0; i < out.blockCount; ++i) {
|
||||
if (packet.getReadPos() >= packet.getSize()) {
|
||||
packet.setReadPos(start);
|
||||
return false;
|
||||
}
|
||||
|
||||
UpdateBlock block;
|
||||
uint8_t updateTypeVal = packet.readUInt8();
|
||||
if (updateTypeVal > static_cast<uint8_t>(UpdateType::NEAR_OBJECTS)) {
|
||||
packet.setReadPos(start);
|
||||
return false;
|
||||
}
|
||||
block.updateType = static_cast<UpdateType>(updateTypeVal);
|
||||
|
||||
bool ok = false;
|
||||
switch (block.updateType) {
|
||||
case UpdateType::VALUES: {
|
||||
block.guid = UpdateObjectParser::readPackedGuid(packet);
|
||||
ok = UpdateObjectParser::parseUpdateFields(packet, block);
|
||||
break;
|
||||
}
|
||||
case UpdateType::MOVEMENT: {
|
||||
block.guid = UpdateObjectParser::readPackedGuid(packet);
|
||||
ok = this->parseMovementBlock(packet, block);
|
||||
break;
|
||||
}
|
||||
case UpdateType::CREATE_OBJECT:
|
||||
case UpdateType::CREATE_OBJECT2: {
|
||||
block.guid = UpdateObjectParser::readPackedGuid(packet);
|
||||
if (packet.getReadPos() >= packet.getSize()) {
|
||||
ok = false;
|
||||
break;
|
||||
}
|
||||
uint8_t objectTypeVal = packet.readUInt8();
|
||||
block.objectType = static_cast<ObjectType>(objectTypeVal);
|
||||
ok = this->parseMovementBlock(packet, block);
|
||||
if (ok) ok = UpdateObjectParser::parseUpdateFields(packet, block);
|
||||
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(block);
|
||||
}
|
||||
data.blocks.push_back(block);
|
||||
return true;
|
||||
};
|
||||
|
||||
size_t startPos = packet.getReadPos();
|
||||
UpdateObjectData parsed;
|
||||
if (parseWithLayout(true, parsed)) {
|
||||
data = std::move(parsed);
|
||||
return true;
|
||||
}
|
||||
|
||||
return true;
|
||||
packet.setReadPos(startPos);
|
||||
if (parseWithLayout(false, parsed)) {
|
||||
LOG_WARNING("[TBC] SMSG_UPDATE_OBJECT parsed without has_transport byte fallback");
|
||||
data = std::move(parsed);
|
||||
return true;
|
||||
}
|
||||
|
||||
packet.setReadPos(startPos);
|
||||
return false;
|
||||
}
|
||||
|
||||
network::Packet TbcPacketParsers::buildAcceptQuestPacket(uint64_t npcGuid, uint32_t questId) {
|
||||
|
|
|
|||
|
|
@ -532,29 +532,45 @@ bool LoginVerifyWorldParser::parse(network::Packet& packet, LoginVerifyWorldData
|
|||
}
|
||||
|
||||
bool AccountDataTimesParser::parse(network::Packet& packet, AccountDataTimesData& data) {
|
||||
// SMSG_ACCOUNT_DATA_TIMES format (WoW 3.3.5a):
|
||||
// uint32 serverTime (Unix timestamp)
|
||||
// uint8 unknown (always 1?)
|
||||
// uint32[8] accountDataTimes (timestamps for each data slot)
|
||||
|
||||
if (packet.getSize() < 37) {
|
||||
LOG_ERROR("SMSG_ACCOUNT_DATA_TIMES packet too small: ", packet.getSize(), " bytes");
|
||||
// Common layouts seen in the wild:
|
||||
// - WotLK-like: uint32 serverTime, uint8 unk, uint32 mask, uint32[up to 8] slotTimes
|
||||
// - Older/variant: uint32 serverTime, uint8 unk, uint32[up to 8] slotTimes
|
||||
// Some servers only send a subset of slots.
|
||||
if (packet.getSize() < 5) {
|
||||
LOG_ERROR("SMSG_ACCOUNT_DATA_TIMES packet too small: ", packet.getSize(),
|
||||
" bytes (need at least 5)");
|
||||
return false;
|
||||
}
|
||||
|
||||
for (uint32_t& t : data.accountDataTimes) {
|
||||
t = 0;
|
||||
}
|
||||
data.serverTime = packet.readUInt32();
|
||||
data.unknown = packet.readUInt8();
|
||||
size_t remaining = packet.getSize() - packet.getReadPos();
|
||||
uint32_t mask = 0xFF;
|
||||
if (remaining >= 4 && ((remaining - 4) % 4) == 0) {
|
||||
// Treat first dword as slot mask when payload shape matches.
|
||||
mask = packet.readUInt32();
|
||||
}
|
||||
remaining = packet.getSize() - packet.getReadPos();
|
||||
size_t slotWords = std::min<size_t>(8, remaining / 4);
|
||||
|
||||
LOG_DEBUG("Parsed SMSG_ACCOUNT_DATA_TIMES:");
|
||||
LOG_DEBUG(" Server time: ", data.serverTime);
|
||||
LOG_DEBUG(" Unknown: ", (int)data.unknown);
|
||||
LOG_DEBUG(" Mask: 0x", std::hex, mask, std::dec, " slotsInPacket=", slotWords);
|
||||
|
||||
for (int i = 0; i < 8; ++i) {
|
||||
for (size_t i = 0; i < slotWords; ++i) {
|
||||
data.accountDataTimes[i] = packet.readUInt32();
|
||||
if (data.accountDataTimes[i] != 0) {
|
||||
if (data.accountDataTimes[i] != 0 || ((mask & (1u << i)) != 0)) {
|
||||
LOG_DEBUG(" Data slot ", i, ": ", data.accountDataTimes[i]);
|
||||
}
|
||||
}
|
||||
if (packet.getReadPos() != packet.getSize()) {
|
||||
LOG_DEBUG(" AccountDataTimes trailing bytes: ", packet.getSize() - packet.getReadPos());
|
||||
packet.setReadPos(packet.getSize());
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
|
@ -886,53 +902,99 @@ bool UpdateObjectParser::parseMovementBlock(network::Packet& packet, UpdateBlock
|
|||
|
||||
// Spline data
|
||||
if (moveFlags & 0x08000000) { // MOVEMENTFLAG_SPLINE_ENABLED
|
||||
auto bytesAvailable = [&](size_t n) -> bool { return packet.getReadPos() + n <= packet.getSize(); };
|
||||
if (!bytesAvailable(4)) return false;
|
||||
uint32_t splineFlags = packet.readUInt32();
|
||||
LOG_DEBUG(" Spline: flags=0x", std::hex, splineFlags, std::dec);
|
||||
|
||||
if (splineFlags & 0x00010000) { // SPLINEFLAG_FINAL_POINT
|
||||
if (!bytesAvailable(12)) return false;
|
||||
/*float finalX =*/ packet.readFloat();
|
||||
/*float finalY =*/ packet.readFloat();
|
||||
/*float finalZ =*/ packet.readFloat();
|
||||
} else if (splineFlags & 0x00020000) { // SPLINEFLAG_FINAL_TARGET
|
||||
if (!bytesAvailable(8)) return false;
|
||||
/*uint64_t finalTarget =*/ packet.readUInt64();
|
||||
} else if (splineFlags & 0x00040000) { // SPLINEFLAG_FINAL_ANGLE
|
||||
if (!bytesAvailable(4)) return false;
|
||||
/*float finalAngle =*/ packet.readFloat();
|
||||
}
|
||||
|
||||
// Legacy UPDATE_OBJECT spline layout used by many servers:
|
||||
// timePassed, duration, splineId, durationMod, durationModNext,
|
||||
// verticalAccel, effectStartTime, pointCount, points, splineMode, endPoint.
|
||||
const size_t legacyStart = packet.getReadPos();
|
||||
if (!bytesAvailable(12 + 8 + 8 + 4)) return false;
|
||||
/*uint32_t timePassed =*/ packet.readUInt32();
|
||||
/*uint32_t duration =*/ packet.readUInt32();
|
||||
/*uint32_t splineId =*/ packet.readUInt32();
|
||||
|
||||
/*float durationMod =*/ packet.readFloat();
|
||||
/*float durationModNext =*/ packet.readFloat();
|
||||
|
||||
/*float verticalAccel =*/ packet.readFloat();
|
||||
/*uint32_t effectStartTime =*/ packet.readUInt32();
|
||||
|
||||
uint32_t pointCount = packet.readUInt32();
|
||||
if (pointCount > 256) {
|
||||
|
||||
const size_t remainingAfterCount = packet.getSize() - packet.getReadPos();
|
||||
const bool legacyCountLooksValid = (pointCount <= 256);
|
||||
const size_t legacyPointsBytes = static_cast<size_t>(pointCount) * 12ull;
|
||||
const bool legacyPayloadFits = (legacyPointsBytes + 13ull) <= remainingAfterCount;
|
||||
|
||||
if (legacyCountLooksValid && legacyPayloadFits) {
|
||||
for (uint32_t i = 0; i < pointCount; i++) {
|
||||
/*float px =*/ packet.readFloat();
|
||||
/*float py =*/ packet.readFloat();
|
||||
/*float pz =*/ packet.readFloat();
|
||||
}
|
||||
/*uint8_t splineMode =*/ packet.readUInt8();
|
||||
/*float endPointX =*/ packet.readFloat();
|
||||
/*float endPointY =*/ packet.readFloat();
|
||||
/*float endPointZ =*/ packet.readFloat();
|
||||
LOG_DEBUG(" Spline pointCount=", pointCount);
|
||||
}
|
||||
|
||||
// Legacy pointCount looks invalid; try compact WotLK layout as recovery.
|
||||
// This keeps malformed/variant packets from desyncing the whole update block.
|
||||
packet.setReadPos(legacyStart);
|
||||
const size_t afterFinalFacingPos = packet.getReadPos();
|
||||
if (splineFlags & 0x00400000) { // Animation
|
||||
if (!bytesAvailable(5)) return false;
|
||||
/*uint8_t animType =*/ packet.readUInt8();
|
||||
/*uint32_t animStart =*/ packet.readUInt32();
|
||||
}
|
||||
if (!bytesAvailable(4)) return false;
|
||||
/*uint32_t duration =*/ packet.readUInt32();
|
||||
if (splineFlags & 0x00000800) { // Parabolic
|
||||
if (!bytesAvailable(8)) return false;
|
||||
/*float verticalAccel =*/ packet.readFloat();
|
||||
/*uint32_t effectStartTime =*/ packet.readUInt32();
|
||||
}
|
||||
if (!bytesAvailable(4)) return false;
|
||||
const uint32_t compactPointCount = packet.readUInt32();
|
||||
if (compactPointCount > 16384) {
|
||||
static uint32_t badSplineCount = 0;
|
||||
++badSplineCount;
|
||||
if (badSplineCount <= 5 || (badSplineCount % 100) == 0) {
|
||||
LOG_WARNING(" Spline pointCount=", pointCount,
|
||||
" exceeds maximum, capping at 0 (readPos=",
|
||||
packet.getReadPos(), "/", packet.getSize(),
|
||||
", occurrence=", badSplineCount, ")");
|
||||
" invalid (legacy+compact) at readPos=",
|
||||
afterFinalFacingPos, "/", packet.getSize(),
|
||||
", occurrence=", badSplineCount);
|
||||
}
|
||||
pointCount = 0;
|
||||
} else {
|
||||
LOG_DEBUG(" Spline pointCount=", pointCount);
|
||||
return false;
|
||||
}
|
||||
for (uint32_t i = 0; i < pointCount; i++) {
|
||||
/*float px =*/ packet.readFloat();
|
||||
/*float py =*/ packet.readFloat();
|
||||
/*float pz =*/ packet.readFloat();
|
||||
const bool uncompressed = (splineFlags & (0x00080000 | 0x00002000)) != 0;
|
||||
size_t compactPayloadBytes = 0;
|
||||
if (compactPointCount > 0) {
|
||||
if (uncompressed) {
|
||||
compactPayloadBytes = static_cast<size_t>(compactPointCount) * 12ull;
|
||||
} else {
|
||||
compactPayloadBytes = 12ull;
|
||||
if (compactPointCount > 1) {
|
||||
compactPayloadBytes += static_cast<size_t>(compactPointCount - 1) * 4ull;
|
||||
}
|
||||
}
|
||||
if (!bytesAvailable(compactPayloadBytes)) return false;
|
||||
packet.setReadPos(packet.getReadPos() + compactPayloadBytes);
|
||||
}
|
||||
|
||||
/*uint8_t splineMode =*/ packet.readUInt8();
|
||||
/*float endPointX =*/ packet.readFloat();
|
||||
/*float endPointY =*/ packet.readFloat();
|
||||
/*float endPointZ =*/ packet.readFloat();
|
||||
}
|
||||
}
|
||||
else if (updateFlags & UPDATEFLAG_POSITION) {
|
||||
|
|
@ -1025,8 +1087,9 @@ bool UpdateObjectParser::parseUpdateFields(network::Packet& packet, UpdateBlock&
|
|||
LOG_DEBUG(" maskBlockCount = ", (int)blockCount);
|
||||
LOG_DEBUG(" fieldsCapacity (blocks * 32) = ", fieldsCapacity);
|
||||
|
||||
// Read update mask
|
||||
std::vector<uint32_t> updateMask(blockCount);
|
||||
// Read update mask into a reused scratch buffer to avoid per-block allocations.
|
||||
static thread_local std::vector<uint32_t> updateMask;
|
||||
updateMask.resize(blockCount);
|
||||
for (int i = 0; i < blockCount; ++i) {
|
||||
updateMask[i] = packet.readUInt32();
|
||||
}
|
||||
|
|
@ -1035,22 +1098,30 @@ bool UpdateObjectParser::parseUpdateFields(network::Packet& packet, UpdateBlock&
|
|||
uint16_t highestSetBit = 0;
|
||||
uint32_t valuesReadCount = 0;
|
||||
|
||||
// Read field values for each bit set in mask
|
||||
// Read only set bits in each mask block (faster than scanning all 32 bits).
|
||||
for (int blockIdx = 0; blockIdx < blockCount; ++blockIdx) {
|
||||
uint32_t mask = updateMask[blockIdx];
|
||||
|
||||
for (int bit = 0; bit < 32; ++bit) {
|
||||
if (mask & (1 << bit)) {
|
||||
uint16_t fieldIndex = blockIdx * 32 + bit;
|
||||
if (fieldIndex > highestSetBit) {
|
||||
highestSetBit = fieldIndex;
|
||||
}
|
||||
uint32_t value = packet.readUInt32();
|
||||
block.fields[fieldIndex] = value;
|
||||
valuesReadCount++;
|
||||
|
||||
LOG_DEBUG(" Field[", fieldIndex, "] = 0x", std::hex, value, std::dec);
|
||||
while (mask != 0) {
|
||||
const uint16_t fieldIndex =
|
||||
#if defined(__GNUC__) || defined(__clang__)
|
||||
static_cast<uint16_t>(blockIdx * 32 + __builtin_ctz(mask));
|
||||
#else
|
||||
static_cast<uint16_t>(blockIdx * 32 + [] (uint32_t v) -> uint16_t {
|
||||
uint16_t b = 0;
|
||||
while ((v & 1u) == 0u) { v >>= 1u; ++b; }
|
||||
return b;
|
||||
}(mask));
|
||||
#endif
|
||||
if (fieldIndex > highestSetBit) {
|
||||
highestSetBit = fieldIndex;
|
||||
}
|
||||
uint32_t value = packet.readUInt32();
|
||||
// fieldIndex is monotonically increasing here, so end() is a good insertion hint.
|
||||
block.fields.emplace_hint(block.fields.end(), fieldIndex, value);
|
||||
valuesReadCount++;
|
||||
|
||||
LOG_DEBUG(" Field[", fieldIndex, "] = 0x", std::hex, value, std::dec);
|
||||
mask &= (mask - 1u);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1131,9 +1202,16 @@ 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;
|
||||
|
||||
// Read block count
|
||||
data.blockCount = packet.readUInt32();
|
||||
if (data.blockCount > kMaxReasonableUpdateBlocks) {
|
||||
LOG_ERROR("SMSG_UPDATE_OBJECT rejected: unreasonable blockCount=", data.blockCount,
|
||||
" packetSize=", packet.getSize());
|
||||
return false;
|
||||
}
|
||||
|
||||
LOG_DEBUG("SMSG_UPDATE_OBJECT:");
|
||||
LOG_DEBUG(" objectCount = ", data.blockCount);
|
||||
|
|
@ -1146,6 +1224,11 @@ bool UpdateObjectParser::parse(network::Packet& packet, UpdateObjectData& data)
|
|||
if (firstByte == static_cast<uint8_t>(UpdateType::OUT_OF_RANGE_OBJECTS)) {
|
||||
// Read out-of-range GUID count
|
||||
uint32_t count = packet.readUInt32();
|
||||
if (count > kMaxReasonableOutOfRangeGuids) {
|
||||
LOG_ERROR("SMSG_UPDATE_OBJECT rejected: unreasonable outOfRange count=", count,
|
||||
" packetSize=", packet.getSize());
|
||||
return false;
|
||||
}
|
||||
|
||||
for (uint32_t i = 0; i < count; ++i) {
|
||||
uint64_t guid = readPackedGuid(packet);
|
||||
|
|
@ -1173,7 +1256,7 @@ bool UpdateObjectParser::parse(network::Packet& packet, UpdateObjectData& data)
|
|||
return false;
|
||||
}
|
||||
|
||||
data.blocks.push_back(block);
|
||||
data.blocks.emplace_back(std::move(block));
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
18
src/main.cpp
18
src/main.cpp
|
|
@ -2,6 +2,9 @@
|
|||
#include "core/logger.hpp"
|
||||
#include <exception>
|
||||
#include <csignal>
|
||||
#include <cstdlib>
|
||||
#include <cctype>
|
||||
#include <string>
|
||||
#include <SDL2/SDL.h>
|
||||
#ifdef __linux__
|
||||
#include <X11/Xlib.h>
|
||||
|
|
@ -27,6 +30,19 @@ static void crashHandler(int sig) {
|
|||
std::raise(sig);
|
||||
}
|
||||
|
||||
static wowee::core::LogLevel readLogLevelFromEnv() {
|
||||
const char* raw = std::getenv("WOWEE_LOG_LEVEL");
|
||||
if (!raw || !*raw) return wowee::core::LogLevel::WARNING;
|
||||
std::string level(raw);
|
||||
for (char& c : level) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
|
||||
if (level == "debug") return wowee::core::LogLevel::DEBUG;
|
||||
if (level == "info") return wowee::core::LogLevel::INFO;
|
||||
if (level == "warn" || level == "warning") return wowee::core::LogLevel::WARNING;
|
||||
if (level == "error") return wowee::core::LogLevel::ERROR;
|
||||
if (level == "fatal") return wowee::core::LogLevel::FATAL;
|
||||
return wowee::core::LogLevel::WARNING;
|
||||
}
|
||||
|
||||
int main([[maybe_unused]] int argc, [[maybe_unused]] char* argv[]) {
|
||||
#ifdef __linux__
|
||||
g_emergencyDisplay = XOpenDisplay(nullptr);
|
||||
|
|
@ -37,7 +53,7 @@ int main([[maybe_unused]] int argc, [[maybe_unused]] char* argv[]) {
|
|||
std::signal(SIGTERM, crashHandler);
|
||||
std::signal(SIGINT, crashHandler);
|
||||
try {
|
||||
wowee::core::Logger::getInstance().setLogLevel(wowee::core::LogLevel::INFO);
|
||||
wowee::core::Logger::getInstance().setLogLevel(readLogLevelFromEnv());
|
||||
LOG_INFO("=== Wowee Native Client ===");
|
||||
LOG_INFO("Starting application...");
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
#include "network/packet.hpp"
|
||||
#include <cstring>
|
||||
#include <utility>
|
||||
|
||||
namespace wowee {
|
||||
namespace network {
|
||||
|
|
@ -9,6 +10,9 @@ Packet::Packet(uint16_t opcode) : opcode(opcode) {}
|
|||
Packet::Packet(uint16_t opcode, const std::vector<uint8_t>& data)
|
||||
: opcode(opcode), data(data), readPos(0) {}
|
||||
|
||||
Packet::Packet(uint16_t opcode, std::vector<uint8_t>&& data)
|
||||
: opcode(opcode), data(std::move(data)), readPos(0) {}
|
||||
|
||||
void Packet::writeUInt8(uint8_t value) {
|
||||
data.push_back(value);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@
|
|||
#include <sstream>
|
||||
#include <cstdio>
|
||||
#include <fstream>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
|
||||
namespace {
|
||||
constexpr size_t kMaxReceiveBufferBytes = 8 * 1024 * 1024;
|
||||
|
|
@ -40,6 +42,13 @@ inline bool isLoginPipelineCmsg(uint16_t opcode) {
|
|||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
inline bool envFlagEnabled(const char* key, bool defaultValue = false) {
|
||||
const char* raw = std::getenv(key);
|
||||
if (!raw || !*raw) return defaultValue;
|
||||
return !(raw[0] == '0' || raw[0] == 'f' || raw[0] == 'F' ||
|
||||
raw[0] == 'n' || raw[0] == 'N');
|
||||
}
|
||||
} // namespace
|
||||
|
||||
namespace wowee {
|
||||
|
|
@ -58,6 +67,19 @@ static const uint8_t DECRYPT_KEY[] = {
|
|||
|
||||
WorldSocket::WorldSocket() {
|
||||
net::ensureInit();
|
||||
// Always reserve baseline receive capacity (safe, behavior-preserving).
|
||||
receiveBuffer.reserve(64 * 1024);
|
||||
useFastRecvAppend_ = envFlagEnabled("WOWEE_NET_FAST_RECV_APPEND", true);
|
||||
useParseScratchQueue_ = envFlagEnabled("WOWEE_NET_PARSE_SCRATCH", false);
|
||||
if (useParseScratchQueue_) {
|
||||
LOG_WARNING("WOWEE_NET_PARSE_SCRATCH is temporarily disabled (known unstable); forcing off");
|
||||
useParseScratchQueue_ = false;
|
||||
}
|
||||
if (useParseScratchQueue_) {
|
||||
parsedPacketsScratch_.reserve(64);
|
||||
}
|
||||
LOG_INFO("WorldSocket net opts: fast_recv_append=", useFastRecvAppend_ ? "on" : "off",
|
||||
" parse_scratch=", useParseScratchQueue_ ? "on" : "off");
|
||||
}
|
||||
|
||||
WorldSocket::~WorldSocket() {
|
||||
|
|
@ -118,6 +140,8 @@ void WorldSocket::disconnect() {
|
|||
encryptionEnabled = false;
|
||||
useVanillaCrypt = false;
|
||||
receiveBuffer.clear();
|
||||
receiveReadOffset_ = 0;
|
||||
parsedPacketsScratch_.clear();
|
||||
headerBytesDecrypted = 0;
|
||||
LOG_INFO("Disconnected from world server");
|
||||
}
|
||||
|
|
@ -128,13 +152,15 @@ bool WorldSocket::isConnected() const {
|
|||
|
||||
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);
|
||||
|
||||
const auto& data = packet.getData();
|
||||
uint16_t opcode = packet.getOpcode();
|
||||
uint16_t payloadLen = static_cast<uint16_t>(data.size());
|
||||
|
||||
// Debug: parse and log character-create payload fields (helps diagnose appearance issues).
|
||||
if (opcode == 0x036) { // CMSG_CHAR_CREATE
|
||||
if (kLogCharCreatePayload && opcode == 0x036) { // CMSG_CHAR_CREATE
|
||||
size_t pos = 0;
|
||||
std::string name;
|
||||
while (pos < data.size()) {
|
||||
|
|
@ -181,7 +207,7 @@ void WorldSocket::send(const Packet& packet) {
|
|||
}
|
||||
}
|
||||
|
||||
if (opcode == 0x10C || opcode == 0x10D) { // CMSG_SWAP_ITEM / CMSG_SWAP_INV_ITEM
|
||||
if (kLogSwapItemPackets && (opcode == 0x10C || opcode == 0x10D)) { // CMSG_SWAP_ITEM / CMSG_SWAP_INV_ITEM
|
||||
std::string hex;
|
||||
for (size_t i = 0; i < data.size(); i++) {
|
||||
char buf[4];
|
||||
|
|
@ -255,6 +281,23 @@ void WorldSocket::send(const Packet& packet) {
|
|||
|
||||
void WorldSocket::update() {
|
||||
if (!connected) return;
|
||||
auto bufferedBytes = [&]() -> size_t {
|
||||
return (receiveBuffer.size() >= receiveReadOffset_)
|
||||
? (receiveBuffer.size() - receiveReadOffset_)
|
||||
: 0;
|
||||
};
|
||||
auto compactReceiveBuffer = [&]() {
|
||||
if (receiveReadOffset_ == 0) return;
|
||||
if (receiveReadOffset_ >= receiveBuffer.size()) {
|
||||
receiveBuffer.clear();
|
||||
receiveReadOffset_ = 0;
|
||||
return;
|
||||
}
|
||||
const size_t remaining = receiveBuffer.size() - receiveReadOffset_;
|
||||
std::memmove(receiveBuffer.data(), receiveBuffer.data() + receiveReadOffset_, remaining);
|
||||
receiveBuffer.resize(remaining);
|
||||
receiveReadOffset_ = 0;
|
||||
};
|
||||
|
||||
// Drain the socket. Some servers send an auth response and immediately close; a single recv()
|
||||
// may read the response, and a subsequent recv() can return 0 (FIN). If we disconnect right
|
||||
|
|
@ -270,10 +313,42 @@ void WorldSocket::update() {
|
|||
if (received > 0) {
|
||||
receivedAny = true;
|
||||
++readOps;
|
||||
bytesReadThisTick += static_cast<size_t>(received);
|
||||
receiveBuffer.insert(receiveBuffer.end(), buffer, buffer + received);
|
||||
if (receiveBuffer.size() > kMaxReceiveBufferBytes) {
|
||||
LOG_ERROR("World socket receive buffer overflow (", receiveBuffer.size(),
|
||||
size_t receivedSize = static_cast<size_t>(received);
|
||||
bytesReadThisTick += receivedSize;
|
||||
if (useFastRecvAppend_) {
|
||||
size_t liveBytes = bufferedBytes();
|
||||
if (liveBytes > kMaxReceiveBufferBytes || receivedSize > (kMaxReceiveBufferBytes - liveBytes)) {
|
||||
compactReceiveBuffer();
|
||||
liveBytes = bufferedBytes();
|
||||
}
|
||||
if (liveBytes > kMaxReceiveBufferBytes || receivedSize > (kMaxReceiveBufferBytes - liveBytes)) {
|
||||
LOG_ERROR("World socket receive buffer would overflow (buffered=", liveBytes,
|
||||
" incoming=", receivedSize, " max=", kMaxReceiveBufferBytes,
|
||||
"). Disconnecting to recover framing.");
|
||||
disconnect();
|
||||
return;
|
||||
}
|
||||
const size_t oldSize = receiveBuffer.size();
|
||||
const size_t needed = oldSize + receivedSize;
|
||||
if (receiveBuffer.capacity() < needed) {
|
||||
size_t newCap = receiveBuffer.capacity() ? receiveBuffer.capacity() : 64 * 1024;
|
||||
while (newCap < needed && newCap < kMaxReceiveBufferBytes) {
|
||||
newCap = std::min(kMaxReceiveBufferBytes, newCap * 2);
|
||||
}
|
||||
if (newCap < needed) {
|
||||
LOG_ERROR("World socket receive buffer capacity growth failed (needed=", needed,
|
||||
" max=", kMaxReceiveBufferBytes, "). Disconnecting to recover framing.");
|
||||
disconnect();
|
||||
return;
|
||||
}
|
||||
receiveBuffer.reserve(newCap);
|
||||
}
|
||||
receiveBuffer.insert(receiveBuffer.end(), buffer, buffer + receivedSize);
|
||||
} else {
|
||||
receiveBuffer.insert(receiveBuffer.end(), buffer, buffer + received);
|
||||
}
|
||||
if (bufferedBytes() > kMaxReceiveBufferBytes) {
|
||||
LOG_ERROR("World socket receive buffer overflow (", bufferedBytes(),
|
||||
" bytes). Disconnecting to recover framing.");
|
||||
disconnect();
|
||||
return;
|
||||
|
|
@ -297,26 +372,29 @@ void WorldSocket::update() {
|
|||
}
|
||||
|
||||
if (receivedAny) {
|
||||
LOG_DEBUG("World socket read ", bytesReadThisTick, " bytes in ", readOps,
|
||||
" recv call(s), buffered=", receiveBuffer.size());
|
||||
// Hex dump received bytes for auth debugging
|
||||
if (bytesReadThisTick <= 128) {
|
||||
const bool debugLog = core::Logger::getInstance().shouldLog(core::LogLevel::DEBUG);
|
||||
if (debugLog) {
|
||||
LOG_DEBUG("World socket read ", bytesReadThisTick, " bytes in ", readOps,
|
||||
" recv call(s), buffered=", bufferedBytes());
|
||||
}
|
||||
// Hex dump received bytes for auth debugging (debug-only to avoid per-frame string work)
|
||||
if (debugLog && bytesReadThisTick <= 128) {
|
||||
std::string hex;
|
||||
for (size_t i = 0; i < receiveBuffer.size(); ++i) {
|
||||
for (size_t i = receiveReadOffset_; i < receiveBuffer.size(); ++i) {
|
||||
char buf[4]; snprintf(buf, sizeof(buf), "%02x ", receiveBuffer[i]); hex += buf;
|
||||
}
|
||||
LOG_DEBUG("World socket raw bytes: ", hex);
|
||||
}
|
||||
tryParsePackets();
|
||||
if (connected && !receiveBuffer.empty()) {
|
||||
LOG_DEBUG("World socket parse left ", receiveBuffer.size(),
|
||||
if (debugLog && connected && bufferedBytes() > 0) {
|
||||
LOG_DEBUG("World socket parse left ", bufferedBytes(),
|
||||
" bytes buffered (awaiting complete packet)");
|
||||
}
|
||||
}
|
||||
|
||||
if (sawClose) {
|
||||
LOG_INFO("World server connection closed (receivedAny=", receivedAny,
|
||||
" buffered=", receiveBuffer.size(), ")");
|
||||
" buffered=", bufferedBytes(), ")");
|
||||
disconnect();
|
||||
return;
|
||||
}
|
||||
|
|
@ -325,27 +403,44 @@ void WorldSocket::update() {
|
|||
void WorldSocket::tryParsePackets() {
|
||||
// World server packets have 4-byte incoming header: size(2) + opcode(2)
|
||||
int parsedThisTick = 0;
|
||||
while (receiveBuffer.size() >= 4 && parsedThisTick < kMaxParsedPacketsPerUpdate) {
|
||||
size_t parseOffset = receiveReadOffset_;
|
||||
size_t localHeaderBytesDecrypted = headerBytesDecrypted;
|
||||
std::vector<Packet> parsedPacketsLocal;
|
||||
std::vector<Packet>* parsedPackets = &parsedPacketsLocal;
|
||||
if (useParseScratchQueue_) {
|
||||
parsedPacketsScratch_.clear();
|
||||
// Keep a warm queue to reduce steady-state allocations, but avoid
|
||||
// retaining pathological capacity after burst/misaligned streams.
|
||||
if (parsedPacketsScratch_.capacity() > 1024) {
|
||||
std::vector<Packet>().swap(parsedPacketsScratch_);
|
||||
} else if (parsedPacketsScratch_.capacity() < 64) {
|
||||
parsedPacketsScratch_.reserve(64);
|
||||
}
|
||||
parsedPackets = &parsedPacketsScratch_;
|
||||
} else {
|
||||
parsedPacketsLocal.reserve(32);
|
||||
}
|
||||
while ((receiveBuffer.size() - parseOffset) >= 4 && parsedThisTick < kMaxParsedPacketsPerUpdate) {
|
||||
uint8_t rawHeader[4] = {0, 0, 0, 0};
|
||||
std::memcpy(rawHeader, receiveBuffer.data(), 4);
|
||||
std::memcpy(rawHeader, receiveBuffer.data() + parseOffset, 4);
|
||||
|
||||
// Decrypt header bytes in-place if encryption is enabled
|
||||
// Only decrypt bytes we haven't already decrypted
|
||||
if (encryptionEnabled && headerBytesDecrypted < 4) {
|
||||
size_t toDecrypt = 4 - headerBytesDecrypted;
|
||||
if (encryptionEnabled && localHeaderBytesDecrypted < 4) {
|
||||
size_t toDecrypt = 4 - localHeaderBytesDecrypted;
|
||||
if (useVanillaCrypt) {
|
||||
vanillaCrypt.decrypt(receiveBuffer.data() + headerBytesDecrypted, toDecrypt);
|
||||
vanillaCrypt.decrypt(receiveBuffer.data() + parseOffset + localHeaderBytesDecrypted, toDecrypt);
|
||||
} else {
|
||||
decryptCipher.process(receiveBuffer.data() + headerBytesDecrypted, toDecrypt);
|
||||
decryptCipher.process(receiveBuffer.data() + parseOffset + localHeaderBytesDecrypted, toDecrypt);
|
||||
}
|
||||
headerBytesDecrypted = 4;
|
||||
localHeaderBytesDecrypted = 4;
|
||||
}
|
||||
|
||||
// Parse header (now decrypted in-place).
|
||||
// Size: 2 bytes big-endian. For world packets, this includes opcode bytes.
|
||||
uint16_t size = (receiveBuffer[0] << 8) | receiveBuffer[1];
|
||||
uint16_t size = (receiveBuffer[parseOffset + 0] << 8) | receiveBuffer[parseOffset + 1];
|
||||
// Opcode: 2 bytes little-endian.
|
||||
uint16_t opcode = receiveBuffer[2] | (receiveBuffer[3] << 8);
|
||||
uint16_t opcode = receiveBuffer[parseOffset + 2] | (receiveBuffer[parseOffset + 3] << 8);
|
||||
if (size < 2) {
|
||||
LOG_ERROR("World packet framing desync: invalid size=", size,
|
||||
" rawHdr=", std::hex,
|
||||
|
|
@ -381,50 +476,79 @@ void WorldSocket::tryParsePackets() {
|
|||
static_cast<int>(rawHeader[2]), " ",
|
||||
static_cast<int>(rawHeader[3]),
|
||||
" dec=",
|
||||
static_cast<int>(receiveBuffer[0]), " ",
|
||||
static_cast<int>(receiveBuffer[1]), " ",
|
||||
static_cast<int>(receiveBuffer[2]), " ",
|
||||
static_cast<int>(receiveBuffer[3]),
|
||||
static_cast<int>(receiveBuffer[parseOffset + 0]), " ",
|
||||
static_cast<int>(receiveBuffer[parseOffset + 1]), " ",
|
||||
static_cast<int>(receiveBuffer[parseOffset + 2]), " ",
|
||||
static_cast<int>(receiveBuffer[parseOffset + 3]),
|
||||
std::dec,
|
||||
" size=", size,
|
||||
" payload=", payloadLen,
|
||||
" opcode=0x", std::hex, opcode, std::dec,
|
||||
" buffered=", receiveBuffer.size());
|
||||
" buffered=", (receiveBuffer.size() - parseOffset));
|
||||
--headerTracePacketsLeft;
|
||||
}
|
||||
if (isLoginPipelineSmsg(opcode)) {
|
||||
LOG_INFO("WS RX LOGIN opcode=0x", std::hex, opcode, std::dec,
|
||||
" size=", size, " payload=", payloadLen,
|
||||
" buffered=", receiveBuffer.size(),
|
||||
" buffered=", (receiveBuffer.size() - parseOffset),
|
||||
" enc=", encryptionEnabled ? "yes" : "no");
|
||||
}
|
||||
|
||||
if (receiveBuffer.size() < totalSize) {
|
||||
if ((receiveBuffer.size() - parseOffset) < totalSize) {
|
||||
// Not enough data yet - header stays decrypted in buffer
|
||||
break;
|
||||
}
|
||||
|
||||
// Extract payload (skip header)
|
||||
std::vector<uint8_t> packetData(receiveBuffer.begin() + 4,
|
||||
receiveBuffer.begin() + totalSize);
|
||||
|
||||
// Create packet with opcode and payload
|
||||
Packet packet(opcode, packetData);
|
||||
|
||||
// Remove parsed data from buffer and reset header decryption counter
|
||||
receiveBuffer.erase(receiveBuffer.begin(), receiveBuffer.begin() + totalSize);
|
||||
headerBytesDecrypted = 0;
|
||||
|
||||
// Call callback if set
|
||||
if (packetCallback) {
|
||||
packetCallback(packet);
|
||||
// Extract payload (skip header). Guard allocation failures so malformed
|
||||
// streams cannot unwind into application-level OOM crashes.
|
||||
try {
|
||||
std::vector<uint8_t> packetData(payloadLen);
|
||||
if (payloadLen > 0) {
|
||||
std::memcpy(packetData.data(), receiveBuffer.data() + parseOffset + 4, payloadLen);
|
||||
}
|
||||
// Queue packet; callbacks run after buffer state is finalized.
|
||||
parsedPackets->emplace_back(opcode, std::move(packetData));
|
||||
} catch (const std::bad_alloc& e) {
|
||||
LOG_ERROR("OOM while queuing world packet opcode=0x", std::hex, opcode, std::dec,
|
||||
" payload=", payloadLen, " buffered=", receiveBuffer.size(),
|
||||
" parseOffset=", parseOffset, " what=", e.what(),
|
||||
". Disconnecting to recover.");
|
||||
disconnect();
|
||||
return;
|
||||
}
|
||||
parseOffset += totalSize;
|
||||
localHeaderBytesDecrypted = 0;
|
||||
++parsedThisTick;
|
||||
}
|
||||
|
||||
if (parsedThisTick >= kMaxParsedPacketsPerUpdate && receiveBuffer.size() >= 4) {
|
||||
if (parseOffset > receiveReadOffset_) {
|
||||
receiveReadOffset_ = parseOffset;
|
||||
// Compact lazily to avoid front-erase memmove every update.
|
||||
if (receiveReadOffset_ >= receiveBuffer.size()) {
|
||||
receiveBuffer.clear();
|
||||
receiveReadOffset_ = 0;
|
||||
} else if (receiveReadOffset_ >= 64 * 1024 || receiveReadOffset_ * 2 >= receiveBuffer.size()) {
|
||||
const size_t remaining = receiveBuffer.size() - receiveReadOffset_;
|
||||
std::memmove(receiveBuffer.data(), receiveBuffer.data() + receiveReadOffset_, remaining);
|
||||
receiveBuffer.resize(remaining);
|
||||
receiveReadOffset_ = 0;
|
||||
}
|
||||
}
|
||||
headerBytesDecrypted = localHeaderBytesDecrypted;
|
||||
|
||||
if (packetCallback) {
|
||||
for (const auto& packet : *parsedPackets) {
|
||||
if (!connected) break;
|
||||
packetCallback(packet);
|
||||
}
|
||||
}
|
||||
|
||||
const size_t buffered = (receiveBuffer.size() >= receiveReadOffset_)
|
||||
? (receiveBuffer.size() - receiveReadOffset_)
|
||||
: 0;
|
||||
if (parsedThisTick >= kMaxParsedPacketsPerUpdate && buffered >= 4) {
|
||||
LOG_DEBUG("World socket parse budget reached (", parsedThisTick,
|
||||
" packets); deferring remaining buffered data=", receiveBuffer.size(), " bytes");
|
||||
" packets); deferring remaining buffered data=", buffered, " bytes");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -147,8 +147,15 @@ BLPImage AssetManager::loadTexture(const std::string& path) {
|
|||
std::vector<uint8_t> blpData = readFile(normalizedPath);
|
||||
if (blpData.empty()) {
|
||||
static std::unordered_set<std::string> loggedMissingTextures;
|
||||
if (loggedMissingTextures.insert(normalizedPath).second) {
|
||||
static bool missingTextureLogSuppressed = false;
|
||||
static constexpr size_t kMaxMissingTextureLogKeys = 20000;
|
||||
if (loggedMissingTextures.size() < kMaxMissingTextureLogKeys &&
|
||||
loggedMissingTextures.insert(normalizedPath).second) {
|
||||
LOG_WARNING("Texture not found: ", normalizedPath);
|
||||
} else if (!missingTextureLogSuppressed && loggedMissingTextures.size() >= kMaxMissingTextureLogKeys) {
|
||||
LOG_WARNING("Texture-not-found warning key cache reached ", kMaxMissingTextureLogKeys,
|
||||
" entries; suppressing new unique texture-miss logs");
|
||||
missingTextureLogSuppressed = true;
|
||||
}
|
||||
return BLPImage();
|
||||
}
|
||||
|
|
@ -156,8 +163,15 @@ BLPImage AssetManager::loadTexture(const std::string& path) {
|
|||
BLPImage image = BLPLoader::load(blpData);
|
||||
if (!image.isValid()) {
|
||||
static std::unordered_set<std::string> loggedDecodeFails;
|
||||
if (loggedDecodeFails.insert(normalizedPath).second) {
|
||||
static bool decodeFailLogSuppressed = false;
|
||||
static constexpr size_t kMaxDecodeFailLogKeys = 8000;
|
||||
if (loggedDecodeFails.size() < kMaxDecodeFailLogKeys &&
|
||||
loggedDecodeFails.insert(normalizedPath).second) {
|
||||
LOG_ERROR("Failed to load texture: ", normalizedPath);
|
||||
} else if (!decodeFailLogSuppressed && loggedDecodeFails.size() >= kMaxDecodeFailLogKeys) {
|
||||
LOG_WARNING("Texture-decode warning key cache reached ", kMaxDecodeFailLogKeys,
|
||||
" entries; suppressing new unique decode-failure logs");
|
||||
decodeFailLogSuppressed = true;
|
||||
}
|
||||
return BLPImage();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
#include "pipeline/terrain_mesh.hpp"
|
||||
#include "core/coordinates.hpp"
|
||||
#include "core/logger.hpp"
|
||||
#include <cmath>
|
||||
|
||||
|
|
@ -49,10 +50,14 @@ ChunkMesh TerrainMeshGenerator::generateChunkMesh(const MapChunk& chunk, int chu
|
|||
mesh.chunkX = chunkX;
|
||||
mesh.chunkY = chunkY;
|
||||
|
||||
// World position from chunk data
|
||||
mesh.worldX = chunk.position[0];
|
||||
mesh.worldY = chunk.position[1];
|
||||
mesh.worldZ = chunk.position[2];
|
||||
// Compute render-space XY from tile/chunk indices.
|
||||
// MCNK XY fields can vary across sources; deriving from tile/chunk indices
|
||||
// keeps chunk placement stable and consistent with ADT naming.
|
||||
const float tileNW_renderX = (32.0f - static_cast<float>(tileY)) * core::coords::TILE_SIZE;
|
||||
const float tileNW_renderY = (32.0f - static_cast<float>(tileX)) * core::coords::TILE_SIZE;
|
||||
mesh.worldX = tileNW_renderX - static_cast<float>(chunkY) * CHUNK_SIZE;
|
||||
mesh.worldY = tileNW_renderY - static_cast<float>(chunkX) * CHUNK_SIZE;
|
||||
mesh.worldZ = chunk.position[2]; // height base from MCNK
|
||||
|
||||
// Generate vertices from heightmap (pass chunk grid indices and tile coords)
|
||||
mesh.vertices = generateVertices(chunk, chunkX, chunkY, tileX, tileY);
|
||||
|
|
@ -167,7 +172,7 @@ ChunkMesh TerrainMeshGenerator::generateChunkMesh(const MapChunk& chunk, int chu
|
|||
return mesh;
|
||||
}
|
||||
|
||||
std::vector<TerrainVertex> TerrainMeshGenerator::generateVertices(const MapChunk& chunk, [[maybe_unused]] int chunkX, [[maybe_unused]] int chunkY, [[maybe_unused]] int tileX, [[maybe_unused]] int tileY) {
|
||||
std::vector<TerrainVertex> TerrainMeshGenerator::generateVertices(const MapChunk& chunk, int chunkX, int chunkY, int tileX, int tileY) {
|
||||
std::vector<TerrainVertex> vertices;
|
||||
vertices.reserve(145); // 145 vertices total
|
||||
|
||||
|
|
@ -176,10 +181,12 @@ std::vector<TerrainVertex> TerrainMeshGenerator::generateVertices(const MapChunk
|
|||
// WoW terrain uses 145 heights stored in a 9x17 row-major grid layout
|
||||
const float unitSize = CHUNK_SIZE / 8.0f; // 66.67 units per vertex step
|
||||
|
||||
// chunk.position contains world coordinates for this chunk's origin
|
||||
// Both X and Y are at world scale (no scaling needed)
|
||||
float chunkBaseX = chunk.position[0];
|
||||
float chunkBaseY = chunk.position[1];
|
||||
// Compute render-space base from tile/chunk indices.
|
||||
const float tileNW_renderX = (32.0f - static_cast<float>(tileY)) * core::coords::TILE_SIZE;
|
||||
const float tileNW_renderY = (32.0f - static_cast<float>(tileX)) * core::coords::TILE_SIZE;
|
||||
float chunkBaseX = tileNW_renderX - static_cast<float>(chunkY) * CHUNK_SIZE;
|
||||
float chunkBaseY = tileNW_renderY - static_cast<float>(chunkX) * CHUNK_SIZE;
|
||||
float chunkBaseZ = chunk.position[2];
|
||||
|
||||
for (int index = 0; index < 145; index++) {
|
||||
int y = index / 17; // Row (0-8)
|
||||
|
|
@ -196,11 +203,10 @@ std::vector<TerrainVertex> TerrainMeshGenerator::generateVertices(const MapChunk
|
|||
|
||||
TerrainVertex vertex;
|
||||
|
||||
// Position - match wowee.js coordinate layout (swap X/Y and negate)
|
||||
// wowee.js: X = -(y * unitSize), Y = -(x * unitSize)
|
||||
// Position in render space.
|
||||
vertex.position[0] = chunkBaseX - (offsetY * unitSize);
|
||||
vertex.position[1] = chunkBaseY - (offsetX * unitSize);
|
||||
vertex.position[2] = chunk.position[2] + heightMap.heights[index];
|
||||
vertex.position[2] = chunkBaseZ + heightMap.heights[index];
|
||||
|
||||
// Normal
|
||||
if (index * 3 + 2 < static_cast<int>(chunk.normals.size())) {
|
||||
|
|
|
|||
|
|
@ -4,9 +4,8 @@
|
|||
#include <imgui_internal.h>
|
||||
#include <imgui_impl_opengl3.h>
|
||||
#include <imgui_impl_sdl2.h>
|
||||
#include <random>
|
||||
#include <chrono>
|
||||
#include <cstdio>
|
||||
#include <filesystem>
|
||||
|
||||
#define STB_IMAGE_IMPLEMENTATION
|
||||
#include "stb_image.h"
|
||||
|
|
@ -15,6 +14,9 @@ namespace wowee {
|
|||
namespace rendering {
|
||||
|
||||
LoadingScreen::LoadingScreen() {
|
||||
imagePaths.push_back("assets/krayonload.png");
|
||||
imagePaths.push_back((std::filesystem::current_path() / "assets/krayonload.png").string());
|
||||
// Fallbacks for environments that don't have the newer image yet.
|
||||
imagePaths.push_back("assets/loading1.jpeg");
|
||||
imagePaths.push_back("assets/loading2.jpeg");
|
||||
}
|
||||
|
|
@ -163,15 +165,15 @@ void LoadingScreen::shutdown() {
|
|||
|
||||
void LoadingScreen::selectRandomImage() {
|
||||
if (imagePaths.empty()) return;
|
||||
|
||||
unsigned seed = static_cast<unsigned>(
|
||||
std::chrono::system_clock::now().time_since_epoch().count());
|
||||
std::default_random_engine generator(seed);
|
||||
std::uniform_int_distribution<int> distribution(0, imagePaths.size() - 1);
|
||||
|
||||
currentImageIndex = distribution(generator);
|
||||
// Vulkan branch uses a single curated loading image; keep deterministic
|
||||
// selection with fallback to legacy assets if needed.
|
||||
for (size_t i = 0; i < imagePaths.size(); ++i) {
|
||||
if (std::filesystem::exists(imagePaths[i])) {
|
||||
currentImageIndex = static_cast<int>(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
LOG_INFO("Selected loading screen: ", imagePaths[currentImageIndex]);
|
||||
|
||||
loadImage(imagePaths[currentImageIndex]);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,9 @@
|
|||
#include "pipeline/asset_manager.hpp"
|
||||
#include "audio/music_manager.hpp"
|
||||
#include "game/expansion_profile.hpp"
|
||||
#include <GL/glew.h>
|
||||
#include <imgui.h>
|
||||
#include "stb_image.h"
|
||||
#include <filesystem>
|
||||
#include <sstream>
|
||||
#include <fstream>
|
||||
|
|
@ -50,6 +52,10 @@ static std::vector<uint8_t> hexDecode(const std::string& hex) {
|
|||
AuthScreen::AuthScreen() {
|
||||
}
|
||||
|
||||
AuthScreen::~AuthScreen() {
|
||||
destroyBackgroundImage();
|
||||
}
|
||||
|
||||
std::string AuthScreen::makeServerKey(const std::string& host, int port) {
|
||||
std::ostringstream ss;
|
||||
ss << host << ":" << port;
|
||||
|
|
@ -159,39 +165,34 @@ void AuthScreen::render(auth::AuthHandler& authHandler) {
|
|||
loginInfoLoaded = true;
|
||||
}
|
||||
|
||||
if (!videoInitAttempted) {
|
||||
videoInitAttempted = true;
|
||||
std::string videoPath = "assets/startscreen.mp4";
|
||||
if (!std::filesystem::exists(videoPath)) {
|
||||
videoPath = (std::filesystem::current_path() / "assets/startscreen.mp4").string();
|
||||
}
|
||||
backgroundVideo.open(videoPath);
|
||||
if (!bgInitAttempted) {
|
||||
bgInitAttempted = true;
|
||||
loadBackgroundImage();
|
||||
}
|
||||
backgroundVideo.update(ImGui::GetIO().DeltaTime);
|
||||
if (backgroundVideo.isReady()) {
|
||||
if (bgTextureId_ != 0) {
|
||||
ImVec2 screen = ImGui::GetIO().DisplaySize;
|
||||
float screenW = screen.x;
|
||||
float screenH = screen.y;
|
||||
float videoW = static_cast<float>(backgroundVideo.getWidth());
|
||||
float videoH = static_cast<float>(backgroundVideo.getHeight());
|
||||
if (videoW > 0.0f && videoH > 0.0f) {
|
||||
float imgW = static_cast<float>(bgWidth_);
|
||||
float imgH = static_cast<float>(bgHeight_);
|
||||
if (imgW > 0.0f && imgH > 0.0f) {
|
||||
float screenAspect = screenW / screenH;
|
||||
float videoAspect = videoW / videoH;
|
||||
float imgAspect = imgW / imgH;
|
||||
ImVec2 uv0(0.0f, 0.0f);
|
||||
ImVec2 uv1(1.0f, 1.0f);
|
||||
if (videoAspect > screenAspect) {
|
||||
float scale = screenAspect / videoAspect;
|
||||
if (imgAspect > screenAspect) {
|
||||
float scale = screenAspect / imgAspect;
|
||||
float crop = (1.0f - scale) * 0.5f;
|
||||
uv0.x = crop;
|
||||
uv1.x = 1.0f - crop;
|
||||
} else if (videoAspect < screenAspect) {
|
||||
float scale = videoAspect / screenAspect;
|
||||
} else if (imgAspect < screenAspect) {
|
||||
float scale = imgAspect / screenAspect;
|
||||
float crop = (1.0f - scale) * 0.5f;
|
||||
uv0.y = crop;
|
||||
uv1.y = 1.0f - crop;
|
||||
}
|
||||
ImDrawList* bg = ImGui::GetBackgroundDrawList();
|
||||
bg->AddImage(static_cast<ImTextureID>(static_cast<uintptr_t>(backgroundVideo.getTextureId())),
|
||||
bg->AddImage(static_cast<ImTextureID>(bgTextureId_),
|
||||
ImVec2(0, 0), ImVec2(screenW, screenH), uv0, uv1);
|
||||
}
|
||||
}
|
||||
|
|
@ -484,6 +485,57 @@ void AuthScreen::render(auth::AuthHandler& authHandler) {
|
|||
ImGui::End();
|
||||
}
|
||||
|
||||
bool AuthScreen::loadBackgroundImage() {
|
||||
destroyBackgroundImage();
|
||||
|
||||
const std::array<std::string, 2> candidates = {
|
||||
"assets/krayonsignin.png",
|
||||
(std::filesystem::current_path() / "assets/krayonsignin.png").string()
|
||||
};
|
||||
|
||||
int channels = 0;
|
||||
unsigned char* data = nullptr;
|
||||
std::string loadedPath;
|
||||
for (const auto& p : candidates) {
|
||||
stbi_set_flip_vertically_on_load(false);
|
||||
data = stbi_load(p.c_str(), &bgWidth_, &bgHeight_, &channels, 4);
|
||||
if (data) {
|
||||
loadedPath = p;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
LOG_WARNING("AuthScreen: failed to load background image assets/krayonsignin.png");
|
||||
bgWidth_ = 0;
|
||||
bgHeight_ = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
glGenTextures(1, &bgTextureId_);
|
||||
glBindTexture(GL_TEXTURE_2D, bgTextureId_);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, bgWidth_, bgHeight_, 0, GL_RGBA, GL_UNSIGNED_BYTE, data);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
|
||||
stbi_image_free(data);
|
||||
LOG_INFO("AuthScreen: loaded sign-in background: ", loadedPath, " (", bgWidth_, "x", bgHeight_, ")");
|
||||
return true;
|
||||
}
|
||||
|
||||
void AuthScreen::destroyBackgroundImage() {
|
||||
if (bgTextureId_ != 0) {
|
||||
GLuint tex = static_cast<GLuint>(bgTextureId_);
|
||||
glDeleteTextures(1, &tex);
|
||||
bgTextureId_ = 0;
|
||||
}
|
||||
bgWidth_ = 0;
|
||||
bgHeight_ = 0;
|
||||
}
|
||||
|
||||
void AuthScreen::stopLoginMusic() {
|
||||
auto& app = core::Application::getInstance();
|
||||
auto* renderer = app.getRenderer();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue