Add teleporter panel and server-compatible coordinate conversions

Teleporter panel (T key) lets the player teleport between Goldshire,
Stormwind Gate, Ironforge, and Westfall in single-player mode. Adds
serverToCanonical/canonicalToServer conversion at the network packet
boundary so positions are compatible with TrinityCore/MaNGOS/AzerothCore
emulator servers.
This commit is contained in:
Kelsi 2026-02-04 18:27:52 -08:00
parent 6690910712
commit d8e2becbaa
8 changed files with 258 additions and 43 deletions

View file

@ -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<std::string>& getUnderwearPaths() const { return underwearPaths_; }

View file

@ -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);

View file

@ -0,0 +1,27 @@
#pragma once
#include <glm/glm.hpp>
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

View file

@ -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

View file

@ -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<char>(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<glm::vec3> 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<float>(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

View file

@ -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 <algorithm>
#include <cctype>
@ -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<uint16_t>(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);

View file

@ -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<int>(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 {

View file

@ -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<float>(window->getWidth()) : 1280.0f;
float screenH = window ? static_cast<float>(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