feat(IIO)!: BREAKING CHANGE - Callback-based message dispatch

## Breaking Change

IIO API redesigned from manual pull+if-forest to callback dispatch.
All modules must update their subscribe() calls to pass handlers.

### Before (OLD API)
```cpp
io->subscribe("input:mouse");

void process(...) {
    while (io->hasMessages()) {
        auto msg = io->pullMessage();
        if (msg.topic == "input:mouse") {
            handleMouse(msg);
        } else if (msg.topic == "input:keyboard") {
            handleKeyboard(msg);
        }
    }
}
```

### After (NEW API)
```cpp
io->subscribe("input:mouse", [this](const Message& msg) {
    handleMouse(msg);
});

void process(...) {
    while (io->hasMessages()) {
        io->pullAndDispatch();  // Callbacks invoked automatically
    }
}
```

## Changes

**Core API (include/grove/IIO.h)**
- Added: `using MessageHandler = std::function<void(const Message&)>`
- Changed: `subscribe()` now requires `MessageHandler` callback parameter
- Changed: `subscribeLowFreq()` now requires `MessageHandler` callback
- Removed: `pullMessage()`
- Added: `pullAndDispatch()` - pulls and auto-dispatches to handlers

**Implementation (src/IntraIO.cpp)**
- Store callbacks in `Subscription.handler`
- `pullAndDispatch()` matches topic against ALL subscriptions (not just first)
- Fixed: Regex pattern compilation supports both wildcards (*) and regex (.*)
- Performance: ~1000 msg/s throughput (unchanged from before)

**Files Updated**
- 31 test/module files migrated to callback API (via parallel agents)
- 8 documentation files updated (DEVELOPER_GUIDE, USER_GUIDE, module READMEs)

## Bugs Fixed During Migration

1. **pullAndDispatch() early return bug**: Was only calling FIRST matching handler
   - Fix: Loop through ALL subscriptions, invoke all matching handlers

2. **Regex pattern compilation bug**: Pattern "player:.*" failed to match
   - Fix: Detect ".*" in pattern → use as regex, otherwise escape and convert wildcards

## Testing

 test_11_io_system: PASSED (IIO pub/sub, pattern matching, batching)
 test_threaded_module_system: 6/6 PASSED
 test_threaded_stress: 5/5 PASSED (50 modules, 100x reload, concurrent ops)
 test_12_datanode: PASSED
 10 TopicTree scenarios: 10/10 PASSED
 benchmark_e2e: ~1000 msg/s throughput

Total: 23+ tests passing

## Performance Impact

No performance regression from callback dispatch:
- IIO throughput: ~1000 msg/s (same as before)
- ThreadedModuleSystem: Speedup ~1.0x (barrier pattern expected)

## Migration Guide

For all modules using IIO:

1. Update subscribe() calls to include handler lambda
2. Replace pullMessage() loops with pullAndDispatch()
3. Move topic-specific logic from if-forest into callbacks

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
StillHammer 2026-01-19 14:19:27 +07:00
parent aefd7921b2
commit 1b7703f07b
33 changed files with 905 additions and 728 deletions

View File

@ -115,10 +115,38 @@ GroveEngine uses a **module-based architecture** with hot-reload support:
| Component | Purpose | Documentation |
|-----------|---------|---------------|
| **IModule** | Module interface | [USER_GUIDE.md](USER_GUIDE.md#imodule) |
| **IIO** | Pub/Sub messaging | [USER_GUIDE.md](USER_GUIDE.md#iio) |
| **IIO** | Pull-based pub/sub with callback dispatch | [USER_GUIDE.md](USER_GUIDE.md#iio) |
| **IDataNode** | Configuration & data | [USER_GUIDE.md](USER_GUIDE.md#idatanode) |
| **ModuleLoader** | Hot-reload system | [USER_GUIDE.md](USER_GUIDE.md#moduleloader) |
#### IIO Callback Dispatch Pattern
GroveEngine uses a **pull-based callback dispatch** pattern for message processing:
```cpp
// OLD API (deprecated):
// io->subscribe("topic:pattern");
// while (io->hasMessages()) {
// auto msg = io->pullMessage();
// if (msg.topic == "topic:pattern") { /* handle */ }
// }
// NEW API (callback-based):
io->subscribe("topic:pattern", [this](const Message& msg) {
// Handle message - no if-forest needed
});
while (io->hasMessages()) {
io->pullAndDispatch(); // Callbacks invoked automatically
}
```
**Key advantages:**
- **No if-forest dispatch**: Register handlers at subscription, not in process loop
- **Module controls WHEN**: Pull-based processing for deterministic ordering
- **Callbacks handle HOW**: Clean separation of concerns
- **Thread-safe**: Callbacks invoked in module's thread context
---
## Available Modules
@ -252,22 +280,19 @@ uiModule->setConfiguration(uiConfig, uiIO.get(), nullptr);
```
```cpp
// In your game module - subscribe to button events
gameIO->subscribe("ui:click");
gameIO->subscribe("ui:action");
// In process()
while (gameIO->hasMessages() > 0) {
auto msg = gameIO->pullMessage();
if (msg.topic == "ui:action") {
// In your game module - subscribe to button events with callbacks (in setConfiguration)
gameIO->subscribe("ui:action", [this](const grove::Message& msg) {
std::string action = msg.data->getString("action", "");
std::string widgetId = msg.data->getString("widgetId", "");
if (action == "start_game" && widgetId == "play_button") {
startGame();
}
}
});
// In process() - pull and dispatch to callbacks
while (gameIO->hasMessages() > 0) {
gameIO->pullAndDispatch(); // Callback invoked automatically
}
```
@ -352,18 +377,11 @@ JsonDataNode input("input");
inputModule->process(input);
```
#### Consuming Input Events
#### Consuming Input Events with Callbacks
```cpp
// Subscribe to input topics
gameIO->subscribe("input:mouse:button");
gameIO->subscribe("input:keyboard:key");
// In process()
while (gameIO->hasMessages() > 0) {
auto msg = gameIO->pullMessage();
if (msg.topic == "input:mouse:button") {
// Subscribe to input topics with callback handlers (in setConfiguration)
gameIO->subscribe("input:mouse:button", [this](const grove::Message& msg) {
int button = msg.data->getInt("button", 0); // 0=left, 1=middle, 2=right
bool pressed = msg.data->getBool("pressed", false);
double x = msg.data->getDouble("x", 0.0);
@ -373,16 +391,20 @@ while (gameIO->hasMessages() > 0) {
// Left mouse button pressed at (x, y)
handleClick(x, y);
}
}
});
if (msg.topic == "input:keyboard:key") {
gameIO->subscribe("input:keyboard:key", [this](const grove::Message& msg) {
int scancode = msg.data->getInt("scancode", 0); // SDL_SCANCODE_*
bool pressed = msg.data->getBool("pressed", false);
if (scancode == SDL_SCANCODE_SPACE && pressed) {
playerJump();
}
}
});
// In process() - pull and auto-dispatch to callbacks
while (gameIO->hasMessages() > 0) {
gameIO->pullAndDispatch(); // Callbacks invoked automatically
}
```
@ -687,25 +709,28 @@ public:
grove::ITaskScheduler* scheduler) override {
m_io = io;
// Subscribe to UI events
m_io->subscribe("ui:action");
m_io->subscribe("ui:click");
// Subscribe to UI events with callback handlers
m_io->subscribe("ui:action", [this](const grove::Message& msg) {
std::string action = msg.data->getString("action", "");
if (action == "start_game") {
startGame();
}
});
m_io->subscribe("ui:click", [this](const grove::Message& msg) {
std::string widgetId = msg.data->getString("widgetId", "");
double x = msg.data->getDouble("x", 0.0);
double y = msg.data->getDouble("y", 0.0);
handleClick(widgetId, x, y);
});
}
void process(const grove::IDataNode& input) override {
double deltaTime = input.getDouble("deltaTime", 0.016);
// Process UI events
// Process UI events - pull and auto-dispatch to callbacks
while (m_io->hasMessages() > 0) {
auto msg = m_io->pullMessage();
if (msg.topic == "ui:action") {
std::string action = msg.data->getString("action", "");
if (action == "start_game") {
startGame();
}
}
m_io->pullAndDispatch(); // Callbacks invoked automatically
}
// Update game logic
@ -905,23 +930,34 @@ io->subscribeLowFreq("analytics:*", config);
#### Request-Response Pattern
```cpp
// Module A: Request pathfinding
// Module A: Subscribe to response first (in setConfiguration)
moduleA_io->subscribe("pathfinding:response", [this](const grove::Message& msg) {
std::string requestId = msg.data->getString("requestId", "");
// ... apply path result ...
});
// Module A: Request pathfinding (in process)
auto request = std::make_unique<JsonDataNode>("request");
request->setString("requestId", "path_123");
request->setDouble("startX", 10.0);
request->setDouble("startY", 20.0);
io->publish("pathfinding:request", std::move(request));
moduleA_io->publish("pathfinding:request", std::move(request));
// Module B: Respond with path
moduleB_io->subscribe("pathfinding:request");
// ... compute path ...
auto response = std::make_unique<JsonDataNode>("response");
response->setString("requestId", "path_123");
// ... add path data ...
moduleB_io->publish("pathfinding:response", std::move(response));
// Module B: Subscribe to request (in setConfiguration)
moduleB_io->subscribe("pathfinding:request", [this](const grove::Message& msg) {
std::string requestId = msg.data->getString("requestId", "");
// ... compute path ...
// Module A: Receive response
moduleA_io->subscribe("pathfinding:response");
auto response = std::make_unique<JsonDataNode>("response");
response->setString("requestId", requestId);
// ... add path data ...
m_io->publish("pathfinding:response", std::move(response));
});
// Module A/B: In process() - pull and dispatch
while (io->hasMessages() > 0) {
io->pullAndDispatch(); // Callbacks invoked automatically
}
```
#### Event Aggregation
@ -932,8 +968,15 @@ io->publish("combat:damage", damageData);
io->publish("combat:kill", killData);
io->publish("combat:levelup", levelupData);
// Analytics module aggregates all combat events
analyticsIO->subscribe("combat:*");
// Analytics module aggregates all combat events (in setConfiguration)
analyticsIO->subscribe("combat:*", [this](const grove::Message& msg) {
aggregateCombatEvent(msg);
});
// In process()
while (analyticsIO->hasMessages() > 0) {
analyticsIO->pullAndDispatch(); // Callback invoked for each event
}
```
### Testing Strategies
@ -978,12 +1021,24 @@ ldd build/modules/GameLogic.so
#### IIO messages not received
```cpp
// Verify subscription BEFORE publishing
io->subscribe("render:sprite"); // Must be before publish
// Verify subscription with callback BEFORE publishing (in setConfiguration)
io->subscribe("render:sprite", [this](const grove::Message& msg) {
handleSprite(msg);
});
// Check topic patterns
io->subscribe("render:*"); // Matches render:sprite, render:text
io->subscribe("render:sprite:*"); // Only matches render:sprite:batch
io->subscribe("render:*", [this](const grove::Message& msg) {
// Matches render:sprite, render:text, etc.
});
io->subscribe("render:sprite:*", [this](const grove::Message& msg) {
// Only matches render:sprite:batch, render:sprite:add, etc.
});
// Remember to pullAndDispatch in process()
while (io->hasMessages() > 0) {
io->pullAndDispatch();
}
```
#### Hot-reload state loss

View File

@ -31,8 +31,10 @@ This is **intentional** to maintain the IIO-based architecture where all communi
**Example:**
```cpp
// Slider value changed
if (msg.topic == "ui:value_changed" && widgetId == "volume_slider") {
// Subscribe to slider value changes (in setConfiguration)
gameIO->subscribe("ui:value_changed", [this](const grove::Message& msg) {
std::string widgetId = msg.data->getString("widgetId", "");
if (widgetId == "volume_slider") {
double value = msg.data->getDouble("value", 0);
setVolume(value);
@ -41,6 +43,12 @@ if (msg.topic == "ui:value_changed" && widgetId == "volume_slider") {
updateMsg->setString("id", "volume_label");
updateMsg->setString("text", "Volume: " + std::to_string((int)value) + "%");
m_io->publish("ui:set_text", std::move(updateMsg));
}
});
// In process()
while (gameIO->hasMessages() > 0) {
gameIO->pullAndDispatch(); // Callback invoked automatically
}
```
@ -94,8 +102,9 @@ Each module runs in its own thread:
void uiThread() {
while(running) {
// Receive inputs from queue (filled by InputModule thread)
// Callbacks registered at subscribe() handle dispatch
while(io->hasMessages()) {
handleMessage(io->pullMessage());
io->pullAndDispatch(); // Auto-dispatch to registered callbacks
}
update(deltaTime);
@ -111,8 +120,9 @@ void uiThread() {
void gameThread() {
while(running) {
// Pull messages from queue (latency < 1ms)
// Callbacks registered at subscribe() handle dispatch
while(io->hasMessages()) {
handleMessage(io->pullMessage()); // Already in queue!
io->pullAndDispatch(); // Auto-dispatch, already in queue!
}
updateGameLogic(deltaTime);
@ -337,12 +347,14 @@ These features violate core design principles and will **never** be added:
## Design Principles
1. **IIO-First:** All communication via topics, no direct coupling
2. **Retained Mode:** Cache state, minimize IIO traffic
3. **Hot-Reload Safe:** Full state preservation across reloads
4. **Thread-Safe:** Designed for multi-threaded production use
5. **Module Independence:** UIModule never imports BgfxRenderer or InputModule headers
6. **Game Logic Separation:** Widgets are dumb views, game modules handle logic
1. **IIO-First:** All communication via topics with callback dispatch, no direct coupling
2. **Callback Dispatch:** Subscribe with handlers, no if-forest dispatch in process()
3. **Pull-Based Control:** Module controls WHEN to process (pullAndDispatch), callbacks handle HOW
4. **Retained Mode:** Cache state, minimize IIO traffic
5. **Hot-Reload Safe:** Full state preservation across reloads
6. **Thread-Safe:** Designed for multi-threaded production use
7. **Module Independence:** UIModule never imports BgfxRenderer or InputModule headers
8. **Game Logic Separation:** Widgets are dumb views, game modules handle logic
## Integration with Other Modules

View File

@ -221,20 +221,22 @@ uiIO->publish("input:text", std::move(textInput));
### Event Logging
```cpp
while (uiIO->hasMessages() > 0) {
auto msg = uiIO->pullMessage();
if (msg.topic == "ui:click") {
// Subscribe to UI events with callbacks (during setup)
uiIO->subscribe("ui:click", [&clickCount, &eventLog](const grove::Message& msg) {
clickCount++;
std::string widgetId = msg.data->getString("widgetId", "");
eventLog.add("🖱️ Click: " + widgetId);
}
else if (msg.topic == "ui:action") {
});
uiIO->subscribe("ui:action", [&actionCount, &eventLog](const grove::Message& msg) {
actionCount++;
std::string action = msg.data->getString("action", "");
eventLog.add("⚡ Action: " + action);
}
// ... handle other events
});
// In main loop - pull and dispatch to callbacks
while (uiIO->hasMessages() > 0) {
uiIO->pullAndDispatch(); // Callbacks invoked automatically
}
```

View File

@ -57,31 +57,28 @@ See [UI Rendering Documentation](UI_RENDERING.md) for details on retained vs imm
## Usage Examples
### Handling UI Events
### Handling UI Events with Callbacks
```cpp
// Subscribe to UI events
gameIO->subscribe("ui:action");
gameIO->subscribe("ui:value_changed");
// In game loop
while (m_io->hasMessages() > 0) {
auto msg = m_io->pullMessage();
if (msg.topic == "ui:action") {
// Subscribe to UI events with callback handlers (in setConfiguration)
gameIO->subscribe("ui:action", [this](const grove::Message& msg) {
std::string action = msg.data->getString("action", "");
if (action == "start_game") {
startGame();
}
}
});
if (msg.topic == "ui:value_changed") {
gameIO->subscribe("ui:value_changed", [this](const grove::Message& msg) {
std::string widgetId = msg.data->getString("widgetId", "");
if (widgetId == "volume_slider") {
double value = msg.data->getDouble("value", 50.0);
setVolume(value);
}
}
});
// In game loop (process method)
while (m_io->hasMessages() > 0) {
m_io->pullAndDispatch(); // Callbacks invoked automatically
}
```
@ -112,7 +109,10 @@ m_io->publish("ui:set_value", std::move(msg));
Common pattern: update a label when a slider changes.
```cpp
if (msg.topic == "ui:value_changed" && widgetId == "volume_slider") {
// Subscribe to slider value changes (in setConfiguration)
gameIO->subscribe("ui:value_changed", [this](const grove::Message& msg) {
std::string widgetId = msg.data->getString("widgetId", "");
if (widgetId == "volume_slider") {
double value = msg.data->getDouble("value", 50.0);
setVolume(value);
@ -121,5 +121,11 @@ if (msg.topic == "ui:value_changed" && widgetId == "volume_slider") {
updateMsg->setString("id", "volume_label");
updateMsg->setString("text", "Volume: " + std::to_string((int)value) + "%");
m_io->publish("ui:set_text", std::move(updateMsg));
}
});
// In process()
while (gameIO->hasMessages() > 0) {
gameIO->pullAndDispatch(); // Callback invoked automatically
}
```

View File

@ -34,7 +34,7 @@ GroveEngine provides:
- Modules contain pure business logic (200-300 lines recommended)
- No infrastructure code in modules (threading, networking, persistence)
- All data via `IDataNode` abstraction (backend agnostic)
- Pull-based message processing (modules control when they read messages)
- Pull-based message processing with callback dispatch (modules control WHEN to process, callbacks handle HOW)
---
@ -57,10 +57,10 @@ Hierarchical data structure for configuration, state, and messages. Supports:
### IIO
Pub/Sub communication interface:
Pull-based pub/sub communication with callback dispatch:
- `publish()`: Send messages to topics
- `subscribe()`: Listen to topic patterns
- `pullMessage()`: Consume received messages
- `subscribe()`: Register callback handler for topic pattern
- `pullAndDispatch()`: Pull and auto-dispatch message to handler
### ModuleLoader
@ -257,11 +257,9 @@ void MyModule::process(const grove::IDataNode& input) {
// Your processing logic here
m_counter++;
// Process incoming messages
// Process incoming messages (dispatch to registered callbacks)
while (m_io && m_io->hasMessages() > 0) {
auto msg = m_io->pullMessage();
m_logger->debug("Received message on topic: {}", msg.topic);
// Handle message...
m_io->pullAndDispatch(); // Callbacks invoked automatically
}
// Publish events if needed
@ -394,7 +392,20 @@ void destroyModule(grove::IModule* module) {
### IIO Pub/Sub System
Modules communicate via topics using publish/subscribe pattern.
Modules communicate via topics using publish/subscribe pattern with callback dispatch.
#### Key Design: Pull-Based with Callback Dispatch
Unlike traditional push-based systems, IIO gives modules control over **WHEN** to process messages while callbacks handle **HOW** to process them:
1. **Subscribe with Callback** (in `setConfiguration`): Register handlers for topic patterns
2. **Pull and Dispatch** (in `process`): Module controls when to process - callbacks invoked automatically
**Benefits:**
- **No if-forest dispatch**: Logic registered at subscription, not scattered in process()
- **Module controls timing**: Pull-based means deterministic frame ordering
- **Thread-safe**: Callbacks invoked in module's own thread context
- **Clean separation**: Subscription setup vs. message processing
#### Publishing Messages
@ -411,7 +422,7 @@ void MyModule::process(const grove::IDataNode& input) {
}
```
#### Subscribing to Topics
#### Subscribing to Topics with Callbacks
```cpp
void MyModule::setConfiguration(const grove::IDataNode& configNode,
@ -419,31 +430,40 @@ void MyModule::setConfiguration(const grove::IDataNode& configNode,
grove::ITaskScheduler* scheduler) {
m_io = io;
// Subscribe to specific topic
m_io->subscribe("game:player:*");
// Subscribe to specific topic with callback handler
m_io->subscribe("game:player:position", [this](const grove::Message& msg) {
double x = msg.data->getDouble("x", 0.0);
double y = msg.data->getDouble("y", 0.0);
// Handle position update...
});
// Subscribe with wildcard pattern
m_io->subscribe("game:player:*", [this](const grove::Message& msg) {
handlePlayerEvent(msg);
});
// Subscribe with low-frequency batching (for non-critical updates)
grove::SubscriptionConfig config;
config.batchInterval = 1000; // 1 second batches
m_io->subscribeLowFreq("analytics:*", config);
m_io->subscribeLowFreq("analytics:*", [this](const grove::Message& msg) {
processBatchedAnalytics(msg);
}, config);
}
```
#### Processing Messages
#### Processing Messages with Callback Dispatch
```cpp
void MyModule::process(const grove::IDataNode& input) {
// Pull-based: module controls when to process messages
// Pull-based: module controls WHEN to process messages
// Callbacks registered at subscribe() handle HOW to process
while (m_io->hasMessages() > 0) {
grove::Message msg = m_io->pullMessage();
if (msg.topic == "game:player:position") {
double x = msg.data->getDouble("x", 0.0);
double y = msg.data->getDouble("y", 0.0);
// Handle position update...
}
m_io->pullAndDispatch(); // Automatically invokes registered callback
}
}
// No more if-forest dispatch - callbacks were registered at subscription:
// subscribe("game:player:position", [this](const Message& msg) { ... });
```
### Topic Patterns
@ -670,10 +690,10 @@ void MyModule::process(const grove::IDataNode& input) {
| Method | Description |
|--------|-------------|
| `publish(topic, data)` | Publish message to topic |
| `subscribe(pattern, config)` | Subscribe to topic pattern |
| `subscribeLowFreq(pattern, config)` | Subscribe with batching |
| `subscribe(pattern, handler, config)` | Subscribe with callback handler |
| `subscribeLowFreq(pattern, handler, config)` | Subscribe with batching and callback |
| `hasMessages()` | Count of pending messages |
| `pullMessage()` | Consume one message |
| `pullAndDispatch()` | Pull and auto-dispatch message to handler |
| `getHealth()` | Get IO health metrics |
### IModule

View File

@ -48,15 +48,28 @@ struct IOHealth {
};
/**
* @brief Pub/Sub communication interface with pull-based synchronous design
* @brief Message handler callback type
*
* Pull-based pub/sub system optimized for game modules. Modules have full control
* over when they process messages, avoiding threading issues.
* Callback invoked when a message matching the subscribed pattern is pulled.
* Module implements this to handle specific message types without if-forest dispatch.
*/
using MessageHandler = std::function<void(const Message&)>;
/**
* @brief Pub/Sub communication interface with pull-based callback dispatch
*
* Pull-based pub/sub system with automatic message dispatch to registered handlers.
* Modules subscribe with callbacks, then pull messages - dispatch is automatic.
*
* Design:
* - Modules retain control over WHEN to process (pull-based)
* - No if-forest dispatch (callbacks registered at subscription)
* - Thread-safe for multi-threaded module systems
*
* Features:
* - Topic patterns with wildcards (e.g., "player:*", "economy:*")
* - Low-frequency subscriptions for bandwidth optimization
* - Message consumption (pull removes message from queue)
* - Automatic callback dispatch on pull
* - Engine health monitoring for backpressure management
*/
class IIO {
@ -71,18 +84,38 @@ public:
virtual void publish(const std::string& topic, std::unique_ptr<IDataNode> message) = 0;
/**
* @brief Subscribe to topic pattern (high-frequency)
* @brief Subscribe to topic pattern with callback handler (high-frequency)
* @param topicPattern Topic pattern with wildcards (e.g., "player:*")
* @param handler Callback invoked when matching message is pulled
* @param config Optional subscription configuration
*
* Example:
* io->subscribe("input:mouse", [this](const Message& msg) {
* handleMouseInput(msg);
* });
*/
virtual void subscribe(const std::string& topicPattern, const SubscriptionConfig& config = {}) = 0;
virtual void subscribe(
const std::string& topicPattern,
MessageHandler handler,
const SubscriptionConfig& config = {}
) = 0;
/**
* @brief Subscribe to topic pattern (low-frequency batched)
* @brief Subscribe to topic pattern with callback (low-frequency batched)
* @param topicPattern Topic pattern with wildcards
* @param handler Callback invoked when matching message is pulled
* @param config Subscription configuration (batchInterval, etc.)
*
* Example:
* io->subscribeLowFreq("analytics:*", [this](const Message& msg) {
* processBatchedAnalytics(msg);
* }, {.batchInterval = 5000});
*/
virtual void subscribeLowFreq(const std::string& topicPattern, const SubscriptionConfig& config = {}) = 0;
virtual void subscribeLowFreq(
const std::string& topicPattern,
MessageHandler handler,
const SubscriptionConfig& config = {}
) = 0;
/**
* @brief Get count of pending messages
@ -91,11 +124,18 @@ public:
virtual int hasMessages() const = 0;
/**
* @brief Pull and consume one message
* @return Message from queue (oldest first). Message is removed from queue.
* @brief Pull and auto-dispatch one message to registered handler
* @throws std::runtime_error if no messages available
*
* Pulls oldest message from queue and invokes the callback registered
* during subscribe(). Message is consumed (removed from queue).
*
* Example usage:
* while (io->hasMessages() > 0) {
* io->pullAndDispatch(); // Callbacks invoked automatically
* }
*/
virtual Message pullMessage() = 0;
virtual void pullAndDispatch() = 0;
/**
* @brief Get IO health status for Engine monitoring

View File

@ -66,6 +66,7 @@ private:
struct Subscription {
std::regex pattern;
std::string originalPattern;
MessageHandler handler; // Callback for this subscription
SubscriptionConfig config;
std::chrono::high_resolution_clock::time_point lastBatch;
std::unordered_map<std::string, Message> batchedMessages; // For replaceable messages
@ -74,7 +75,7 @@ private:
// Default constructor
Subscription() = default;
// Move-only (Message contains unique_ptr)
// Move-only (Message contains unique_ptr, handler is copyable)
Subscription(Subscription&&) = default;
Subscription& operator=(Subscription&&) = default;
Subscription(const Subscription&) = delete;
@ -113,10 +114,10 @@ public:
// IIO implementation
void publish(const std::string& topic, std::unique_ptr<IDataNode> message) override;
void subscribe(const std::string& topicPattern, const SubscriptionConfig& config = {}) override;
void subscribeLowFreq(const std::string& topicPattern, const SubscriptionConfig& config = {}) override;
void subscribe(const std::string& topicPattern, MessageHandler handler, const SubscriptionConfig& config = {}) override;
void subscribeLowFreq(const std::string& topicPattern, MessageHandler handler, const SubscriptionConfig& config = {}) override;
int hasMessages() const override;
Message pullMessage() override;
void pullAndDispatch() override;
IOHealth getHealth() const override;
IOType getType() const override;

View File

@ -8,22 +8,9 @@
namespace grove {
void SceneCollector::setup(IIO* io, uint16_t width, uint16_t height) {
// Subscribe to all render topics (multi-level wildcard .* matches render:sprite AND render:debug:line)
io->subscribe("render:.*");
// Initialize default view with provided dimensions (will be overridden by camera messages)
initDefaultView(width > 0 ? width : 1280, height > 0 ? height : 720);
}
void SceneCollector::collect(IIO* io, float deltaTime) {
m_deltaTime = deltaTime;
m_frameNumber++;
// Pull all pending messages
while (io->hasMessages() > 0) {
Message msg = io->pullMessage();
if (!msg.data) continue;
// Subscribe to all render topics with callback handler
io->subscribe("render:.*", [this](const Message& msg) {
if (!msg.data) return;
// Route message based on topic
// Retained mode (new) - sprites
@ -77,6 +64,19 @@ void SceneCollector::collect(IIO* io, float deltaTime) {
else if (msg.topic == "render:debug:rect") {
parseDebugRect(*msg.data);
}
});
// Initialize default view with provided dimensions (will be overridden by camera messages)
initDefaultView(width > 0 ? width : 1280, height > 0 ? height : 720);
}
void SceneCollector::collect(IIO* io, float deltaTime) {
m_deltaTime = deltaTime;
m_frameNumber++;
// Pull and dispatch all pending messages (callbacks invoked automatically)
while (io->hasMessages() > 0) {
io->pullAndDispatch();
}
}

View File

@ -95,9 +95,20 @@ config.setBool("enableMouse", true);
config.setBool("enableKeyboard", true);
inputModule->setConfiguration(config, inputIO.get(), nullptr);
// Subscribe to events
gameIO->subscribe("input:mouse:button");
gameIO->subscribe("input:keyboard:key");
// Subscribe to events with callback handlers
gameIO->subscribe("input:mouse:button", [this](const grove::Message& msg) {
int button = msg.data->getInt("button", 0);
bool pressed = msg.data->getBool("pressed", false);
double x = msg.data->getDouble("x", 0.0);
double y = msg.data->getDouble("y", 0.0);
handleMouseButton(button, pressed, x, y);
});
gameIO->subscribe("input:keyboard:key", [this](const grove::Message& msg) {
int scancode = msg.data->getInt("scancode", 0);
bool pressed = msg.data->getBool("pressed", false);
handleKeyboard(scancode, pressed);
});
// Main loop
while (running) {
@ -111,15 +122,9 @@ while (running) {
grove::JsonDataNode input("input");
inputModule->process(input);
// 3. Process game logic
// 3. Process game logic - pull and auto-dispatch to callbacks
while (gameIO->hasMessages() > 0) {
auto msg = gameIO->pullMessage();
if (msg.topic == "input:mouse:button") {
int button = msg.data->getInt("button", 0);
bool pressed = msg.data->getBool("pressed", false);
// Handle click...
}
gameIO->pullAndDispatch(); // Callbacks invoked automatically
}
}

View File

@ -37,19 +37,23 @@ config.setString("layoutFile", "./ui/menu.json");
config.setInt("baseLayer", 1000);
uiModule->setConfiguration(config, uiIO.get(), nullptr);
// Subscribe to UI events
gameIO->subscribe("ui:action");
gameIO->subscribe("ui:value_changed");
// Subscribe to UI events with callback handlers
gameIO->subscribe("ui:action", [this](const grove::Message& msg) {
std::string action = msg.data->getString("action", "");
handleAction(action);
});
gameIO->subscribe("ui:value_changed", [this](const grove::Message& msg) {
std::string widgetId = msg.data->getString("widgetId", "");
double value = msg.data->getDouble("value", 0.0);
handleValueChange(widgetId, value);
});
// Game loop
while(running) {
// Handle UI events
// Handle UI events - pull and auto-dispatch to callbacks
while (gameIO->hasMessages() > 0) {
auto msg = gameIO->pullMessage();
if (msg.topic == "ui:action") {
std::string action = msg.data->getString("action", "");
handleAction(action);
}
gameIO->pullAndDispatch(); // Callbacks invoked automatically
}
uiModule->process(deltaTime);

View File

@ -73,54 +73,14 @@ void UIModule::setConfiguration(const IDataNode& config, IIO* io, ITaskScheduler
}
}
// Subscribe to input topics
// Subscribe to input topics with callbacks
if (m_io) {
m_io->subscribe("input:mouse:move");
m_io->subscribe("input:mouse:button");
m_io->subscribe("input:mouse:wheel");
m_io->subscribe("input:keyboard");
m_io->subscribe("ui:load"); // Load new layout
m_io->subscribe("ui:set_value"); // Set widget value
m_io->subscribe("ui:set_visible"); // Show/hide widget
m_io->subscribe("ui:set_text"); // Set widget text (for labels)
}
m_logger->info("UIModule initialized");
}
void UIModule::process(const IDataNode& input) {
float deltaTime = static_cast<float>(input.getDouble("deltaTime", 0.016));
// Begin new frame
m_context->beginFrame();
m_renderer->beginFrame();
// Process input messages from IIO
processInput();
// Update UI logic
updateUI(deltaTime);
// Render UI
renderUI();
m_frameCount++;
}
void UIModule::processInput() {
if (!m_io) return;
while (m_io->hasMessages() > 0) {
auto msg = m_io->pullMessage();
if (msg.topic == "input:mouse:move") {
m_io->subscribe("input:mouse:move", [this](const Message& msg) {
m_context->mouseX = static_cast<float>(msg.data->getDouble("x", 0.0));
m_context->mouseY = static_cast<float>(msg.data->getDouble("y", 0.0));
}
else if (msg.topic == "input:mouse:wheel") {
m_context->mouseWheelDelta = static_cast<float>(msg.data->getDouble("delta", 0.0));
}
else if (msg.topic == "input:mouse:button") {
});
m_io->subscribe("input:mouse:button", [this](const Message& msg) {
bool pressed = msg.data->getBool("pressed", false);
if (pressed && !m_context->mouseDown) {
m_context->mousePressed = true;
@ -129,19 +89,26 @@ void UIModule::processInput() {
m_context->mouseReleased = true;
}
m_context->mouseDown = pressed;
}
else if (msg.topic == "input:keyboard") {
});
m_io->subscribe("input:mouse:wheel", [this](const Message& msg) {
m_context->mouseWheelDelta = static_cast<float>(msg.data->getDouble("delta", 0.0));
});
m_io->subscribe("input:keyboard", [this](const Message& msg) {
m_context->keyPressed = true;
m_context->keyCode = msg.data->getInt("keyCode", 0);
m_context->keyChar = static_cast<char>(msg.data->getInt("char", 0));
}
else if (msg.topic == "ui:load") {
});
m_io->subscribe("ui:load", [this](const Message& msg) {
std::string layoutPath = msg.data->getString("path", "");
if (!layoutPath.empty()) {
loadLayout(layoutPath);
}
}
else if (msg.topic == "ui:set_visible") {
});
m_io->subscribe("ui:set_visible", [this](const Message& msg) {
std::string widgetId = msg.data->getString("id", "");
bool visible = msg.data->getBool("visible", true);
if (m_root) {
@ -149,8 +116,9 @@ void UIModule::processInput() {
widget->visible = visible;
}
}
}
else if (msg.topic == "ui:set_text") {
});
m_io->subscribe("ui:set_text", [this](const Message& msg) {
// Timestamp on receive
auto now = std::chrono::high_resolution_clock::now();
auto micros = std::chrono::duration_cast<std::chrono::microseconds>(now.time_since_epoch()).count();
@ -179,7 +147,37 @@ void UIModule::processInput() {
}
}
}
});
}
m_logger->info("UIModule initialized");
}
void UIModule::process(const IDataNode& input) {
float deltaTime = static_cast<float>(input.getDouble("deltaTime", 0.016));
// Begin new frame
m_context->beginFrame();
m_renderer->beginFrame();
// Process input messages from IIO
processInput();
// Update UI logic
updateUI(deltaTime);
// Render UI
renderUI();
m_frameCount++;
}
void UIModule::processInput() {
if (!m_io) return;
// Pull and dispatch all pending messages (callbacks invoked automatically)
while (m_io->hasMessages() > 0) {
m_io->pullAndDispatch();
}
}

View File

@ -429,10 +429,7 @@ void DebugEngine::processClientMessages() {
for (int j = 0; j < messagesToProcess; ++j) {
try {
auto message = socket->pullMessage();
std::string dataPreview = message.data ? message.data->getData()->toString() : "null";
logger->debug("📩 Client {} message: topic='{}', data present={}",
i, message.topic, message.data != nullptr);
socket->pullAndDispatch();
// TODO: Route message to appropriate module or process it
logger->trace("🚧 TODO: Route client message to modules");
@ -456,9 +453,7 @@ void DebugEngine::processCoordinatorMessages() {
for (int i = 0; i < messagesToProcess; ++i) {
try {
auto message = coordinatorSocket->pullMessage();
logger->debug("📩 Coordinator message: topic='{}', data present={}",
message.topic, message.data != nullptr);
coordinatorSocket->pullAndDispatch();
// TODO: Handle coordinator commands (shutdown, config reload, etc.)
logger->trace("🚧 TODO: Handle coordinator commands");

View File

@ -46,12 +46,13 @@ void IntraIO::publish(const std::string& topic, std::unique_ptr<IDataNode> messa
IntraIOManager::getInstance().routeMessage(instanceId, topic, jsonData);
}
void IntraIO::subscribe(const std::string& topicPattern, const SubscriptionConfig& config) {
void IntraIO::subscribe(const std::string& topicPattern, MessageHandler handler, const SubscriptionConfig& config) {
std::lock_guard<std::mutex> lock(operationMutex);
Subscription sub;
sub.originalPattern = topicPattern;
sub.pattern = compileTopicPattern(topicPattern);
sub.handler = handler; // Store callback
sub.config = config;
sub.lastBatch = std::chrono::high_resolution_clock::now();
@ -61,12 +62,13 @@ void IntraIO::subscribe(const std::string& topicPattern, const SubscriptionConfi
IntraIOManager::getInstance().registerSubscription(instanceId, topicPattern, false);
}
void IntraIO::subscribeLowFreq(const std::string& topicPattern, const SubscriptionConfig& config) {
void IntraIO::subscribeLowFreq(const std::string& topicPattern, MessageHandler handler, const SubscriptionConfig& config) {
std::lock_guard<std::mutex> lock(operationMutex);
Subscription sub;
sub.originalPattern = topicPattern;
sub.pattern = compileTopicPattern(topicPattern);
sub.handler = handler; // Store callback
sub.config = config;
sub.lastBatch = std::chrono::high_resolution_clock::now();
@ -81,24 +83,38 @@ int IntraIO::hasMessages() const {
return static_cast<int>(messageQueue.size() + lowFreqMessageQueue.size());
}
Message IntraIO::pullMessage() {
void IntraIO::pullAndDispatch() {
std::lock_guard<std::mutex> lock(operationMutex);
if (messageQueue.empty() && lowFreqMessageQueue.empty()) {
throw std::runtime_error("No messages available");
}
// Pull message from queue
Message msg;
bool isLowFreq = false;
if (!messageQueue.empty()) {
msg = std::move(messageQueue.front());
messageQueue.pop();
} else {
msg = std::move(lowFreqMessageQueue.front());
lowFreqMessageQueue.pop();
isLowFreq = true;
}
totalPulled++;
return msg;
// Find ALL matching handlers and dispatch to each
const auto& subscriptions = isLowFreq ? lowFreqSubscriptions : highFreqSubscriptions;
for (const auto& sub : subscriptions) {
if (matchesPattern(msg.topic, sub.pattern)) {
// Found matching subscription - invoke handler
if (sub.handler) {
sub.handler(msg);
}
}
}
}
IOHealth IntraIO::getHealth() const {
@ -224,17 +240,26 @@ bool IntraIO::matchesPattern(const std::string& topic, const std::regex& pattern
}
std::regex IntraIO::compileTopicPattern(const std::string& pattern) const {
// Convert wildcard pattern to regex
std::string regexPattern = pattern;
// Patterns can be:
// 1. Simple wildcard: "*" → convert to ".*" regex
// 2. Regex patterns: "player:.*", "test:.*" → use as-is
// Escape special regex characters except *
// If pattern contains ".*" already, assume it's a regex pattern
if (pattern.find(".*") != std::string::npos) {
// Already a regex pattern - use as-is
return std::regex(pattern);
}
// Otherwise, convert simple wildcards to regex
std::string escaped;
for (char c : regexPattern) {
for (char c : pattern) {
if (c == '*') {
// Simple wildcard: convert to regex ".*"
escaped += ".*";
} else if (c == '.' || c == '+' || c == '?' || c == '^' || c == '$' ||
} else if (c == '+' || c == '?' || c == '^' || c == '$' ||
c == '(' || c == ')' || c == '[' || c == ']' || c == '{' ||
c == '}' || c == '|' || c == '\\') {
c == '}' || c == '|' || c == '\\' || c == '.') {
// Escape special regex characters
escaped += '\\';
escaped += c;
} else {

View File

@ -110,17 +110,28 @@ TEST_CASE("IT_014: UIModule Full Integration", "[integration][ui][phase7]") {
std::cout << "⚠️ Renderer not healthy (expected for noop backend), skipping renderer process calls\n";
}
// Subscribe to events we want to verify
gameIO->subscribe("ui:click");
gameIO->subscribe("ui:action");
gameIO->subscribe("ui:value_changed");
gameIO->subscribe("ui:hover");
int clickCount = 0;
int actionCount = 0;
int valueChangeCount = 0;
int hoverCount = 0;
// Subscribe to events we want to verify with callbacks
gameIO->subscribe("ui:click", [&](const Message& msg) {
clickCount++;
});
gameIO->subscribe("ui:action", [&](const Message& msg) {
actionCount++;
});
gameIO->subscribe("ui:value_changed", [&](const Message& msg) {
valueChangeCount++;
});
gameIO->subscribe("ui:hover", [&](const Message& msg) {
bool enter = msg.data->getBool("enter", false);
if (enter) {
hoverCount++;
}
});
// Simulate 60 frames (~1 second at 60fps)
for (int frame = 0; frame < 60; frame++) {
// Simulate mouse movement
@ -169,30 +180,9 @@ TEST_CASE("IT_014: UIModule Full Integration", "[integration][ui][phase7]") {
renderer->process(frameInput);
}
// Check for events
// Dispatch events (callbacks handle counting and logging)
while (gameIO->hasMessages() > 0) {
auto msg = gameIO->pullMessage();
if (msg.topic == "ui:click") {
clickCount++;
std::cout << " Frame " << frame << ": Click event received\n";
}
else if (msg.topic == "ui:action") {
actionCount++;
std::string action = msg.data->getString("action", "");
std::cout << " Frame " << frame << ": Action event: " << action << "\n";
}
else if (msg.topic == "ui:value_changed") {
valueChangeCount++;
std::cout << " Frame " << frame << ": Value changed\n";
}
else if (msg.topic == "ui:hover") {
bool enter = msg.data->getBool("enter", false);
if (enter) {
hoverCount++;
std::cout << " Frame " << frame << ": Hover event\n";
}
}
gameIO->pullAndDispatch();
}
// Small delay to simulate real-time

View File

@ -53,14 +53,22 @@ TEST_CASE("IT_015: UIModule Input Integration", "[integration][input][ui][phase3
REQUIRE_NOTHROW(uiModule->setConfiguration(uiConfig, uiIO.get(), nullptr));
std::cout << "✅ UIModule loaded\n\n";
// Subscribe to events
testIO->subscribe("ui:click");
testIO->subscribe("ui:hover");
testIO->subscribe("ui:action");
int uiClicksReceived = 0;
int uiHoversReceived = 0;
// Subscribe to events with callbacks
testIO->subscribe("ui:click", [&](const Message& msg) {
uiClicksReceived++;
std::cout << "✅ Received ui:click event\n";
});
testIO->subscribe("ui:hover", [&](const Message& msg) {
uiHoversReceived++;
std::cout << "✅ Received ui:hover event\n";
});
testIO->subscribe("ui:action", [](const Message& msg) {
// Just acknowledge action events
});
// Publish input events via IIO (simulates InputModule output)
std::cout << "Publishing input events...\n";
@ -85,16 +93,9 @@ TEST_CASE("IT_015: UIModule Input Integration", "[integration][input][ui][phase3
// Process UIModule again
uiModule->process(inputData);
// Collect UI events
// Dispatch UI events (callbacks handle counting)
while (testIO->hasMessages() > 0) {
auto msg = testIO->pullMessage();
if (msg.topic == "ui:click") {
uiClicksReceived++;
std::cout << "✅ Received ui:click event\n";
} else if (msg.topic == "ui:hover") {
uiHoversReceived++;
std::cout << "✅ Received ui:hover event\n";
}
testIO->pullAndDispatch();
}
std::cout << "\nResults:\n";

View File

@ -24,15 +24,26 @@ TEST_CASE("IT_015_Minimal: IIO Message Publishing", "[integration][input][ui][mi
auto publisher = ioManager.createInstance("publisher");
auto subscriber = ioManager.createInstance("subscriber");
// Subscribe to input events
subscriber->subscribe("input:mouse:move");
subscriber->subscribe("input:mouse:button");
subscriber->subscribe("input:keyboard:key");
int mouseMoveCount = 0;
int mouseButtonCount = 0;
int keyboardKeyCount = 0;
// Subscribe to input events with callbacks
subscriber->subscribe("input:mouse:move", [&](const Message& msg) {
mouseMoveCount++;
int x = msg.data->getInt("x", 0);
int y = msg.data->getInt("y", 0);
std::cout << "✅ Received input:mouse:move (" << x << ", " << y << ")\n";
});
subscriber->subscribe("input:mouse:button", [&](const Message& msg) {
mouseButtonCount++;
std::cout << "✅ Received input:mouse:button\n";
});
subscriber->subscribe("input:keyboard:key", [&](const Message& msg) {
keyboardKeyCount++;
std::cout << "✅ Received input:keyboard:key\n";
});
// Publish input events
std::cout << "Publishing input events...\n";
@ -56,24 +67,9 @@ TEST_CASE("IT_015_Minimal: IIO Message Publishing", "[integration][input][ui][mi
keyData->setBool("pressed", true);
publisher->publish("input:keyboard:key", std::move(keyData));
// Collect messages
// Dispatch messages to trigger callbacks
while (subscriber->hasMessages() > 0) {
auto msg = subscriber->pullMessage();
if (msg.topic == "input:mouse:move") {
mouseMoveCount++;
int x = msg.data->getInt("x", 0);
int y = msg.data->getInt("y", 0);
std::cout << "✅ Received input:mouse:move (" << x << ", " << y << ")\n";
}
else if (msg.topic == "input:mouse:button") {
mouseButtonCount++;
std::cout << "✅ Received input:mouse:button\n";
}
else if (msg.topic == "input:keyboard:key") {
keyboardKeyCount++;
std::cout << "✅ Received input:keyboard:key\n";
}
subscriber->pullAndDispatch();
}
// Verify

View File

@ -225,8 +225,13 @@ int main() {
// ========================================================================
std::cout << "=== TEST 1: Basic Publish-Subscribe ===\n";
// Consumer subscribes to "test:basic"
consumerIO->subscribe("test:basic");
// Count received messages
int receivedCount = 0;
// Consumer subscribes to "test:basic" with callback
consumerIO->subscribe("test:basic", [&](const Message& msg) {
receivedCount++;
});
// Publish 100 messages
for (int i = 0; i < 100; i++) {
@ -240,11 +245,9 @@ int main() {
// Process to allow routing
std::this_thread::sleep_for(std::chrono::milliseconds(10));
// Count received messages
int receivedCount = 0;
// Dispatch messages to trigger callbacks
while (consumerIO->hasMessages() > 0) {
auto msg = consumerIO->pullMessage();
receivedCount++;
consumerIO->pullAndDispatch();
}
ASSERT_EQ(receivedCount, 100, "Should receive all 100 messages");
@ -258,8 +261,15 @@ int main() {
// ========================================================================
std::cout << "=== TEST 2: Pattern Matching ===\n";
// Subscribe to patterns
consumerIO->subscribe("player:.*");
// Count player messages (should match 3 of 4)
int playerMsgCount = 0;
// Subscribe to patterns with callback
consumerIO->subscribe("player:.*", [&](const Message& msg) {
if (msg.topic.find("player:") == 0) {
playerMsgCount++;
}
});
// Publish test messages
std::vector<std::string> testTopics = {
@ -276,13 +286,9 @@ int main() {
std::this_thread::sleep_for(std::chrono::milliseconds(10));
// Count player messages (should match 3 of 4)
int playerMsgCount = 0;
// Dispatch messages to trigger callbacks
while (consumerIO->hasMessages() > 0) {
auto msg = consumerIO->pullMessage();
if (msg.topic.find("player:") == 0) {
playerMsgCount++;
}
consumerIO->pullAndDispatch();
}
std::cout << " Pattern 'player:.*' matched " << playerMsgCount << " messages\n";
@ -297,11 +303,25 @@ int main() {
std::cout << "=== TEST 3: Multi-Module Routing (1-to-many) ===\n";
std::cout << " Testing for known bug: std::move limitation in routing\n";
// All modules subscribe to "broadcast:.*"
consumerIO->subscribe("broadcast:.*");
broadcastIO->subscribe("broadcast:.*");
batchIO->subscribe("broadcast:.*");
stressIO->subscribe("broadcast:.*");
// Track received messages per module
int consumerReceived = 0;
int broadcastReceived = 0;
int batchReceived = 0;
int stressReceived = 0;
// All modules subscribe to "broadcast:.*" with callbacks
consumerIO->subscribe("broadcast:.*", [&](const Message& msg) {
consumerReceived++;
});
broadcastIO->subscribe("broadcast:.*", [&](const Message& msg) {
broadcastReceived++;
});
batchIO->subscribe("broadcast:.*", [&](const Message& msg) {
batchReceived++;
});
stressIO->subscribe("broadcast:.*", [&](const Message& msg) {
stressReceived++;
});
// Publish 10 broadcast messages
for (int i = 0; i < 10; i++) {
@ -311,11 +331,11 @@ int main() {
std::this_thread::sleep_for(std::chrono::milliseconds(10));
// Check which modules received messages
int consumerReceived = consumerIO->hasMessages();
int broadcastReceived = broadcastIO->hasMessages();
int batchReceived = batchIO->hasMessages();
int stressReceived = stressIO->hasMessages();
// Dispatch messages to all subscribers
while (consumerIO->hasMessages() > 0) consumerIO->pullAndDispatch();
while (broadcastIO->hasMessages() > 0) broadcastIO->pullAndDispatch();
while (batchIO->hasMessages() > 0) batchIO->pullAndDispatch();
while (stressIO->hasMessages() > 0) stressIO->pullAndDispatch();
std::cout << " Broadcast distribution:\n";
std::cout << " ConsumerModule: " << consumerReceived << " messages\n";
@ -340,21 +360,25 @@ int main() {
reporter.addAssertion("multi_module_routing_tested", true);
std::cout << "✓ TEST 3 COMPLETED (bug documented)\n\n";
// Clean up for next test
while (consumerIO->hasMessages() > 0) consumerIO->pullMessage();
while (broadcastIO->hasMessages() > 0) broadcastIO->pullMessage();
while (batchIO->hasMessages() > 0) batchIO->pullMessage();
while (stressIO->hasMessages() > 0) stressIO->pullMessage();
// Clean up for next test (already dispatched, so just clear any remaining)
while (consumerIO->hasMessages() > 0) consumerIO->pullAndDispatch();
while (broadcastIO->hasMessages() > 0) broadcastIO->pullAndDispatch();
while (batchIO->hasMessages() > 0) batchIO->pullAndDispatch();
while (stressIO->hasMessages() > 0) stressIO->pullAndDispatch();
// ========================================================================
// TEST 4: Low-Frequency Subscriptions (Batching)
// ========================================================================
std::cout << "=== TEST 4: Low-Frequency Subscriptions ===\n";
int batchesReceived = 0;
SubscriptionConfig batchConfig;
batchConfig.replaceable = true;
batchConfig.batchInterval = 1000; // 1 second
batchIO->subscribeLowFreq("batch:.*", batchConfig);
batchIO->subscribeLowFreq("batch:.*", [&](const Message& msg) {
batchesReceived++;
}, batchConfig);
std::cout << " Publishing 100 messages over 2 seconds...\n";
int batchPublished = 0;
@ -375,10 +399,8 @@ int main() {
// Check batched messages
std::this_thread::sleep_for(std::chrono::milliseconds(100));
int batchesReceived = 0;
while (batchIO->hasMessages() > 0) {
auto msg = batchIO->pullMessage();
batchesReceived++;
batchIO->pullAndDispatch();
}
std::cout << " Published: " << batchPublished << " messages over " << batchDuration << "s\n";
@ -397,7 +419,9 @@ int main() {
// ========================================================================
std::cout << "=== TEST 5: Backpressure & Queue Overflow ===\n";
consumerIO->subscribe("stress:flood");
consumerIO->subscribe("stress:flood", [](const Message& msg) {
// Just consume the message (counting not needed for this test)
});
std::cout << " Publishing 10000 messages without pulling...\n";
for (int i = 0; i < 10000; i++) {
@ -421,19 +445,21 @@ int main() {
std::cout << "✓ TEST 5 PASSED\n\n";
// Clean up queue
while (consumerIO->hasMessages() > 0) consumerIO->pullMessage();
while (consumerIO->hasMessages() > 0) consumerIO->pullAndDispatch();
// ========================================================================
// TEST 6: Thread Safety (Concurrent Pub/Pull)
// ========================================================================
std::cout << "=== TEST 6: Thread Safety ===\n";
consumerIO->subscribe("thread:.*");
std::atomic<int> publishedTotal{0};
std::atomic<int> receivedTotal{0};
std::atomic<bool> running{true};
consumerIO->subscribe("thread:.*", [&](const Message& msg) {
receivedTotal++;
});
std::cout << " Launching 5 publisher threads...\n";
std::vector<std::thread> publishers;
for (int t = 0; t < 5; t++) {
@ -457,8 +483,7 @@ int main() {
while (running || consumerIO->hasMessages() > 0) {
if (consumerIO->hasMessages() > 0) {
try {
auto msg = consumerIO->pullMessage();
receivedTotal++;
consumerIO->pullAndDispatch();
} catch (...) {
// Expected: may have race conditions
}

View File

@ -79,11 +79,28 @@ int main() {
// Load config
tree->loadConfigFile("gameplay.json");
// Player subscribes to config changes
playerIO->subscribe("config:gameplay:changed");
// Track config change events
std::atomic<int> configChangedEvents{0};
// Player subscribes to config changes with callback
playerIO->subscribe("config:gameplay:changed", [&](const Message& msg) {
configChangedEvents++;
// Read new config from tree
auto configRoot = tree->getConfigRoot();
auto gameplay = configRoot->getChild("gameplay");
if (gameplay) {
std::string difficulty = gameplay->getString("difficulty");
double hpMult = gameplay->getDouble("hpMultiplier");
std::cout << " PlayerModule received config change: difficulty=" << difficulty
<< ", hpMult=" << hpMult << "\n";
ASSERT_EQ(difficulty, "hard", "Difficulty should be updated");
ASSERT_TRUE(std::abs(hpMult - 1.5) < 0.001, "HP multiplier should be updated");
}
});
// Setup reload callback for ConfigWatcher
std::atomic<int> configChangedEvents{0};
tree->onTreeReloaded([&]() {
std::cout << " → Config reloaded, publishing event...\n";
auto data = std::make_unique<JsonDataNode>("configChange", nlohmann::json{
@ -111,24 +128,9 @@ int main() {
std::this_thread::sleep_for(std::chrono::milliseconds(10));
// Check if player received message
if (playerIO->hasMessages() > 0) {
auto msg = playerIO->pullMessage();
configChangedEvents++;
// Read new config from tree
auto configRoot = tree->getConfigRoot();
auto gameplay = configRoot->getChild("gameplay");
if (gameplay) {
std::string difficulty = gameplay->getString("difficulty");
double hpMult = gameplay->getDouble("hpMultiplier");
std::cout << " PlayerModule received config change: difficulty=" << difficulty
<< ", hpMult=" << hpMult << "\n";
ASSERT_EQ(difficulty, "hard", "Difficulty should be updated");
ASSERT_TRUE(std::abs(hpMult - 1.5) < 0.001, "HP multiplier should be updated");
}
// Dispatch player messages (callback handles verification)
while (playerIO->hasMessages() > 0) {
playerIO->pullAndDispatch();
}
auto reloadEnd = std::chrono::high_resolution_clock::now();
@ -166,23 +168,12 @@ int main() {
std::cout << " Data saved to disk\n";
// Economy subscribes to player events FIRST
economyIO->subscribe("player:*");
// Then publish level up event
auto levelUpData = std::make_unique<JsonDataNode>("levelUp", nlohmann::json{
{"event", "level_up"},
{"newLevel", 6},
{"goldBonus", 500}
});
playerIO->publish("player:level_up", std::move(levelUpData));
std::this_thread::sleep_for(std::chrono::milliseconds(10));
// Economy processes message
int messagesReceived = 0;
while (economyIO->hasMessages() > 0) {
auto msg = economyIO->pullMessage();
int syncErrors = 0; // Will be used in TEST 3
// Economy subscribes to player events with callback
// This callback will be reused across TEST 2 and TEST 3
economyIO->subscribe("player:*", [&](const Message& msg) {
messagesReceived++;
std::cout << " EconomyModule received: " << msg.topic << "\n";
@ -195,11 +186,39 @@ int main() {
if (profileData) {
int gold = profileData->getInt("gold");
std::cout << " Player gold: " << gold << "\n";
// For TEST 2: verify initial gold
if (msg.topic == "player:level_up") {
ASSERT_EQ(gold, 1000, "Gold should match saved value");
}
// For TEST 3: verify synchronization
if (msg.topic == "player:gold:updated") {
int msgGold = msg.data->getInt("gold");
if (msgGold != gold) {
std::cerr << " SYNC ERROR: msg=" << msgGold << " data=" << gold << "\n";
syncErrors++;
}
}
}
}
}
});
// Then publish level up event
auto levelUpData = std::make_unique<JsonDataNode>("levelUp", nlohmann::json{
{"event", "level_up"},
{"newLevel", 6},
{"goldBonus", 500}
});
playerIO->publish("player:level_up", std::move(levelUpData));
std::this_thread::sleep_for(std::chrono::milliseconds(10));
// Dispatch economy messages (callback handles verification)
while (economyIO->hasMessages() > 0) {
economyIO->pullAndDispatch();
}
ASSERT_EQ(messagesReceived, 1, "Should receive 1 player event");
@ -211,7 +230,8 @@ int main() {
// ========================================================================
std::cout << "\n=== TEST 3: Multi-Module State Synchronization ===\n";
int syncErrors = 0;
// Note: syncErrors is already declared earlier and captured by the economyIO callback
// The callback will automatically verify synchronization for "player:gold:updated" messages
for (int i = 0; i < 10; i++) {
// Update gold in DataNode using read-only access
@ -236,27 +256,9 @@ int main() {
std::this_thread::sleep_for(std::chrono::milliseconds(5));
// Economy verifies synchronization
if (economyIO->hasMessages() > 0) {
auto msg = economyIO->pullMessage();
int msgGold = msg.data->getInt("gold");
// Read from DataNode using read-only access
auto dataRoot = tree->getDataRoot();
if (dataRoot) {
auto playerCheck = dataRoot->getChildReadOnly("player");
if (playerCheck) {
auto profileCheck = playerCheck->getChildReadOnly("profile");
if (profileCheck) {
int dataGold = profileCheck->getInt("gold");
if (msgGold != dataGold) {
std::cerr << " SYNC ERROR: msg=" << msgGold << " data=" << dataGold << "\n";
syncErrors++;
}
}
}
}
// Dispatch economy messages (callback will verify synchronization)
while (economyIO->hasMessages() > 0) {
economyIO->pullAndDispatch();
}
}
@ -274,12 +276,16 @@ int main() {
auto runtimeRoot = tree->getRuntimeRoot();
// Subscribe to metrics with low-frequency
int snapshotsReceived = 0;
// Subscribe to metrics with low-frequency and callback
SubscriptionConfig metricsConfig;
metricsConfig.replaceable = true;
metricsConfig.batchInterval = 1000; // 1 second
playerIO->subscribeLowFreq("metrics:*", metricsConfig);
playerIO->subscribeLowFreq("metrics:*", [&](const Message& msg) {
snapshotsReceived++;
}, metricsConfig);
// Publish 20 metrics over 2 seconds
for (int i = 0; i < 20; i++) {
@ -295,10 +301,8 @@ int main() {
// Check batched messages
std::this_thread::sleep_for(std::chrono::milliseconds(200));
int snapshotsReceived = 0;
while (playerIO->hasMessages() > 0) {
playerIO->pullMessage();
snapshotsReceived++;
playerIO->pullAndDispatch();
}
std::cout << "Snapshots received: " << snapshotsReceived << " (expected ~2 due to batching)\n";

View File

@ -54,8 +54,20 @@ TEST_CASE("IIO sprite message routing between modules", "[bgfx][integration]") {
auto gameIO = ioManager.createInstance("test_game_module");
auto rendererIO = ioManager.createInstance("test_renderer_module");
// Renderer subscribes to render topics
rendererIO->subscribe("render:*");
int messageCount = 0;
bool firstMessageVerified = false;
// Renderer subscribes to render topics with callback
rendererIO->subscribe("render:*", [&](const Message& msg) {
messageCount++;
if (messageCount == 1) {
// Verify first message
REQUIRE(msg.topic == "render:sprite");
REQUIRE(msg.data != nullptr);
REQUIRE_THAT(msg.data->getDouble("x"), WithinAbs(100.0, 0.01));
firstMessageVerified = true;
}
});
// Game module publishes sprites via IIO
for (int i = 0; i < 3; ++i) {
@ -71,14 +83,14 @@ TEST_CASE("IIO sprite message routing between modules", "[bgfx][integration]") {
gameIO->publish("render:sprite", std::move(spriteData));
}
// Messages should be routed to renderer
REQUIRE(rendererIO->hasMessages() == 3);
// Dispatch messages to trigger callbacks
while (rendererIO->hasMessages() > 0) {
rendererIO->pullAndDispatch();
}
// Pull and verify first message
auto msg1 = rendererIO->pullMessage();
REQUIRE(msg1.topic == "render:sprite");
REQUIRE(msg1.data != nullptr);
REQUIRE_THAT(msg1.data->getDouble("x"), WithinAbs(100.0, 0.01));
// Verify we received all 3 messages
REQUIRE(messageCount == 3);
REQUIRE(firstMessageVerified);
// Cleanup
rendererIO->clearAllMessages();

View File

@ -129,12 +129,34 @@ int main() {
std::cout << "\n=== Phase 4: Setup IIO Subscriptions ===\n";
testIO->subscribe("ui:click");
testIO->subscribe("ui:action");
testIO->subscribe("ui:value_changed");
testIO->subscribe("ui:hover");
testIO->subscribe("render:sprite");
testIO->subscribe("render:text");
int uiClickCount = 0;
int uiActionCount = 0;
int uiValueChangeCount = 0;
int uiHoverCount = 0;
int renderSpriteCount = 0;
int renderTextCount = 0;
testIO->subscribe("ui:click", [&](const Message& msg) {
uiClickCount++;
});
testIO->subscribe("ui:action", [&](const Message& msg) {
uiActionCount++;
});
testIO->subscribe("ui:value_changed", [&](const Message& msg) {
uiValueChangeCount++;
});
testIO->subscribe("ui:hover", [&](const Message& msg) {
bool enter = msg.data->getBool("enter", false);
if (enter) {
uiHoverCount++;
}
});
testIO->subscribe("render:sprite", [&](const Message& msg) {
renderSpriteCount++;
});
testIO->subscribe("render:text", [&](const Message& msg) {
renderTextCount++;
});
std::cout << " ✓ Subscribed to UI events (click, action, value_changed, hover)\n";
std::cout << " ✓ Subscribed to render events (sprite, text)\n";
@ -145,13 +167,6 @@ int main() {
std::cout << "\n=== Phase 5: Run Parallel Processing (100 frames) ===\n";
int uiClickCount = 0;
int uiActionCount = 0;
int uiValueChangeCount = 0;
int uiHoverCount = 0;
int renderSpriteCount = 0;
int renderTextCount = 0;
for (int frame = 0; frame < 100; frame++) {
// Simulate mouse input at specific frames
if (frame == 10) {
@ -182,41 +197,9 @@ int main() {
// Process all modules in parallel
system->processModules(1.0f / 60.0f);
// Collect IIO messages from modules
// Dispatch IIO messages from modules (callbacks handle counting)
while (testIO->hasMessages() > 0) {
auto msg = testIO->pullMessage();
if (msg.topic == "ui:click") {
uiClickCount++;
if (frame < 30) {
std::cout << " Frame " << frame << ": UI click event\n";
}
}
else if (msg.topic == "ui:action") {
uiActionCount++;
if (frame < 30) {
std::string action = msg.data->getString("action", "");
std::cout << " Frame " << frame << ": UI action '" << action << "'\n";
}
}
else if (msg.topic == "ui:value_changed") {
uiValueChangeCount++;
}
else if (msg.topic == "ui:hover") {
bool enter = msg.data->getBool("enter", false);
if (enter) {
uiHoverCount++;
if (frame < 30) {
std::cout << " Frame " << frame << ": UI hover event\n";
}
}
}
else if (msg.topic == "render:sprite") {
renderSpriteCount++;
}
else if (msg.topic == "render:text") {
renderTextCount++;
}
testIO->pullAndDispatch();
}
if ((frame + 1) % 20 == 0) {

View File

@ -41,13 +41,10 @@ public:
void process(const IDataNode& input) override {
processCount++;
// Check for incoming messages
// Pull and auto-dispatch incoming messages
if (io && !subscribeTopic.empty()) {
while (io->hasMessages() > 0) {
auto msg = io->pullMessage();
if (msg.topic == subscribeTopic) {
logger->info("{}: Received message on '{}'", name, subscribeTopic);
}
io->pullAndDispatch(); // Callback invoked automatically
}
}
@ -63,9 +60,11 @@ public:
void setConfiguration(const IDataNode& configNode, IIO* ioLayer, ITaskScheduler* scheduler) override {
io = ioLayer;
// Subscribe if needed
// Subscribe with callback handler
if (io && !subscribeTopic.empty()) {
io->subscribe(subscribeTopic);
io->subscribe(subscribeTopic, [this](const Message& msg) {
logger->info("{}: Received message on '{}'", name, msg.topic);
});
logger->info("{}: Subscribed to '{}'", name, subscribeTopic);
}

View File

@ -15,17 +15,10 @@ BatchModule::~BatchModule() {
void BatchModule::process(const IDataNode& input) {
if (!io) return;
// Pull batched messages (should be low-frequency)
// Pull and dispatch batched messages (callbacks invoked automatically)
while (io->hasMessages() > 0) {
try {
auto msg = io->pullMessage();
batchCount++;
bool verbose = input.getBool("verbose", false);
if (verbose) {
std::cout << "[BatchModule] Received batch #" << batchCount
<< " on topic: " << msg.topic << std::endl;
}
io->pullAndDispatch();
} catch (const std::exception& e) {
std::cerr << "[BatchModule] Error pulling message: " << e.what() << std::endl;
}
@ -39,6 +32,13 @@ void BatchModule::setConfiguration(const IDataNode& configNode, IIO* ioPtr, ITas
this->scheduler = schedulerPtr;
config = std::make_unique<JsonDataNode>("config", nlohmann::json::object());
// Subscribe to all messages with callback that counts batches
if (io) {
io->subscribe("*", [this](const Message& msg) {
batchCount++;
});
}
}
const IDataNode& BatchModule::getConfiguration() {

View File

@ -15,17 +15,10 @@ BroadcastModule::~BroadcastModule() {
void BroadcastModule::process(const IDataNode& input) {
if (!io) return;
// Pull all available messages
// Pull and dispatch all available messages (callbacks invoked automatically)
while (io->hasMessages() > 0) {
try {
auto msg = io->pullMessage();
receivedCount++;
bool verbose = input.getBool("verbose", false);
if (verbose) {
std::cout << "[BroadcastModule] Received message #" << receivedCount
<< " on topic: " << msg.topic << std::endl;
}
io->pullAndDispatch();
} catch (const std::exception& e) {
std::cerr << "[BroadcastModule] Error pulling message: " << e.what() << std::endl;
}
@ -39,6 +32,13 @@ void BroadcastModule::setConfiguration(const IDataNode& configNode, IIO* ioPtr,
this->scheduler = schedulerPtr;
config = std::make_unique<JsonDataNode>("config", nlohmann::json::object());
// Subscribe to all messages with callback that counts them
if (io) {
io->subscribe("*", [this](const Message& msg) {
receivedCount++;
});
}
}
const IDataNode& BroadcastModule::getConfiguration() {

View File

@ -15,18 +15,10 @@ ConsumerModule::~ConsumerModule() {
void ConsumerModule::process(const IDataNode& input) {
if (!io) return;
// Pull all available messages
// Pull and dispatch all available messages (callbacks invoked automatically)
while (io->hasMessages() > 0) {
try {
auto msg = io->pullMessage();
receivedCount++;
// Optionally log message details
bool verbose = input.getBool("verbose", false);
if (verbose) {
std::cout << "[ConsumerModule] Received message #" << receivedCount
<< " on topic: " << msg.topic << std::endl;
}
io->pullAndDispatch();
} catch (const std::exception& e) {
std::cerr << "[ConsumerModule] Error pulling message: " << e.what() << std::endl;
}
@ -41,6 +33,13 @@ void ConsumerModule::setConfiguration(const IDataNode& configNode, IIO* ioPtr, I
// Store config
config = std::make_unique<JsonDataNode>("config", nlohmann::json::object());
// Subscribe to all messages with callback that counts them
if (io) {
io->subscribe("*", [this](const Message& msg) {
receivedCount++;
});
}
}
const IDataNode& ConsumerModule::getConfiguration() {

View File

@ -12,11 +12,11 @@ EconomyModule::~EconomyModule() {
}
void EconomyModule::process(const IDataNode& input) {
// Process incoming messages from IO
if (io && io->hasMessages() > 0) {
auto msg = io->pullMessage();
playerEventsProcessed++;
handlePlayerEvent(msg.topic, msg.data.get());
// Pull and dispatch all pending messages (callbacks invoked automatically)
if (io) {
while (io->hasMessages() > 0) {
io->pullAndDispatch();
}
}
}
@ -29,9 +29,12 @@ void EconomyModule::setConfiguration(const IDataNode& configNode, IIO* ioPtr, IT
// Store config
config = std::make_unique<JsonDataNode>("config", nlohmann::json::object());
// Subscribe to player events
// Subscribe to player events with callback
if (io) {
io->subscribe("player:*");
io->subscribe("player:*", [this](const Message& msg) {
playerEventsProcessed++;
handlePlayerEvent(msg.topic, msg.data.get());
});
}
}

View File

@ -15,11 +15,10 @@ IOStressModule::~IOStressModule() {
void IOStressModule::process(const IDataNode& input) {
if (!io) return;
// Pull all available messages (high-frequency consumer)
// Pull and dispatch all available messages (high-frequency consumer)
while (io->hasMessages() > 0) {
try {
auto msg = io->pullMessage();
receivedCount++;
io->pullAndDispatch();
} catch (const std::exception& e) {
std::cerr << "[IOStressModule] Error pulling message: " << e.what() << std::endl;
}
@ -33,6 +32,13 @@ void IOStressModule::setConfiguration(const IDataNode& configNode, IIO* ioPtr, I
this->scheduler = schedulerPtr;
config = std::make_unique<JsonDataNode>("config", nlohmann::json::object());
// Subscribe to all messages with callback that counts them
if (io) {
io->subscribe("*", [this](const Message& msg) {
receivedCount++;
});
}
}
const IDataNode& IOStressModule::getConfiguration() {

View File

@ -22,10 +22,11 @@ void MetricsModule::process(const IDataNode& input) {
accumulator = 0.0f;
}
// Process incoming messages from IO
if (io && io->hasMessages() > 0) {
auto msg = io->pullMessage();
std::cout << "[MetricsModule] Received: " << msg.topic << std::endl;
// Pull and dispatch all pending messages (callbacks invoked automatically)
if (io) {
while (io->hasMessages() > 0) {
io->pullAndDispatch();
}
}
}
@ -38,9 +39,11 @@ void MetricsModule::setConfiguration(const IDataNode& configNode, IIO* ioPtr, IT
// Store config
config = std::make_unique<JsonDataNode>("config", nlohmann::json::object());
// Subscribe to economy events
// Subscribe to economy events with callback
if (io) {
io->subscribe("economy:*");
io->subscribe("economy:*", [this](const Message& msg) {
std::cout << "[MetricsModule] Received: " << msg.topic << std::endl;
});
}
}

View File

@ -12,12 +12,10 @@ PlayerModule::~PlayerModule() {
}
void PlayerModule::process(const IDataNode& input) {
// Process incoming messages from IO
if (io && io->hasMessages() > 0) {
auto msg = io->pullMessage();
if (msg.topic.find("config:") == 0) {
handleConfigChange();
// Pull and dispatch all pending messages (callbacks invoked automatically)
if (io) {
while (io->hasMessages() > 0) {
io->pullAndDispatch();
}
}
}
@ -31,9 +29,11 @@ void PlayerModule::setConfiguration(const IDataNode& configNode, IIO* ioPtr, ITa
// Store config
config = std::make_unique<JsonDataNode>("config", nlohmann::json::object());
// Subscribe to config changes
// Subscribe to config changes with callback
if (io) {
io->subscribe("config:gameplay:changed");
io->subscribe("config:gameplay:changed", [this](const Message& msg) {
handleConfigChange();
});
}
}

View File

@ -29,16 +29,39 @@ public:
std::cout << "[TestController] Initializing...\n";
// Subscribe to UI events
// Subscribe to UI events with callbacks
if (m_io) {
m_io->subscribe("ui:click");
m_io->subscribe("ui:action");
m_io->subscribe("ui:value_changed");
m_io->subscribe("ui:text_changed");
m_io->subscribe("ui:text_submit");
m_io->subscribe("ui:hover");
m_io->subscribe("ui:focus_gained");
m_io->subscribe("ui:focus_lost");
m_io->subscribe("ui:click", [this](const grove::Message& msg) {
handleClick(*msg.data);
});
m_io->subscribe("ui:action", [this](const grove::Message& msg) {
handleAction(*msg.data);
});
m_io->subscribe("ui:value_changed", [this](const grove::Message& msg) {
handleValueChanged(*msg.data);
});
m_io->subscribe("ui:text_changed", [this](const grove::Message& msg) {
handleTextChanged(*msg.data);
});
m_io->subscribe("ui:text_submit", [this](const grove::Message& msg) {
handleTextSubmit(*msg.data);
});
m_io->subscribe("ui:hover", [this](const grove::Message& msg) {
handleHover(*msg.data);
});
m_io->subscribe("ui:focus_gained", [this](const grove::Message& msg) {
handleFocusGained(*msg.data);
});
m_io->subscribe("ui:focus_lost", [this](const grove::Message& msg) {
handleFocusLost(*msg.data);
});
}
std::cout << "[TestController] Subscribed to UI events\n";
@ -49,34 +72,9 @@ public:
m_frameCount++;
// Process incoming UI events
// Pull and dispatch all pending messages (callbacks invoked automatically)
while (m_io->hasMessages() > 0) {
auto msg = m_io->pullMessage();
if (msg.topic == "ui:click") {
handleClick(*msg.data);
}
else if (msg.topic == "ui:action") {
handleAction(*msg.data);
}
else if (msg.topic == "ui:value_changed") {
handleValueChanged(*msg.data);
}
else if (msg.topic == "ui:text_changed") {
handleTextChanged(*msg.data);
}
else if (msg.topic == "ui:text_submit") {
handleTextSubmit(*msg.data);
}
else if (msg.topic == "ui:hover") {
handleHover(*msg.data);
}
else if (msg.topic == "ui:focus_gained") {
handleFocusGained(*msg.data);
}
else if (msg.topic == "ui:focus_lost") {
handleFocusLost(*msg.data);
}
m_io->pullAndDispatch();
}
// Simulate some game logic

View File

@ -80,10 +80,31 @@ int main(int argc, char* argv[]) {
std::cout << "IIO Manager setup complete\n";
// Subscribe to UI events to see button clicks
uiIO->subscribe("ui:click");
uiIO->subscribe("ui:hover");
uiIO->subscribe("ui:action");
// Subscribe to UI events to see button clicks with callbacks
uiIO->subscribe("ui:click", [](const Message& msg) {
std::string widgetId = msg.data->getString("widgetId", "");
std::cout << " [UI EVENT] Click: " << widgetId << "\n";
});
uiIO->subscribe("ui:hover", [](const Message& msg) {
std::string widgetId = msg.data->getString("widgetId", "");
bool enter = msg.data->getBool("enter", false);
if (enter && !widgetId.empty()) {
std::cout << " [UI EVENT] Hover: " << widgetId << "\n";
}
});
bool running = true; // Will be captured by callback
uiIO->subscribe("ui:action", [&running](const Message& msg) {
std::string action = msg.data->getString("action", "");
std::string widgetId = msg.data->getString("widgetId", "");
std::cout << " [UI EVENT] Action: " << action << " (from " << widgetId << ")\n";
// Handle quit action
if (action == "app:quit") {
std::cout << "\nQuit button clicked - exiting!\n";
running = false;
}
});
// ========================================
// Load BgfxRenderer module
@ -186,7 +207,7 @@ int main(int argc, char* argv[]) {
std::cout << "\nMove mouse over buttons and click them!\n";
std::cout << "Press ESC to exit or wait 30 seconds\n\n";
bool running = true;
// running is already declared above with callbacks
uint32_t frameCount = 0;
Uint32 startTime = SDL_GetTicks();
const Uint32 testDuration = 30000; // 30 seconds
@ -234,32 +255,9 @@ int main(int argc, char* argv[]) {
running = false;
}
// Check for UI events
// Dispatch UI events (callbacks handle logging and quit action)
while (uiIO->hasMessages() > 0) {
auto msg = uiIO->pullMessage();
if (msg.topic == "ui:click") {
std::string widgetId = msg.data->getString("widgetId", "");
std::cout << " [UI EVENT] Click: " << widgetId << "\n";
}
else if (msg.topic == "ui:hover") {
std::string widgetId = msg.data->getString("widgetId", "");
bool enter = msg.data->getBool("enter", false);
if (enter && !widgetId.empty()) {
std::cout << " [UI EVENT] Hover: " << widgetId << "\n";
}
}
else if (msg.topic == "ui:action") {
std::string action = msg.data->getString("action", "");
std::string widgetId = msg.data->getString("widgetId", "");
std::cout << " [UI EVENT] Action: " << action << " (from " << widgetId << ")\n";
// Handle quit action
if (action == "app:quit") {
std::cout << "\nQuit button clicked - exiting!\n";
running = false;
}
}
uiIO->pullAndDispatch();
}
// ========================================

View File

@ -131,11 +131,72 @@ int main(int argc, char* argv[]) {
// Subscribe to input events
// ========================================
testIO->subscribe("input:mouse:move");
testIO->subscribe("input:mouse:button");
testIO->subscribe("input:mouse:wheel");
testIO->subscribe("input:keyboard:key");
testIO->subscribe("input:keyboard:text");
// Track last mouse move to avoid spam
int lastMouseX = -1;
int lastMouseY = -1;
testIO->subscribe("input:mouse:move", [&](const Message& msg) {
int x = msg.data->getInt("x", 0);
int y = msg.data->getInt("y", 0);
// Only print if position changed (reduce spam)
if (x != lastMouseX || y != lastMouseY) {
std::cout << "[MOUSE MOVE] x=" << std::setw(4) << x
<< ", y=" << std::setw(4) << y << "\n";
lastMouseX = x;
lastMouseY = y;
}
});
testIO->subscribe("input:mouse:button", [](const Message& msg) {
int button = msg.data->getInt("button", 0);
bool pressed = msg.data->getBool("pressed", false);
int x = msg.data->getInt("x", 0);
int y = msg.data->getInt("y", 0);
const char* buttonNames[] = { "LEFT", "MIDDLE", "RIGHT" };
const char* buttonName = (button >= 0 && button < 3) ? buttonNames[button] : "UNKNOWN";
std::cout << "[MOUSE BUTTON] " << buttonName
<< " " << (pressed ? "PRESSED" : "RELEASED")
<< " at (" << x << ", " << y << ")\n";
});
testIO->subscribe("input:mouse:wheel", [](const Message& msg) {
double delta = msg.data->getDouble("delta", 0.0);
std::cout << "[MOUSE WHEEL] delta=" << delta
<< " (" << (delta > 0 ? "UP" : "DOWN") << ")\n";
});
testIO->subscribe("input:keyboard:key", [](const Message& msg) {
int scancode = msg.data->getInt("scancode", 0);
bool pressed = msg.data->getBool("pressed", false);
bool repeat = msg.data->getBool("repeat", false);
bool shift = msg.data->getBool("shift", false);
bool ctrl = msg.data->getBool("ctrl", false);
bool alt = msg.data->getBool("alt", false);
const char* keyName = SDL_GetScancodeName(static_cast<SDL_Scancode>(scancode));
std::cout << "[KEYBOARD KEY] " << keyName
<< " " << (pressed ? "PRESSED" : "RELEASED");
if (repeat) std::cout << " (REPEAT)";
if (shift || ctrl || alt) {
std::cout << " [";
if (shift) std::cout << "SHIFT ";
if (ctrl) std::cout << "CTRL ";
if (alt) std::cout << "ALT";
std::cout << "]";
}
std::cout << "\n";
});
testIO->subscribe("input:keyboard:text", [](const Message& msg) {
std::string text = msg.data->getString("text", "");
std::cout << "[KEYBOARD TEXT] \"" << text << "\"\n";
});
std::cout << "Subscribed to all input topics\n";
std::cout << "========================================\n\n";
@ -148,10 +209,6 @@ int main(int argc, char* argv[]) {
uint32_t frameCount = 0;
uint32_t lastTime = SDL_GetTicks();
// Track last mouse move to avoid spam
int lastMouseX = -1;
int lastMouseY = -1;
while (running) {
frameCount++;
@ -174,68 +231,9 @@ int main(int argc, char* argv[]) {
grove::JsonDataNode input("input");
inputModule->process(input);
// 3. Process IIO messages from InputModule
// 3. Dispatch IIO messages from InputModule (callbacks handle printing)
while (testIO->hasMessages() > 0) {
auto msg = testIO->pullMessage();
if (msg.topic == "input:mouse:move") {
int x = msg.data->getInt("x", 0);
int y = msg.data->getInt("y", 0);
// Only print if position changed (reduce spam)
if (x != lastMouseX || y != lastMouseY) {
std::cout << "[MOUSE MOVE] x=" << std::setw(4) << x
<< ", y=" << std::setw(4) << y << "\n";
lastMouseX = x;
lastMouseY = y;
}
}
else if (msg.topic == "input:mouse:button") {
int button = msg.data->getInt("button", 0);
bool pressed = msg.data->getBool("pressed", false);
int x = msg.data->getInt("x", 0);
int y = msg.data->getInt("y", 0);
const char* buttonNames[] = { "LEFT", "MIDDLE", "RIGHT" };
const char* buttonName = (button >= 0 && button < 3) ? buttonNames[button] : "UNKNOWN";
std::cout << "[MOUSE BUTTON] " << buttonName
<< " " << (pressed ? "PRESSED" : "RELEASED")
<< " at (" << x << ", " << y << ")\n";
}
else if (msg.topic == "input:mouse:wheel") {
double delta = msg.data->getDouble("delta", 0.0);
std::cout << "[MOUSE WHEEL] delta=" << delta
<< " (" << (delta > 0 ? "UP" : "DOWN") << ")\n";
}
else if (msg.topic == "input:keyboard:key") {
int scancode = msg.data->getInt("scancode", 0);
bool pressed = msg.data->getBool("pressed", false);
bool repeat = msg.data->getBool("repeat", false);
bool shift = msg.data->getBool("shift", false);
bool ctrl = msg.data->getBool("ctrl", false);
bool alt = msg.data->getBool("alt", false);
const char* keyName = SDL_GetScancodeName(static_cast<SDL_Scancode>(scancode));
std::cout << "[KEYBOARD KEY] " << keyName
<< " " << (pressed ? "PRESSED" : "RELEASED");
if (repeat) std::cout << " (REPEAT)";
if (shift || ctrl || alt) {
std::cout << " [";
if (shift) std::cout << "SHIFT ";
if (ctrl) std::cout << "CTRL ";
if (alt) std::cout << "ALT";
std::cout << "]";
}
std::cout << "\n";
}
else if (msg.topic == "input:keyboard:text") {
std::string text = msg.data->getString("text", "");
std::cout << "[KEYBOARD TEXT] \"" << text << "\"\n";
}
testIO->pullAndDispatch();
}
// 4. Cap at ~60 FPS

View File

@ -48,9 +48,15 @@ int main(int argc, char* argv[]) {
auto uiIO = IntraIOManager::getInstance().createInstance("ui");
auto rendererIO = IntraIOManager::getInstance().createInstance("renderer");
gameIO->subscribe("ui:hover");
gameIO->subscribe("ui:click");
gameIO->subscribe("ui:action");
gameIO->subscribe("ui:hover", [](const Message& msg) {
// Hover events (not logged to avoid spam)
});
gameIO->subscribe("ui:click", [&logger](const Message& msg) {
logger->info("🖱️ BOUTON CLICKÉ!");
});
gameIO->subscribe("ui:action", [&logger](const Message& msg) {
logger->info("🖱️ ACTION!");
});
// Initialize BgfxRenderer WITH 3 TEXTURES loaded via config
auto renderer = std::make_unique<BgfxRendererModule>();
@ -176,12 +182,9 @@ int main(int argc, char* argv[]) {
}
}
// Check for UI events
// Dispatch UI events (callbacks handle logging)
while (gameIO->hasMessages() > 0) {
auto msg = gameIO->pullMessage();
if (msg.topic == "ui:action") {
logger->info("🖱️ BOUTON CLICKÉ!");
}
gameIO->pullAndDispatch();
}
// Update modules

View File

@ -45,10 +45,22 @@ int main(int argc, char* argv[]) {
auto uiIO = IntraIOManager::getInstance().createInstance("ui");
auto gameIO = IntraIOManager::getInstance().createInstance("game");
// Subscribe to UI events for logging
gameIO->subscribe("ui:hover");
gameIO->subscribe("ui:click");
gameIO->subscribe("ui:action");
// Subscribe to UI events for logging with callbacks
gameIO->subscribe("ui:hover", [&logger](const Message& msg) {
std::string widgetId = msg.data->getString("widgetId", "");
bool enter = msg.data->getBool("enter", false);
logger->info("[UI EVENT] HOVER {} widget '{}'",
enter ? "ENTER" : "LEAVE", widgetId);
});
gameIO->subscribe("ui:click", [&logger](const Message& msg) {
std::string widgetId = msg.data->getString("widgetId", "");
logger->info("[UI EVENT] CLICK on widget '{}'", widgetId);
});
gameIO->subscribe("ui:action", [&logger](const Message& msg) {
std::string action = msg.data->getString("action", "");
std::string widgetId = msg.data->getString("widgetId", "");
logger->info("[UI EVENT] ACTION '{}' from widget '{}'", action, widgetId);
});
// Initialize BgfxRenderer
auto renderer = std::make_unique<BgfxRendererModule>();
@ -176,25 +188,9 @@ int main(int argc, char* argv[]) {
}
}
// Check for UI events
// Dispatch UI events (callbacks handle logging)
while (gameIO->hasMessages() > 0) {
auto msg = gameIO->pullMessage();
if (msg.topic == "ui:hover") {
std::string widgetId = msg.data->getString("widgetId", "");
bool enter = msg.data->getBool("enter", false);
logger->info("[UI EVENT] HOVER {} widget '{}'",
enter ? "ENTER" : "LEAVE", widgetId);
}
else if (msg.topic == "ui:click") {
std::string widgetId = msg.data->getString("widgetId", "");
logger->info("[UI EVENT] CLICK on widget '{}'", widgetId);
}
else if (msg.topic == "ui:action") {
std::string action = msg.data->getString("action", "");
std::string widgetId = msg.data->getString("widgetId", "");
logger->info("[UI EVENT] ACTION '{}' from widget '{}'", action, widgetId);
}
gameIO->pullAndDispatch();
}
JsonDataNode input("input");