mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-03-22 15:20:15 +00:00
feat: add SavedVariables persistence for Lua addons
Addons can now persist data across sessions using the standard WoW
SavedVariables pattern:
1. Declare in .toc: ## SavedVariables: MyAddonDB
2. Use the global in Lua: MyAddonDB = MyAddonDB or {default = true}
3. Data is automatically saved on logout and restored on next login
Implementation:
- TocFile::getSavedVariables() parses comma-separated variable names
- LuaEngine::loadSavedVariables() executes saved .lua file to restore globals
- LuaEngine::saveSavedVariables() serializes Lua tables/values to valid Lua
- Serializer handles tables (nested), strings, numbers, booleans, nil
- Save triggered on PLAYER_LEAVING_WORLD and AddonManager::shutdown()
- Files stored as <AddonDir>/<AddonName>.lua.saved
Updated HelloWorld addon to track login count across sessions.
This commit is contained in:
parent
5ea5588c14
commit
062cfd1e4a
9 changed files with 177 additions and 2 deletions
|
|
@ -1,5 +1,11 @@
|
|||
-- HelloWorld addon — demonstrates the WoWee addon system
|
||||
|
||||
-- Initialize saved variables (persisted across sessions)
|
||||
if not HelloWorldDB then
|
||||
HelloWorldDB = { loginCount = 0 }
|
||||
end
|
||||
HelloWorldDB.loginCount = (HelloWorldDB.loginCount or 0) + 1
|
||||
|
||||
-- Create a frame and register for events (standard WoW addon pattern)
|
||||
local f = CreateFrame("Frame", "HelloWorldFrame")
|
||||
f:RegisterEvent("PLAYER_ENTERING_WORLD")
|
||||
|
|
@ -10,6 +16,7 @@ f:SetScript("OnEvent", function(self, event, ...)
|
|||
local name = UnitName("player")
|
||||
local level = UnitLevel("player")
|
||||
print("|cff00ff00[HelloWorld]|r Welcome, " .. name .. "! (Level " .. level .. ")")
|
||||
print("|cff00ff00[HelloWorld]|r Login count: " .. HelloWorldDB.loginCount)
|
||||
elseif event == "CHAT_MSG_SAY" then
|
||||
local msg, sender = ...
|
||||
if msg and sender then
|
||||
|
|
@ -23,6 +30,7 @@ SLASH_HELLOWORLD1 = "/hello"
|
|||
SLASH_HELLOWORLD2 = "/hw"
|
||||
SlashCmdList["HELLOWORLD"] = function(args)
|
||||
print("|cff00ff00[HelloWorld]|r Hello! " .. (args ~= "" and args or "Type /hello <message>"))
|
||||
print("|cff00ff00[HelloWorld]|r Sessions: " .. HelloWorldDB.loginCount)
|
||||
end
|
||||
|
||||
print("|cff00ff00[HelloWorld]|r Addon loaded. Type /hello to test slash commands.")
|
||||
print("|cff00ff00[HelloWorld]|r Addon loaded. Type /hello to test.")
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
## Interface: 30300
|
||||
## Title: Hello World
|
||||
## Notes: Test addon for the WoWee addon system
|
||||
## SavedVariables: HelloWorldDB
|
||||
HelloWorld.lua
|
||||
|
|
|
|||
|
|
@ -25,11 +25,14 @@ public:
|
|||
LuaEngine* getLuaEngine() { return &luaEngine_; }
|
||||
bool isInitialized() const { return luaEngine_.isInitialized(); }
|
||||
|
||||
void saveAllSavedVariables();
|
||||
|
||||
private:
|
||||
LuaEngine luaEngine_;
|
||||
std::vector<TocFile> addons_;
|
||||
|
||||
bool loadAddon(const TocFile& addon);
|
||||
std::string getSavedVariablesPath(const TocFile& addon) const;
|
||||
};
|
||||
|
||||
} // namespace wowee::addons
|
||||
|
|
|
|||
|
|
@ -35,6 +35,10 @@ public:
|
|||
// Call OnUpdate scripts on all frames that have one.
|
||||
void dispatchOnUpdate(float elapsed);
|
||||
|
||||
// SavedVariables: load globals from file, save globals to file
|
||||
bool loadSavedVariables(const std::string& path);
|
||||
bool saveSavedVariables(const std::string& path, const std::vector<std::string>& varNames);
|
||||
|
||||
lua_State* getState() { return L_; }
|
||||
bool isInitialized() const { return L_ != nullptr; }
|
||||
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ struct TocFile {
|
|||
std::string getTitle() const;
|
||||
std::string getInterface() const;
|
||||
bool isLoadOnDemand() const;
|
||||
std::vector<std::string> getSavedVariables() const;
|
||||
};
|
||||
|
||||
std::optional<TocFile> parseTocFile(const std::string& tocPath);
|
||||
|
|
|
|||
|
|
@ -61,10 +61,21 @@ void AddonManager::loadAllAddons() {
|
|||
(failed > 0 ? (", " + std::to_string(failed) + " failed") : ""));
|
||||
}
|
||||
|
||||
std::string AddonManager::getSavedVariablesPath(const TocFile& addon) const {
|
||||
return addon.basePath + "/" + addon.addonName + ".lua.saved";
|
||||
}
|
||||
|
||||
bool AddonManager::loadAddon(const TocFile& addon) {
|
||||
// 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, "'");
|
||||
}
|
||||
|
||||
bool success = true;
|
||||
for (const auto& filename : addon.files) {
|
||||
// For Step 1: only load .lua files, skip .xml (frame system not yet implemented)
|
||||
std::string lower = filename;
|
||||
for (char& c : lower) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
|
||||
|
||||
|
|
@ -93,7 +104,18 @@ void AddonManager::update(float deltaTime) {
|
|||
luaEngine_.dispatchOnUpdate(deltaTime);
|
||||
}
|
||||
|
||||
void AddonManager::saveAllSavedVariables() {
|
||||
for (const auto& addon : addons_) {
|
||||
auto savedVars = addon.getSavedVariables();
|
||||
if (!savedVars.empty()) {
|
||||
std::string svPath = getSavedVariablesPath(addon);
|
||||
luaEngine_.saveSavedVariables(svPath, savedVars);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void AddonManager::shutdown() {
|
||||
saveAllSavedVariables();
|
||||
addons_.clear();
|
||||
luaEngine_.shutdown();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@
|
|||
#include "game/entity.hpp"
|
||||
#include "core/logger.hpp"
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
#include <filesystem>
|
||||
|
||||
extern "C" {
|
||||
#include <lua.h>
|
||||
|
|
@ -1046,6 +1048,118 @@ bool LuaEngine::dispatchSlashCommand(const std::string& command, const std::stri
|
|||
return false;
|
||||
}
|
||||
|
||||
// ---- SavedVariables serialization ----
|
||||
|
||||
static void serializeLuaValue(lua_State* L, int idx, std::string& out, int indent);
|
||||
|
||||
static void serializeLuaTable(lua_State* L, int idx, std::string& out, int indent) {
|
||||
out += "{\n";
|
||||
std::string pad(indent + 2, ' ');
|
||||
lua_pushnil(L);
|
||||
while (lua_next(L, idx) != 0) {
|
||||
out += pad;
|
||||
// Key
|
||||
if (lua_type(L, -2) == LUA_TSTRING) {
|
||||
const char* k = lua_tostring(L, -2);
|
||||
out += "[\"";
|
||||
for (const char* p = k; *p; ++p) {
|
||||
if (*p == '"' || *p == '\\') out += '\\';
|
||||
out += *p;
|
||||
}
|
||||
out += "\"] = ";
|
||||
} else if (lua_type(L, -2) == LUA_TNUMBER) {
|
||||
out += "[" + std::to_string(static_cast<long long>(lua_tonumber(L, -2))) + "] = ";
|
||||
} else {
|
||||
lua_pop(L, 1);
|
||||
continue;
|
||||
}
|
||||
// Value
|
||||
serializeLuaValue(L, lua_gettop(L), out, indent + 2);
|
||||
out += ",\n";
|
||||
lua_pop(L, 1);
|
||||
}
|
||||
out += std::string(indent, ' ') + "}";
|
||||
}
|
||||
|
||||
static void serializeLuaValue(lua_State* L, int idx, std::string& out, int indent) {
|
||||
switch (lua_type(L, idx)) {
|
||||
case LUA_TNIL: out += "nil"; break;
|
||||
case LUA_TBOOLEAN: out += lua_toboolean(L, idx) ? "true" : "false"; break;
|
||||
case LUA_TNUMBER: {
|
||||
double v = lua_tonumber(L, idx);
|
||||
char buf[64];
|
||||
snprintf(buf, sizeof(buf), "%.17g", v);
|
||||
out += buf;
|
||||
break;
|
||||
}
|
||||
case LUA_TSTRING: {
|
||||
const char* s = lua_tostring(L, idx);
|
||||
out += "\"";
|
||||
for (const char* p = s; *p; ++p) {
|
||||
if (*p == '"' || *p == '\\') out += '\\';
|
||||
else if (*p == '\n') { out += "\\n"; continue; }
|
||||
else if (*p == '\r') continue;
|
||||
out += *p;
|
||||
}
|
||||
out += "\"";
|
||||
break;
|
||||
}
|
||||
case LUA_TTABLE:
|
||||
serializeLuaTable(L, idx, out, indent);
|
||||
break;
|
||||
default:
|
||||
out += "nil"; // Functions, userdata, etc. can't be serialized
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
bool LuaEngine::loadSavedVariables(const std::string& path) {
|
||||
if (!L_) return false;
|
||||
std::ifstream f(path);
|
||||
if (!f.is_open()) return false; // No saved data yet — not an error
|
||||
std::string content((std::istreambuf_iterator<char>(f)), std::istreambuf_iterator<char>());
|
||||
if (content.empty()) return true;
|
||||
int err = luaL_dostring(L_, content.c_str());
|
||||
if (err != 0) {
|
||||
LOG_WARNING("LuaEngine: error loading saved variables from '", path, "': ",
|
||||
lua_tostring(L_, -1));
|
||||
lua_pop(L_, 1);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool LuaEngine::saveSavedVariables(const std::string& path, const std::vector<std::string>& varNames) {
|
||||
if (!L_ || varNames.empty()) return false;
|
||||
std::string output;
|
||||
for (const auto& name : varNames) {
|
||||
lua_getglobal(L_, name.c_str());
|
||||
if (!lua_isnil(L_, -1)) {
|
||||
output += name + " = ";
|
||||
serializeLuaValue(L_, lua_gettop(L_), output, 0);
|
||||
output += "\n";
|
||||
}
|
||||
lua_pop(L_, 1);
|
||||
}
|
||||
if (output.empty()) return true;
|
||||
|
||||
// Ensure directory exists
|
||||
size_t lastSlash = path.find_last_of("/\\");
|
||||
if (lastSlash != std::string::npos) {
|
||||
std::error_code ec;
|
||||
std::filesystem::create_directories(path.substr(0, lastSlash), ec);
|
||||
}
|
||||
|
||||
std::ofstream f(path);
|
||||
if (!f.is_open()) {
|
||||
LOG_WARNING("LuaEngine: cannot write saved variables to '", path, "'");
|
||||
return false;
|
||||
}
|
||||
f << output;
|
||||
LOG_INFO("LuaEngine: saved variables to '", path, "' (", output.size(), " bytes)");
|
||||
return true;
|
||||
}
|
||||
|
||||
bool LuaEngine::executeFile(const std::string& path) {
|
||||
if (!L_) return false;
|
||||
|
||||
|
|
|
|||
|
|
@ -19,6 +19,27 @@ bool TocFile::isLoadOnDemand() const {
|
|||
return (it != directives.end()) && it->second == "1";
|
||||
}
|
||||
|
||||
std::vector<std::string> TocFile::getSavedVariables() const {
|
||||
std::vector<std::string> result;
|
||||
auto it = directives.find("SavedVariables");
|
||||
if (it == directives.end()) return result;
|
||||
// Parse comma-separated variable names
|
||||
std::string val = it->second;
|
||||
size_t pos = 0;
|
||||
while (pos <= val.size()) {
|
||||
size_t comma = val.find(',', pos);
|
||||
std::string name = (comma != std::string::npos) ? val.substr(pos, comma - pos) : val.substr(pos);
|
||||
// Trim whitespace
|
||||
size_t start = name.find_first_not_of(" \t");
|
||||
size_t end = name.find_last_not_of(" \t");
|
||||
if (start != std::string::npos)
|
||||
result.push_back(name.substr(start, end - start + 1));
|
||||
if (comma == std::string::npos) break;
|
||||
pos = comma + 1;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
std::optional<TocFile> parseTocFile(const std::string& tocPath) {
|
||||
std::ifstream f(tocPath);
|
||||
if (!f.is_open()) return std::nullopt;
|
||||
|
|
|
|||
|
|
@ -693,6 +693,7 @@ void Application::setState(AppState newState) {
|
|||
// If we reuse a previously spawned instance without forcing a respawn, appearance (notably hair) can desync.
|
||||
if (addonManager_ && addonsLoaded_) {
|
||||
addonManager_->fireEvent("PLAYER_LEAVING_WORLD");
|
||||
addonManager_->saveAllSavedVariables();
|
||||
}
|
||||
npcsSpawned = false;
|
||||
playerCharacterSpawned = false;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue