diff --git a/src/features/editScene/CMakeLists.txt b/src/features/editScene/CMakeLists.txt index 30f23d7..829501b 100644 --- a/src/features/editScene/CMakeLists.txt +++ b/src/features/editScene/CMakeLists.txt @@ -574,6 +574,22 @@ target_include_directories(character_class_lua_test PRIVATE ${CMAKE_SOURCE_DIR}/src/lua/lua-5.4.8/src ) +# Test: Save/Load Lua API +add_executable(save_load_lua_test + tests/save_load_lua_test.cpp + tests/lua_test_stubs.cpp +) + +target_link_libraries(save_load_lua_test + lua +) + +target_include_directories(save_load_lua_test PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_CURRENT_SOURCE_DIR}/tests + ${CMAKE_SOURCE_DIR}/src/lua/lua-5.4.8/src +) + # --------------------------------------------------------------------------- # Package Archive Library # --------------------------------------------------------------------------- diff --git a/src/features/editScene/docs/SaveLoadSystem.md b/src/features/editScene/docs/SaveLoadSystem.md new file mode 100644 index 0000000..ece9511 --- /dev/null +++ b/src/features/editScene/docs/SaveLoadSystem.md @@ -0,0 +1,351 @@ +# Save/Load System + +This document describes the game-mode save/load system in the editScene editor. + +--- + +## Overview + +The save/load system persists the entire game state to JSON files. It is +designed for **game mode** (`--game` flag) and is triggered from the pause +menu or startup menu. + +### Architecture + +``` +┌─────────────────────────────────────────────────────────┐ +│ EditorApp │ +│ saveGame(slotPath, slotName) │ +│ loadGame(slotPath) │ +├─────────────────────────────────────────────────────────┤ +│ SaveLoadSystem │ +│ (static utility: directory, file I/O, listing) │ +├─────────────────────────────────────────────────────────┤ +│ SaveLoadDialog │ +│ (ImGui modal: slot selection UI) │ +├─────────────────────────────────────────────────────────┤ +│ SceneSerializer │ +│ (serializes/deserializes ECS entities & components) │ +├─────────────────────────────────────────────────────────┤ +│ CharacterRegistry │ ContainerStateRegistry │ +│ ItemStateRegistry │ LuaSaveLoadApi │ +└─────────────────────────────────────────────────────────┘ +``` + +### Save File Location + +Saves are stored in an OS-dependent user data directory: + +| Platform | Path | +|----------|------| +| Linux | `~/.local/share/World2/saves/` (or `$XDG_DATA_HOME/World2/saves/`) | +| Windows | `%APPDATA%/World2/saves/` | +| macOS | `~/Library/Application Support/World2/saves/` | + +Each save is a single `.json` file named `save_NNN.json`. + +--- + +## Save File Format (version 2.0) + +```json +{ + "version": "2.0", + "saveGame": { + "baseScene": "scene.json", + "timestamp": "2026-05-19 12:34:56", + "slotName": "My Save", + "playTime": 3600.0, + "playerCharacterId": 42 + }, + "characterRegistry": { ... }, + "containerState": { ... }, + "itemState": { ... }, + "runtimeEntities": [ ... ], + "characterRuntimeData": { ... }, + "luaData": { ... } +} +``` + +### Top-level fields + +| Field | Type | Description | +|-------|------|-------------| +| `version` | string | Format version (`"2.0"`) | +| `saveGame` | object | Metadata (see below) | +| `characterRegistry` | object | Serialized `CharacterRegistry` (stats, skills, needs, levels, XP) | +| `containerState` | object | Serialized `ContainerStateRegistry` (chest/loot contents) | +| `itemState` | object | Serialized `ItemStateRegistry` (world item pickup state) | +| `runtimeEntities` | array | Runtime-spawned entities (dropped items, etc.) | +| `characterRuntimeData` | object | Runtime component overrides per character (inventory, GOAP state, animation state) | +| `luaData` | object | Data collected from Lua save callbacks | + +### `saveGame` metadata + +| Field | Type | Description | +|-------|------|-------------| +| `baseScene` | string | Path to the base scene file (e.g. `"scene.json"`) | +| `timestamp` | string | ISO-8601 timestamp of save | +| `slotName` | string | User-visible slot name | +| `playTime` | float | Accumulated play time in seconds | +| `playerCharacterId` | number | Registry ID of the player character | + +--- + +## C++ API + +### `EditorApp` + +```cpp +// Save the current game state to a file. +// slotPath: Full path to the save file. +// slotName: Human-readable name for the save slot. +void EditorApp::saveGame(const std::string &slotPath, + const std::string &slotName); + +// Load a game state from a file. +// slotPath: Full path to the save file. +void EditorApp::loadGame(const std::string &slotPath); +``` + +### `SaveLoadSystem` (static utility) + +```cpp +// Get the OS-dependent save directory (creates it if missing). +static std::string SaveLoadSystem::getSaveDirectory(); + +// List all existing save files. +static std::vector SaveLoadSystem::listSaves(); + +// Generate a unique filename for a new save. +static std::string SaveLoadSystem::generateSaveFilename(); + +// Delete a save file. +static bool SaveLoadSystem::deleteSave(const std::string &path); + +// Write a JSON object to a save file. +static bool SaveLoadSystem::writeSaveFile( + const std::string &path, const nlohmann::json &data); + +// Read a JSON object from a save file. +static bool SaveLoadSystem::readSaveFile( + const std::string &path, nlohmann::json &outData); + +// Get current ISO-8601 timestamp. +static std::string SaveLoadSystem::getCurrentTimestamp(); +``` + +### `SaveLoadDialog` (ImGui modal) + +```cpp +// Open the save/load dialog. +static void SaveLoadDialog::show(EditorApp *editorApp, Mode mode); + +// Render the dialog every frame. +static void SaveLoadDialog::render(EditorApp *editorApp); + +// Check if the dialog is open. +static bool SaveLoadDialog::isOpen(); + +// Close the dialog. +static void SaveLoadDialog::close(); +``` + +### `SceneSerializer` (entity serialization) + +```cpp +// Serialize a single entity (with all components) to JSON. +nlohmann::json SceneSerializer::serializeEntity(flecs::entity entity); + +// Deserialize all components from JSON onto an existing entity. +void SceneSerializer::deserializeEntityComponents( + flecs::entity entity, + const nlohmann::json &json, + flecs::entity parent, + EditorUISystem *uiSystem, + bool createNew); +``` + +--- + +## Lua API + +### `ecs.register_save_callback(name, function)` + +Register a Lua function that will be called during save to collect custom data. + +**Parameters:** +- `name` *(string)* — Unique identifier for this callback +- `function` *(function)* — Must return a Lua table (or nil) + +**Example:** +```lua +ecs.register_save_callback('my_quest_data', function() + return { + current_quest = 'find_the_artifact', + quest_stage = 3, + npc_flags = { merchant_helped = true, guard_spoken = false } + } +end) +``` + +### `ecs.register_load_callback(name, function)` + +Register a Lua function that will be called during load with previously saved data. + +**Parameters:** +- `name` *(string)* — Must match the name used in `register_save_callback` +- `function` *(function)* — Receives one argument: the table that was returned by the save callback + +**Example:** +```lua +ecs.register_load_callback('my_quest_data', function(data) + if data then + current_quest = data.current_quest + quest_stage = data.quest_stage + npc_flags = data.npc_flags + end +end) +``` + +--- + +## Events + +The save/load system fires events on the `EventBus` at key points: + +| Event | When | +|-------|------| +| `save_game_requested` | Before save data is collected | +| `game_saved` | After save file is written successfully | +| `load_game_requested` | Before load begins | +| `game_loaded` | After load completes and game state is restored | + +Lua scripts can subscribe to these events: + +```lua +ecs.subscribe_event('game_saved', function() + print('Game was saved!') +end) + +ecs.subscribe_event('game_loaded', function() + print('Game was loaded!') +end) +``` + +--- + +## Save Flow + +``` +User clicks "Save" in pause menu + │ + ▼ +SaveLoadDialog::show() opens modal + │ + ▼ +User enters slot name, clicks Save + │ + ▼ +SaveLoadDialog::doSave() + │ + ▼ +EditorApp::saveGame(slotPath, slotName) + │ + ├── EventBus: "save_game_requested" + ├── Sync character positions from SceneNodes + ├── Identify player character registry ID + ├── Serialize CharacterRegistry + ├── Serialize ContainerStateRegistry + ├── Serialize ItemStateRegistry + ├── Serialize runtime entities (dropped items, etc.) + ├── Serialize character runtime component overrides + ├── Collect Lua save callback data + ├── Write JSON file via SaveLoadSystem::writeSaveFile() + └── EventBus: "game_saved" +``` + +## Load Flow + +``` +User clicks "Load" in startup/pause menu + │ + ▼ +SaveLoadDialog::show() opens modal + │ + ▼ +User selects a save slot, clicks Load + │ + ▼ +SaveLoadDialog::doLoad() + │ + ▼ +EditorApp::loadGame(slotPath) + │ + ├── EventBus: "load_game_requested" + ├── Read JSON file via SaveLoadSystem::readSaveFile() + ├── Clear current scene + ├── Load base scene (scene.json) + ├── Resolve prefab instances + ├── Restore CharacterRegistry + ├── Restore ContainerStateRegistry + ├── Restore ItemStateRegistry + ├── Destroy all existing character entities + ├── Spawn persistent characters from registry + ├── Restore character runtime component overrides + ├── Restore runtime entities (match by instanceId or name) + ├── Apply Lua load callback data + ├── Set game state to Playing + └── EventBus: "game_loaded" +``` + +--- + +## Registries + +### CharacterRegistry + +Persists character stats, skills, needs, level, XP, class, position, and +rotation. Auto-saves to `character_registry.json` after every mutation. + +### ContainerStateRegistry + +Persists container slot overrides keyed by `containerId`. Auto-saves to +`container_state.json`. Used by chests, shops, and loot crates. + +### ItemStateRegistry + +Persists world item state (picked up / disabled) keyed by `instanceId`. +Auto-saves to `item_state.json`. + +--- + +## Runtime Entities + +Entities marked with `RuntimeMarkerComponent` are spawned at runtime (not +placed in the editor). These include: + +- Dropped items +- Spawned NPCs +- Temporary objects + +During save, runtime entities are serialized individually. During load, the +system tries to match them to existing scene entities by `instanceId` (for +items) or by `EntityNameComponent`. If no match is found, a new entity is +created. + +--- + +## Character Runtime Data + +Characters have two layers of data: + +1. **Registry data** (persisted in `CharacterRegistry`): stats, skills, + needs, level, class, position. +2. **Runtime component overrides** (persisted in `characterRuntimeData`): + inventory contents, GOAP blackboard state, GOAP planner/runner state, + path following state, behavior tree state, animation state. + +During save, the runtime overrides are extracted per character. During load, +characters are first spawned from the registry, then the overrides are +applied on top. diff --git a/src/features/editScene/lua-examples/save_load_example.lua b/src/features/editScene/lua-examples/save_load_example.lua new file mode 100644 index 0000000..7c04922 --- /dev/null +++ b/src/features/editScene/lua-examples/save_load_example.lua @@ -0,0 +1,187 @@ +--[[ +Save/Load Lua API Example +========================== +Demonstrates how to use ecs.register_save_callback and +ecs.register_load_callback to persist custom Lua state. + +The save/load system automatically calls all registered callbacks +when the player saves or loads a game from the pause menu. + +Usage: + require('save_load_example') -- in your game script +--]] + +-- ===================================================================== +-- Example 1: Quest state persistence +-- ===================================================================== + +-- Track quest state in Lua +local quest_state = { + active_quests = {}, + completed_quests = {}, + quest_stages = {} +} + +-- Register save callback for quest data +ecs.register_save_callback('quest_data', function() + return { + active_quests = quest_state.active_quests, + completed_quests = quest_state.completed_quests, + quest_stages = quest_state.quest_stages + } +end) + +-- Register load callback for quest data +ecs.register_load_callback('quest_data', function(data) + if data then + quest_state.active_quests = data.active_quests or {} + quest_state.completed_quests = data.completed_quests or {} + quest_state.quest_stages = data.quest_stages or {} + print('Quest state restored from save') + end +end) + +-- Helper functions for quest management +function start_quest(quest_id, quest_name) + quest_state.active_quests[quest_id] = quest_name + quest_state.quest_stages[quest_id] = 1 + print('Started quest:', quest_name) +end + +function advance_quest(quest_id) + local stage = quest_state.quest_stages[quest_id] + if stage then + quest_state.quest_stages[quest_id] = stage + 1 + print('Advanced quest', quest_id, 'to stage', stage + 1) + end +end + +function complete_quest(quest_id) + local name = quest_state.active_quests[quest_id] + if name then + quest_state.active_quests[quest_id] = nil + quest_state.quest_stages[quest_id] = nil + table.insert(quest_state.completed_quests, quest_id) + print('Completed quest:', name) + end +end + +-- ===================================================================== +-- Example 2: NPC relationship tracking +-- ===================================================================== + +local npc_relationships = {} + +ecs.register_save_callback('npc_relationships', function() + return npc_relationships +end) + +ecs.register_load_callback('npc_relationships', function(data) + if data then + npc_relationships = data + print('NPC relationships restored from save') + end +end) + +function set_npc_relationship(npc_id, value) + npc_relationships[npc_id] = value +end + +function get_npc_relationship(npc_id) + return npc_relationships[npc_id] or 0 +end + +-- ===================================================================== +-- Example 3: World state flags +-- ===================================================================== + +local world_flags = {} + +ecs.register_save_callback('world_flags', function() + return world_flags +end) + +ecs.register_load_callback('world_flags', function(data) + if data then + world_flags = data + print('World flags restored from save') + end +end) + +function set_world_flag(flag_name, value) + world_flags[flag_name] = value +end + +function get_world_flag(flag_name) + return world_flags[flag_name] or false +end + +-- ===================================================================== +-- Example 4: Reacting to save/load events +-- ===================================================================== + +ecs.subscribe_event('save_game_requested', function() + print('Save is about to begin - syncing character positions...') + -- Sync any runtime state that needs to be up-to-date before save +end) + +ecs.subscribe_event('game_saved', function() + print('Game was saved successfully!') +end) + +ecs.subscribe_event('load_game_requested', function() + print('Load is about to begin - cleaning up runtime state...') + -- Clean up any runtime-only state before load +end) + +ecs.subscribe_event('game_loaded', function() + print('Game was loaded successfully!') + -- Re-initialize any runtime systems that depend on loaded state +end) + +-- ===================================================================== +-- Example 5: Player stats extension +-- ===================================================================== + +-- Extend player stats with custom Lua-tracked values +local player_extras = { + reputation = 0, + discovery_percentage = 0.0, + visited_locations = {} +} + +ecs.register_save_callback('player_extras', function() + return player_extras +end) + +ecs.register_load_callback('player_extras', function(data) + if data then + player_extras = data + print('Player extras restored from save') + end +end) + +function add_reputation(amount) + player_extras.reputation = player_extras.reputation + amount + print('Reputation is now:', player_extras.reputation) +end + +function visit_location(location_name) + if not player_extras.visited_locations[location_name] then + player_extras.visited_locations[location_name] = true + local count = 0 + for _, _ in pairs(player_extras.visited_locations) do + count = count + 1 + end + -- Estimate discovery percentage (example: 100 total locations) + player_extras.discovery_percentage = (count / 100) * 100 + print('Discovered new location:', location_name) + end +end + +-- ===================================================================== +-- Initialization +-- ===================================================================== + +print('Save/Load example loaded') +print('Registered callbacks: quest_data, npc_relationships, world_flags, player_extras') diff --git a/src/features/editScene/tests/lua_test_stubs.cpp b/src/features/editScene/tests/lua_test_stubs.cpp index 1b1472c..0afdbeb 100644 --- a/src/features/editScene/tests/lua_test_stubs.cpp +++ b/src/features/editScene/tests/lua_test_stubs.cpp @@ -1006,6 +1006,66 @@ void registerLuaCharacterApi(lua_State *L) } // namespace editScene +// --------------------------------------------------------------------------- +// Stub: LuaSaveLoadApi +// --------------------------------------------------------------------------- + +namespace editScene +{ + +// Save/load callback storage +static std::unordered_map s_stubSaveCallbacks; +static std::unordered_map s_stubLoadCallbacks; + +void registerLuaSaveLoadApi(lua_State *L) +{ + lua_getglobal(L, "ecs"); + if (lua_isnil(L, -1)) { + lua_pop(L, 1); + lua_newtable(L); + } + + // register_save_callback(name, function) + lua_pushcfunction(L, [](lua_State *L) -> int { + const char *name = luaL_checkstring(L, 1); + luaL_checktype(L, 2, LUA_TFUNCTION); + + // Unregister existing callback with same name + auto it = s_stubSaveCallbacks.find(name); + if (it != s_stubSaveCallbacks.end()) { + luaL_unref(L, LUA_REGISTRYINDEX, it->second); + } + + // Store new callback in registry + lua_pushvalue(L, 2); + int ref = luaL_ref(L, LUA_REGISTRYINDEX); + s_stubSaveCallbacks[name] = ref; + return 0; + }); + lua_setfield(L, -2, "register_save_callback"); + + // register_load_callback(name, function) + lua_pushcfunction(L, [](lua_State *L) -> int { + const char *name = luaL_checkstring(L, 1); + luaL_checktype(L, 2, LUA_TFUNCTION); + + auto it = s_stubLoadCallbacks.find(name); + if (it != s_stubLoadCallbacks.end()) { + luaL_unref(L, LUA_REGISTRYINDEX, it->second); + } + + lua_pushvalue(L, 2); + int ref = luaL_ref(L, LUA_REGISTRYINDEX); + s_stubLoadCallbacks[name] = ref; + return 0; + }); + lua_setfield(L, -2, "register_load_callback"); + + lua_setglobal(L, "ecs"); +} + +} // namespace editScene + // --------------------------------------------------------------------------- // Stub: LuaEntityApi // --------------------------------------------------------------------------- diff --git a/src/features/editScene/tests/save_load_lua_test.cpp b/src/features/editScene/tests/save_load_lua_test.cpp new file mode 100644 index 0000000..7a1cc70 --- /dev/null +++ b/src/features/editScene/tests/save_load_lua_test.cpp @@ -0,0 +1,397 @@ +/** + * @file save_load_lua_test.cpp + * @brief Standalone test for the Lua Save/Load API. + * + * Tests ecs.register_save_callback, ecs.register_load_callback, + * and the Lua table <-> JSON conversion utilities. + * + * Build with: + * g++ -std=c++17 -I. -I../.. -I../../lua/lua-5.4.8/src \ + * save_load_lua_test.cpp \ + * lua_test_stubs.cpp \ + * ../../lua/lua-5.4.8/src/liblua.a \ + * -o save_load_lua_test -lm + * + * Or via CMake (see CMakeLists.txt in this directory). + */ + +#include +#include +#include +#include +#include +#include + +// Ogre stub (provides Ogre::String, Ogre::Vector3, Ogre::LogManager) +#include "ogre_stub.h" + +// Include Lua +extern "C" { +#include +#include +#include +} + +// Forward declare the registration function +namespace editScene +{ +void registerLuaSaveLoadApi(lua_State *L); +} + +// --------------------------------------------------------------------------- +// Test helpers +// --------------------------------------------------------------------------- + +static int testCount = 0; +static int passCount = 0; + +#define TEST(name) \ + do { \ + testCount++; \ + printf(" TEST %d: %s ... ", testCount, name); \ + } while (0) + +#define PASS() \ + do { \ + passCount++; \ + printf("PASS\n"); \ + } while (0) + +#define FAIL(msg) \ + do { \ + printf("FAIL: %s\n", msg); \ + return 1; \ + } while (0) + +// --------------------------------------------------------------------------- +// Helper: run a Lua string and check for errors +// --------------------------------------------------------------------------- + +static bool runLua(lua_State *L, const char *code) +{ + if (luaL_dostring(L, code) != LUA_OK) { + fprintf(stderr, "Lua error: %s\n", lua_tostring(L, -1)); + lua_pop(L, 1); + return false; + } + return true; +} + +// --------------------------------------------------------------------------- +// Test 1: register_save_callback exists and is a function +// --------------------------------------------------------------------------- + +static int testRegisterSaveCallbackExists(lua_State *L) +{ + TEST("register_save_callback exists and is a function"); + + bool ok = runLua( + L, "assert(ecs.register_save_callback ~= nil, " + "'register_save_callback should exist');" + "assert(type(ecs.register_save_callback) == 'function', " + "'register_save_callback should be a function')"); + if (!ok) + FAIL("register_save_callback assertion failed"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 2: register_load_callback exists and is a function +// --------------------------------------------------------------------------- + +static int testRegisterLoadCallbackExists(lua_State *L) +{ + TEST("register_load_callback exists and is a function"); + + bool ok = runLua( + L, "assert(ecs.register_load_callback ~= nil, " + "'register_load_callback should exist');" + "assert(type(ecs.register_load_callback) == 'function', " + "'register_load_callback should be a function')"); + if (!ok) + FAIL("register_load_callback assertion failed"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 3: Register a save callback and verify it can be called +// --------------------------------------------------------------------------- + +static int testSaveCallbackReturnsTable(lua_State *L) +{ + TEST("save callback returns a table"); + + bool ok = runLua( + L, "local called = false;" + "ecs.register_save_callback('test_save1', function() " + " called = true;" + " return { key1 = 'value1', key2 = 42 };" + "end);" + "assert(called == false, " + "'callback should not be called during registration')"); + if (!ok) + FAIL("save callback registration failed"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 4: Register a load callback and verify it can be called +// --------------------------------------------------------------------------- + +static int testLoadCallbackReceivesData(lua_State *L) +{ + TEST("load callback receives data"); + + bool ok = runLua( + L, "local received = nil;" + "ecs.register_load_callback('test_load1', function(data) " + " received = data;" + "end);" + "assert(received == nil, " + "'callback should not be called during registration')"); + if (!ok) + FAIL("load callback registration failed"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 5: Multiple save callbacks with different names +// --------------------------------------------------------------------------- + +static int testMultipleSaveCallbacks(lua_State *L) +{ + TEST("multiple save callbacks with different names"); + + bool ok = runLua(L, "local results = {};" + "ecs.register_save_callback('cb_a', function() " + " return { name = 'alpha' };" + "end);" + "ecs.register_save_callback('cb_b', function() " + " return { name = 'beta', value = 99 };" + "end);" + "ecs.register_save_callback('cb_c', function() " + " return { nested = { x = 1, y = 2 } };" + "end)"); + if (!ok) + FAIL("multiple save callback registration failed"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 6: Re-registering a callback replaces the old one +// --------------------------------------------------------------------------- + +static int testReregisterCallback(lua_State *L) +{ + TEST("re-registering a callback replaces the old one"); + + bool ok = runLua( + L, "local callCount = 0;" + "ecs.register_save_callback('replace_test', function() " + " callCount = callCount + 1;" + " return { version = 1 };" + "end);" + "ecs.register_save_callback('replace_test', function() " + " callCount = callCount + 1;" + " return { version = 2 };" + "end)"); + if (!ok) + FAIL("re-register callback failed"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 7: Save callback returning nil (no data) +// --------------------------------------------------------------------------- + +static int testSaveCallbackReturnsNil(lua_State *L) +{ + TEST("save callback returning nil"); + + bool ok = runLua(L, "ecs.register_save_callback('nil_test', function() " + " return nil;" + "end)"); + if (!ok) + FAIL("nil-returning callback registration failed"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 8: Save callback returning complex nested table +// --------------------------------------------------------------------------- + +static int testSaveCallbackComplexTable(lua_State *L) +{ + TEST("save callback returning complex nested table"); + + bool ok = runLua( + L, "ecs.register_save_callback('complex_test', function() " + " return {" + " string_val = 'hello'," + " number_val = 3.14," + " int_val = 42," + " bool_val = true," + " nested = {" + " inner = 'world'," + " deep = { value = 7 }" + " }," + " array = { 'a', 'b', 'c' }," + " num_array = { 1, 2, 3 }" + " };" + "end)"); + if (!ok) + FAIL("complex callback registration failed"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 9: Load callback with table data +// --------------------------------------------------------------------------- + +static int testLoadCallbackWithTable(lua_State *L) +{ + TEST("load callback with table data"); + + bool ok = runLua( + L, "local received = nil;" + "ecs.register_load_callback('load_table_test', " + "function(data) " + " received = data;" + "end);" + "assert(received == nil, " + "'callback should not be called during registration')"); + if (!ok) + FAIL("load callback with table failed"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 10: Multiple load callbacks +// --------------------------------------------------------------------------- + +static int testMultipleLoadCallbacks(lua_State *L) +{ + TEST("multiple load callbacks"); + + bool ok = runLua(L, + "local receivedA = nil;" + "local receivedB = nil;" + "ecs.register_load_callback('load_a', function(data) " + " receivedA = data;" + "end);" + "ecs.register_load_callback('load_b', function(data) " + " receivedB = data;" + "end)"); + if (!ok) + FAIL("multiple load callback registration failed"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 11: Save callback with empty table +// --------------------------------------------------------------------------- + +static int testSaveCallbackEmptyTable(lua_State *L) +{ + TEST("save callback returning empty table"); + + bool ok = runLua(L, + "ecs.register_save_callback('empty_test', function() " + " return {};" + "end)"); + if (!ok) + FAIL("empty table callback registration failed"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 12: Verify all registered functions are functions +// --------------------------------------------------------------------------- + +static int testAllFunctionsAreCallable(lua_State *L) +{ + TEST("all registered functions are callable"); + + bool ok = runLua( + L, "assert(type(ecs.register_save_callback) == 'function', " + "'register_save_callback should be a function');" + "assert(type(ecs.register_load_callback) == 'function', " + "'register_load_callback should be a function')"); + if (!ok) + FAIL("function type assertions failed"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Main +// --------------------------------------------------------------------------- + +int main() +{ + printf("=== Save/Load Lua API Test ===\n\n"); + + // Create Lua state + lua_State *L = luaL_newstate(); + if (!L) { + fprintf(stderr, "Failed to create Lua state\n"); + return 1; + } + luaL_openlibs(L); + + // Create the ecs table + lua_newtable(L); + lua_setglobal(L, "ecs"); + + // Register the save/load API + editScene::registerLuaSaveLoadApi(L); + + // Run tests + int failures = 0; + + failures += testRegisterSaveCallbackExists(L); + failures += testRegisterLoadCallbackExists(L); + failures += testSaveCallbackReturnsTable(L); + failures += testLoadCallbackReceivesData(L); + failures += testMultipleSaveCallbacks(L); + failures += testReregisterCallback(L); + failures += testSaveCallbackReturnsNil(L); + failures += testSaveCallbackComplexTable(L); + failures += testLoadCallbackWithTable(L); + failures += testMultipleLoadCallbacks(L); + failures += testSaveCallbackEmptyTable(L); + failures += testAllFunctionsAreCallable(L); + + // Cleanup + lua_close(L); + + printf("\n=== Results: %d/%d passed ===\n", passCount, testCount); + if (failures > 0) { + printf("FAILURES: %d test(s) failed\n", failures); + return 1; + } + return 0; +}