2026-03-20 11:12:07 -07:00
|
|
|
#include "addons/addon_manager.hpp"
|
|
|
|
|
#include "core/logger.hpp"
|
|
|
|
|
#include <algorithm>
|
|
|
|
|
#include <filesystem>
|
|
|
|
|
|
|
|
|
|
namespace fs = std::filesystem;
|
|
|
|
|
|
|
|
|
|
namespace wowee::addons {
|
|
|
|
|
|
|
|
|
|
AddonManager::AddonManager() = default;
|
|
|
|
|
AddonManager::~AddonManager() { shutdown(); }
|
|
|
|
|
|
|
|
|
|
bool AddonManager::initialize(game::GameHandler* gameHandler) {
|
2026-03-20 16:17:04 -07:00
|
|
|
gameHandler_ = gameHandler;
|
2026-03-20 11:12:07 -07:00
|
|
|
if (!luaEngine_.initialize()) return false;
|
|
|
|
|
luaEngine_.setGameHandler(gameHandler);
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void AddonManager::scanAddons(const std::string& addonsPath) {
|
2026-03-20 16:17:04 -07:00
|
|
|
addonsPath_ = addonsPath;
|
2026-03-20 11:12:07 -07:00
|
|
|
addons_.clear();
|
|
|
|
|
|
|
|
|
|
std::error_code ec;
|
|
|
|
|
if (!fs::is_directory(addonsPath, ec)) {
|
|
|
|
|
LOG_INFO("AddonManager: no AddOns directory at ", addonsPath);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::vector<fs::path> dirs;
|
|
|
|
|
for (const auto& entry : fs::directory_iterator(addonsPath, ec)) {
|
|
|
|
|
if (entry.is_directory()) dirs.push_back(entry.path());
|
|
|
|
|
}
|
|
|
|
|
// Sort alphabetically for deterministic load order
|
|
|
|
|
std::sort(dirs.begin(), dirs.end());
|
|
|
|
|
|
|
|
|
|
for (const auto& dir : dirs) {
|
|
|
|
|
std::string dirName = dir.filename().string();
|
|
|
|
|
std::string tocPath = (dir / (dirName + ".toc")).string();
|
|
|
|
|
auto toc = parseTocFile(tocPath);
|
|
|
|
|
if (!toc) continue;
|
|
|
|
|
|
|
|
|
|
if (toc->isLoadOnDemand()) {
|
|
|
|
|
LOG_DEBUG("AddonManager: skipping LoadOnDemand addon: ", dirName);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
LOG_INFO("AddonManager: registered addon '", toc->getTitle(),
|
|
|
|
|
"' (", toc->files.size(), " files)");
|
|
|
|
|
addons_.push_back(std::move(*toc));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
LOG_INFO("AddonManager: scanned ", addons_.size(), " addons");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void AddonManager::loadAllAddons() {
|
2026-03-20 13:07:45 -07:00
|
|
|
luaEngine_.setAddonList(addons_);
|
2026-03-20 11:12:07 -07:00
|
|
|
int loaded = 0, failed = 0;
|
|
|
|
|
for (const auto& addon : addons_) {
|
|
|
|
|
if (loadAddon(addon)) loaded++;
|
|
|
|
|
else failed++;
|
|
|
|
|
}
|
|
|
|
|
LOG_INFO("AddonManager: loaded ", loaded, " addons",
|
|
|
|
|
(failed > 0 ? (", " + std::to_string(failed) + " failed") : ""));
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-20 12:22:50 -07:00
|
|
|
std::string AddonManager::getSavedVariablesPath(const TocFile& addon) const {
|
|
|
|
|
return addon.basePath + "/" + addon.addonName + ".lua.saved";
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-21 02:46:21 -07:00
|
|
|
std::string AddonManager::getSavedVariablesPerCharacterPath(const TocFile& addon) const {
|
|
|
|
|
if (characterName_.empty()) return "";
|
|
|
|
|
return addon.basePath + "/" + addon.addonName + "." + characterName_ + ".lua.saved";
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-20 11:12:07 -07:00
|
|
|
bool AddonManager::loadAddon(const TocFile& addon) {
|
2026-03-20 12:22:50 -07:00
|
|
|
// Load SavedVariables before addon code (so globals are available at load time)
|
|
|
|
|
auto savedVars = addon.getSavedVariables();
|
|
|
|
|
if (!savedVars.empty()) {
|
|
|
|
|
std::string svPath = getSavedVariablesPath(addon);
|
|
|
|
|
luaEngine_.loadSavedVariables(svPath);
|
|
|
|
|
LOG_DEBUG("AddonManager: loaded saved variables for '", addon.addonName, "'");
|
|
|
|
|
}
|
2026-03-21 02:46:21 -07:00
|
|
|
// Load per-character SavedVariables
|
|
|
|
|
auto savedVarsPC = addon.getSavedVariablesPerCharacter();
|
|
|
|
|
if (!savedVarsPC.empty()) {
|
|
|
|
|
std::string svpcPath = getSavedVariablesPerCharacterPath(addon);
|
|
|
|
|
if (!svpcPath.empty()) {
|
|
|
|
|
luaEngine_.loadSavedVariables(svpcPath);
|
|
|
|
|
LOG_DEBUG("AddonManager: loaded per-character saved variables for '", addon.addonName, "'");
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-03-20 12:22:50 -07:00
|
|
|
|
2026-03-20 11:12:07 -07:00
|
|
|
bool success = true;
|
|
|
|
|
for (const auto& filename : addon.files) {
|
|
|
|
|
std::string lower = filename;
|
|
|
|
|
for (char& c : lower) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
|
|
|
|
|
|
|
|
|
|
if (lower.size() >= 4 && lower.substr(lower.size() - 4) == ".lua") {
|
|
|
|
|
std::string fullPath = addon.basePath + "/" + filename;
|
|
|
|
|
if (!luaEngine_.executeFile(fullPath)) {
|
|
|
|
|
success = false;
|
|
|
|
|
}
|
|
|
|
|
} else if (lower.size() >= 4 && lower.substr(lower.size() - 4) == ".xml") {
|
|
|
|
|
LOG_DEBUG("AddonManager: skipping XML file '", filename,
|
|
|
|
|
"' in addon '", addon.addonName, "' (XML frames not yet implemented)");
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-03-20 12:52:25 -07:00
|
|
|
|
|
|
|
|
// Fire ADDON_LOADED event after all addon files are executed
|
|
|
|
|
// This is the standard WoW pattern for addon initialization
|
|
|
|
|
if (success) {
|
|
|
|
|
luaEngine_.fireEvent("ADDON_LOADED", {addon.addonName});
|
|
|
|
|
}
|
2026-03-20 11:12:07 -07:00
|
|
|
return success;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool AddonManager::runScript(const std::string& code) {
|
|
|
|
|
return luaEngine_.executeString(code);
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-20 11:23:38 -07:00
|
|
|
void AddonManager::fireEvent(const std::string& event, const std::vector<std::string>& args) {
|
|
|
|
|
luaEngine_.fireEvent(event, args);
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-20 12:07:22 -07:00
|
|
|
void AddonManager::update(float deltaTime) {
|
|
|
|
|
luaEngine_.dispatchOnUpdate(deltaTime);
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-20 12:22:50 -07:00
|
|
|
void AddonManager::saveAllSavedVariables() {
|
|
|
|
|
for (const auto& addon : addons_) {
|
|
|
|
|
auto savedVars = addon.getSavedVariables();
|
|
|
|
|
if (!savedVars.empty()) {
|
|
|
|
|
std::string svPath = getSavedVariablesPath(addon);
|
|
|
|
|
luaEngine_.saveSavedVariables(svPath, savedVars);
|
|
|
|
|
}
|
2026-03-21 02:46:21 -07:00
|
|
|
auto savedVarsPC = addon.getSavedVariablesPerCharacter();
|
|
|
|
|
if (!savedVarsPC.empty()) {
|
|
|
|
|
std::string svpcPath = getSavedVariablesPerCharacterPath(addon);
|
|
|
|
|
if (!svpcPath.empty()) {
|
|
|
|
|
luaEngine_.saveSavedVariables(svpcPath, savedVarsPC);
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-03-20 12:22:50 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-20 16:17:04 -07:00
|
|
|
bool AddonManager::reload() {
|
|
|
|
|
LOG_INFO("AddonManager: reloading all addons...");
|
|
|
|
|
saveAllSavedVariables();
|
|
|
|
|
addons_.clear();
|
|
|
|
|
luaEngine_.shutdown();
|
|
|
|
|
|
|
|
|
|
if (!luaEngine_.initialize()) {
|
|
|
|
|
LOG_ERROR("AddonManager: failed to reinitialize Lua VM during reload");
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
luaEngine_.setGameHandler(gameHandler_);
|
|
|
|
|
|
|
|
|
|
if (!addonsPath_.empty()) {
|
|
|
|
|
scanAddons(addonsPath_);
|
|
|
|
|
loadAllAddons();
|
|
|
|
|
}
|
|
|
|
|
LOG_INFO("AddonManager: reload complete");
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-20 11:12:07 -07:00
|
|
|
void AddonManager::shutdown() {
|
2026-03-20 12:22:50 -07:00
|
|
|
saveAllSavedVariables();
|
2026-03-20 11:12:07 -07:00
|
|
|
addons_.clear();
|
|
|
|
|
luaEngine_.shutdown();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
} // namespace wowee::addons
|