Documentation update
This commit is contained in:
@@ -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
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@@ -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<SaveInfo> 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.
|
||||
@@ -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')
|
||||
@@ -1006,6 +1006,66 @@ void registerLuaCharacterApi(lua_State *L)
|
||||
|
||||
} // namespace editScene
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Stub: LuaSaveLoadApi
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
namespace editScene
|
||||
{
|
||||
|
||||
// Save/load callback storage
|
||||
static std::unordered_map<std::string, int> s_stubSaveCallbacks;
|
||||
static std::unordered_map<std::string, int> 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
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -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 <cstdio>
|
||||
#include <cstring>
|
||||
#include <cassert>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <map>
|
||||
|
||||
// Ogre stub (provides Ogre::String, Ogre::Vector3, Ogre::LogManager)
|
||||
#include "ogre_stub.h"
|
||||
|
||||
// Include Lua
|
||||
extern "C" {
|
||||
#include <lua.h>
|
||||
#include <lauxlib.h>
|
||||
#include <lualib.h>
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
Reference in New Issue
Block a user