Files
WoWee/include/core/application.hpp
Kelsi 639ea1cc22 feat(ui): give the addon API a widget system that actually draws
CreateFrame answered and events dispatched, but CreateTexture handed back a
table whose every method was a no-op — SetTexture, SetPoint, SetVertexColor, all
of them, with a metatable turning any remaining capitalised name into another
no-op. An addon could be written, loaded and run without ever putting a pixel on
the screen. The API looked supported and nothing happened.

There is now a real retained tree behind it. Frames and regions are still Lua
tables, so existing addon code is untouched, but each carries a handle into C++
state that holds its geometry and its art, and a renderer walks it.

The layout is WoW's: anchors as constraints rather than positions. An anchor
says "this fraction of my rect sits at that point", so one anchor plus a size
places a frame and two opposing anchors give the size as well — which is how
SetAllPoints works, and how most of FrameXML sizes its backgrounds without ever
stating a size. Draw order is strata, then frame level, then layer, then
sublevel, then creation order, so a child draws over its parent and an OVERLAY
in a low stratum still falls behind a BACKGROUND in a high one.

Coordinates are kept in WoW's convention throughout — origin bottom-left, y
upward — and flipped once at the point of drawing, so every anchor rule reads
the way Blizzard documents it rather than mirrored.

The tree is deliberately free of Vulkan and ImGui so the layout rules are
testable without a device; thirteen cases cover the anchor solver, inheritance
of visibility and strata, and the draw ordering. Rendering is separate and
loads Interface art through the existing asset path, the same route the action
bar already takes for its backpack button — the player's own install, nothing
new shipped.

addons/WoweeWidgetDemo is a hand-written addon using only the ordinary API:
anchored art from Interface\Buttons, layer ordering, a sibling-relative anchor
and a slash command. Copy it into the AddOns directory to see it.

This is the piece both goals needed. Addons can draw now, and the same tree is
what FrameXML targets, which is what makes running the original interface
possible rather than imitating it.
2026-08-01 10:50:20 -07:00

231 lines
9.4 KiB
C++

#pragma once
#include "core/window.hpp"
#include "ui/widget_renderer.hpp"
#include "core/input.hpp"
#include "core/entity_spawner.hpp"
#include "core/appearance_composer.hpp"
#include "core/world_loader.hpp"
#include "game/character.hpp"
#include "game/game_services.hpp"
#include "pipeline/blp_loader.hpp"
#include <memory>
#include <string>
#include <vector>
#include <deque>
#include <unordered_map>
#include <unordered_set>
#include <array>
#include <optional>
#include <future>
#include <mutex>
#include <thread>
#include <atomic>
namespace wowee {
// Forward declarations
namespace rendering { class Renderer; }
namespace ui { class UIManager; }
namespace auth { class AuthHandler; }
namespace game { class GameHandler; class World; class ExpansionRegistry; }
namespace pipeline { class AssetManager; class DBCLayout; struct M2Model; struct WMOModel; }
namespace audio { enum class VoiceType; class AudioCoordinator; }
namespace addons { class AddonManager; }
namespace core {
// Handler forward declarations
class NPCInteractionCallbackHandler;
class AudioCallbackHandler;
class EntitySpawnCallbackHandler;
class AnimationCallbackHandler;
class TransportCallbackHandler;
class WorldEntryCallbackHandler;
class UIScreenCallbackHandler;
enum class AppState {
AUTHENTICATION,
REALM_SELECTION,
CHARACTER_CREATION,
CHARACTER_SELECTION,
IN_GAME,
DISCONNECTED
};
class Application {
friend class WorldLoader;
public:
Application();
~Application();
Application(const Application&) = delete;
Application& operator=(const Application&) = delete;
bool initialize();
void run();
void shutdown();
/// Tell the stall watchdog we are alive. Call from long synchronous work that
/// keeps presenting frames itself (e.g. the world load) so it is not mistaken
/// for a hung main loop.
void beatWatchdog();
// State management
AppState getState() const { return state; }
void setState(AppState newState);
// Accessors
Window* getWindow() { return window.get(); }
rendering::Renderer* getRenderer() { return renderer.get(); }
ui::UIManager* getUIManager() { return uiManager.get(); }
auth::AuthHandler* getAuthHandler() { return authHandler.get(); }
game::GameHandler* getGameHandler() { return gameHandler.get(); }
game::World* getWorld() { return world.get(); }
pipeline::AssetManager* getAssetManager() { return assetManager.get(); }
addons::AddonManager* getAddonManager() { return addonManager_.get(); }
game::ExpansionRegistry* getExpansionRegistry() { return expansionRegistry_.get(); }
pipeline::DBCLayout* getDBCLayout() { return dbcLayout_.get(); }
bool setAssetExpansionOverride(const std::string& id);
const std::string& getAssetExpansionOverride() const { return assetExpansionOverrideId_; }
void reloadExpansionData(); // Reload DBC layouts, opcodes, etc. after expansion change
// Singleton access
static Application& getInstance() { return *instance; }
// Logout to login screen
void logoutToLogin();
// Render bounds lookup (for click targeting / selection) — delegates to EntitySpawner
bool getRenderBoundsForGuid(uint64_t guid, glm::vec3& outCenter, float& outRadius) const;
bool getRenderFootZForGuid(uint64_t guid, float& outFootZ) const;
bool getRenderPositionForGuid(uint64_t guid, glm::vec3& outPos) const;
// Character skin composite state — delegated to AppearanceComposer
const std::string& getBodySkinPath() const { return appearanceComposer_ ? appearanceComposer_->getBodySkinPath() : emptyString_; }
const std::vector<std::string>& getUnderwearPaths() const { return appearanceComposer_ ? appearanceComposer_->getUnderwearPaths() : emptyStringVec_; }
uint32_t getSkinTextureSlotIndex() const { return appearanceComposer_ ? appearanceComposer_->getSkinTextureSlotIndex() : 0; }
uint32_t getCloakTextureSlotIndex() const { return appearanceComposer_ ? appearanceComposer_->getCloakTextureSlotIndex() : 0; }
uint32_t getGryphonDisplayId() const { return entitySpawner_ ? entitySpawner_->getGryphonDisplayId() : 0; }
uint32_t getWyvernDisplayId() const { return entitySpawner_ ? entitySpawner_->getWyvernDisplayId() : 0; }
// Entity spawner access
EntitySpawner* getEntitySpawner() { return entitySpawner_.get(); }
// Appearance composer access
AppearanceComposer* getAppearanceComposer() { return appearanceComposer_.get(); }
// World loader access
WorldLoader* getWorldLoader() { return worldLoader_.get(); }
// Audio coordinator access
audio::AudioCoordinator* getAudioCoordinator() { return audioCoordinator_.get(); }
private:
void update(float deltaTime);
void render();
void performLogoutToLogin();
void processDeferredLogoutToLogin();
void setupUICallbacks();
void spawnPlayerCharacter();
// Re-spawn the in-world player model in place after a live appearance change
// (barber shop), so the new hair/facial hair shows without a restart.
void refreshPlayerCharacterModel();
void buildFactionHostilityMap(uint8_t playerRace);
void setupTestTransport(); // Test transport boat for development
static Application* instance;
game::GameServices gameServices_;
std::unique_ptr<Window> window;
std::unique_ptr<rendering::Renderer> renderer;
std::unique_ptr<ui::UIManager> uiManager;
std::unique_ptr<auth::AuthHandler> authHandler;
std::unique_ptr<game::GameHandler> gameHandler;
std::unique_ptr<game::World> world;
std::unique_ptr<pipeline::AssetManager> assetManager;
std::unique_ptr<addons::AddonManager> addonManager_;
/// Draws the widget tree addons build through CreateFrame/CreateTexture.
/// Holds the texture cache for Interface\ art, so it lives as long as the app.
ui::WidgetRenderer widgetRenderer_;
bool addonsLoaded_ = false;
std::unique_ptr<game::ExpansionRegistry> expansionRegistry_;
// Empty means assets follow the active protocol profile. "legacy" selects
// the root WOW_DATA_PATH manifest; otherwise this is an expansion id.
std::string assetExpansionOverrideId_;
std::unique_ptr<pipeline::DBCLayout> dbcLayout_;
std::unique_ptr<EntitySpawner> entitySpawner_;
std::unique_ptr<AppearanceComposer> appearanceComposer_;
std::unique_ptr<WorldLoader> worldLoader_;
std::unique_ptr<audio::AudioCoordinator> audioCoordinator_;
// Callback handlers (extracted from setupUICallbacks)
std::unique_ptr<NPCInteractionCallbackHandler> npcInteractionCallbacks_;
std::unique_ptr<AudioCallbackHandler> audioCallbacks_;
std::unique_ptr<EntitySpawnCallbackHandler> entitySpawnCallbacks_;
std::unique_ptr<AnimationCallbackHandler> animationCallbacks_;
std::unique_ptr<TransportCallbackHandler> transportCallbacks_;
std::unique_ptr<WorldEntryCallbackHandler> worldEntryCallbacks_;
std::unique_ptr<UIScreenCallbackHandler> uiScreenCallbacks_;
// Beat by the main loop each iteration; the watchdog treats a long silence as a
// hang. Long but healthy work that renders its own frames (world load) must beat
// it too, or the watchdog mistakes the load for a hang.
std::atomic<int64_t> watchdogHeartbeatMs_{0};
AppState state = AppState::AUTHENTICATION;
bool running = false;
bool renderingFrame_ = false;
bool logoutToLoginPending_ = false;
bool playerCharacterSpawned = false;
bool npcsSpawned = false;
bool spawnSnapToGround = true;
float lastFrameTime = 0.0f;
// Player character info (for model spawning)
game::Race playerRace_ = game::Race::HUMAN;
game::Gender playerGender_ = game::Gender::MALE;
game::Class playerClass_ = game::Class::WARRIOR;
uint64_t spawnedPlayerGuid_ = 0;
uint32_t spawnedAppearanceBytes_ = 0;
uint8_t spawnedFacialFeatures_ = 0;
// Static empty values for null-safe delegation
static inline const std::string emptyString_;
static inline const std::vector<std::string> emptyStringVec_;
float facingSendCooldown_ = 0.0f; // Rate-limits MSG_MOVE_SET_FACING
float lastSentCanonicalYaw_ = 1000.0f; // Sentinel — triggers first send
float taxiStreamCooldown_ = 0.0f;
bool idleYawned_ = false;
// M2 transport riding: last frame's locked (canonical) render position, used to
// detect how far the player tried to walk this frame so that delta can be applied
// on top of the fixed ride offset instead of either fully locking movement or
// recomputing the offset from an absolute position (see application.cpp's "M2
// transport riding" block for why the latter is a no-op identity).
glm::vec3 lastM2RideLockedCanonical_ = glm::vec3(0.0f);
bool hasM2RideLock_ = false;
glm::vec3 lastWMORideLockedRender_ = glm::vec3(0.0f);
bool hasWMORideLock_ = false;
uint64_t lastWMORideTransportGuid_ = 0;
uint32_t lastWMORideMapId_ = 0xFFFFFFFFu;
// Set when a rider boards or transfers onto a WMO ship whose deck collision hasn't
// finished loading yet — holds the boarding-time offset (and freezes camera follow)
// until this exact transport instance's deck floor exists, instead of letting gravity
// fold into the attachment and drop the rider through the hull.
bool deckFloorPending_ = false;
bool wasAutoAttacking_ = false;
// Quest marker billboard sprites (above NPCs)
void loadQuestMarkerModels(); // Now loads BLP textures
void updateQuestMarkers(); // Updates billboard positions
};
} // namespace core
} // namespace wowee