#include "GameEngine.h" #include "ActionCode.h" #include "McRFPy_API.h" #include "PyScene.h" #include "UITestScene.h" #include "Resources.h" #include "Animation.h" #include "Timer.h" #include GameEngine::GameEngine() : GameEngine(McRogueFaceConfig{}) { } GameEngine::GameEngine(const McRogueFaceConfig& cfg) : config(cfg), headless(cfg.headless) { Resources::font.loadFromFile("./assets/JetbrainsMono.ttf"); Resources::game = this; window_title = "McRogueFace Engine"; // Initialize rendering based on headless mode if (headless) { headless_renderer = std::make_unique(); if (!headless_renderer->init(1024, 768)) { throw std::runtime_error("Failed to initialize headless renderer"); } render_target = &headless_renderer->getRenderTarget(); } else { window = std::make_unique(); window->create(sf::VideoMode(1024, 768), window_title, sf::Style::Titlebar | sf::Style::Close | sf::Style::Resize); window->setFramerateLimit(60); render_target = window.get(); } visible = render_target->getDefaultView(); // Initialize the game view gameView.setSize(static_cast(gameResolution.x), static_cast(gameResolution.y)); // Use integer center coordinates for pixel-perfect rendering gameView.setCenter(std::floor(gameResolution.x / 2.0f), std::floor(gameResolution.y / 2.0f)); updateViewport(); scene = "uitest"; scenes["uitest"] = new UITestScene(this); McRFPy_API::game = this; // Only load game.py if no custom script/command/module/exec is specified bool should_load_game = config.script_path.empty() && config.python_command.empty() && config.python_module.empty() && config.exec_scripts.empty() && !config.interactive_mode && !config.python_mode; if (should_load_game) { if (!Py_IsInitialized()) { McRFPy_API::api_init(); } McRFPy_API::executePyString("import mcrfpy"); McRFPy_API::executeScript("scripts/game.py"); } // Execute any --exec scripts in order if (!config.exec_scripts.empty()) { if (!Py_IsInitialized()) { McRFPy_API::api_init(); } McRFPy_API::executePyString("import mcrfpy"); for (const auto& exec_script : config.exec_scripts) { std::cout << "Executing script: " << exec_script << std::endl; McRFPy_API::executeScript(exec_script.string()); } std::cout << "All --exec scripts completed" << std::endl; } clock.restart(); runtime.restart(); } GameEngine::~GameEngine() { cleanup(); for (auto& [name, scene] : scenes) { delete scene; } } void GameEngine::cleanup() { if (cleaned_up) return; cleaned_up = true; // Clear all animations first (RAII handles invalidation) AnimationManager::getInstance().clear(); // Clear Python references before destroying C++ objects // Clear all timers (they hold Python callables) timers.clear(); // Clear McRFPy_API's reference to this game engine if (McRFPy_API::game == this) { McRFPy_API::game = nullptr; } // Force close the window if it's still open if (window && window->isOpen()) { window->close(); } } Scene* GameEngine::currentScene() { return scenes[scene]; } void GameEngine::changeScene(std::string s) { changeScene(s, TransitionType::None, 0.0f); } void GameEngine::changeScene(std::string sceneName, TransitionType transitionType, float duration) { if (scenes.find(sceneName) == scenes.end()) { std::cout << "Attempted to change to a scene that doesn't exist (`" << sceneName << "`)" << std::endl; return; } if (transitionType == TransitionType::None || duration <= 0.0f) { // Immediate scene change std::string old_scene = scene; scene = sceneName; // Trigger Python scene lifecycle events McRFPy_API::triggerSceneChange(old_scene, sceneName); } else { // Start transition transition.start(transitionType, scene, sceneName, duration); // Render current scene to texture sf::RenderTarget* original_target = render_target; render_target = transition.oldSceneTexture.get(); transition.oldSceneTexture->clear(); currentScene()->render(); transition.oldSceneTexture->display(); // Change to new scene std::string old_scene = scene; scene = sceneName; // Render new scene to texture render_target = transition.newSceneTexture.get(); transition.newSceneTexture->clear(); currentScene()->render(); transition.newSceneTexture->display(); // Restore original render target and scene render_target = original_target; scene = old_scene; } } void GameEngine::quit() { running = false; } void GameEngine::setPause(bool p) { paused = p; } sf::Font & GameEngine::getFont() { /*return font; */ return Resources::font; } sf::RenderWindow & GameEngine::getWindow() { if (!window) { throw std::runtime_error("Window not available in headless mode"); } return *window; } sf::RenderTarget & GameEngine::getRenderTarget() { return *render_target; } void GameEngine::createScene(std::string s) { scenes[s] = new PyScene(this); } void GameEngine::setWindowScale(float multiplier) { if (!headless && window) { window->setSize(sf::Vector2u(gameResolution.x * multiplier, gameResolution.y * multiplier)); updateViewport(); } } void GameEngine::run() { //std::cout << "GameEngine::run() starting main loop..." << std::endl; float fps = 0.0; frameTime = 0.016f; // Initialize to ~60 FPS clock.restart(); while (running) { // Reset per-frame metrics metrics.resetPerFrame(); currentScene()->update(); testTimers(); // Update Python scenes McRFPy_API::updatePythonScenes(frameTime); // Update animations (only if frameTime is valid) if (frameTime > 0.0f && frameTime < 1.0f) { AnimationManager::getInstance().update(frameTime); } if (!headless) { sUserInput(); } if (!paused) { } // Handle scene transitions if (transition.type != TransitionType::None) { transition.update(frameTime); if (transition.isComplete()) { // Transition complete - finalize scene change scene = transition.toScene; transition.type = TransitionType::None; // Trigger Python scene lifecycle events McRFPy_API::triggerSceneChange(transition.fromScene, transition.toScene); } else { // Render transition render_target->clear(); transition.render(*render_target); } } else { // Normal scene rendering currentScene()->render(); } // Display the frame if (headless) { headless_renderer->display(); // Take screenshot if requested if (config.take_screenshot) { headless_renderer->saveScreenshot(config.screenshot_path.empty() ? "screenshot.png" : config.screenshot_path); config.take_screenshot = false; // Only take one screenshot } } else { window->display(); } currentFrame++; frameTime = clock.restart().asSeconds(); fps = 1 / frameTime; // Update profiling metrics metrics.updateFrameTime(frameTime * 1000.0f); // Convert to milliseconds int whole_fps = metrics.fps; int tenth_fps = (metrics.fps * 10) % 10; if (!headless && window) { window->setTitle(window_title); } // In windowed mode, check if window was closed if (!headless && window && !window->isOpen()) { running = false; } } // Clean up before exiting the run loop cleanup(); } std::shared_ptr GameEngine::getTimer(const std::string& name) { auto it = timers.find(name); if (it != timers.end()) { return it->second; } return nullptr; } void GameEngine::manageTimer(std::string name, PyObject* target, int interval) { auto it = timers.find(name); if (it != timers.end()) // overwrite existing { if (target == NULL || target == Py_None) { // Delete: Overwrite existing timer with one that calls None. This will be deleted in the next timer check // see gitea issue #4: this allows for a timer to be deleted during its own call to itself timers[name] = std::make_shared(Py_None, 1000, runtime.getElapsedTime().asMilliseconds()); return; } } if (target == NULL || target == Py_None) { std::cout << "Refusing to initialize timer to None. It's not an error, it's just pointless." << std::endl; return; } timers[name] = std::make_shared(target, interval, runtime.getElapsedTime().asMilliseconds()); } void GameEngine::testTimers() { int now = runtime.getElapsedTime().asMilliseconds(); auto it = timers.begin(); while (it != timers.end()) { it->second->test(now); // Remove timers that have been cancelled or are one-shot and fired if (!it->second->getCallback() || it->second->getCallback() == Py_None) { it = timers.erase(it); } else it++; } } void GameEngine::processEvent(const sf::Event& event) { std::string actionType; int actionCode = 0; if (event.type == sf::Event::Closed) { running = false; return; } // Handle window resize events else if (event.type == sf::Event::Resized) { // Update the viewport to handle the new window size updateViewport(); // Notify Python scenes about the resize McRFPy_API::triggerResize(event.size.width, event.size.height); } else if (event.type == sf::Event::KeyPressed || event.type == sf::Event::MouseButtonPressed || event.type == sf::Event::MouseWheelScrolled) actionType = "start"; else if (event.type == sf::Event::KeyReleased || event.type == sf::Event::MouseButtonReleased) actionType = "end"; if (event.type == sf::Event::MouseButtonPressed || event.type == sf::Event::MouseButtonReleased) actionCode = ActionCode::keycode(event.mouseButton.button); else if (event.type == sf::Event::KeyPressed || event.type == sf::Event::KeyReleased) actionCode = ActionCode::keycode(event.key.code); else if (event.type == sf::Event::MouseWheelScrolled) { if (event.mouseWheelScroll.wheel == sf::Mouse::VerticalWheel) { int delta = 1; if (event.mouseWheelScroll.delta < 0) delta = -1; actionCode = ActionCode::keycode(event.mouseWheelScroll.wheel, delta ); } } else return; if (currentScene()->hasAction(actionCode)) { std::string name = currentScene()->action(actionCode); currentScene()->doAction(name, actionType); } else if (currentScene()->key_callable && (event.type == sf::Event::KeyPressed || event.type == sf::Event::KeyReleased)) { currentScene()->key_callable->call(ActionCode::key_str(event.key.code), actionType); } } void GameEngine::sUserInput() { sf::Event event; while (window && window->pollEvent(event)) { processEvent(event); } } std::shared_ptr>> GameEngine::scene_ui(std::string target) { /* // facts about maps // You just can't do this during scenes["new_menu"] being assigned. std::cout << "Current scene is: " << scene << ". Searching for: " << target << ".\n"; std::cout << "scenes.size(): " << scenes.size() << std::endl; std::cout << "scenes.count(target): " << scenes.count(target) << std::endl; std::cout << "scenes.find(target): " << std::distance(scenes.begin(), scenes.find(target)) << std::endl; std::cout << "iterators: " << std::distance(scenes.begin(), scenes.begin()) << " " << std::distance(scenes.begin(), scenes.end()) << std::endl; std::cout << "scenes.contains(target): " << scenes.contains(target) << std::endl; std::cout << "scenes[target]: " << (long)(scenes[target]) << std::endl; */ if (scenes.count(target) == 0) return NULL; return scenes[target]->ui_elements; } void GameEngine::setWindowTitle(const std::string& title) { window_title = title; if (!headless && window) { window->setTitle(title); } } void GameEngine::setVSync(bool enabled) { vsync_enabled = enabled; if (!headless && window) { window->setVerticalSyncEnabled(enabled); } } void GameEngine::setFramerateLimit(unsigned int limit) { framerate_limit = limit; if (!headless && window) { window->setFramerateLimit(limit); } } void GameEngine::setGameResolution(unsigned int width, unsigned int height) { gameResolution = sf::Vector2u(width, height); gameView.setSize(static_cast(width), static_cast(height)); // Use integer center coordinates for pixel-perfect rendering gameView.setCenter(std::floor(width / 2.0f), std::floor(height / 2.0f)); updateViewport(); } void GameEngine::setViewportMode(ViewportMode mode) { viewportMode = mode; updateViewport(); } std::string GameEngine::getViewportModeString() const { switch (viewportMode) { case ViewportMode::Center: return "center"; case ViewportMode::Stretch: return "stretch"; case ViewportMode::Fit: return "fit"; } return "unknown"; } void GameEngine::updateViewport() { if (!render_target) return; auto windowSize = render_target->getSize(); switch (viewportMode) { case ViewportMode::Center: { // 1:1 pixels, centered in window float viewportWidth = std::min(static_cast(gameResolution.x), static_cast(windowSize.x)); float viewportHeight = std::min(static_cast(gameResolution.y), static_cast(windowSize.y)); // Floor offsets to ensure integer pixel alignment float offsetX = std::floor((windowSize.x - viewportWidth) / 2.0f); float offsetY = std::floor((windowSize.y - viewportHeight) / 2.0f); gameView.setViewport(sf::FloatRect( offsetX / windowSize.x, offsetY / windowSize.y, viewportWidth / windowSize.x, viewportHeight / windowSize.y )); break; } case ViewportMode::Stretch: { // Fill entire window, ignore aspect ratio gameView.setViewport(sf::FloatRect(0, 0, 1, 1)); break; } case ViewportMode::Fit: { // Maintain aspect ratio with black bars float windowAspect = static_cast(windowSize.x) / windowSize.y; float gameAspect = static_cast(gameResolution.x) / gameResolution.y; float viewportWidth, viewportHeight; float offsetX = 0, offsetY = 0; if (windowAspect > gameAspect) { // Window is wider - black bars on sides // Calculate viewport size in pixels and floor for pixel-perfect scaling float pixelHeight = static_cast(windowSize.y); float pixelWidth = std::floor(pixelHeight * gameAspect); viewportHeight = 1.0f; viewportWidth = pixelWidth / windowSize.x; offsetX = (1.0f - viewportWidth) / 2.0f; } else { // Window is taller - black bars on top/bottom // Calculate viewport size in pixels and floor for pixel-perfect scaling float pixelWidth = static_cast(windowSize.x); float pixelHeight = std::floor(pixelWidth / gameAspect); viewportWidth = 1.0f; viewportHeight = pixelHeight / windowSize.y; offsetY = (1.0f - viewportHeight) / 2.0f; } gameView.setViewport(sf::FloatRect(offsetX, offsetY, viewportWidth, viewportHeight)); break; } } // Apply the view render_target->setView(gameView); } sf::Vector2f GameEngine::windowToGameCoords(const sf::Vector2f& windowPos) const { if (!render_target) return windowPos; // Convert window coordinates to game coordinates using the view return render_target->mapPixelToCoords(sf::Vector2i(windowPos), gameView); }