mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-03-22 23:30:14 +00:00
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:
parent
6690910712
commit
d8e2becbaa
8 changed files with 258 additions and 43 deletions
|
|
@ -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_; }
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
27
include/core/spawn_presets.hpp
Normal file
27
include/core/spawn_presets.hpp
Normal 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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue