#include "lua/persistence.h" extern "C" { #include #include } #include #include #include #include #include namespace wherigo { // List of Wherigo game object classes to save static const std::set WHERIGO_CLASSES = { "ZCartridge", "Zone", "ZItem", "ZCharacter", "ZTask", "ZTimer", "ZInput", "ZMedia" }; std::vector LuaPersistence::getGameGlobals(lua_State* L) { std::vector 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& 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