Files
wx_wherigo/main/src/lua/persistence.cpp
Peter Siegmund fe79f46d44 latest code update
- app icon
- starting with map view
- code cleanup

Signed-off-by: Peter Siegmund <mars3142@noreply.mars3142.dev>
2026-02-14 09:43:19 +01:00

359 lines
9.5 KiB
C++

#include "lua/persistence.h"
extern "C" {
#include <lua.h>
#include <lauxlib.h>
}
#include <wx/log.h>
#include <fstream>
#include <sstream>
#include <set>
#include <cstdio>
namespace wherigo {
// List of Wherigo game object classes to save
static const std::set<std::string> WHERIGO_CLASSES = {
"ZCartridge", "Zone", "ZItem", "ZCharacter",
"ZTask", "ZTimer", "ZInput", "ZMedia"
};
std::vector<std::string> LuaPersistence::getGameGlobals(lua_State* L) {
std::vector<std::string> globals;
lua_getglobal(L, "_G");
lua_pushnil(L);
while (lua_next(L, -2) != 0) {
if (lua_isstring(L, -2)) {
const char* key = lua_tostring(L, -2);
// Check if it's a Wherigo game object
if (lua_istable(L, -1)) {
lua_getfield(L, -1, "ClassName");
if (lua_isstring(L, -1)) {
std::string className = lua_tostring(L, -1);
if (WHERIGO_CLASSES.count(className) > 0) {
globals.push_back(key);
}
}
lua_pop(L, 1); // pop ClassName
}
// Also save simple global variables (strings, numbers, booleans)
else if (lua_isnumber(L, -1) || lua_isstring(L, -1) || lua_isboolean(L, -1)) {
std::string keyStr = key;
// Skip internal Lua variables
if (keyStr != "_G" && keyStr != "_VERSION" && keyStr.find("_") != 0) {
globals.push_back(key);
}
}
}
lua_pop(L, 1); // pop value
}
lua_pop(L, 1); // pop _G
// Always include Player object
globals.push_back("Player");
return globals;
}
void LuaPersistence::serializeValue(lua_State* L, int index, std::string& output, int depth) {
if (depth > 10) {
output += "nil"; // Prevent infinite recursion
return;
}
int type = lua_type(L, index);
switch (type) {
case LUA_TNIL:
output += "nil";
break;
case LUA_TBOOLEAN:
output += lua_toboolean(L, index) ? "true" : "false";
break;
case LUA_TNUMBER:
output += std::to_string(lua_tonumber(L, index));
break;
case LUA_TSTRING: {
const char* str = lua_tostring(L, index);
output += "\"";
// Properly escape all special characters for Lua strings
for (const char* p = str; *p; ++p) {
unsigned char c = (unsigned char)*p;
switch (c) {
case '"':
output += "\\\"";
break;
case '\\':
output += "\\\\";
break;
case '\n':
output += "\\n";
break;
case '\r':
output += "\\r";
break;
case '\t':
output += "\\t";
break;
case '\b':
output += "\\b";
break;
case '\f':
output += "\\f";
break;
default:
// For control characters and high bytes, use decimal escape
if (c < 32 || c >= 127) {
char hex[6];
snprintf(hex, sizeof(hex), "\\%d", c);
output += hex;
} else {
output += c;
}
break;
}
}
output += "\"";
break;
}
case LUA_TTABLE:
serializeTable(L, index, output, depth);
break;
default:
// Functions, userdata, threads -> save as nil
output += "nil";
break;
}
}
void LuaPersistence::serializeTable(lua_State* L, int index, std::string& output, int depth) {
output += "{";
// Normalize stack index
if (index < 0) index = lua_gettop(L) + index + 1;
bool first = true;
lua_pushnil(L);
while (lua_next(L, index) != 0) {
if (!first) output += ",";
first = false;
// Serialize key
output += "[";
serializeValue(L, -2, output, depth + 1);
output += "]=";
// Serialize value
serializeValue(L, -1, output, depth + 1);
lua_pop(L, 1); // pop value, keep key for next iteration
}
output += "}";
}
bool LuaPersistence::saveGlobals(lua_State* L, const std::string& filePath,
const std::vector<std::string>& globals) {
std::string output = "-- Wherigo Save State\nreturn {\n";
for (const auto& globalName : globals) {
lua_getglobal(L, globalName.c_str());
if (!lua_isnil(L, -1)) {
output += " [\"" + globalName + "\"] = ";
serializeValue(L, -1, output, 0);
output += ",\n";
}
lua_pop(L, 1);
}
output += "}\n";
// Write to file
std::ofstream file(filePath);
if (!file.is_open()) {
wxLogError("Failed to open save file: %s", filePath);
return false;
}
file << output;
file.close();
wxLogDebug("Saved %zu globals to %s", globals.size(), filePath);
return true;
}
bool LuaPersistence::saveState(lua_State* L, const std::string& filePath) {
auto globals = getGameGlobals(L);
return saveGlobals(L, filePath, globals);
}
bool LuaPersistence::loadGlobals(lua_State* L, const std::string& filePath) {
// Load the file
std::ifstream file(filePath);
if (!file.is_open()) {
wxLogError("Failed to open load file: %s", filePath);
return false;
}
std::stringstream buffer;
buffer << file.rdbuf();
std::string content = buffer.str();
file.close();
// Execute the Lua file
if (luaL_loadstring(L, content.c_str()) != 0) {
wxLogError("Failed to parse save file: %s", lua_tostring(L, -1));
lua_pop(L, 1);
return false;
}
if (lua_pcall(L, 0, 1, 0) != 0) {
wxLogError("Failed to execute save file: %s", lua_tostring(L, -1));
lua_pop(L, 1);
return false;
}
// Result should be a table
if (!lua_istable(L, -1)) {
wxLogError("Save file did not return a table");
lua_pop(L, 1);
return false;
}
// Restore globals - but for tables (Wherigo objects), merge instead of replace
// Strategy: For each saved object, find the matching existing object and merge
int count = 0;
lua_pushnil(L);
while (lua_next(L, -2) != 0) {
if (lua_isstring(L, -2) && lua_istable(L, -1)) {
const char* savedKey = lua_tostring(L, -2);
wxLogDebug("Restoring global: %s", savedKey);
int stackBefore = lua_gettop(L);
// Get the Name field of the saved object (if it exists)
lua_getfield(L, -1, "Name");
std::string savedName;
if (lua_isstring(L, -1)) {
savedName = lua_tostring(L, -1);
}
lua_pop(L, 1);
// Try to find matching existing object
bool merged = false;
if (!savedName.empty()) {
// Search for existing object with same Name
lua_getglobal(L, "_G");
lua_pushnil(L);
while (lua_next(L, -2) != 0) {
if (lua_istable(L, -1)) {
lua_getfield(L, -1, "Name");
if (lua_isstring(L, -1)) {
if (savedName == lua_tostring(L, -1)) {
lua_pop(L, 1); // pop Name
// Found matching object! Merge data
lua_pushnil(L);
while (lua_next(L, -3) != 0) {
if (lua_isstring(L, -2)) {
const char* field = lua_tostring(L, -2);
if (!lua_isfunction(L, -1)) {
lua_pushvalue(L, -1);
lua_setfield(L, -4, field);
}
}
lua_pop(L, 1);
}
merged = true;
lua_pop(L, 2); // pop iterator and object
break;
}
}
lua_pop(L, 1);
}
lua_pop(L, 1);
}
lua_pop(L, 1); // pop _G
}
if (!merged) {
// No match found or no Name field - use key matching
lua_getglobal(L, savedKey);
if (lua_istable(L, -1)) {
// Object exists by key - merge it
lua_pushnil(L);
while (lua_next(L, -3) != 0) {
if (lua_isstring(L, -2)) {
const char* field = lua_tostring(L, -2);
if (!lua_isfunction(L, -1)) {
lua_getfield(L, -3, field); // get existing value
if (lua_istable(L, -1) && lua_istable(L, -2)) {
// Both are tables: merge recursively
lua_pushnil(L);
while (lua_next(L, -3) != 0) {
if (lua_isstring(L, -2)) {
const char* subfield = lua_tostring(L, -2);
if (!lua_isfunction(L, -1)) {
lua_pushvalue(L, -1);
lua_setfield(L, -5, subfield); // merge into existing table
}
}
lua_pop(L, 1);
}
lua_pop(L, 1); // pop existing value
} else {
lua_pop(L, 1); // pop existing value
lua_pushvalue(L, -1);
lua_setfield(L, -3, field);
}
}
}
lua_pop(L, 1);
}
lua_pop(L, 1); // pop existing object
merged = true;
} else {
lua_pop(L, 1); // pop nil
}
}
if (!merged) {
// No existing object found - create new global
lua_pushvalue(L, -1);
lua_setglobal(L, savedKey);
}
int stackAfter = lua_gettop(L);
if (stackBefore != stackAfter) {
wxLogError("Stack imbalance detected for global %s: before=%d after=%d", savedKey, stackBefore, stackAfter);
}
count++;
}
lua_pop(L, 1); // pop value from lua_next
}
lua_pop(L, 1); // pop table
lua_pop(L, 1); // pop table
wxLogDebug("Restored %d globals from %s", count, filePath);
return true;
}
bool LuaPersistence::loadState(lua_State* L, const std::string& filePath) {
return loadGlobals(L, filePath);
}
} // namespace wherigo