project-mobile-command/docs/STORAGEMODULE_PUBSUB_FLOW.txt
StillHammer 0953451fea Implement 7 modules: 4 core (game-agnostic) + 3 MC-specific
Core Modules (game-agnostic, reusable for WarFactory):
- ResourceModule: Inventory, crafting system (465 lines)
- StorageModule: Save/load with pub/sub state collection (424 lines)
- CombatModule: Combat resolver, damage/armor/morale (580 lines)
- EventModule: JSON event scripting with choices/outcomes (651 lines)

MC-Specific Modules:
- GameModule v2: State machine + event subscriptions (updated)
- TrainBuilderModule: 3 wagons, 2-axis balance, performance malus (530 lines)
- ExpeditionModule: A→B expeditions, team management, events integration (641 lines)

Features:
- All modules hot-reload compatible (state preservation)
- Pure pub/sub architecture (zero direct coupling)
- 7 config files (resources, storage, combat, events, train, expeditions)
- 7 test suites (GameModuleTest: 12/12 PASSED)
- CMakeLists.txt updated for all modules + tests

Total: ~3,500 lines of production code + comprehensive tests

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-02 16:40:54 +08:00

276 lines
16 KiB
Plaintext

═══════════════════════════════════════════════════════════════════════
StorageModule - Pub/Sub State Collection Flow (Game-Agnostic)
═══════════════════════════════════════════════════════════════════════
SAVE OPERATION FLOW
═══════════════════════════════════════════════════════════════════════
┌──────────────┐
│ User/Game │
└──────┬───────┘
│ (1) User presses F5 (quick save)
┌─────────────────────────────────────────────────────────────────────┐
│ GameModule │
│ │
│ auto request = make_unique<JsonDataNode>("request"); │
│ request->setString("filename", "quicksave"); │
│ io->publish("game:request_save", move(request)); │
└────────────────────────────┬─────────────────────────────────────────┘
│ Topic: "game:request_save"
│ Payload: {filename: "quicksave"}
┌─────────────────────────────────────────────────────────────────────┐
│ StorageModule │
│ │
│ void onRequestSave(const IDataNode& data) { │
│ collectModuleStates(); // Broadcast state request │
│ } │
└────────────────────────────┬─────────────────────────────────────────┘
│ (2) Broadcast to ALL modules
┌───────────────────────────────────────┐
│ Topic: "storage:collect_states" │
│ Payload: {action: "collect_state"} │
└───────────┬───────┬───────┬───────────┘
│ │ │
┌──────────┘ │ └──────────┐
▼ ▼ ▼
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ ResourceModule │ │ TrainModule │ │ GameModule │
│ │ │ │ │ │
│ (subscribed) │ │ (subscribed) │ │ (subscribed) │
└────────┬────────┘ └────────┬────────┘ └────────┬────────┘
│ │ │
│ (3) Each module sends its state │
│ │ │
│ Topic: "storage:module_state" │
│ Payload: {moduleName: "ResourceModule", │
│ inventory: {...}} │
│ │ │
└────────────────────┼────────────────────┘
┌─────────────────────────────────────────────────────────────────────┐
│ StorageModule │
│ │
│ void onModuleState(const IDataNode& data) { │
│ string moduleName = data.getString("moduleName"); │
│ m_collectedStates[moduleName] = move(stateNode); │
│ } │
│ │
│ // After collection complete: │
│ m_collectedStates = { │
│ "ResourceModule": {inventory: {...}, craftQueue: [...]}, │
│ "TrainModule": {wagons: [...], balance: {...}}, │
│ "GameModule": {frameCount: 54321, gameState: "InGame"} │
│ } │
└────────────────────────────┬─────────────────────────────────────────┘
│ (4) Aggregate and serialize to JSON
┌───────────────┐
│ Save File │
│ quicksave │
│ .json │
└───────┬───────┘
│ (5) Notify completion
┌──────────────────────────────────┐
│ Topic: "storage:save_complete" │
│ Payload: {filename, timestamp} │
└──────────────┬───────────────────┘
┌──────────────┐
│ GameModule │
│ (displays │
│ "Saved!") │
└──────────────┘
LOAD OPERATION FLOW
═══════════════════════════════════════════════════════════════════════
┌──────────────┐
│ User/Game │
└──────┬───────┘
│ (1) User selects "Load Game"
┌─────────────────────────────────────────────────────────────────────┐
│ GameModule │
│ │
│ auto request = make_unique<JsonDataNode>("request"); │
│ request->setString("filename", "quicksave"); │
│ io->publish("game:request_load", move(request)); │
└────────────────────────────┬─────────────────────────────────────────┘
│ Topic: "game:request_load"
│ Payload: {filename: "quicksave"}
┌─────────────────────────────────────────────────────────────────────┐
│ StorageModule │
│ │
│ void onRequestLoad(const IDataNode& data) { │
│ loadGame(filename); │
│ } │
│ │
│ // Read and parse JSON file: │
│ savedData = { │
│ version: "0.1.0", │
│ modules: { │
│ "ResourceModule": {...}, │
│ "TrainModule": {...}, │
│ "GameModule": {...} │
│ } │
│ } │
└────────────────────────────┬─────────────────────────────────────────┘
│ (2) Publish restore for EACH module
┌───────────────┼───────────────┐
▼ ▼ ▼
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ Topic: │ │ Topic: │ │ Topic: │
│ "storage: │ │ "storage: │ │ "storage: │
│ restore_state: │ │ restore_state: │ │ restore_state: │
│ ResourceModule" │ │ TrainModule" │ │ GameModule" │
│ │ │ │ │ │
│ Payload: │ │ Payload: │ │ Payload: │
│ {inventory: {...}│ │ {wagons: [...], │ │ {frameCount: │
│ craftQueue: [..]│ │ balance: {...}} │ │ 54321, ...} │
└────────┬─────────┘ └────────┬─────────┘ └────────┬─────────┘
│ │ │
│ (3) Each module restores its own state │
│ │ │
▼ ▼ ▼
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ ResourceModule │ │ TrainModule │ │ GameModule │
│ │ │ │ │ │
│ setState(data) │ │ setState(data) │ │ setState(data) │
│ │ │ │ │ │
│ m_inventory = │ │ m_wagons = │ │ m_frameCount = │
│ {scrap: 150} │ │ [loco, cargo] │ │ 54321 │
└─────────────────┘ └─────────────────┘ └─────────────────┘
│ (4) Notify completion
┌──────────────────────────────────┐
│ Topic: "storage:load_complete" │
│ Payload: {filename, version} │
└──────────────┬───────────────────┘
┌──────────────┐
│ GameModule │
│ (displays │
│ "Loaded!") │
└──────────────┘
KEY ARCHITECTURAL PRINCIPLES
═══════════════════════════════════════════════════════════════════════
1. DECOUPLING
- StorageModule never directly calls other modules
- Modules never directly call StorageModule
- All communication via IIO pub/sub
2. GAME-AGNOSTIC
- StorageModule doesn't know what "train" or "factory" means
- It just collects/restores arbitrary JSON data
- Same code works for Mobile Command AND WarFactory
3. SCALABILITY
- Add new module? It auto-participates if it subscribes
- Remove module? Save/load still works
- No code changes in StorageModule
4. MODULE RESPONSIBILITY
- Each module knows how to serialize its OWN state
- StorageModule just orchestrates the collection
- Separation of concerns
5. TOPIC NAMING CONVENTION
- "game:*" = User-initiated actions (save, load, quit)
- "storage:*" = Storage system events (save_complete, collect_states)
- "storage:restore_state:{name}" = Module-specific restore
MOBILE COMMAND EXAMPLE
═══════════════════════════════════════════════════════════════════════
Module: TrainBuilderModule
Subscribe:
- "storage:collect_states"
- "storage:restore_state:TrainBuilderModule"
On "storage:collect_states":
auto state = make_unique<JsonDataNode>("state");
state->setString("moduleName", "TrainBuilderModule");
// Serialize train
for (auto& wagon : m_wagons) {
// ... add wagon data ...
}
io->publish("storage:module_state", move(state));
On "storage:restore_state:TrainBuilderModule":
// Restore train from data
m_wagons.clear();
auto wagonsNode = data.getChild("wagons");
for (auto& wagonData : wagonsNode) {
Wagon wagon;
wagon.type = wagonData.getString("type");
wagon.health = wagonData.getInt("health");
m_wagons.push_back(wagon);
}
WARFACTORY EXAMPLE (Future)
═══════════════════════════════════════════════════════════════════════
Module: ProductionModule
Subscribe:
- "storage:collect_states"
- "storage:restore_state:ProductionModule"
On "storage:collect_states":
auto state = make_unique<JsonDataNode>("state");
state->setString("moduleName", "ProductionModule");
// Serialize production lines
for (auto& line : m_productionLines) {
// ... add line data ...
}
io->publish("storage:module_state", move(state));
On "storage:restore_state:ProductionModule":
// Restore production lines from data
m_productionLines.clear();
auto linesNode = data.getChild("productionLines");
for (auto& lineData : linesNode) {
ProductionLine line;
line.recipe = lineData.getString("recipe");
line.progress = lineData.getDouble("progress");
m_productionLines.push_back(line);
}
SAME StorageModule.cpp CODE FOR BOTH GAMES!
═══════════════════════════════════════════════════════════════════════