feat: Add ImGui-based developer console overlay

Integrates Dear ImGui for an in-game debug console that replaces the
blocking Python REPL. Press ~ (grave/tilde) to toggle the console.

Features:
- Python code execution without blocking the game loop
- Output capture with color coding (yellow=input, red=errors, gray=output)
- Expression results show repr() automatically
- Command history navigation with up/down arrows
- Word wrapping for long output lines
- Auto-scroll that doesn't fight manual scrolling
- mcrfpy.setDevConsole(bool) API to disable for shipping

Technical changes:
- Update imgui submodule to v1.89.9 (stable)
- Update imgui-sfml submodule to 2.6.x branch (SFML 2.x compatible)
- Add ImGui sources to CMakeLists.txt with OpenGL dependency
- Integrate ImGui lifecycle into GameEngine
- Add ImGuiConsole class for console overlay

closes #36, closes #65, closes #75

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
John McCardle 2025-11-26 20:03:58 -05:00
commit 3f6ea4fe33
9 changed files with 405 additions and 10 deletions

View file

@ -6,6 +6,8 @@
#include "Resources.h"
#include "Animation.h"
#include "Timer.h"
#include "imgui.h"
#include "imgui-SFML.h"
#include <cmath>
GameEngine::GameEngine() : GameEngine(McRogueFaceConfig{})
@ -31,8 +33,13 @@ GameEngine::GameEngine(const McRogueFaceConfig& cfg)
window->create(sf::VideoMode(1024, 768), window_title, sf::Style::Titlebar | sf::Style::Close | sf::Style::Resize);
window->setFramerateLimit(60);
render_target = window.get();
// Initialize ImGui for the window
if (ImGui::SFML::Init(*window)) {
imguiInitialized = true;
}
}
visible = render_target->getDefaultView();
// Initialize the game view
@ -116,6 +123,12 @@ void GameEngine::cleanup()
McRFPy_API::game = nullptr;
}
// Shutdown ImGui before closing window
if (imguiInitialized) {
ImGui::SFML::Shutdown();
imguiInitialized = false;
}
// Force close the window if it's still open
if (window && window->isOpen()) {
window->close();
@ -224,6 +237,11 @@ void GameEngine::run()
if (!headless) {
sUserInput();
// Update ImGui
if (imguiInitialized) {
ImGui::SFML::Update(*window, clock.getElapsedTime());
}
}
if (!paused)
{
@ -262,6 +280,12 @@ void GameEngine::run()
profilerOverlay->render(*render_target);
}
// Render ImGui console overlay
if (imguiInitialized && !headless) {
console.render();
ImGui::SFML::Render(*window);
}
// Display the frame
if (headless) {
headless_renderer->display();
@ -420,6 +444,26 @@ void GameEngine::sUserInput()
sf::Event event;
while (window && window->pollEvent(event))
{
// Process event through ImGui first
if (imguiInitialized) {
ImGui::SFML::ProcessEvent(*window, event);
}
// Handle grave/tilde key for console toggle (before other processing)
if (event.type == sf::Event::KeyPressed && event.key.code == sf::Keyboard::Grave) {
console.toggle();
continue; // Don't pass grave key to game
}
// If console wants keyboard, don't pass keyboard events to game
if (console.wantsKeyboardInput()) {
// Still process non-keyboard events (mouse, window close, etc.)
if (event.type == sf::Event::KeyPressed || event.type == sf::Event::KeyReleased ||
event.type == sf::Event::TextEntered) {
continue;
}
}
processEvent(event);
}
}