Merge commit '32bb0becc8' into chore/game-screen-extract

This commit is contained in:
Paul 2026-03-31 19:51:37 +03:00
commit 43aecab1ef
145 changed files with 3237 additions and 2849 deletions

View file

@ -3,6 +3,7 @@
#include "core/window.hpp"
#include "core/input.hpp"
#include "game/character.hpp"
#include "game/game_services.hpp"
#include "pipeline/blp_loader.hpp"
#include <memory>
#include <string>
@ -126,6 +127,7 @@ private:
static Application* instance;
game::GameServices gameServices_;
std::unique_ptr<Window> window;
std::unique_ptr<rendering::Renderer> renderer;
std::unique_ptr<ui::UIManager> uiManager;

View file

@ -93,7 +93,9 @@ public:
float impliedVX = (destX - fromX) / durationSec;
float impliedVY = (destY - fromY) / durationSec;
float impliedVZ = (destZ - fromZ) / durationSec;
// Exponentially smooth velocity so jittery packet timing doesn't snap speed.
// Exponential moving average on velocity — 65% new sample, 35% previous.
// Smooths out jitter from irregular server update intervals (~200-600ms)
// without introducing visible lag on direction changes.
const float alpha = 0.65f;
velX_ = alpha * impliedVX + (1.0f - alpha) * velX_;
velY_ = alpha * impliedVY + (1.0f - alpha) * velY_;

View file

@ -13,6 +13,7 @@
#include "game/quest_handler.hpp"
#include "game/movement_handler.hpp"
#include "game/entity_controller.hpp"
#include "game/game_services.hpp"
#include "network/packet.hpp"
#include <glm/glm.hpp>
#include <memory>
@ -130,9 +131,11 @@ public:
using TalentEntry = game::TalentEntry;
using TalentTabEntry = game::TalentTabEntry;
GameHandler();
explicit GameHandler(GameServices& services);
~GameHandler();
const GameServices& services() const { return services_; }
/** Access the active opcode table (wire ↔ logical mapping). */
const OpcodeTable& getOpcodeTable() const { return opcodeTable_; }
OpcodeTable& getOpcodeTable() { return opcodeTable_; }
@ -2298,6 +2301,9 @@ private:
float localOrientation);
void clearTransportAttachment(uint64_t childGuid);
// Explicit service dependencies (owned by Application)
GameServices& services_;
// Domain handlers — each manages a specific concern extracted from GameHandler
std::unique_ptr<ChatHandler> chatHandler_;
std::unique_ptr<MovementHandler> movementHandler_;

View file

@ -0,0 +1,23 @@
#pragma once
#include <cstdint>
namespace wowee {
namespace rendering { class Renderer; }
namespace pipeline { class AssetManager; }
namespace game { class ExpansionRegistry; }
namespace game {
// Explicit service dependencies for game handlers.
// Owned by Application, passed by reference to GameHandler at construction.
// Replaces hidden Application::getInstance() singleton access.
struct GameServices {
rendering::Renderer* renderer = nullptr;
pipeline::AssetManager* assetManager = nullptr;
ExpansionRegistry* expansionRegistry = nullptr;
uint32_t gryphonDisplayId = 0;
uint32_t wyvernDisplayId = 0;
};
} // namespace game
} // namespace wowee

View file

@ -70,7 +70,12 @@ class Inventory {
public:
static constexpr int BACKPACK_SLOTS = 16;
static constexpr int KEYRING_SLOTS = 32;
// WoW slot layout: 0-22 are equipment (head, neck, ... tabard, mainhand, offhand, ranged, ammo).
// Backpack inventory starts at slot 23 in bag 0xFF, so packet slot = NUM_EQUIP_SLOTS + backpackIndex.
static constexpr int NUM_EQUIP_SLOTS = 23;
// Bag containers occupy equipment slots 19-22 (bag1, bag2, bag3, bag4).
// Packet bag byte = FIRST_BAG_EQUIP_SLOT + bagIndex.
static constexpr int FIRST_BAG_EQUIP_SLOT = 19;
static constexpr int NUM_BAG_SLOTS = 4;
static constexpr int MAX_BAG_SIZE = 36;
static constexpr int BANK_SLOTS = 28;

View file

@ -122,8 +122,11 @@ public:
};
const std::unordered_map<uint32_t, TaxiNode>& getTaxiNodes() const { return taxiNodes_; }
// WotLK 3.3.5a TaxiNodes.dbc has 384 entries; the known-taxi bitmask
// is 12 × uint32 = 384 bits. Node IDs outside this range are invalid.
static constexpr uint32_t kMaxTaxiNodeId = 384;
bool isKnownTaxiNode(uint32_t nodeId) const {
if (nodeId == 0 || nodeId > 384) return false;
if (nodeId == 0 || nodeId > kMaxTaxiNodeId) return false;
uint32_t idx = nodeId - 1;
return (knownTaxiMask_[idx / 32] & (1u << (idx % 32))) != 0;
}

View file

@ -249,6 +249,9 @@ private:
void handleChannelUpdate(network::Packet& packet);
// --- Internal helpers ---
// Find the on-use spell for an item (trigger=0 Use or trigger=5 NoDelay).
// CMSG_USE_ITEM requires a valid spellId or the server silently ignores it.
uint32_t findOnUseSpellId(uint32_t itemId) const;
void loadSpellNameCache() const;
void loadSkillLineAbilityDbc();
void categorizeTrainerSpells();

View file

@ -138,6 +138,10 @@ public:
*/
std::vector<uint8_t> readData(uint32_t address, size_t size);
// Look up an already-registered API stub address by DLL and function name.
// Returns 0 if not found. Used by WardenModule::bindAPIs() for IAT patching.
uint32_t getAPIAddress(const std::string& dllName, const std::string& funcName) const;
private:
uc_engine* uc_; // Unicorn engine instance
uint32_t moduleBase_; // Module base address

View file

@ -13,6 +13,7 @@ namespace game {
// Forward declarations
class WardenEmulator;
class WardenCrypto;
/**
* Represents Warden callback functions exported by loaded module
@ -36,18 +37,19 @@ struct WardenFuncList {
* Warden module loader and executor
*
* IMPLEMENTATION STATUS:
* Module metadata parsing
* Basic validation framework
* RC4 decryption (uses existing WardenCrypto)
* RSA signature verification (TODO - requires OpenSSL RSA)
* zlib decompression (TODO - requires zlib library)
* Custom executable format parsing (TODO - major reverse engineering)
* Address relocation (TODO - x86 address fixups)
* API binding (TODO - kernel32/user32 function resolution)
* Native code execution (TODO - execute loaded x86 code)
* Module metadata parsing and validation
* RC4 decryption (WardenCrypto)
* RSA-2048 signature verification (OpenSSL EVP real Blizzard modulus)
* zlib decompression
* Custom executable format parsing (3 pair-format variants)
* Address relocation (delta-encoded fixups)
* x86 emulation via Unicorn Engine (cross-platform)
* Client callbacks (sendPacket, validateModule, generateRC4)
* API binding / IAT patching (parses import table, auto-stubs unknown APIs)
* RSA modulus verified (Blizzard key, same across 1.12.1/2.4.3/3.3.5a)
*
* For strict servers like Warmane, ALL TODOs must be implemented.
* For permissive servers, fake responses in GameHandler work.
* Non-fatal verification: RSA mismatch logs warning but continues loading,
* so private-server modules signed with custom keys still work.
*/
class WardenModule {
public:
@ -126,6 +128,12 @@ public:
size_t getModuleSize() const { return moduleSize_; }
const std::vector<uint8_t>& getDecompressedData() const { return decompressedData_; }
// Inject dependencies for module callbacks (sendPacket, generateRC4).
// Must be called before initializeModule() so callbacks can reach the
// network layer and crypto state.
using SendPacketFunc = std::function<void(const uint8_t*, size_t)>;
void setCallbackDependencies(WardenCrypto* crypto, SendPacketFunc sendFunc);
private:
bool loaded_; // Module successfully loaded
std::vector<uint8_t> md5Hash_; // Module identifier
@ -142,6 +150,11 @@ private:
std::unique_ptr<WardenEmulator> emulator_; // Cross-platform x86 emulator
uint32_t emulatedPacketHandlerAddr_ = 0; // Raw emulated VA for 4-arg PacketHandler call
// Dependencies injected via setCallbackDependencies() for module callbacks.
// These are NOT owned — the handler owns the crypto and socket lifetime.
WardenCrypto* callbackCrypto_ = nullptr;
SendPacketFunc callbackSendPacket_;
// Validation and loading steps
bool verifyMD5(const std::vector<uint8_t>& data,
const std::vector<uint8_t>& expectedHash);

View file

@ -1,129 +0,0 @@
#pragma once
#include <string>
#include <vector>
#include <cstdint>
#include <memory>
#include <map>
#include <unordered_map>
#include <unordered_set>
#include <mutex>
#include <shared_mutex>
// Forward declare StormLib handle
typedef void* HANDLE;
namespace wowee {
namespace pipeline {
/**
* MPQManager - Manages MPQ archive loading and file reading
*
* WoW 3.3.5a stores all game assets in MPQ archives.
* This manager loads multiple archives and provides unified file access.
*/
class MPQManager {
public:
MPQManager();
~MPQManager();
/**
* Initialize the MPQ system
* @param dataPath Path to WoW Data directory
* @return true if initialization succeeded
*/
bool initialize(const std::string& dataPath);
/**
* Shutdown and close all archives
*/
void shutdown();
/**
* Load a single MPQ archive
* @param path Full path to MPQ file
* @param priority Priority for file resolution (higher = checked first)
* @return true if archive loaded successfully
*/
bool loadArchive(const std::string& path, int priority = 0);
/**
* Check if a file exists in any loaded archive
* @param filename Virtual file path (e.g., "World\\Maps\\Azeroth\\Azeroth.wdt")
* @return true if file exists
*/
bool fileExists(const std::string& filename) const;
/**
* Read a file from MPQ archives
* @param filename Virtual file path
* @return File contents as byte vector (empty if not found)
*/
std::vector<uint8_t> readFile(const std::string& filename) const;
/**
* Get file size without reading it
* @param filename Virtual file path
* @return File size in bytes (0 if not found)
*/
uint32_t getFileSize(const std::string& filename) const;
/**
* Check if MPQ system is initialized
*/
bool isInitialized() const { return initialized; }
/**
* Get list of loaded archives
*/
const std::vector<std::string>& getLoadedArchives() const { return archiveNames; }
private:
struct ArchiveEntry {
HANDLE handle;
std::string path;
int priority;
};
bool initialized = false;
std::string dataPath;
std::vector<ArchiveEntry> archives;
std::vector<std::string> archiveNames;
/**
* Find archive containing a file
* @param filename File to search for
* @return Archive handle or nullptr if not found
*/
HANDLE findFileArchive(const std::string& filename) const;
/**
* Load patch archives (e.g., patch.MPQ, patch-2.MPQ, etc.)
*/
bool loadPatchArchives();
/**
* Load locale-specific archives
* @param locale Locale string (e.g., "enUS")
*/
bool loadLocaleArchives(const std::string& locale);
void logMissingFileOnce(const std::string& filename) const;
// Cache for mapping "virtual filename" -> archive handle (or INVALID_HANDLE_VALUE for not found).
// This avoids scanning every archive for repeated lookups, which can otherwise appear as a hang
// on screens that trigger many asset probes (character select, character preview, etc.).
//
// Important: caching misses can blow up memory if the game probes many unique non-existent filenames.
// Miss caching is disabled by default and must be explicitly enabled.
mutable std::shared_mutex fileArchiveCacheMutex_;
mutable std::unordered_map<std::string, HANDLE> fileArchiveCache_;
size_t fileArchiveCacheMaxEntries_ = 500000;
bool fileArchiveCacheMisses_ = false;
mutable std::mutex missingFileMutex_;
mutable std::unordered_set<std::string> missingFileWarnings_;
};
} // namespace pipeline
} // namespace wowee

View file

@ -81,6 +81,8 @@ private:
// ImGui texture handle for displaying the preview (VkDescriptorSet in Vulkan backend)
VkDescriptorSet imguiTextureId_ = VK_NULL_HANDLE;
// 4:5 portrait aspect ratio — taller than wide to show full character body
// from head to feet in the character creation/selection screen
static constexpr int fboWidth_ = 400;
static constexpr int fboHeight_ = 500;

View file

@ -251,6 +251,10 @@ public:
private:
// Create 1×1 fallback textures used when real textures are missing or still loading.
// Called during both init and clear to ensure valid descriptor bindings at all times.
void createFallbackTextures(VkDevice device);
VkContext* vkCtx_ = nullptr;
VkRenderPass renderPassOverride_ = VK_NULL_HANDLE;
VkSampleCountFlagBits msaaSamplesOverride_ = VK_SAMPLE_COUNT_1_BIT;

View file

@ -1,33 +0,0 @@
#pragma once
#include <vector>
#include <GL/glew.h>
#include <glm/glm.hpp>
namespace wowee {
namespace rendering {
struct Vertex {
glm::vec3 position;
glm::vec3 normal;
glm::vec2 texCoord;
};
class Mesh {
public:
Mesh() = default;
~Mesh();
void create(const std::vector<Vertex>& vertices, const std::vector<uint32_t>& indices);
void destroy();
void draw() const;
private:
GLuint VAO = 0;
GLuint VBO = 0;
GLuint EBO = 0;
size_t indexCount = 0;
};
} // namespace rendering
} // namespace wowee

View file

@ -30,7 +30,6 @@ namespace rendering {
class Camera;
class CameraController;
class Scene;
class TerrainRenderer;
class TerrainManager;
class PerformanceHUD;
@ -54,7 +53,6 @@ class Minimap;
class WorldMap;
class QuestMarkerRenderer;
class CharacterPreview;
class Shader;
class AmdFsr3Runtime;
class Renderer {
@ -119,7 +117,6 @@ public:
Camera* getCamera() { return camera.get(); }
CameraController* getCameraController() { return cameraController.get(); }
Scene* getScene() { return scene.get(); }
TerrainRenderer* getTerrainRenderer() const { return terrainRenderer.get(); }
TerrainManager* getTerrainManager() const { return terrainManager.get(); }
PerformanceHUD* getPerformanceHUD() { return performanceHUD.get(); }
@ -219,7 +216,6 @@ private:
core::Window* window = nullptr;
std::unique_ptr<Camera> camera;
std::unique_ptr<CameraController> cameraController;
std::unique_ptr<Scene> scene;
std::unique_ptr<TerrainRenderer> terrainRenderer;
std::unique_ptr<TerrainManager> terrainManager;
std::unique_ptr<PerformanceHUD> performanceHUD;

View file

@ -1,27 +0,0 @@
#pragma once
#include <vector>
#include <memory>
namespace wowee {
namespace rendering {
class Mesh;
class Scene {
public:
Scene() = default;
~Scene() = default;
void addMesh(std::shared_ptr<Mesh> mesh);
void removeMesh(const std::shared_ptr<Mesh>& mesh);
void clear();
const std::vector<std::shared_ptr<Mesh>>& getMeshes() const { return meshes; }
private:
std::vector<std::shared_ptr<Mesh>> meshes;
};
} // namespace rendering
} // namespace wowee

View file

@ -1,51 +0,0 @@
#pragma once
#include <string>
#include <unordered_map>
#include <GL/glew.h>
#include <glm/glm.hpp>
namespace wowee {
namespace rendering {
class Shader {
public:
Shader() = default;
~Shader();
[[nodiscard]] bool loadFromFile(const std::string& vertexPath, const std::string& fragmentPath);
[[nodiscard]] bool loadFromSource(const std::string& vertexSource, const std::string& fragmentSource);
void use() const;
void unuse() const;
void setUniform(const std::string& name, int value);
void setUniform(const std::string& name, float value);
void setUniform(const std::string& name, const glm::vec2& value);
void setUniform(const std::string& name, const glm::vec3& value);
void setUniform(const std::string& name, const glm::vec4& value);
void setUniform(const std::string& name, const glm::mat3& value);
void setUniform(const std::string& name, const glm::mat4& value);
void setUniformMatrixArray(const std::string& name, const glm::mat4* matrices, int count);
GLuint getProgram() const { return program; }
// Adopt an externally-created program (no ownership of individual shaders)
void setProgram(GLuint prog) { program = prog; }
// Release ownership without deleting (caller retains the GL program)
void releaseProgram() { program = 0; vertexShader = 0; fragmentShader = 0; }
private:
bool compile(const std::string& vertexSource, const std::string& fragmentSource);
GLint getUniformLocation(const std::string& name) const;
GLuint program = 0;
GLuint vertexShader = 0;
GLuint fragmentShader = 0;
// Cache uniform locations to avoid expensive glGetUniformLocation calls
mutable std::unordered_map<std::string, GLint> uniformLocationCache;
};
} // namespace rendering
} // namespace wowee

View file

@ -1,51 +0,0 @@
#pragma once
#include <string>
#include <cstdint>
#include <vector>
typedef unsigned int GLuint;
namespace wowee {
namespace rendering {
class VideoPlayer {
public:
VideoPlayer();
~VideoPlayer();
bool open(const std::string& path);
void update(float deltaTime);
void close();
bool isReady() const { return textureReady; }
GLuint getTextureId() const { return textureId; }
int getWidth() const { return width; }
int getHeight() const { return height; }
private:
bool decodeNextFrame();
void uploadFrame();
void* formatCtx = nullptr;
void* codecCtx = nullptr;
void* frame = nullptr;
void* rgbFrame = nullptr;
void* packet = nullptr;
void* swsCtx = nullptr;
int videoStreamIndex = -1;
int width = 0;
int height = 0;
double frameTime = 1.0 / 30.0;
double accumulator = 0.0;
bool eof = false;
GLuint textureId = 0;
bool textureReady = false;
std::string sourcePath;
std::vector<uint8_t> rgbBuffer;
};
} // namespace rendering
} // namespace wowee

View file

@ -68,6 +68,8 @@ public:
private:
void generateMipmaps(VkContext& ctx, VkFormat format, uint32_t width, uint32_t height);
// Shared sampler finalization: prefer the global cache, fall back to direct creation
bool finalizeSampler(VkDevice device, const VkSamplerCreateInfo& samplerInfo);
AllocatedImage image_{};
VkSampler sampler_ = VK_NULL_HANDLE;