diff --git a/include/core/application.hpp b/include/core/application.hpp index 988489c3..de2fa37e 100644 --- a/include/core/application.hpp +++ b/include/core/application.hpp @@ -60,6 +60,9 @@ public: // Weapon loading (called at spawn and on equipment change) void loadEquippedWeapons(); + // Teleport to a spawn preset location (single-player only) + void teleportTo(int presetIndex); + // Character skin composite state (saved at spawn for re-compositing on equipment change) const std::string& getBodySkinPath() const { return bodySkinPath_; } const std::vector& getUnderwearPaths() const { return underwearPaths_; } diff --git a/include/core/coordinates.hpp b/include/core/coordinates.hpp index 98e97b92..9d6cfb3b 100644 --- a/include/core/coordinates.hpp +++ b/include/core/coordinates.hpp @@ -24,6 +24,24 @@ inline constexpr float ZEROPOINT = 32.0f * TILE_SIZE; // Range [0, 34133.333] with center at ZEROPOINT (17066.666). // adtY = height; adtX/adtZ are horizontal. +// ---- Server / emulator coordinate system ---- +// WoW emulators (TrinityCore, MaNGOS, AzerothCore, CMaNGOS) send positions +// over the wire as (X, Y, Z) where: +// server.X = canonical.Y (west axis) +// server.Y = canonical.X (north axis) +// server.Z = canonical.Z (height) +// This is also the byte order inside movement packets on the wire. + +// Convert server/wire coordinates → canonical WoW coordinates. +inline glm::vec3 serverToCanonical(const glm::vec3& server) { + return glm::vec3(server.y, server.x, server.z); +} + +// Convert canonical WoW coordinates → server/wire coordinates. +inline glm::vec3 canonicalToServer(const glm::vec3& canonical) { + return glm::vec3(canonical.y, canonical.x, canonical.z); +} + // Convert between canonical WoW and engine rendering coordinates (just swap X/Y). inline glm::vec3 canonicalToRender(const glm::vec3& wow) { return glm::vec3(wow.y, wow.x, wow.z); diff --git a/include/core/spawn_presets.hpp b/include/core/spawn_presets.hpp new file mode 100644 index 00000000..cfdcac62 --- /dev/null +++ b/include/core/spawn_presets.hpp @@ -0,0 +1,27 @@ +#pragma once + +#include + +namespace wowee::core { + +struct SpawnPreset { + const char* key; + const char* label; + const char* mapName; // Map name for ADT paths (e.g., "Azeroth") + glm::vec3 spawnCanonical; // Canonical WoW coords: +X=North, +Y=West, +Z=Up + float yawDeg; + float pitchDeg; +}; + +// Spawn positions in canonical WoW world coordinates (X=north, Y=west, Z=up). +// Tile is computed from position via: tileN = floor(32 - wowN / 533.33333) +inline const SpawnPreset SPAWN_PRESETS[] = { + {"goldshire", "Goldshire", "Azeroth", glm::vec3( 62.0f, -9464.0f, 200.0f), 0.0f, -5.0f}, + {"stormwind", "Stormwind Gate", "Azeroth", glm::vec3( 425.0f, -9176.0f, 120.0f), 35.0f, -8.0f}, + {"ironforge", "Ironforge", "Azeroth", glm::vec3( -882.0f, -4981.0f, 510.0f), -20.0f, -8.0f}, + {"westfall", "Westfall", "Azeroth", glm::vec3( 1215.0f,-10440.0f, 80.0f), 10.0f, -8.0f}, +}; + +inline constexpr int SPAWN_PRESET_COUNT = 4; + +} // namespace wowee::core diff --git a/include/ui/game_screen.hpp b/include/ui/game_screen.hpp index 738d7906..3e39ac4f 100644 --- a/include/ui/game_screen.hpp +++ b/include/ui/game_screen.hpp @@ -29,6 +29,16 @@ public: */ bool isChatInputActive() const { return chatInputActive; } + /** + * Toggle the teleporter panel + */ + void toggleTeleporter() { showTeleporter = !showTeleporter; } + + /** + * Check if teleporter panel is open + */ + bool isTeleporterOpen() const { return showTeleporter; } + private: // Chat state char chatInputBuffer[512] = ""; @@ -40,6 +50,7 @@ private: bool showChatWindow = true; bool showPlayerInfo = false; bool refocusChatInput = false; + bool showTeleporter = false; /** * Render player info window @@ -106,6 +117,7 @@ private: void renderLootWindow(game::GameHandler& gameHandler); void renderGossipWindow(game::GameHandler& gameHandler); void renderVendorWindow(game::GameHandler& gameHandler); + void renderTeleporterPanel(); /** * Inventory screen diff --git a/src/core/application.cpp b/src/core/application.cpp index db6c8901..ec261b0a 100644 --- a/src/core/application.cpp +++ b/src/core/application.cpp @@ -1,5 +1,6 @@ #include "core/application.hpp" #include "core/coordinates.hpp" +#include "core/spawn_presets.hpp" #include "core/logger.hpp" #include "rendering/renderer.hpp" #include "rendering/camera.hpp" @@ -43,27 +44,9 @@ namespace core { namespace { -struct SpawnPreset { - const char* key; - const char* label; - const char* mapName; // Map name for ADT paths (e.g., "Azeroth") - glm::vec3 spawnCanonical; // Canonical WoW coords: +X=North, +Y=West, +Z=Up - float yawDeg; - float pitchDeg; -}; - const SpawnPreset* selectSpawnPreset(const char* envValue) { - // Spawn positions in canonical WoW world coordinates (X=north, Y=west, Z=up). - // Tile is computed from position via: tileN = floor(32 - wowN / 533.33333) - static const SpawnPreset presets[] = { - {"goldshire", "Goldshire", "Azeroth", glm::vec3( 62.0f, -9464.0f, 200.0f), 0.0f, -5.0f}, - {"stormwind", "Stormwind", "Azeroth", glm::vec3( -365.0f, -8345.0f, 180.0f), 35.0f, -8.0f}, - {"ironforge", "Ironforge Area", "Azeroth", glm::vec3( -300.0f,-11240.0f, 260.0f), -20.0f, -8.0f}, - {"westfall", "Westfall", "Azeroth", glm::vec3(-1820.0f, -9380.0f, 190.0f), 10.0f, -8.0f}, - }; - if (!envValue || !*envValue) { - return &presets[0]; + return &SPAWN_PRESETS[0]; } std::string key = envValue; @@ -71,13 +54,13 @@ const SpawnPreset* selectSpawnPreset(const char* envValue) { return static_cast(std::tolower(c)); }); - for (const auto& preset : presets) { - if (key == preset.key) return &preset; + for (int i = 0; i < SPAWN_PRESET_COUNT; i++) { + if (key == SPAWN_PRESETS[i].key) return &SPAWN_PRESETS[i]; } LOG_WARNING("Unknown WOW_SPAWN='", key, "', falling back to goldshire"); LOG_INFO("Available WOW_SPAWN presets: goldshire, stormwind, ironforge, westfall"); - return &presets[0]; + return &SPAWN_PRESETS[0]; } std::optional parseVec3Csv(const char* raw) { @@ -259,6 +242,12 @@ void Application::run() { renderer->getMinimap()->toggle(); } } + // T: Toggle teleporter panel + else if (event.key.keysym.scancode == SDL_SCANCODE_T) { + if (state == AppState::IN_GAME && uiManager) { + uiManager->getGameScreen().toggleTeleporter(); + } + } } } @@ -1057,5 +1046,92 @@ void Application::startSinglePlayer() { LOG_INFO("Single-player mode started - press F1 for performance HUD"); } +void Application::teleportTo(int presetIndex) { + // Guard: only in single-player + IN_GAME state + if (!singlePlayerMode || state != AppState::IN_GAME) return; + if (presetIndex < 0 || presetIndex >= SPAWN_PRESET_COUNT) return; + + const auto& preset = SPAWN_PRESETS[presetIndex]; + LOG_INFO("Teleporting to: ", preset.label); + + // Convert canonical WoW → engine rendering coordinates (swap X/Y) + glm::vec3 spawnRender = core::coords::canonicalToRender(preset.spawnCanonical); + + // Update camera default spawn + if (renderer && renderer->getCameraController()) { + renderer->getCameraController()->setDefaultSpawn(spawnRender, preset.yawDeg, preset.pitchDeg); + } + + // Unload all current terrain + if (renderer && renderer->getTerrainManager()) { + renderer->getTerrainManager()->unloadAll(); + } + + // Compute ADT path from canonical spawn coordinates + auto [tileX, tileY] = core::coords::canonicalToTile(preset.spawnCanonical.x, preset.spawnCanonical.y); + std::string mapName = preset.mapName; + std::string adtPath = "World\\Maps\\" + mapName + "\\" + mapName + "_" + + std::to_string(tileX) + "_" + std::to_string(tileY) + ".adt"; + LOG_INFO("Teleport ADT tile [", tileX, ",", tileY, "]"); + + // Set map name on terrain manager + if (renderer && renderer->getTerrainManager()) { + renderer->getTerrainManager()->setMapName(mapName); + } + + // Load the initial tile + bool terrainOk = false; + if (renderer && assetManager && assetManager->isInitialized()) { + terrainOk = renderer->loadTestTerrain(assetManager.get(), adtPath); + } + + // Stream surrounding tiles + if (terrainOk && renderer->getTerrainManager() && renderer->getCamera()) { + auto* terrainMgr = renderer->getTerrainManager(); + auto* camera = renderer->getCamera(); + + terrainMgr->update(*camera, 1.0f); + + auto startTime = std::chrono::high_resolution_clock::now(); + const float maxWaitSeconds = 8.0f; + + while (terrainMgr->getPendingTileCount() > 0) { + SDL_Event event; + while (SDL_PollEvent(&event)) { + if (event.type == SDL_QUIT) { + window->setShouldClose(true); + return; + } + } + + terrainMgr->update(*camera, 0.016f); + + auto elapsed = std::chrono::high_resolution_clock::now() - startTime; + if (std::chrono::duration(elapsed).count() > maxWaitSeconds) { + LOG_WARNING("Teleport terrain streaming timeout after ", maxWaitSeconds, "s"); + break; + } + + SDL_Delay(16); + } + + LOG_INFO("Teleport terrain streaming complete: ", terrainMgr->getLoadedTileCount(), " tiles loaded"); + } + + // Reset camera — this snaps character position to terrain via followTarget + if (renderer && renderer->getCameraController()) { + renderer->getCameraController()->reset(); + } + + // Sync the terrain-snapped character position to game handler + if (renderer && gameHandler) { + glm::vec3 snappedRender = renderer->getCharacterPosition(); + glm::vec3 snappedCanonical = core::coords::renderToCanonical(snappedRender); + gameHandler->setPosition(snappedCanonical.x, snappedCanonical.y, snappedCanonical.z); + } + + LOG_INFO("Teleport to ", preset.label, " complete"); +} + } // namespace core } // namespace wowee diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index 8d7fa8ab..950e188e 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -2,6 +2,7 @@ #include "game/opcodes.hpp" #include "network/world_socket.hpp" #include "network/packet.hpp" +#include "core/coordinates.hpp" #include "core/logger.hpp" #include #include @@ -557,10 +558,11 @@ void GameHandler::handleLoginVerifyWorld(network::Packet& packet) { LOG_INFO("Orientation: ", data.orientation, " radians"); LOG_INFO("Player is now in the game world"); - // Initialize movement info with world entry position - movementInfo.x = data.x; - movementInfo.y = data.y; - movementInfo.z = data.z; + // Initialize movement info with world entry position (server → canonical) + glm::vec3 canonical = core::coords::serverToCanonical(glm::vec3(data.x, data.y, data.z)); + movementInfo.x = canonical.x; + movementInfo.y = canonical.y; + movementInfo.z = canonical.z; movementInfo.orientation = data.orientation; movementInfo.flags = 0; movementInfo.flags2 = 0; @@ -698,8 +700,15 @@ void GameHandler::sendMovement(Opcode opcode) { LOG_DEBUG("Sending movement packet: opcode=0x", std::hex, static_cast(opcode), std::dec); + // Convert canonical → server coordinates for the wire + MovementInfo wireInfo = movementInfo; + glm::vec3 serverPos = core::coords::canonicalToServer(glm::vec3(wireInfo.x, wireInfo.y, wireInfo.z)); + wireInfo.x = serverPos.x; + wireInfo.y = serverPos.y; + wireInfo.z = serverPos.z; + // Build and send movement packet - auto packet = MovementPacket::build(opcode, movementInfo); + auto packet = MovementPacket::build(opcode, wireInfo); socket->send(packet); } @@ -762,10 +771,11 @@ void GameHandler::handleUpdateObject(network::Packet& packet) { break; } - // Set position from movement block + // Set position from movement block (server → canonical) if (block.hasMovement) { - entity->setPosition(block.x, block.y, block.z, block.orientation); - LOG_DEBUG(" Position: (", block.x, ", ", block.y, ", ", block.z, ")"); + glm::vec3 pos = core::coords::serverToCanonical(glm::vec3(block.x, block.y, block.z)); + entity->setPosition(pos.x, pos.y, pos.z, block.orientation); + LOG_DEBUG(" Position: (", pos.x, ", ", pos.y, ", ", pos.z, ")"); } // Set fields @@ -838,10 +848,11 @@ void GameHandler::handleUpdateObject(network::Packet& packet) { } case UpdateType::MOVEMENT: { - // Update entity position + // Update entity position (server → canonical) auto entity = entityManager.getEntity(block.guid); if (entity) { - entity->setPosition(block.x, block.y, block.z, block.orientation); + glm::vec3 pos = core::coords::serverToCanonical(glm::vec3(block.x, block.y, block.z)); + entity->setPosition(pos.x, pos.y, pos.z, block.orientation); LOG_DEBUG("Updated entity position: 0x", std::hex, block.guid, std::dec); } else { LOG_WARNING("MOVEMENT update for unknown entity: 0x", std::hex, block.guid, std::dec); diff --git a/src/rendering/terrain_manager.cpp b/src/rendering/terrain_manager.cpp index 3fe2e53c..8895b134 100644 --- a/src/rendering/terrain_manager.cpp +++ b/src/rendering/terrain_manager.cpp @@ -723,7 +723,7 @@ void TerrainManager::unloadTile(int x, int y) { } void TerrainManager::unloadAll() { - // Stop worker thread + // Stop worker threads if (workerRunning.load()) { workerRunning.store(false); queueCV.notify_all(); @@ -748,6 +748,10 @@ void TerrainManager::unloadAll() { loadedTiles.clear(); failedTiles.clear(); + // Reset tile tracking so streaming re-triggers at the new location + currentTile = {-1, -1}; + lastStreamTile = {-1, -1}; + // Clear terrain renderer if (terrainRenderer) { terrainRenderer->clear(); @@ -757,6 +761,23 @@ void TerrainManager::unloadAll() { if (waterRenderer) { waterRenderer->clear(); } + + // Clear WMO and M2 renderers so old-location geometry doesn't persist + if (wmoRenderer) { + wmoRenderer->clearInstances(); + } + if (m2Renderer) { + m2Renderer->clear(); + } + + // Restart worker threads so streaming can resume + workerRunning.store(true); + unsigned hc = std::thread::hardware_concurrency(); + workerCount = static_cast(hc > 0 ? std::min(4u, std::max(2u, hc - 1)) : 2u); + workerThreads.reserve(workerCount); + for (int i = 0; i < workerCount; i++) { + workerThreads.emplace_back(&TerrainManager::workerLoop, this); + } } TileCoord TerrainManager::worldToTile(float glX, float glY) const { diff --git a/src/ui/game_screen.cpp b/src/ui/game_screen.cpp index 195be59b..b7a833df 100644 --- a/src/ui/game_screen.cpp +++ b/src/ui/game_screen.cpp @@ -1,6 +1,7 @@ #include "ui/game_screen.hpp" #include "core/application.hpp" #include "core/coordinates.hpp" +#include "core/spawn_presets.hpp" #include "core/input.hpp" #include "rendering/renderer.hpp" #include "rendering/character_renderer.hpp" @@ -79,6 +80,9 @@ void GameScreen::render(game::GameHandler& gameHandler) { renderGossipWindow(gameHandler); renderVendorWindow(gameHandler); + // Teleporter panel (T key toggle handled in Application event loop) + renderTeleporterPanel(); + // Spellbook (P key toggle handled inside) spellbookScreen.render(gameHandler, core::Application::getInstance().getAssetManager()); @@ -337,7 +341,9 @@ void GameScreen::processTargetInput(game::GameHandler& gameHandler) { } if (input.isKeyJustPressed(SDL_SCANCODE_ESCAPE)) { - if (gameHandler.isCasting()) { + if (showTeleporter) { + showTeleporter = false; + } else if (gameHandler.isCasting()) { gameHandler.cancelCast(); } else if (gameHandler.isLootWindowOpen()) { gameHandler.closeLoot(); @@ -348,15 +354,6 @@ void GameScreen::processTargetInput(game::GameHandler& gameHandler) { } } - // Auto-attack (T key) - if (input.isKeyJustPressed(SDL_SCANCODE_T)) { - if (gameHandler.hasTarget() && !gameHandler.isAutoAttacking()) { - gameHandler.startAutoAttack(gameHandler.getTargetGuid()); - } else if (gameHandler.isAutoAttacking()) { - gameHandler.stopAutoAttack(); - } - } - // Action bar keys (1-9, 0, -, =) static const SDL_Scancode actionBarKeys[] = { SDL_SCANCODE_1, SDL_SCANCODE_2, SDL_SCANCODE_3, SDL_SCANCODE_4, @@ -1511,4 +1508,54 @@ void GameScreen::renderVendorWindow(game::GameHandler& gameHandler) { } } +// ============================================================ +// Teleporter Panel +// ============================================================ + +void GameScreen::renderTeleporterPanel() { + if (!showTeleporter) return; + + auto* window = core::Application::getInstance().getWindow(); + float screenW = window ? static_cast(window->getWidth()) : 1280.0f; + float screenH = window ? static_cast(window->getHeight()) : 720.0f; + + float panelW = 280.0f; + float panelH = 0.0f; // Auto-size height + ImGui::SetNextWindowPos(ImVec2((screenW - panelW) / 2.0f, screenH * 0.25f), ImGuiCond_Always); + ImGui::SetNextWindowSize(ImVec2(panelW, panelH), ImGuiCond_Always); + + ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 6.0f); + ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.08f, 0.08f, 0.15f, 0.92f)); + + ImGuiWindowFlags flags = ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | + ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_AlwaysAutoResize; + + if (ImGui::Begin("Teleporter", &showTeleporter, flags)) { + ImGui::Spacing(); + + for (int i = 0; i < core::SPAWN_PRESET_COUNT; i++) { + const auto& preset = core::SPAWN_PRESETS[i]; + char label[128]; + snprintf(label, sizeof(label), "%s\n(%.0f, %.0f, %.0f)", + preset.label, + preset.spawnCanonical.x, preset.spawnCanonical.y, preset.spawnCanonical.z); + + if (ImGui::Button(label, ImVec2(-1, 50))) { + core::Application::getInstance().teleportTo(i); + showTeleporter = false; + } + + if (i < core::SPAWN_PRESET_COUNT - 1) { + ImGui::Spacing(); + } + } + + ImGui::Spacing(); + } + ImGui::End(); + + ImGui::PopStyleColor(); + ImGui::PopStyleVar(); +} + }} // namespace wowee::ui