From 3b27401f29fde65d6a5adc52f26cc2d475488e42 Mon Sep 17 00:00:00 2001 From: John McCardle Date: Sun, 1 Feb 2026 16:40:23 -0500 Subject: [PATCH 1/2] Remove debugging output --- src/GameEngine.cpp | 10 ------ src/McRFPy_API.cpp | 22 +------------ src/platform/SDL2Renderer.cpp | 59 ++--------------------------------- 3 files changed, 3 insertions(+), 88 deletions(-) diff --git a/src/GameEngine.cpp b/src/GameEngine.cpp index 68c3f8e..aa433a6 100644 --- a/src/GameEngine.cpp +++ b/src/GameEngine.cpp @@ -123,21 +123,11 @@ GameEngine::GameEngine(const McRogueFaceConfig& cfg) !config.python_mode; if (should_load_game) { - std::cerr << "[DEBUG] GameEngine: loading default game.py" << std::endl; - std::cerr.flush(); if (!Py_IsInitialized()) { - std::cerr << "[DEBUG] GameEngine: initializing Python API" << std::endl; - std::cerr.flush(); McRFPy_API::api_init(); } - std::cerr << "[DEBUG] GameEngine: importing mcrfpy" << std::endl; - std::cerr.flush(); McRFPy_API::executePyString("import mcrfpy"); - std::cerr << "[DEBUG] GameEngine: executing scripts/game.py" << std::endl; - std::cerr.flush(); McRFPy_API::executeScript("scripts/game.py"); - std::cerr << "[DEBUG] GameEngine: game.py execution complete" << std::endl; - std::cerr.flush(); } // Note: --exec scripts are NOT executed here. diff --git a/src/McRFPy_API.cpp b/src/McRFPy_API.cpp index 292236d..94e2c85 100644 --- a/src/McRFPy_API.cpp +++ b/src/McRFPy_API.cpp @@ -699,28 +699,18 @@ PyObject* PyInit_mcrfpy() // init_python - configure interpreter details here PyStatus init_python(const char *program_name) { - std::cerr << "[DEBUG] api_init: starting" << std::endl; - std::cerr.flush(); - PyStatus status; - //**preconfig to establish locale** + // Preconfig to establish locale PyPreConfig preconfig; PyPreConfig_InitIsolatedConfig(&preconfig); preconfig.utf8_mode = 1; - std::cerr << "[DEBUG] api_init: Py_PreInitialize" << std::endl; - std::cerr.flush(); - status = Py_PreInitialize(&preconfig); if (PyStatus_Exception(status)) { - std::cerr << "[DEBUG] api_init: PreInit failed" << std::endl; Py_ExitStatusException(status); } - std::cerr << "[DEBUG] api_init: PyConfig setup" << std::endl; - std::cerr.flush(); - PyConfig config; PyConfig_InitIsolatedConfig(&config); config.dev_mode = 0; @@ -731,9 +721,6 @@ PyStatus init_python(const char *program_name) config.configure_c_stdio = 1; #ifdef __EMSCRIPTEN__ - std::cerr << "[DEBUG] api_init: WASM path config" << std::endl; - std::cerr.flush(); - // WASM: Use absolute paths in virtual filesystem PyConfig_SetString(&config, &config.executable, L"/mcrogueface"); PyConfig_SetString(&config, &config.home, L"/lib/python3.14"); @@ -814,18 +801,11 @@ PyStatus init_python(const char *program_name) PyStatus McRFPy_API::init_python_with_config(const McRogueFaceConfig& config) { - std::cerr << "[DEBUG] init_python_with_config: starting" << std::endl; - std::cerr.flush(); - // If Python is already initialized, just return success if (Py_IsInitialized()) { - std::cerr << "[DEBUG] init_python_with_config: already initialized" << std::endl; return PyStatus_Ok(); } - std::cerr << "[DEBUG] init_python_with_config: PyConfig_InitIsolatedConfig" << std::endl; - std::cerr.flush(); - PyStatus status; PyConfig pyconfig; PyConfig_InitIsolatedConfig(&pyconfig); diff --git a/src/platform/SDL2Renderer.cpp b/src/platform/SDL2Renderer.cpp index 1968ed4..3ed9a13 100644 --- a/src/platform/SDL2Renderer.cpp +++ b/src/platform/SDL2Renderer.cpp @@ -377,16 +377,9 @@ void SDL2Renderer::setProjection(float left, float right, float bottom, float to projectionMatrix_[15] = 1.0f; } -static int clearCount = 0; void SDL2Renderer::clear(float r, float g, float b, float a) { glClearColor(r, g, b, a); glClear(GL_COLOR_BUFFER_BIT); - - // Debug: Log first few clears to confirm render loop is running - if (clearCount < 5) { - std::cout << "SDL2Renderer::clear(" << r << ", " << g << ", " << b << ", " << a << ") #" << clearCount << std::endl; - clearCount++; - } } void SDL2Renderer::pushRenderState(unsigned int width, unsigned int height) { @@ -466,22 +459,6 @@ void SDL2Renderer::drawTriangles(const float* vertices, size_t vertexCount, glBindTexture(GL_TEXTURE_2D, textureId); int texLoc = glGetUniformLocation(program, "u_texture"); glUniform1i(texLoc, 0); - - // Debug: verify texture binding for text shader - static int texBindDebug = 0; - if (texBindDebug < 2 && shaderType == ShaderType::Text) { - std::cout << "drawTriangles(Text): textureId=" << textureId - << " texLoc=" << texLoc << " program=" << program << std::endl; - texBindDebug++; - } - } else if (shaderType == ShaderType::Text) { - // This would explain solid boxes - texture not being bound! - static bool warnOnce = true; - if (warnOnce) { - std::cerr << "WARNING: Text shader used but texture not bound! texCoords=" - << (texCoords ? "valid" : "null") << " textureId=" << textureId << std::endl; - warnOnce = false; - } } // Draw @@ -541,8 +518,6 @@ void RenderWindow::create(VideoMode mode, const std::string& title, uint32_t sty // Set the canvas size explicitly before creating the window emscripten_set_canvas_element_size("#canvas", mode.width, mode.height); - - std::cout << "Emscripten: Setting canvas to " << mode.width << "x" << mode.height << std::endl; #endif // Create window @@ -605,37 +580,16 @@ void RenderWindow::create(VideoMode mode, const std::string& title, uint32_t sty // Set up OpenGL state glViewport(0, 0, mode.width, mode.height); - std::cout << "GL viewport set to " << mode.width << "x" << mode.height << std::endl; - - GLenum err = glGetError(); - if (err != GL_NO_ERROR) { - std::cerr << "GL error after viewport: " << err << std::endl; - } - SDL2Renderer::getInstance().setProjection(0, mode.width, mode.height, 0); // Enable blending for transparency glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); - // Initial clear to a visible color to confirm GL is working - glClearColor(0.2f, 0.3f, 0.4f, 1.0f); // Blue-gray + // Initial clear + glClearColor(0.2f, 0.3f, 0.4f, 1.0f); glClear(GL_COLOR_BUFFER_BIT); - - err = glGetError(); - if (err != GL_NO_ERROR) { - std::cerr << "GL error after clear: " << err << std::endl; - } - SDL_GL_SwapWindow(window); - - err = glGetError(); - if (err != GL_NO_ERROR) { - std::cerr << "GL error after swap: " << err << std::endl; - } - - std::cout << "RenderWindow: Created " << mode.width << "x" << mode.height << " window" << std::endl; - std::cout << "WebGL context should now show blue-gray" << std::endl; } void RenderWindow::close() { @@ -1277,7 +1231,6 @@ bool Font::loadFromFile(const std::string& filename) { ftStroker_ = stroker; loaded_ = true; - std::cout << "Font: Loaded " << filename << " with FreeType" << std::endl; return true; } @@ -2129,10 +2082,6 @@ bool FontAtlas::load(const Font* font, float fontSize) { } textureId_ = SDL2Renderer::getInstance().createTexture(ATLAS_SIZE, ATLAS_SIZE, rgbaPixels.data()); - - std::cout << "FontAtlas: created " << ATLAS_SIZE << "x" << ATLAS_SIZE - << " atlas with " << simpleGlyphCache_.size() << " glyphs, textureId=" << textureId_ << std::endl; - return true; } @@ -2234,10 +2183,6 @@ bool FontAtlas::load(const unsigned char* fontData, size_t dataSize, float fontS } textureId_ = SDL2Renderer::getInstance().createTexture(ATLAS_SIZE, ATLAS_SIZE, rgbaPixels.data()); - - std::cout << "FontAtlas: created " << ATLAS_SIZE << "x" << ATLAS_SIZE - << " atlas with " << simpleGlyphCache_.size() << " glyphs, textureId=" << textureId_ << std::endl; - return true; } From 2fb29a102e07ffe7dd48810b90db2f1e1319b626 Mon Sep 17 00:00:00 2001 From: John McCardle Date: Sun, 1 Feb 2026 21:17:29 -0500 Subject: [PATCH 2/2] Animation and Scene clean up functions. Playground build target --- CMakeLists.txt | 11 +- src/Animation.cpp | 13 +- src/Animation.h | 12 +- src/GameEngine.cpp | 61 ++++++-- src/GameEngine.h | 6 +- src/McRFPy_API.cpp | 36 +++++ src/McRFPy_API.h | 3 + src/PyAnimation.cpp | 15 ++ src/PyAnimation.h | 1 + src/PySceneObject.cpp | 263 +++++++++++++++++++-------------- src/scripts_playground/game.py | 25 ++++ 11 files changed, 323 insertions(+), 123 deletions(-) create mode 100644 src/scripts_playground/game.py diff --git a/CMakeLists.txt b/CMakeLists.txt index f17a944..5eb1393 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -14,6 +14,9 @@ option(MCRF_HEADLESS "Build without graphics dependencies (SFML, ImGui)" OFF) # SDL2 backend option (SDL2 + OpenGL ES 2 - for Emscripten/WebGL, Android, cross-platform) option(MCRF_SDL2 "Build with SDL2+OpenGL ES 2 backend instead of SFML" OFF) +# Playground mode - minimal scripts for web playground (REPL-focused) +option(MCRF_PLAYGROUND "Build with minimal playground scripts instead of full game" OFF) + # Emscripten builds: use SDL2 if specified, otherwise fall back to headless if(EMSCRIPTEN) if(MCRF_SDL2) @@ -29,6 +32,10 @@ if(MCRF_SDL2) message(STATUS "Building with SDL2 backend - SDL2+OpenGL ES 2") endif() +if(MCRF_PLAYGROUND) + message(STATUS "Building in PLAYGROUND mode - minimal scripts for web REPL") +endif() + if(MCRF_HEADLESS) message(STATUS "Building in HEADLESS mode - no SFML/ImGui dependencies") endif() @@ -266,8 +273,8 @@ if(EMSCRIPTEN) -sALLOW_UNIMPLEMENTED_SYSCALLS=1 # Preload Python stdlib into virtual filesystem at /lib/python3.14 --preload-file=${CMAKE_SOURCE_DIR}/wasm_stdlib/lib@/lib - # Preload game scripts into /scripts - --preload-file=${CMAKE_SOURCE_DIR}/src/scripts@/scripts + # Preload game scripts into /scripts (use playground scripts if MCRF_PLAYGROUND is set) + --preload-file=${CMAKE_SOURCE_DIR}/src/$,scripts_playground,scripts>@/scripts # Preload assets --preload-file=${CMAKE_SOURCE_DIR}/assets@/assets # Use custom HTML shell for crisp pixel rendering diff --git a/src/Animation.cpp b/src/Animation.cpp index 22ba433..9ad8aac 100644 --- a/src/Animation.cpp +++ b/src/Animation.cpp @@ -188,7 +188,7 @@ void Animation::clearCallback() { void Animation::complete() { // Jump to end of animation elapsed = duration; - + // Apply final value if (auto target = targetWeak.lock()) { AnimationValue finalValue = interpolate(1.0f); @@ -200,7 +200,18 @@ void Animation::complete() { } } +void Animation::stop() { + // Mark as stopped - no final value applied, no callback triggered + stopped = true; + // AnimationManager will remove this on next update() call +} + bool Animation::update(float deltaTime) { + // Check if animation was stopped + if (stopped) { + return false; // Signal removal from AnimationManager + } + // Try to lock weak_ptr to get shared_ptr std::shared_ptr target = targetWeak.lock(); std::shared_ptr entity = entityTargetWeak.lock(); diff --git a/src/Animation.h b/src/Animation.h index 5a65479..40b057f 100644 --- a/src/Animation.h +++ b/src/Animation.h @@ -61,7 +61,10 @@ public: // Complete the animation immediately (jump to final value) void complete(); - + + // Stop the animation without completing (no final value applied, no callback) + void stop(); + // Update animation (called each frame) // Returns true if animation is still running, false if complete bool update(float deltaTime); @@ -79,7 +82,8 @@ public: std::string getTargetProperty() const { return targetProperty; } float getDuration() const { return duration; } float getElapsed() const { return elapsed; } - bool isComplete() const { return elapsed >= duration; } + bool isComplete() const { return elapsed >= duration || stopped; } + bool isStopped() const { return stopped; } bool isDelta() const { return delta; } // Get raw target pointer for property locking (#120) @@ -97,6 +101,7 @@ private: float elapsed = 0.0f; // Elapsed time EasingFunction easingFunc; // Easing function to use bool delta; // If true, targetValue is relative to start + bool stopped = false; // If true, animation was stopped without completing // RAII: Use weak_ptr for safe target tracking std::weak_ptr targetWeak; @@ -196,6 +201,9 @@ public: // Get active animation count (for debugging/testing) size_t getActiveAnimationCount() const { return activeAnimations.size(); } + // Get all active animations (for mcrfpy.animations) + const std::vector>& getActiveAnimations() const { return activeAnimations; } + private: AnimationManager() = default; std::vector> activeAnimations; diff --git a/src/GameEngine.cpp b/src/GameEngine.cpp index aa433a6..eec7fed 100644 --- a/src/GameEngine.cpp +++ b/src/GameEngine.cpp @@ -107,7 +107,7 @@ GameEngine::GameEngine(const McRogueFaceConfig& cfg) gameView.setCenter(std::floor(gameResolution.x / 2.0f), std::floor(gameResolution.y / 2.0f)); updateViewport(); scene = "uitest"; - scenes["uitest"] = new UITestScene(this); + scenes["uitest"] = std::make_shared(this); McRFPy_API::game = this; @@ -159,9 +159,8 @@ void GameEngine::executeStartupScripts() GameEngine::~GameEngine() { cleanup(); - for (auto& [name, scene] : scenes) { - delete scene; - } + // scenes map uses shared_ptr, will clean up automatically + scenes.clear(); delete profilerOverlay; } @@ -198,10 +197,10 @@ void GameEngine::cleanup() #endif } -Scene* GameEngine::currentScene() { return scenes[scene]; } +Scene* GameEngine::currentScene() { return scenes[scene].get(); } Scene* GameEngine::getScene(const std::string& name) { auto it = scenes.find(name); - return (it != scenes.end()) ? it->second : nullptr; + return (it != scenes.end()) ? it->second.get() : nullptr; } std::vector GameEngine::getSceneNames() const { @@ -276,7 +275,29 @@ sf::RenderTarget & GameEngine::getRenderTarget() { return *render_target; } -void GameEngine::createScene(std::string s) { scenes[s] = new PyScene(this); } +void GameEngine::createScene(std::string s) { scenes[s] = std::make_shared(this); } + +void GameEngine::registerScene(const std::string& name, std::shared_ptr scenePtr) { + scenes[name] = scenePtr; +} + +void GameEngine::unregisterScene(const std::string& name) { + auto it = scenes.find(name); + if (it != scenes.end()) { + // If this was the active scene, we need to handle that + if (scene == name) { + // Find another scene to switch to, or leave empty + scene.clear(); + for (const auto& [sceneName, scenePtr] : scenes) { + if (sceneName != name) { + scene = sceneName; + break; + } + } + } + scenes.erase(it); + } +} void GameEngine::setWindowScale(float multiplier) { @@ -310,8 +331,16 @@ void GameEngine::run() #ifdef __EMSCRIPTEN__ // Browser: use callback-based loop (non-blocking) - // 0 = use requestAnimationFrame, 1 = simulate infinite loop + // Start with 0 (requestAnimationFrame), then set timing based on framerate_limit emscripten_set_main_loop_arg(emscriptenMainLoopCallback, this, 0, 1); + + // Apply framerate_limit setting (0 = use RAF, >0 = use setTimeout) + if (framerate_limit == 0) { + emscripten_set_main_loop_timing(EM_TIMING_RAF, 1); + } else { + int interval_ms = 1000 / framerate_limit; + emscripten_set_main_loop_timing(EM_TIMING_SETTIMEOUT, interval_ms); + } #else // Desktop: traditional blocking loop while (running) @@ -628,7 +657,7 @@ std::shared_ptr>> GameEngine::scene_ui(s 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; + std::cout << "scenes[target]: " << (long)(scenes[target].get()) << std::endl; */ if (scenes.count(target) == 0) return NULL; return scenes[target]->ui_elements; @@ -653,9 +682,23 @@ void GameEngine::setVSync(bool enabled) void GameEngine::setFramerateLimit(unsigned int limit) { framerate_limit = limit; + +#ifdef __EMSCRIPTEN__ + // For Emscripten: change loop timing dynamically + if (limit == 0) { + // Unlimited: use requestAnimationFrame (browser-controlled, typically 60fps) + emscripten_set_main_loop_timing(EM_TIMING_RAF, 1); + } else { + // Specific FPS: use setTimeout with calculated interval + int interval_ms = 1000 / limit; + emscripten_set_main_loop_timing(EM_TIMING_SETTIMEOUT, interval_ms); + } +#else + // For desktop: use SFML's setFramerateLimit if (!headless && window) { window->setFramerateLimit(limit); } +#endif } void GameEngine::setGameResolution(unsigned int width, unsigned int height) { diff --git a/src/GameEngine.h b/src/GameEngine.h index 36d507c..f7ca4a4 100644 --- a/src/GameEngine.h +++ b/src/GameEngine.h @@ -157,9 +157,9 @@ private: std::unique_ptr window; std::unique_ptr headless_renderer; sf::RenderTarget* render_target; - + sf::Font font; - std::map scenes; + std::map> scenes; bool running = true; bool paused = false; int currentFrame = 0; @@ -230,6 +230,8 @@ public: void changeScene(std::string); void changeScene(std::string sceneName, TransitionType transitionType, float duration); void createScene(std::string); + void registerScene(const std::string& name, std::shared_ptr scene); + void unregisterScene(const std::string& name); void quit(); void setPause(bool); sf::Font & getFont(); diff --git a/src/McRFPy_API.cpp b/src/McRFPy_API.cpp index 94e2c85..a5096b3 100644 --- a/src/McRFPy_API.cpp +++ b/src/McRFPy_API.cpp @@ -111,6 +111,10 @@ static PyObject* mcrfpy_module_getattr(PyObject* self, PyObject* args) return McRFPy_API::api_get_timers(); } + if (strcmp(name, "animations") == 0) { + return McRFPy_API::api_get_animations(); + } + if (strcmp(name, "default_transition") == 0) { return PyTransition::to_python(PyTransition::default_transition); } @@ -144,6 +148,11 @@ static int mcrfpy_module_setattro(PyObject* self, PyObject* name, PyObject* valu return -1; } + if (strcmp(name_str, "animations") == 0) { + PyErr_SetString(PyExc_AttributeError, "'animations' is read-only"); + return -1; + } + if (strcmp(name_str, "default_transition") == 0) { TransitionType trans; if (!PyTransition::from_arg(value, &trans, nullptr)) { @@ -1252,6 +1261,33 @@ PyObject* McRFPy_API::api_get_timers() return tuple; } +// Module-level animation collection accessor +PyObject* McRFPy_API::api_get_animations() +{ + auto& manager = AnimationManager::getInstance(); + const auto& animations = manager.getActiveAnimations(); + + PyObject* tuple = PyTuple_New(animations.size()); + if (!tuple) return NULL; + + Py_ssize_t i = 0; + for (const auto& anim : animations) { + // Create a PyAnimation wrapper for each animation + PyAnimationObject* pyAnim = (PyAnimationObject*)mcrfpydef::PyAnimationType.tp_alloc(&mcrfpydef::PyAnimationType, 0); + if (pyAnim) { + pyAnim->data = anim; + PyTuple_SET_ITEM(tuple, i, (PyObject*)pyAnim); + } else { + // Failed to allocate - fill with None + Py_INCREF(Py_None); + PyTuple_SET_ITEM(tuple, i, Py_None); + } + i++; + } + + return tuple; +} + // #153 - Headless simulation control PyObject* McRFPy_API::_step(PyObject* self, PyObject* args) { PyObject* dt_obj = Py_None; diff --git a/src/McRFPy_API.h b/src/McRFPy_API.h index b8276d4..a5e767a 100644 --- a/src/McRFPy_API.h +++ b/src/McRFPy_API.h @@ -93,6 +93,9 @@ public: // #173: Module-level timer collection accessor static PyObject* api_get_timers(); + // Module-level animation collection accessor + static PyObject* api_get_animations(); + // Exception handling - signal game loop to exit on unhandled Python exceptions static std::atomic exception_occurred; static std::atomic exit_code; diff --git a/src/PyAnimation.cpp b/src/PyAnimation.cpp index 0081365..272df99 100644 --- a/src/PyAnimation.cpp +++ b/src/PyAnimation.cpp @@ -331,6 +331,13 @@ PyObject* PyAnimation::complete(PyAnimationObject* self, PyObject* args) { Py_RETURN_NONE; } +PyObject* PyAnimation::stop(PyAnimationObject* self, PyObject* args) { + if (self->data) { + self->data->stop(); + } + Py_RETURN_NONE; +} + PyObject* PyAnimation::has_valid_target(PyAnimationObject* self, PyObject* args) { if (self->data && self->data->hasValidTarget()) { Py_RETURN_TRUE; @@ -390,6 +397,14 @@ PyMethodDef PyAnimation::methods[] = { MCRF_RETURNS("None") MCRF_NOTE("Sets elapsed = duration and applies target value immediately. Completion callback will be called if set.") )}, + {"stop", (PyCFunction)stop, METH_NOARGS, + MCRF_METHOD(Animation, stop, + MCRF_SIG("()", "None"), + MCRF_DESC("Stop the animation without completing it."), + MCRF_RETURNS("None") + MCRF_NOTE("Unlike complete(), this does NOT apply the final value and does NOT trigger the callback. " + "The animation is simply cancelled and will be removed from the AnimationManager.") + )}, {"hasValidTarget", (PyCFunction)has_valid_target, METH_NOARGS, MCRF_METHOD(Animation, hasValidTarget, MCRF_SIG("()", "bool"), diff --git a/src/PyAnimation.h b/src/PyAnimation.h index ec463f9..cc38e95 100644 --- a/src/PyAnimation.h +++ b/src/PyAnimation.h @@ -30,6 +30,7 @@ public: static PyObject* update(PyAnimationObject* self, PyObject* args); static PyObject* get_current_value(PyAnimationObject* self, PyObject* args); static PyObject* complete(PyAnimationObject* self, PyObject* args); + static PyObject* stop(PyAnimationObject* self, PyObject* args); static PyObject* has_valid_target(PyAnimationObject* self, PyObject* args); static PyGetSetDef getsetters[]; diff --git a/src/PySceneObject.cpp b/src/PySceneObject.cpp index 8d78ca5..d21634f 100644 --- a/src/PySceneObject.cpp +++ b/src/PySceneObject.cpp @@ -25,50 +25,57 @@ int PySceneClass::__init__(PySceneObject* self, PyObject* args, PyObject* kwds) { static const char* keywords[] = {"name", nullptr}; const char* name = nullptr; - + if (!PyArg_ParseTupleAndKeywords(args, kwds, "s", const_cast(keywords), &name)) { return -1; } - - // Check if scene with this name already exists - if (python_scenes.count(name) > 0) { - PyErr_Format(PyExc_ValueError, "Scene with name '%s' already exists", name); - return -1; - } - - self->name = name; - - // Create the C++ PyScene - McRFPy_API::game->createScene(name); - - // Get reference to the created scene + GameEngine* game = McRFPy_API::game; if (!game) { PyErr_SetString(PyExc_RuntimeError, "No game engine initialized"); return -1; } - - // Store this Python object in our registry + + // If scene with this name already exists in python_scenes, unregister the old one + if (python_scenes.count(name) > 0) { + PySceneObject* old_scene = python_scenes[name]; + // Remove old scene from registries (but its shared_ptr keeps the C++ object alive) + Py_DECREF(old_scene); + python_scenes.erase(name); + game->unregisterScene(name); + } + + self->name = name; + + // Create the C++ PyScene with shared ownership + self->scene = std::make_shared(game); + + // Register with the game engine (game engine also holds a reference) + game->registerScene(name, self->scene); + + // Store this Python object in our registry for lifecycle callbacks python_scenes[name] = self; - Py_INCREF(self); // Keep a reference - - // Create a Python function that routes to on_keypress - // We'll register this after the object is fully initialized - + Py_INCREF(self); // python_scenes holds a reference + self->initialized = true; - + return 0; } void PySceneClass::__dealloc(PyObject* self_obj) { PySceneObject* self = (PySceneObject*)self_obj; - - // Remove from registry + + // Remove from python_scenes registry if we're the registered scene if (python_scenes.count(self->name) > 0 && python_scenes[self->name] == self) { python_scenes.erase(self->name); } - + + // Release our shared_ptr reference to the C++ scene + // If GameEngine still holds a reference, the scene survives + // If not, this may be the last reference and the scene is deleted + self->scene.reset(); + // Call Python object destructor Py_TYPE(self)->tp_free(self); } @@ -122,6 +129,15 @@ PyObject* PySceneClass::activate(PySceneObject* self, PyObject* args, PyObject* return NULL; } + // Auto-register if this scene is not currently registered + if (!game->getScene(self->name)) { + game->registerScene(self->name, self->scene); + if (python_scenes.count(self->name) == 0 || python_scenes[self->name] != self) { + python_scenes[self->name] = self; + Py_INCREF(self); + } + } + // Call game->changeScene directly with proper transition game->changeScene(self->name, transition_type, duration); @@ -141,17 +157,11 @@ static PyObject* PySceneClass_get_children(PySceneObject* self, void* closure) // on_key property getter static PyObject* PySceneClass_get_on_key(PySceneObject* self, void* closure) { - GameEngine* game = McRFPy_API::game; - if (!game) { + if (!self->scene || !self->scene->key_callable) { Py_RETURN_NONE; } - auto scene = game->getScene(self->name); - if (!scene || !scene->key_callable) { - Py_RETURN_NONE; - } - - PyObject* callable = scene->key_callable->borrow(); + PyObject* callable = self->scene->key_callable->borrow(); if (callable && callable != Py_None) { Py_INCREF(callable); return callable; @@ -162,22 +172,15 @@ static PyObject* PySceneClass_get_on_key(PySceneObject* self, void* closure) // on_key property setter static int PySceneClass_set_on_key(PySceneObject* self, PyObject* value, void* closure) { - GameEngine* game = McRFPy_API::game; - if (!game) { - PyErr_SetString(PyExc_RuntimeError, "No game engine"); - return -1; - } - - auto scene = game->getScene(self->name); - if (!scene) { - PyErr_SetString(PyExc_RuntimeError, "Scene not found"); + if (!self->scene) { + PyErr_SetString(PyExc_RuntimeError, "Scene not initialized"); return -1; } if (value == Py_None || value == NULL) { - scene->key_unregister(); + self->scene->key_unregister(); } else if (PyCallable_Check(value)) { - scene->key_register(value); + self->scene->key_register(value); } else { PyErr_SetString(PyExc_TypeError, "on_key must be callable or None"); return -1; @@ -203,21 +206,14 @@ PyObject* PySceneClass::get_active(PySceneObject* self, void* closure) // #118: Scene position getter static PyObject* PySceneClass_get_pos(PySceneObject* self, void* closure) { - GameEngine* game = McRFPy_API::game; - if (!game) { - Py_RETURN_NONE; - } - - // Get the scene by name using the public accessor - auto scene = game->getScene(self->name); - if (!scene) { + if (!self->scene) { Py_RETURN_NONE; } // Create a Vector object auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Vector"); if (!type) return NULL; - PyObject* args = Py_BuildValue("(ff)", scene->position.x, scene->position.y); + PyObject* args = Py_BuildValue("(ff)", self->scene->position.x, self->scene->position.y); PyObject* result = PyObject_CallObject((PyObject*)type, args); Py_DECREF(type); Py_DECREF(args); @@ -227,15 +223,8 @@ static PyObject* PySceneClass_get_pos(PySceneObject* self, void* closure) // #118: Scene position setter static int PySceneClass_set_pos(PySceneObject* self, PyObject* value, void* closure) { - GameEngine* game = McRFPy_API::game; - if (!game) { - PyErr_SetString(PyExc_RuntimeError, "No game engine"); - return -1; - } - - auto scene = game->getScene(self->name); - if (!scene) { - PyErr_SetString(PyExc_RuntimeError, "Scene not found"); + if (!self->scene) { + PyErr_SetString(PyExc_RuntimeError, "Scene not initialized"); return -1; } @@ -256,38 +245,25 @@ static int PySceneClass_set_pos(PySceneObject* self, PyObject* value, void* clos return -1; } - scene->position = sf::Vector2f(x, y); + self->scene->position = sf::Vector2f(x, y); return 0; } // #118: Scene visible getter static PyObject* PySceneClass_get_visible(PySceneObject* self, void* closure) { - GameEngine* game = McRFPy_API::game; - if (!game) { + if (!self->scene) { Py_RETURN_TRUE; } - auto scene = game->getScene(self->name); - if (!scene) { - Py_RETURN_TRUE; - } - - return PyBool_FromLong(scene->visible); + return PyBool_FromLong(self->scene->visible); } // #118: Scene visible setter static int PySceneClass_set_visible(PySceneObject* self, PyObject* value, void* closure) { - GameEngine* game = McRFPy_API::game; - if (!game) { - PyErr_SetString(PyExc_RuntimeError, "No game engine"); - return -1; - } - - auto scene = game->getScene(self->name); - if (!scene) { - PyErr_SetString(PyExc_RuntimeError, "Scene not found"); + if (!self->scene) { + PyErr_SetString(PyExc_RuntimeError, "Scene not initialized"); return -1; } @@ -296,38 +272,25 @@ static int PySceneClass_set_visible(PySceneObject* self, PyObject* value, void* return -1; } - scene->visible = PyObject_IsTrue(value); + self->scene->visible = PyObject_IsTrue(value); return 0; } // #118: Scene opacity getter static PyObject* PySceneClass_get_opacity(PySceneObject* self, void* closure) { - GameEngine* game = McRFPy_API::game; - if (!game) { + if (!self->scene) { return PyFloat_FromDouble(1.0); } - auto scene = game->getScene(self->name); - if (!scene) { - return PyFloat_FromDouble(1.0); - } - - return PyFloat_FromDouble(scene->opacity); + return PyFloat_FromDouble(self->scene->opacity); } // #118: Scene opacity setter static int PySceneClass_set_opacity(PySceneObject* self, PyObject* value, void* closure) { - GameEngine* game = McRFPy_API::game; - if (!game) { - PyErr_SetString(PyExc_RuntimeError, "No game engine"); - return -1; - } - - auto scene = game->getScene(self->name); - if (!scene) { - PyErr_SetString(PyExc_RuntimeError, "Scene not found"); + if (!self->scene) { + PyErr_SetString(PyExc_RuntimeError, "Scene not initialized"); return -1; } @@ -345,7 +308,7 @@ static int PySceneClass_set_opacity(PySceneObject* self, PyObject* value, void* if (opacity < 0.0) opacity = 0.0; if (opacity > 1.0) opacity = 1.0; - scene->opacity = opacity; + self->scene->opacity = opacity; return 0; } @@ -494,12 +457,29 @@ void PySceneClass::call_on_resize(PySceneObject* self, sf::Vector2u new_size) } } +// registered property getter +static PyObject* PySceneClass_get_registered(PySceneObject* self, void* closure) +{ + GameEngine* game = McRFPy_API::game; + if (!game || !self->scene) { + Py_RETURN_FALSE; + } + + // Check if THIS scene object is the one registered under this name + // (not just that a scene with this name exists) + Scene* registered_scene = game->getScene(self->name); + return PyBool_FromLong(registered_scene == self->scene.get()); +} + // Properties PyGetSetDef PySceneClass::getsetters[] = { {"name", (getter)get_name, NULL, MCRF_PROPERTY(name, "Scene name (str, read-only). Unique identifier for this scene."), NULL}, {"active", (getter)get_active, NULL, MCRF_PROPERTY(active, "Whether this scene is currently active (bool, read-only). Only one scene can be active at a time."), NULL}, + {"registered", (getter)PySceneClass_get_registered, NULL, + MCRF_PROPERTY(registered, "Whether this scene is registered with the game engine (bool, read-only). " + "Unregistered scenes still exist but won't receive lifecycle callbacks."), NULL}, // #118: Scene-level UIDrawable-like properties {"pos", (getter)PySceneClass_get_pos, (setter)PySceneClass_set_pos, MCRF_PROPERTY(pos, "Scene position offset (Vector). Applied to all UI elements during rendering."), NULL}, @@ -520,6 +500,23 @@ PyGetSetDef PySceneClass::getsetters[] = { // Scene.realign() - recalculate alignment for all children static PyObject* PySceneClass_realign(PySceneObject* self, PyObject* args) +{ + if (!self->scene || !self->scene->ui_elements) { + Py_RETURN_NONE; + } + + // Iterate through all UI elements and realign those with alignment set + for (auto& drawable : *self->scene->ui_elements) { + if (drawable && drawable->align_type != AlignmentType::NONE) { + drawable->applyAlignment(); + } + } + + Py_RETURN_NONE; +} + +// Scene.register() - add scene to GameEngine's registry +static PyObject* PySceneClass_register(PySceneObject* self, PyObject* args) { GameEngine* game = McRFPy_API::game; if (!game) { @@ -527,18 +524,54 @@ static PyObject* PySceneClass_realign(PySceneObject* self, PyObject* args) return NULL; } - auto scene = game->getScene(self->name); - if (!scene || !scene->ui_elements) { - Py_RETURN_NONE; + if (!self->scene) { + PyErr_SetString(PyExc_RuntimeError, "Scene not initialized"); + return NULL; } - // Iterate through all UI elements and realign those with alignment set - for (auto& drawable : *scene->ui_elements) { - if (drawable && drawable->align_type != AlignmentType::NONE) { - drawable->applyAlignment(); - } + // If another scene with this name is registered, unregister it first + if (python_scenes.count(self->name) > 0 && python_scenes[self->name] != self) { + PySceneObject* old = python_scenes[self->name]; + Py_DECREF(old); + python_scenes.erase(self->name); } + // Unregister from GameEngine (removes old reference if any) + game->unregisterScene(self->name); + + // Register this scene with GameEngine + game->registerScene(self->name, self->scene); + + // Register in python_scenes if not already + if (python_scenes.count(self->name) == 0 || python_scenes[self->name] != self) { + python_scenes[self->name] = self; + Py_INCREF(self); + } + + Py_RETURN_NONE; +} + +// Scene.unregister() - remove scene from GameEngine's registry (but keep Python object alive) +static PyObject* PySceneClass_unregister(PySceneObject* self, PyObject* args) +{ + GameEngine* game = McRFPy_API::game; + if (!game) { + PyErr_SetString(PyExc_RuntimeError, "No game engine"); + return NULL; + } + + // Remove from GameEngine's scenes map + game->unregisterScene(self->name); + + // Remove from python_scenes callback registry + if (python_scenes.count(self->name) > 0 && python_scenes[self->name] == self) { + Py_DECREF(self); + python_scenes.erase(self->name); + } + + // Note: self->scene shared_ptr still holds the C++ object! + // The scene survives because this PySceneObject still has a reference + Py_RETURN_NONE; } @@ -561,6 +594,22 @@ PyMethodDef PySceneClass::methods[] = { MCRF_NOTE("Call this after window resize or when game_resolution changes. " "For responsive layouts, connect this to on_resize callback.") )}, + {"register", (PyCFunction)PySceneClass_register, METH_NOARGS, + MCRF_METHOD(SceneClass, register, + MCRF_SIG("()", "None"), + MCRF_DESC("Register this scene with the game engine."), + MCRF_NOTE("Makes the scene available for activation and receives lifecycle callbacks. " + "If another scene with the same name exists, it will be unregistered first. " + "Called automatically by activate() if needed.") + )}, + {"unregister", (PyCFunction)PySceneClass_unregister, METH_NOARGS, + MCRF_METHOD(SceneClass, unregister, + MCRF_SIG("()", "None"), + MCRF_DESC("Unregister this scene from the game engine."), + MCRF_NOTE("Removes the scene from the engine's registry but keeps the Python object alive. " + "The scene's UI elements and state are preserved. Call register() to re-add it. " + "Useful for temporary scenes or scene pooling.") + )}, {NULL} }; diff --git a/src/scripts_playground/game.py b/src/scripts_playground/game.py new file mode 100644 index 0000000..e52dc29 --- /dev/null +++ b/src/scripts_playground/game.py @@ -0,0 +1,25 @@ +import mcrfpy + +class PlaygroundScene(mcrfpy.Scene): + """Scene with reset capability for playground idempotency""" + def __init__(self, name="playground"): + super().__init__(name) + + def reset(self): + """Clear scene state for fresh execution""" + # Stop all timers + for t in mcrfpy.timers: + t.stop() + for a in mcrfpy.animations: + a.stop() + for s in mcrfpy.scenes: + s.unregister() + while self.children: + self.children.pop() + self.activate() + +scene = PlaygroundScene() +scene.activate() + +# REPL calls this each "run" before executing the code in the web form. +_reset = scene.reset