Migration Gitea
This commit is contained in:
parent
21590418f1
commit
5127dd5bf2
1
Testing/Temporary/CTestCostData.txt
Normal file
1
Testing/Temporary/CTestCostData.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
---
|
||||||
567
plans/later/PLAN_INPUT_MODULE_PHASE2_GAMEPAD.md
Normal file
567
plans/later/PLAN_INPUT_MODULE_PHASE2_GAMEPAD.md
Normal file
@ -0,0 +1,567 @@
|
|||||||
|
# InputModule - Phase 2: Gamepad Support
|
||||||
|
|
||||||
|
## Vue d'ensemble
|
||||||
|
|
||||||
|
Extension de l'InputModule pour supporter les manettes de jeu (gamepad/controller) via SDL2. Cette phase ajoute le support complet des boutons, axes analogiques, et gestion de la connexion/déconnexion de manettes.
|
||||||
|
|
||||||
|
## Prérequis
|
||||||
|
|
||||||
|
- ✅ Phase 1 complétée (souris + clavier)
|
||||||
|
- ✅ SDL2 installé et fonctionnel
|
||||||
|
- ✅ InputModule compilé et testé
|
||||||
|
|
||||||
|
## Objectifs
|
||||||
|
|
||||||
|
- 🎮 Support des boutons de gamepad (face buttons, shoulder buttons, etc.)
|
||||||
|
- 🕹️ Support des axes analogiques (joysticks, triggers)
|
||||||
|
- 🔌 Détection de connexion/déconnexion de manettes
|
||||||
|
- 🎯 Support multi-manettes (jusqu'à 4 joueurs)
|
||||||
|
- 🔄 Hot-reload avec préservation de l'état des manettes
|
||||||
|
- 📊 Deadzone configurable pour les axes analogiques
|
||||||
|
|
||||||
|
## Topics IIO publiés
|
||||||
|
|
||||||
|
### Gamepad Buttons
|
||||||
|
| Topic | Payload | Description |
|
||||||
|
|-------|---------|-------------|
|
||||||
|
| `input:gamepad:button` | `{id, button, pressed}` | Bouton de manette (id=manette 0-3, button=index) |
|
||||||
|
|
||||||
|
**Boutons SDL2 (SDL_GameControllerButton):**
|
||||||
|
- 0-3: A, B, X, Y (face buttons)
|
||||||
|
- 4-5: Back, Guide, Start
|
||||||
|
- 6-7: Left Stick Click, Right Stick Click
|
||||||
|
- 8-11: D-Pad Up, Down, Left, Right
|
||||||
|
- 12-13: Left Shoulder, Right Shoulder
|
||||||
|
|
||||||
|
### Gamepad Axes
|
||||||
|
| Topic | Payload | Description |
|
||||||
|
|-------|---------|-------------|
|
||||||
|
| `input:gamepad:axis` | `{id, axis, value}` | Axe analogique (value: -1.0 à 1.0) |
|
||||||
|
|
||||||
|
**Axes SDL2 (SDL_GameControllerAxis):**
|
||||||
|
- 0-1: Left Stick X, Left Stick Y
|
||||||
|
- 2-3: Right Stick X, Right Stick Y
|
||||||
|
- 4-5: Left Trigger, Right Trigger
|
||||||
|
|
||||||
|
### Gamepad Connection
|
||||||
|
| Topic | Payload | Description |
|
||||||
|
|-------|---------|-------------|
|
||||||
|
| `input:gamepad:connected` | `{id, name, connected}` | Connexion/déconnexion de manette |
|
||||||
|
|
||||||
|
**Payload example:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": 0,
|
||||||
|
"name": "Xbox 360 Controller",
|
||||||
|
"connected": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### Fichiers à créer
|
||||||
|
|
||||||
|
```
|
||||||
|
modules/InputModule/
|
||||||
|
├── Backends/
|
||||||
|
│ ├── SDLGamepadBackend.h # NEW - Conversion SDL gamepad → Generic
|
||||||
|
│ └── SDLGamepadBackend.cpp # NEW
|
||||||
|
└── Core/
|
||||||
|
└── GamepadState.h/cpp # NEW - État des manettes connectées
|
||||||
|
```
|
||||||
|
|
||||||
|
### Modifications aux fichiers existants
|
||||||
|
|
||||||
|
**InputModule.h** - Ajouter membres privés :
|
||||||
|
```cpp
|
||||||
|
std::unique_ptr<GamepadState> m_gamepadState;
|
||||||
|
std::array<SDL_GameController*, 4> m_controllers; // Max 4 manettes
|
||||||
|
```
|
||||||
|
|
||||||
|
**InputModule.cpp** - Ajouter dans `process()` :
|
||||||
|
```cpp
|
||||||
|
case SDLBackend::InputEvent::GamepadButton:
|
||||||
|
if (m_enableGamepad) {
|
||||||
|
m_gamepadState->setButton(genericEvent.gamepadId,
|
||||||
|
genericEvent.button,
|
||||||
|
genericEvent.pressed);
|
||||||
|
m_converter->publishGamepadButton(...);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SDLBackend::InputEvent::GamepadAxis:
|
||||||
|
if (m_enableGamepad) {
|
||||||
|
float value = applyDeadzone(genericEvent.axisValue, m_axisDeadzone);
|
||||||
|
m_gamepadState->setAxis(genericEvent.gamepadId,
|
||||||
|
genericEvent.axis,
|
||||||
|
value);
|
||||||
|
m_converter->publishGamepadAxis(...);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
```
|
||||||
|
|
||||||
|
## Implémentation détaillée
|
||||||
|
|
||||||
|
### 1. GamepadState.h/cpp
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// GamepadState.h
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace grove {
|
||||||
|
|
||||||
|
class GamepadState {
|
||||||
|
public:
|
||||||
|
static constexpr int MAX_GAMEPADS = 4;
|
||||||
|
static constexpr int MAX_BUTTONS = 16;
|
||||||
|
static constexpr int MAX_AXES = 6;
|
||||||
|
|
||||||
|
struct Gamepad {
|
||||||
|
bool connected = false;
|
||||||
|
std::string name;
|
||||||
|
std::array<bool, MAX_BUTTONS> buttons = {};
|
||||||
|
std::array<float, MAX_AXES> axes = {};
|
||||||
|
};
|
||||||
|
|
||||||
|
GamepadState() = default;
|
||||||
|
~GamepadState() = default;
|
||||||
|
|
||||||
|
// Connection
|
||||||
|
void connect(int id, const std::string& name);
|
||||||
|
void disconnect(int id);
|
||||||
|
bool isConnected(int id) const;
|
||||||
|
|
||||||
|
// Buttons
|
||||||
|
void setButton(int id, int button, bool pressed);
|
||||||
|
bool isButtonPressed(int id, int button) const;
|
||||||
|
|
||||||
|
// Axes
|
||||||
|
void setAxis(int id, int axis, float value);
|
||||||
|
float getAxisValue(int id, int axis) const;
|
||||||
|
|
||||||
|
// Query
|
||||||
|
const Gamepad& getGamepad(int id) const;
|
||||||
|
int getConnectedCount() const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::array<Gamepad, MAX_GAMEPADS> m_gamepads;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace grove
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. SDLGamepadBackend.h
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// SDLGamepadBackend.h
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "SDLBackend.h"
|
||||||
|
#include <SDL.h>
|
||||||
|
|
||||||
|
namespace grove {
|
||||||
|
|
||||||
|
class SDLGamepadBackend {
|
||||||
|
public:
|
||||||
|
// Extend InputEvent with gamepad fields
|
||||||
|
static bool convertGamepad(const SDL_Event& sdlEvent,
|
||||||
|
SDLBackend::InputEvent& outEvent);
|
||||||
|
|
||||||
|
// Helper: Apply deadzone to axis value
|
||||||
|
static float applyDeadzone(float value, float deadzone);
|
||||||
|
|
||||||
|
// Helper: Get gamepad name from SDL_GameController
|
||||||
|
static const char* getGamepadName(SDL_GameController* controller);
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace grove
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. SDLGamepadBackend.cpp
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// SDLGamepadBackend.cpp
|
||||||
|
#include "SDLGamepadBackend.h"
|
||||||
|
#include <cmath>
|
||||||
|
|
||||||
|
namespace grove {
|
||||||
|
|
||||||
|
bool SDLGamepadBackend::convertGamepad(const SDL_Event& sdlEvent,
|
||||||
|
SDLBackend::InputEvent& outEvent) {
|
||||||
|
switch (sdlEvent.type) {
|
||||||
|
case SDL_CONTROLLERBUTTONDOWN:
|
||||||
|
case SDL_CONTROLLERBUTTONUP:
|
||||||
|
outEvent.type = SDLBackend::InputEvent::GamepadButton;
|
||||||
|
outEvent.gamepadId = sdlEvent.cbutton.which;
|
||||||
|
outEvent.button = sdlEvent.cbutton.button;
|
||||||
|
outEvent.pressed = (sdlEvent.type == SDL_CONTROLLERBUTTONDOWN);
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case SDL_CONTROLLERAXISMOTION:
|
||||||
|
outEvent.type = SDLBackend::InputEvent::GamepadAxis;
|
||||||
|
outEvent.gamepadId = sdlEvent.caxis.which;
|
||||||
|
outEvent.axis = sdlEvent.caxis.axis;
|
||||||
|
// Convert SDL int16 (-32768 to 32767) to float (-1.0 to 1.0)
|
||||||
|
outEvent.axisValue = sdlEvent.caxis.value / 32768.0f;
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case SDL_CONTROLLERDEVICEADDED:
|
||||||
|
outEvent.type = SDLBackend::InputEvent::GamepadConnected;
|
||||||
|
outEvent.gamepadId = sdlEvent.cdevice.which;
|
||||||
|
outEvent.gamepadConnected = true;
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case SDL_CONTROLLERDEVICEREMOVED:
|
||||||
|
outEvent.type = SDLBackend::InputEvent::GamepadConnected;
|
||||||
|
outEvent.gamepadId = sdlEvent.cdevice.which;
|
||||||
|
outEvent.gamepadConnected = false;
|
||||||
|
return true;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
float SDLGamepadBackend::applyDeadzone(float value, float deadzone) {
|
||||||
|
if (std::abs(value) < deadzone) {
|
||||||
|
return 0.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rescale to maintain smooth transition
|
||||||
|
float sign = (value > 0.0f) ? 1.0f : -1.0f;
|
||||||
|
float absValue = std::abs(value);
|
||||||
|
return sign * ((absValue - deadzone) / (1.0f - deadzone));
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* SDLGamepadBackend::getGamepadName(SDL_GameController* controller) {
|
||||||
|
return SDL_GameControllerName(controller);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace grove
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Extend SDLBackend::InputEvent
|
||||||
|
|
||||||
|
**Dans SDLBackend.h**, ajouter au enum Type :
|
||||||
|
```cpp
|
||||||
|
enum Type {
|
||||||
|
MouseMove,
|
||||||
|
MouseButton,
|
||||||
|
MouseWheel,
|
||||||
|
KeyboardKey,
|
||||||
|
KeyboardText,
|
||||||
|
GamepadButton, // NEW
|
||||||
|
GamepadAxis, // NEW
|
||||||
|
GamepadConnected // NEW
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
Ajouter les champs :
|
||||||
|
```cpp
|
||||||
|
// Gamepad data
|
||||||
|
int gamepadId = 0; // 0-3
|
||||||
|
int axis = 0; // 0-5
|
||||||
|
float axisValue = 0.0f; // -1.0 to 1.0
|
||||||
|
bool gamepadConnected = false;
|
||||||
|
std::string gamepadName;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Extend InputConverter
|
||||||
|
|
||||||
|
**InputConverter.h** - Ajouter méthodes :
|
||||||
|
```cpp
|
||||||
|
void publishGamepadButton(int id, int button, bool pressed);
|
||||||
|
void publishGamepadAxis(int id, int axis, float value);
|
||||||
|
void publishGamepadConnected(int id, const std::string& name, bool connected);
|
||||||
|
```
|
||||||
|
|
||||||
|
**InputConverter.cpp** - Implémentation :
|
||||||
|
```cpp
|
||||||
|
void InputConverter::publishGamepadButton(int id, int button, bool pressed) {
|
||||||
|
auto msg = std::make_unique<JsonDataNode>("gamepad_button");
|
||||||
|
msg->setInt("id", id);
|
||||||
|
msg->setInt("button", button);
|
||||||
|
msg->setBool("pressed", pressed);
|
||||||
|
m_io->publish("input:gamepad:button", std::move(msg));
|
||||||
|
}
|
||||||
|
|
||||||
|
void InputConverter::publishGamepadAxis(int id, int axis, float value) {
|
||||||
|
auto msg = std::make_unique<JsonDataNode>("gamepad_axis");
|
||||||
|
msg->setInt("id", id);
|
||||||
|
msg->setInt("axis", axis);
|
||||||
|
msg->setDouble("value", static_cast<double>(value));
|
||||||
|
m_io->publish("input:gamepad:axis", std::move(msg));
|
||||||
|
}
|
||||||
|
|
||||||
|
void InputConverter::publishGamepadConnected(int id, const std::string& name,
|
||||||
|
bool connected) {
|
||||||
|
auto msg = std::make_unique<JsonDataNode>("gamepad_connected");
|
||||||
|
msg->setInt("id", id);
|
||||||
|
msg->setString("name", name);
|
||||||
|
msg->setBool("connected", connected);
|
||||||
|
m_io->publish("input:gamepad:connected", std::move(msg));
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. Configuration JSON
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"backend": "sdl",
|
||||||
|
"enableMouse": true,
|
||||||
|
"enableKeyboard": true,
|
||||||
|
"enableGamepad": true,
|
||||||
|
"gamepad": {
|
||||||
|
"deadzone": 0.15,
|
||||||
|
"maxGamepads": 4,
|
||||||
|
"autoConnect": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7. Hot-Reload Support
|
||||||
|
|
||||||
|
**getState()** - Ajouter sérialisation gamepad :
|
||||||
|
```cpp
|
||||||
|
// Gamepad state
|
||||||
|
auto gamepads = std::make_unique<JsonDataNode>("gamepads");
|
||||||
|
for (int i = 0; i < 4; i++) {
|
||||||
|
if (m_gamepadState->isConnected(i)) {
|
||||||
|
auto gp = std::make_unique<JsonDataNode>("gamepad");
|
||||||
|
gp->setBool("connected", true);
|
||||||
|
gp->setString("name", m_gamepadState->getGamepad(i).name);
|
||||||
|
// Save button/axis state si nécessaire
|
||||||
|
gamepads->setChild(std::to_string(i), std::move(gp));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
state->setChild("gamepads", std::move(gamepads));
|
||||||
|
```
|
||||||
|
|
||||||
|
**setState()** - Ajouter restauration gamepad :
|
||||||
|
```cpp
|
||||||
|
// Restore gamepad state
|
||||||
|
if (state.hasChild("gamepads")) {
|
||||||
|
auto& gamepads = state.getChild("gamepads");
|
||||||
|
// Restore connections et state
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Test Phase 2
|
||||||
|
|
||||||
|
### test_31_input_gamepad.cpp
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
/**
|
||||||
|
* Test: InputModule Gamepad Test
|
||||||
|
*
|
||||||
|
* Instructions:
|
||||||
|
* - Connect a gamepad/controller
|
||||||
|
* - Press buttons to see gamepad:button events
|
||||||
|
* - Move joysticks to see gamepad:axis events
|
||||||
|
* - Disconnect/reconnect to test gamepad:connected events
|
||||||
|
* - Press ESC to exit
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <SDL.h>
|
||||||
|
#include <grove/ModuleLoader.h>
|
||||||
|
#include <grove/IntraIOManager.h>
|
||||||
|
#include "modules/InputModule/InputModule.h"
|
||||||
|
|
||||||
|
#include <iostream>
|
||||||
|
#include <iomanip>
|
||||||
|
|
||||||
|
int main(int argc, char* argv[]) {
|
||||||
|
std::cout << "========================================\n";
|
||||||
|
std::cout << "InputModule Gamepad Test\n";
|
||||||
|
std::cout << "========================================\n\n";
|
||||||
|
|
||||||
|
// Initialize SDL with gamepad support
|
||||||
|
if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_GAMECONTROLLER) < 0) {
|
||||||
|
std::cerr << "SDL_Init failed: " << SDL_GetError() << "\n";
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_Window* window = SDL_CreateWindow(
|
||||||
|
"Gamepad Test - Press ESC to exit",
|
||||||
|
SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED,
|
||||||
|
800, 600, SDL_WINDOW_SHOWN
|
||||||
|
);
|
||||||
|
|
||||||
|
// Setup IIO
|
||||||
|
auto& ioManager = grove::IntraIOManager::getInstance();
|
||||||
|
auto inputIO = ioManager.createInstance("input_module");
|
||||||
|
auto testIO = ioManager.createInstance("test_controller");
|
||||||
|
|
||||||
|
// Load InputModule
|
||||||
|
grove::ModuleLoader inputLoader;
|
||||||
|
auto inputModule = inputLoader.load("../modules/InputModule.dll", "input_module");
|
||||||
|
|
||||||
|
grove::JsonDataNode config("config");
|
||||||
|
config.setBool("enableGamepad", true);
|
||||||
|
config.setDouble("gamepad.deadzone", 0.15);
|
||||||
|
|
||||||
|
inputModule->setConfiguration(config, inputIO.get(), nullptr);
|
||||||
|
|
||||||
|
// Subscribe to gamepad events
|
||||||
|
testIO->subscribe("input:gamepad:button");
|
||||||
|
testIO->subscribe("input:gamepad:axis");
|
||||||
|
testIO->subscribe("input:gamepad:connected");
|
||||||
|
|
||||||
|
bool running = true;
|
||||||
|
while (running) {
|
||||||
|
SDL_Event event;
|
||||||
|
while (SDL_PollEvent(&event)) {
|
||||||
|
if (event.type == SDL_QUIT) running = false;
|
||||||
|
if (event.type == SDL_KEYDOWN &&
|
||||||
|
event.key.keysym.scancode == SDL_SCANCODE_ESCAPE) {
|
||||||
|
running = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
inputModule->feedEvent(&event);
|
||||||
|
}
|
||||||
|
|
||||||
|
grove::JsonDataNode input("input");
|
||||||
|
inputModule->process(input);
|
||||||
|
|
||||||
|
// Display gamepad events
|
||||||
|
while (testIO->hasMessages() > 0) {
|
||||||
|
auto msg = testIO->pullMessage();
|
||||||
|
|
||||||
|
if (msg.topic == "input:gamepad:button") {
|
||||||
|
int id = msg.data->getInt("id", 0);
|
||||||
|
int button = msg.data->getInt("button", 0);
|
||||||
|
bool pressed = msg.data->getBool("pressed", false);
|
||||||
|
|
||||||
|
const char* buttonNames[] = {
|
||||||
|
"A", "B", "X", "Y",
|
||||||
|
"BACK", "GUIDE", "START",
|
||||||
|
"L3", "R3",
|
||||||
|
"DPAD_UP", "DPAD_DOWN", "DPAD_LEFT", "DPAD_RIGHT",
|
||||||
|
"LB", "RB"
|
||||||
|
};
|
||||||
|
|
||||||
|
std::cout << "[GAMEPAD " << id << "] Button "
|
||||||
|
<< buttonNames[button]
|
||||||
|
<< " " << (pressed ? "PRESSED" : "RELEASED") << "\n";
|
||||||
|
}
|
||||||
|
else if (msg.topic == "input:gamepad:axis") {
|
||||||
|
int id = msg.data->getInt("id", 0);
|
||||||
|
int axis = msg.data->getInt("axis", 0);
|
||||||
|
float value = msg.data->getDouble("value", 0.0);
|
||||||
|
|
||||||
|
const char* axisNames[] = {
|
||||||
|
"LEFT_X", "LEFT_Y",
|
||||||
|
"RIGHT_X", "RIGHT_Y",
|
||||||
|
"LT", "RT"
|
||||||
|
};
|
||||||
|
|
||||||
|
// Only print if value is significant
|
||||||
|
if (std::abs(value) > 0.01f) {
|
||||||
|
std::cout << "[GAMEPAD " << id << "] Axis "
|
||||||
|
<< axisNames[axis]
|
||||||
|
<< " = " << std::fixed << std::setprecision(2)
|
||||||
|
<< value << "\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (msg.topic == "input:gamepad:connected") {
|
||||||
|
int id = msg.data->getInt("id", 0);
|
||||||
|
std::string name = msg.data->getString("name", "Unknown");
|
||||||
|
bool connected = msg.data->getBool("connected", false);
|
||||||
|
|
||||||
|
std::cout << "[GAMEPAD " << id << "] "
|
||||||
|
<< (connected ? "CONNECTED" : "DISCONNECTED")
|
||||||
|
<< " - " << name << "\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_Delay(16);
|
||||||
|
}
|
||||||
|
|
||||||
|
inputModule->shutdown();
|
||||||
|
SDL_DestroyWindow(window);
|
||||||
|
SDL_Quit();
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration CMakeLists.txt
|
||||||
|
|
||||||
|
Ajouter dans `modules/InputModule/CMakeLists.txt` :
|
||||||
|
```cmake
|
||||||
|
# Phase 2 files (gamepad support)
|
||||||
|
if(GROVE_INPUT_MODULE_GAMEPAD)
|
||||||
|
target_sources(InputModule PRIVATE
|
||||||
|
Core/GamepadState.cpp
|
||||||
|
Backends/SDLGamepadBackend.cpp
|
||||||
|
)
|
||||||
|
target_compile_definitions(InputModule PRIVATE GROVE_GAMEPAD_SUPPORT)
|
||||||
|
endif()
|
||||||
|
```
|
||||||
|
|
||||||
|
Ajouter dans `tests/CMakeLists.txt` :
|
||||||
|
```cmake
|
||||||
|
# Test 31: InputModule Gamepad Test
|
||||||
|
if(GROVE_BUILD_INPUT_MODULE AND GROVE_INPUT_MODULE_GAMEPAD)
|
||||||
|
add_executable(test_31_input_gamepad
|
||||||
|
visual/test_31_input_gamepad.cpp
|
||||||
|
)
|
||||||
|
|
||||||
|
target_link_libraries(test_31_input_gamepad PRIVATE
|
||||||
|
GroveEngine::impl
|
||||||
|
SDL2
|
||||||
|
pthread
|
||||||
|
dl
|
||||||
|
X11
|
||||||
|
)
|
||||||
|
|
||||||
|
message(STATUS "Visual test 'test_31_input_gamepad' enabled (run manually)")
|
||||||
|
endif()
|
||||||
|
```
|
||||||
|
|
||||||
|
## Estimation
|
||||||
|
|
||||||
|
| Tâche | Complexité | Temps estimé |
|
||||||
|
|-------|------------|--------------|
|
||||||
|
| GamepadState.h/cpp | Facile | 30min |
|
||||||
|
| SDLGamepadBackend.h/cpp | Moyenne | 1h |
|
||||||
|
| Extend InputConverter | Facile | 30min |
|
||||||
|
| Modifications InputModule | Facile | 30min |
|
||||||
|
| test_31_input_gamepad.cpp | Moyenne | 1h |
|
||||||
|
| Debug & Polish | Moyenne | 30min |
|
||||||
|
| **Total Phase 2** | **4h** | **Support gamepad complet** |
|
||||||
|
|
||||||
|
## Ordre d'implémentation recommandé
|
||||||
|
|
||||||
|
1. ✅ Créer GamepadState.h/cpp
|
||||||
|
2. ✅ Créer SDLGamepadBackend.h/cpp
|
||||||
|
3. ✅ Extend SDLBackend::InputEvent enum + fields
|
||||||
|
4. ✅ Extend InputConverter (3 nouvelles méthodes)
|
||||||
|
5. ✅ Modifier InputModule.h (membres privés)
|
||||||
|
6. ✅ Modifier InputModule.cpp (process() + init/shutdown)
|
||||||
|
7. ✅ Tester avec test_31_input_gamepad.cpp
|
||||||
|
8. ✅ Valider hot-reload avec gamepad connecté
|
||||||
|
|
||||||
|
## Notes techniques
|
||||||
|
|
||||||
|
### Deadzone
|
||||||
|
La deadzone par défaut (0.15 = 15%) évite les micro-mouvements indésirables des joysticks au repos. Configurable via JSON.
|
||||||
|
|
||||||
|
### Multi-gamepad
|
||||||
|
SDL2 supporte jusqu'à 16 manettes, mais on limite à 4 pour des raisons pratiques (local multiplayer classique).
|
||||||
|
|
||||||
|
### Hot-reload
|
||||||
|
Pendant un hot-reload, les manettes connectées restent actives (SDL_GameController* restent valides). On peut restaurer l'état des boutons/axes si nécessaire.
|
||||||
|
|
||||||
|
### Performance
|
||||||
|
- Événements gamepad = ~20-50 events/frame max (2 joysticks + triggers + boutons)
|
||||||
|
- Overhead négligeable vs souris/clavier
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Status:** 📋 Planifié - Implémentation future
|
||||||
|
|
||||||
|
**Dépendances:** Phase 1 complétée, SDL2 avec support gamecontroller
|
||||||
283
tests/integration/IT_015_input_ui_integration.cpp.backup
Normal file
283
tests/integration/IT_015_input_ui_integration.cpp.backup
Normal file
@ -0,0 +1,283 @@
|
|||||||
|
/**
|
||||||
|
* Integration Test IT_015: UIModule Input Event Integration
|
||||||
|
*
|
||||||
|
* Tests input event processing pipeline:
|
||||||
|
* - Direct IIO input event publishing (bypasses InputModule/SDL)
|
||||||
|
* - UIModule (consumes input events, detects clicks/hover)
|
||||||
|
* - BgfxRenderer (renders UI feedback)
|
||||||
|
*
|
||||||
|
* Verifies:
|
||||||
|
* - UIModule receives and processes input:mouse:* and input:keyboard:* events
|
||||||
|
* - UI widgets respond to mouse clicks and hover
|
||||||
|
* - Button clicks trigger ui:action events
|
||||||
|
* - End-to-end flow: IIO input events → UIModule → BgfxRenderer
|
||||||
|
*
|
||||||
|
* Note: This test bypasses InputModule and publishes IIO messages directly.
|
||||||
|
* For a full SDL → InputModule → IIO test, see test_30_input_module.cpp
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <catch2/catch_test_macros.hpp>
|
||||||
|
#include <grove/ModuleLoader.h>
|
||||||
|
#include <grove/IntraIOManager.h>
|
||||||
|
#include <grove/IntraIO.h>
|
||||||
|
#include <grove/JsonDataNode.h>
|
||||||
|
#include <thread>
|
||||||
|
#include <chrono>
|
||||||
|
#include <iostream>
|
||||||
|
|
||||||
|
using namespace grove;
|
||||||
|
|
||||||
|
TEST_CASE("IT_015: UIModule Input Integration", "[integration][input][ui][phase3]") {
|
||||||
|
std::cout << "\n========================================\n";
|
||||||
|
std::cout << "IT_015: Input → UI Integration Test\n";
|
||||||
|
std::cout << "========================================\n\n";
|
||||||
|
|
||||||
|
auto& ioManager = IntraIOManager::getInstance();
|
||||||
|
|
||||||
|
// Create IIO instances
|
||||||
|
auto inputPublisher = ioManager.createInstance("input_publisher"); // Simulates InputModule
|
||||||
|
auto uiIO = ioManager.createInstance("ui_module");
|
||||||
|
auto rendererIO = ioManager.createInstance("bgfx_renderer");
|
||||||
|
auto testIO = ioManager.createInstance("test_observer");
|
||||||
|
|
||||||
|
SECTION("Load UI and Renderer modules") {
|
||||||
|
ModuleLoader uiLoader;
|
||||||
|
ModuleLoader rendererLoader;
|
||||||
|
|
||||||
|
std::string uiPath = "../modules/libUIModule.so";
|
||||||
|
std::string rendererPath = "../modules/libBgfxRenderer.so";
|
||||||
|
|
||||||
|
#ifdef _WIN32
|
||||||
|
uiPath = "../modules/UIModule.dll";
|
||||||
|
rendererPath = "../modules/BgfxRenderer.dll";
|
||||||
|
#endif
|
||||||
|
|
||||||
|
SECTION("Load UIModule") {
|
||||||
|
std::unique_ptr<IModule> uiModule;
|
||||||
|
REQUIRE_NOTHROW(uiModule = uiLoader.load(uiPath, "ui_module"));
|
||||||
|
REQUIRE(uiModule != nullptr);
|
||||||
|
|
||||||
|
// Configure UIModule
|
||||||
|
JsonDataNode uiConfig("config");
|
||||||
|
uiConfig.setInt("windowWidth", 800);
|
||||||
|
uiConfig.setInt("windowHeight", 600);
|
||||||
|
uiConfig.setString("layoutFile", "../../assets/ui/test_buttons.json");
|
||||||
|
uiConfig.setInt("baseLayer", 1000);
|
||||||
|
|
||||||
|
REQUIRE_NOTHROW(uiModule->setConfiguration(uiConfig, uiIO.get(), nullptr));
|
||||||
|
|
||||||
|
std::cout << "✅ UIModule loaded and configured\n";
|
||||||
|
|
||||||
|
SECTION("Load BgfxRenderer (optional)") {
|
||||||
|
std::unique_ptr<IModule> renderer;
|
||||||
|
|
||||||
|
// Try to load renderer, but don't fail if not available
|
||||||
|
try {
|
||||||
|
renderer = rendererLoader.load(rendererPath, "bgfx_renderer");
|
||||||
|
|
||||||
|
if (renderer) {
|
||||||
|
JsonDataNode rendererConfig("config");
|
||||||
|
rendererConfig.setInt("windowWidth", 800);
|
||||||
|
rendererConfig.setInt("windowHeight", 600);
|
||||||
|
rendererConfig.setString("backend", "noop"); // Headless
|
||||||
|
rendererConfig.setBool("vsync", false);
|
||||||
|
|
||||||
|
renderer->setConfiguration(rendererConfig, rendererIO.get(), nullptr);
|
||||||
|
std::cout << "✅ BgfxRenderer loaded (headless mode)\n";
|
||||||
|
}
|
||||||
|
} catch (...) {
|
||||||
|
std::cout << "⚠️ BgfxRenderer not available, testing without renderer\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTION("Test input event flow") {
|
||||||
|
std::cout << "\n--- Testing Input Event Flow ---\n";
|
||||||
|
|
||||||
|
// Subscribe test observer to all relevant topics
|
||||||
|
testIO->subscribe("input:mouse:move");
|
||||||
|
testIO->subscribe("input:mouse:button");
|
||||||
|
testIO->subscribe("input:keyboard:key");
|
||||||
|
testIO->subscribe("ui:click");
|
||||||
|
testIO->subscribe("ui:hover");
|
||||||
|
testIO->subscribe("ui:action");
|
||||||
|
|
||||||
|
int mouseMovesPublished = 0;
|
||||||
|
int mouseClicksPublished = 0;
|
||||||
|
int keyEventsPublished = 0;
|
||||||
|
int uiClicksReceived = 0;
|
||||||
|
int uiHoversReceived = 0;
|
||||||
|
int uiActionsReceived = 0;
|
||||||
|
|
||||||
|
// Helper: Publish mouse move event via IIO
|
||||||
|
auto publishMouseMove = [&](int x, int y) {
|
||||||
|
auto data = std::make_unique<JsonDataNode>("data");
|
||||||
|
data->setInt("x", x);
|
||||||
|
data->setInt("y", y);
|
||||||
|
inputPublisher->publish("input:mouse:move", std::move(data));
|
||||||
|
mouseMovesPublished++;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper: Publish mouse button event via IIO
|
||||||
|
auto publishMouseButton = [&](bool pressed, int button, int x, int y) {
|
||||||
|
auto data = std::make_unique<JsonDataNode>("data");
|
||||||
|
data->setInt("button", button);
|
||||||
|
data->setBool("pressed", pressed);
|
||||||
|
data->setInt("x", x);
|
||||||
|
data->setInt("y", y);
|
||||||
|
inputPublisher->publish("input:mouse:button", std::move(data));
|
||||||
|
mouseClicksPublished++;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper: Publish keyboard key event via IIO
|
||||||
|
auto publishKeyEvent = [&](bool pressed, int scancode) {
|
||||||
|
auto data = std::make_unique<JsonDataNode>("data");
|
||||||
|
data->setInt("scancode", scancode);
|
||||||
|
data->setBool("pressed", pressed);
|
||||||
|
data->setBool("repeat", false);
|
||||||
|
data->setBool("shift", false);
|
||||||
|
data->setBool("ctrl", false);
|
||||||
|
data->setBool("alt", false);
|
||||||
|
inputPublisher->publish("input:keyboard:key", std::move(data));
|
||||||
|
keyEventsPublished++;
|
||||||
|
};
|
||||||
|
|
||||||
|
std::cout << "\n--- Publishing Input Events via IIO ---\n";
|
||||||
|
|
||||||
|
// Simulate 100 frames of input processing
|
||||||
|
for (int frame = 0; frame < 100; frame++) {
|
||||||
|
// Frame 10: Move mouse to center
|
||||||
|
if (frame == 10) {
|
||||||
|
publishMouseMove(400, 300);
|
||||||
|
std::cout << "[Frame " << frame << "] Publish: input:mouse:move (400, 300)\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Frame 20: Move mouse to button position (assuming button at 100, 100)
|
||||||
|
if (frame == 20) {
|
||||||
|
publishMouseMove(100, 100);
|
||||||
|
std::cout << "[Frame " << frame << "] Publish: input:mouse:move (100, 100)\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Frame 30: Click mouse (press)
|
||||||
|
if (frame == 30) {
|
||||||
|
publishMouseButton(true, 0, 100, 100);
|
||||||
|
std::cout << "[Frame " << frame << "] Publish: input:mouse:button DOWN at (100, 100)\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Frame 32: Release mouse
|
||||||
|
if (frame == 32) {
|
||||||
|
publishMouseButton(false, 0, 100, 100);
|
||||||
|
std::cout << "[Frame " << frame << "] Publish: input:mouse:button UP at (100, 100)\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Frame 50: Press Space key (scancode 44)
|
||||||
|
if (frame == 50) {
|
||||||
|
publishKeyEvent(true, 44); // SDL_SCANCODE_SPACE = 44
|
||||||
|
std::cout << "[Frame " << frame << "] Publish: input:keyboard:key DOWN (scancode=44)\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Frame 52: Release Space key
|
||||||
|
if (frame == 52) {
|
||||||
|
publishKeyEvent(false, 44);
|
||||||
|
std::cout << "[Frame " << frame << "] Publish: input:keyboard:key UP (scancode=44)\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process UIModule (consumes input events, generates UI events)
|
||||||
|
JsonDataNode uiInputData("input");
|
||||||
|
uiModule->process(uiInputData);
|
||||||
|
|
||||||
|
// Process Renderer if available
|
||||||
|
if (renderer) {
|
||||||
|
JsonDataNode rendererInputData("input");
|
||||||
|
renderer->process(rendererInputData);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collect published events
|
||||||
|
while (testIO->hasMessages() > 0) {
|
||||||
|
auto msg = testIO->pullMessage();
|
||||||
|
|
||||||
|
if (msg.topic == "input:mouse:move") {
|
||||||
|
mouseMovesPublished++;
|
||||||
|
int x = msg.data->getInt("x", 0);
|
||||||
|
int y = msg.data->getInt("y", 0);
|
||||||
|
std::cout << "[Frame " << frame << "] Received: input:mouse:move ("
|
||||||
|
<< x << ", " << y << ")\n";
|
||||||
|
}
|
||||||
|
else if (msg.topic == "input:mouse:button") {
|
||||||
|
mouseClicksPublished++;
|
||||||
|
bool pressed = msg.data->getBool("pressed", false);
|
||||||
|
int x = msg.data->getInt("x", 0);
|
||||||
|
int y = msg.data->getInt("y", 0);
|
||||||
|
std::cout << "[Frame " << frame << "] Received: input:mouse:button "
|
||||||
|
<< (pressed ? "DOWN" : "UP") << " at (" << x << ", " << y << ")\n";
|
||||||
|
}
|
||||||
|
else if (msg.topic == "input:keyboard:key") {
|
||||||
|
keyEventsPublished++;
|
||||||
|
int scancode = msg.data->getInt("scancode", 0);
|
||||||
|
bool pressed = msg.data->getBool("pressed", false);
|
||||||
|
std::cout << "[Frame " << frame << "] Received: input:keyboard:key "
|
||||||
|
<< scancode << " " << (pressed ? "DOWN" : "UP") << "\n";
|
||||||
|
}
|
||||||
|
else if (msg.topic == "ui:click") {
|
||||||
|
uiClicksReceived++;
|
||||||
|
std::string widgetId = msg.data->getString("widgetId", "");
|
||||||
|
std::cout << "[Frame " << frame << "] Received: ui:click on widget '"
|
||||||
|
<< widgetId << "'\n";
|
||||||
|
}
|
||||||
|
else if (msg.topic == "ui:hover") {
|
||||||
|
uiHoversReceived++;
|
||||||
|
std::string widgetId = msg.data->getString("widgetId", "");
|
||||||
|
std::cout << "[Frame " << frame << "] Received: ui:hover on widget '"
|
||||||
|
<< widgetId << "'\n";
|
||||||
|
}
|
||||||
|
else if (msg.topic == "ui:action") {
|
||||||
|
uiActionsReceived++;
|
||||||
|
std::string widgetId = msg.data->getString("widgetId", "");
|
||||||
|
std::string action = msg.data->getString("action", "");
|
||||||
|
std::cout << "[Frame " << frame << "] Received: ui:action '"
|
||||||
|
<< action << "' from widget '" << widgetId << "'\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Small delay to simulate real frame timing
|
||||||
|
std::this_thread::sleep_for(std::chrono::milliseconds(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
std::cout << "\n--- Integration Test Results ---\n";
|
||||||
|
std::cout << "Input events published:\n";
|
||||||
|
std::cout << " - Mouse moves: " << mouseMovesPublished << "\n";
|
||||||
|
std::cout << " - Mouse clicks: " << mouseClicksPublished << "\n";
|
||||||
|
std::cout << " - Key events: " << keyEventsPublished << "\n";
|
||||||
|
std::cout << "\nUI events received:\n";
|
||||||
|
std::cout << " - UI clicks: " << uiClicksReceived << "\n";
|
||||||
|
std::cout << " - UI hovers: " << uiHoversReceived << "\n";
|
||||||
|
std::cout << " - UI actions: " << uiActionsReceived << "\n";
|
||||||
|
|
||||||
|
// Verify we published input events
|
||||||
|
REQUIRE(mouseMovesPublished == 2); // 2 mouse moves
|
||||||
|
REQUIRE(mouseClicksPublished == 2); // press + release
|
||||||
|
REQUIRE(keyEventsPublished == 2); // press + release
|
||||||
|
|
||||||
|
std::cout << "\n✅ Successfully published input events via IIO\n";
|
||||||
|
|
||||||
|
// Note: UI events depend on layout file existing and being valid
|
||||||
|
// If layout file is missing, UI events might be 0, which is OK for this test
|
||||||
|
if (uiClicksReceived > 0 || uiHoversReceived > 0) {
|
||||||
|
std::cout << "✅ UIModule correctly processed input events\n";
|
||||||
|
} else {
|
||||||
|
std::cout << "⚠️ No UI events received (layout file may be missing)\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
std::cout << "\n✅ IT_015: Integration test PASSED\n";
|
||||||
|
std::cout << "\n========================================\n";
|
||||||
|
} // End SECTION("Test input event flow")
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
if (renderer) {
|
||||||
|
renderer->shutdown();
|
||||||
|
}
|
||||||
|
} // End SECTION("Load BgfxRenderer")
|
||||||
|
|
||||||
|
uiModule->shutdown();
|
||||||
|
} // End SECTION("Load UIModule")
|
||||||
|
} // End SECTION("Load UI and Renderer modules")
|
||||||
|
} // End all SECTIONs
|
||||||
|
} // End TEST_CASE
|
||||||
Loading…
Reference in New Issue
Block a user