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>
276 lines
16 KiB
Plaintext
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!
|
|
═══════════════════════════════════════════════════════════════════════
|
|
|