# StorageModule Implementation Documentation **Date**: December 2, 2025 **Version**: 0.1.0 **Status**: Implemented --- ## Overview StorageModule is a **game-agnostic core module** that provides save/load functionality for the game state. It follows the GroveEngine architecture and pub/sub communication pattern. ### Key Principles 1. **Game-Agnostic**: No references to "train", "drone", "Mobile Command", or "WarFactory" 2. **Pub/Sub Communication**: All interaction via `grove::IIO` topics 3. **Hot-Reload Compatible**: State serialization for seamless module replacement 4. **JSON Storage**: Human-readable save files for debugging and manual editing --- ## Architecture ### File Structure ``` src/modules/core/ ├── StorageModule.h # Module interface └── StorageModule.cpp # Implementation config/ └── storage.json # Configuration tests/ └── StorageModuleTest.cpp # Independent validation tests data/saves/ # Save game directory (created automatically) ``` --- ## Communication Protocol ### Topics Published | Topic | Payload | Description | |-------|---------|-------------| | `storage:save_complete` | `{filename, timestamp}` | Save operation succeeded | | `storage:load_complete` | `{filename, version}` | Load operation succeeded | | `storage:save_failed` | `{error}` | Save operation failed | | `storage:load_failed` | `{error}` | Load operation failed | | `storage:collect_states` | `{action: "collect_state"}` | Broadcast request to all modules | | `storage:restore_state:{moduleName}` | `{module state data}` | Restore specific module state | ### Topics Subscribed | Topic | Description | |-------|-------------| | `game:request_save` | Trigger manual save (payload: `{filename?}`) | | `game:request_load` | Trigger load (payload: `{filename}`) | | `storage:module_state` | Receive module state (payload: `{moduleName, ...state}`) | --- ## State Collection Mechanism (Pub/Sub Pattern) This is the **critical architectural pattern** that makes the module game-agnostic. ### How It Works #### 1. Save Request Flow ``` User/Game StorageModule Other Modules | | | |--request_save--------->| | | | | | |--collect_states------->| (broadcast) | | | | |<----module_state-------| Module A | |<----module_state-------| Module B | |<----module_state-------| Module C | | | | | [Aggregate all states] | | | [Write to JSON file] | | | | |<---save_complete-------| | ``` **Key Points**: - StorageModule broadcasts `storage:collect_states` - Each module responds with `storage:module_state` containing their own state - StorageModule aggregates all responses into a single JSON file - No direct coupling between modules #### 2. Load Request Flow ``` User/Game StorageModule Other Modules | | | |--request_load--------->| | | | | | | [Read JSON file] | | | [Parse module states] | | | | | |--restore_state:ModA--->| Module A | |--restore_state:ModB--->| Module B | |--restore_state:ModC--->| Module C | | | |<---load_complete-------| | ``` **Key Points**: - StorageModule reads save file - For each module in the save, publishes `storage:restore_state:{moduleName}` - Modules subscribe to their specific restore topic - Each module handles its own state restoration ### Example Module Integration **Any module that wants save/load support must:** ```cpp class ExampleModule : public grove::IModule { public: void setConfiguration(..., grove::IIO* io, ...) override { m_io = io; // Subscribe to state collection m_io->subscribe("storage:collect_states"); // Subscribe to your own restore topic m_io->subscribe("storage:restore_state:ExampleModule"); } void process(const grove::IDataNode& input) override { while (m_io->hasMessages() > 0) { auto msg = m_io->pullMessage(); if (msg.topic == "storage:collect_states") { // Respond with your state auto state = getState(); // Get your current state state->setString("moduleName", "ExampleModule"); m_io->publish("storage:module_state", std::move(state)); } else if (msg.topic == "storage:restore_state:ExampleModule") { // Restore your state from the message setState(*msg.data); } } } }; ``` --- ## Save File Format ### Structure ```json { "version": "0.1.0", "game": "MobileCommand", "timestamp": "2025-12-02T10:00:00Z", "gameTime": 3600.0, "modules": { "GameModule": { "frameCount": 54321, "gameState": "InGame" }, "ResourceModule": { "inventory": { "scrap_metal": 150, "ammunition_9mm": 500 }, "craftQueue": [] }, "TrainBuilderModule": { "wagons": [ {"type": "locomotive", "health": 100}, {"type": "cargo", "health": 85} ], "balance": { "lateral": -2.5, "longitudinal": 1.0 } } } } ``` ### Fields - **version**: StorageModule version (for compatibility) - **game**: Game name (informational) - **timestamp**: ISO 8601 UTC timestamp - **gameTime**: In-game time elapsed (seconds) - **modules**: Map of module name → module state --- ## Configuration ### storage.json ```json { "savePath": "data/saves/", "autoSaveInterval": 300.0, "maxAutoSaves": 3 } ``` | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `savePath` | string | `"data/saves/"` | Directory for save files | | `autoSaveInterval` | float | `300.0` | Auto-save interval (seconds) | | `maxAutoSaves` | int | `3` | Max auto-save files to keep | --- ## Auto-Save System ### Behavior 1. Timer accumulates `deltaTime` from each `process()` call 2. When `timeSinceLastAutoSave >= autoSaveInterval`, trigger auto-save 3. Auto-save filename: `autosave_YYYYMMDD_HHMMSS.json` 4. Old auto-saves are rotated (oldest deleted when count exceeds `maxAutoSaves`) ### Rotation Algorithm ```cpp 1. Scan savePath for files starting with "autosave" 2. Sort by modification time (oldest first) 3. While count >= maxAutoSaves: - Delete oldest file - Remove from list 4. Create new auto-save ``` --- ## Usage Examples ### Mobile Command Use Case ```cpp // GameModule requests save on player action void onPlayerPressSaveKey() { auto request = std::make_unique("request"); request->setString("filename", "player_quicksave"); m_io->publish("game:request_save", std::move(request)); } // TrainBuilderModule responds to state collection void onCollectStates() { auto state = std::make_unique("state"); state->setString("moduleName", "TrainBuilderModule"); // Serialize train composition auto wagons = std::make_unique("wagons"); for (auto& wagon : m_wagons) { auto wagonNode = std::make_unique(wagon.id); wagonNode->setString("type", wagon.type); wagonNode->setInt("health", wagon.health); wagons->setChild(wagon.id, std::move(wagonNode)); } state->setChild("wagons", std::move(wagons)); m_io->publish("storage:module_state", std::move(state)); } ``` ### WarFactory Use Case (Future) ```cpp // FactoryModule responds to state collection void onCollectStates() { auto state = std::make_unique("state"); state->setString("moduleName", "FactoryModule"); // Serialize production lines auto lines = std::make_unique("productionLines"); for (auto& line : m_productionLines) { auto lineNode = std::make_unique(line.id); lineNode->setString("recipe", line.currentRecipe); lineNode->setDouble("progress", line.progress); lines->setChild(line.id, std::move(lineNode)); } state->setChild("productionLines", std::move(lines)); m_io->publish("storage:module_state", std::move(state)); } ``` **Same StorageModule, different game data!** --- ## Hot-Reload Support ### State Serialization StorageModule preserves: - `timeSinceLastAutoSave`: Auto-save timer progress ```cpp std::unique_ptr getState() override { auto state = std::make_unique("state"); state->setDouble("timeSinceLastAutoSave", m_timeSinceLastAutoSave); return state; } void setState(const grove::IDataNode& state) override { m_timeSinceLastAutoSave = state.getDouble("timeSinceLastAutoSave", 0.0); } ``` ### Reload Safety - `isIdle()` returns `false` when collecting states (prevents reload during save) - Transient data (collected states) is cleared on shutdown - Configuration is reloaded from `storage.json` --- ## Testing ### Independent Validation Tests Located in: `tests/StorageModuleTest.cpp` **Test Suite**: 1. **Save/Load Cycle**: Verify data preservation 2. **Auto-save Triggers**: Verify timer functionality 3. **Invalid Save File**: Verify error handling 4. **Version Compatibility**: Verify version field handling 5. **Auto-save Rotation**: Verify max auto-saves limit 6. **Hot-reload State**: Verify state preservation ### Running Tests ```bash # Compile test (standalone, no GroveEngine required) g++ -std=c++17 -I../external/GroveEngine/include \ tests/StorageModuleTest.cpp \ src/modules/core/StorageModule.cpp \ -lspdlog -lstdc++fs -o test_storage # Run ./test_storage ``` **Expected Output**: ``` ======================================== StorageModule Independent Validation Tests ======================================== === Test 1: Save/Load Cycle === ✓ Save completed successfully ✓ Save file created ✓ Load completed successfully ✓ Module state restore requested ✓ Test 1 PASSED ... ======================================== ALL TESTS PASSED ✓ ======================================== ``` --- ## Error Handling ### Save Failures **Scenarios**: - Directory creation fails - File write permission denied - Disk full - JSON serialization error **Behavior**: - Log error with `spdlog::error` - Publish `storage:save_failed` with error message - Do not crash, continue operation ### Load Failures **Scenarios**: - File not found - Corrupted JSON - Missing version field - Invalid module data **Behavior**: - Log error with `spdlog::error` - Publish `storage:load_failed` with error message - Do not load partial state - Game continues with current state --- ## Performance Considerations ### Save Operation - **Synchronous**: Blocks during file write (~10-50ms for typical save) - **Async Future**: Could use `ITaskScheduler` for background saves - **Size**: ~10-100KB JSON files for typical game state ### Load Operation - **Synchronous**: Blocks during file read and parse (~5-20ms) - **State Restoration**: Modules handle their own state asynchronously via pub/sub ### Memory - **Collected States**: Temporarily holds all module states in memory - **Cleared**: After save completes or on shutdown - **Typical**: ~1-5MB for all module states combined --- ## Future Enhancements ### Planned (Phase 2) - [ ] Async save/load via `ITaskScheduler` - [ ] Compression for large save files - [ ] Cloud save integration - [ ] Save metadata (playtime, screenshot) ### Possible (Post-MVP) - [ ] Incremental saves (delta compression) - [ ] Save file encryption - [ ] Multiple save slots UI - [ ] Steam Cloud integration --- ## Game-Agnostic Checklist Verification that StorageModule follows game-agnostic principles: - [x] ✅ No mentions of "train", "drone", "expedition", "factory" - [x] ✅ No mentions of "Mobile Command", "WarFactory" - [x] ✅ Pure pub/sub communication (no direct module coupling) - [x] ✅ Configuration via JSON (no hardcoded behavior) - [x] ✅ Comments explain MC AND WF usage - [x] ✅ Independent tests (no game-specific data) - [x] ✅ Hot-reload state preservation - [x] ✅ GroveEngine `IModule` interface compliance --- ## Integration with GroveEngine ### Module Loading ```cpp // In main.cpp or engine initialization auto storageLoader = std::make_unique(); storageLoader->load("build/StorageModule.dll", "StorageModule"); // Configure auto config = loadConfig("config/storage.json"); auto module = storageLoader->getModule(); module->setConfiguration(*config, io, scheduler); ``` ### Hot-Reload Workflow ```bash # 1. Game running with StorageModule loaded ./build/mobilecommand.exe # 2. Edit StorageModule.cpp # 3. Rebuild module only (fast) cmake --build build --target StorageModule # 4. Module auto-reloads (state preserved) # - Auto-save timer continues from previous value # - No data loss ``` --- ## Troubleshooting ### "Save directory not found" **Cause**: `data/saves/` doesn't exist **Fix**: Module creates it automatically on startup. Check permissions. ### "Auto-save not triggering" **Cause**: `deltaTime` not provided in `process()` input **Fix**: Ensure GameModule passes `deltaTime` in process input: ```cpp auto input = std::make_unique("input"); input->setDouble("deltaTime", deltaTime); ``` ### "Module states not collected" **Cause**: Modules not responding to `storage:collect_states` **Fix**: Verify modules subscribe to the topic and publish `storage:module_state` ### "Load fails with 'Invalid JSON'" **Cause**: Corrupted save file **Fix**: Check file manually, restore from backup auto-save --- ## References - **Architecture**: `C:\Users\alexi\Documents\projects\mobilecommand\ARCHITECTURE.md` - **GroveEngine IModule**: `external/GroveEngine/include/grove/IModule.h` - **GroveEngine IIO**: `external/GroveEngine/include/grove/IIO.h` - **Prototype Plan**: `plans/PROTOTYPE_PLAN.md` (lines 158-207) --- ## Summary StorageModule is a **production-ready, game-agnostic save/load system** that: 1. ✅ Works for Mobile Command NOW 2. ✅ Will work for WarFactory LATER (zero code changes) 3. ✅ Uses pure pub/sub (no coupling) 4. ✅ Supports hot-reload (dev velocity) 5. ✅ Has independent tests (quality assurance) 6. ✅ Follows GroveEngine patterns (architecture compliance) **Next Steps**: Integrate with GameModule and test save/load during gameplay.