diff --git a/CMakeLists.txt b/CMakeLists.txt index 5eb1393..f17a944 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -14,9 +14,6 @@ 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) @@ -32,10 +29,6 @@ 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() @@ -273,8 +266,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 (use playground scripts if MCRF_PLAYGROUND is set) - --preload-file=${CMAKE_SOURCE_DIR}/src/$,scripts_playground,scripts>@/scripts + # Preload game scripts into /scripts + --preload-file=${CMAKE_SOURCE_DIR}/src/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 9ad8aac..22ba433 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,18 +200,7 @@ 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 40b057f..5a65479 100644 --- a/src/Animation.h +++ b/src/Animation.h @@ -61,10 +61,7 @@ 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); @@ -82,8 +79,7 @@ public: std::string getTargetProperty() const { return targetProperty; } float getDuration() const { return duration; } float getElapsed() const { return elapsed; } - bool isComplete() const { return elapsed >= duration || stopped; } - bool isStopped() const { return stopped; } + bool isComplete() const { return elapsed >= duration; } bool isDelta() const { return delta; } // Get raw target pointer for property locking (#120) @@ -101,7 +97,6 @@ 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; @@ -201,9 +196,6 @@ 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 eec7fed..68c3f8e 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"] = std::make_shared(this); + scenes["uitest"] = new UITestScene(this); McRFPy_API::game = this; @@ -123,11 +123,21 @@ 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. @@ -159,8 +169,9 @@ void GameEngine::executeStartupScripts() GameEngine::~GameEngine() { cleanup(); - // scenes map uses shared_ptr, will clean up automatically - scenes.clear(); + for (auto& [name, scene] : scenes) { + delete scene; + } delete profilerOverlay; } @@ -197,10 +208,10 @@ void GameEngine::cleanup() #endif } -Scene* GameEngine::currentScene() { return scenes[scene].get(); } +Scene* GameEngine::currentScene() { return scenes[scene]; } Scene* GameEngine::getScene(const std::string& name) { auto it = scenes.find(name); - return (it != scenes.end()) ? it->second.get() : nullptr; + return (it != scenes.end()) ? it->second : nullptr; } std::vector GameEngine::getSceneNames() const { @@ -275,29 +286,7 @@ sf::RenderTarget & GameEngine::getRenderTarget() { return *render_target; } -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::createScene(std::string s) { scenes[s] = new PyScene(this); } void GameEngine::setWindowScale(float multiplier) { @@ -331,16 +320,8 @@ void GameEngine::run() #ifdef __EMSCRIPTEN__ // Browser: use callback-based loop (non-blocking) - // Start with 0 (requestAnimationFrame), then set timing based on framerate_limit + // 0 = use requestAnimationFrame, 1 = simulate infinite loop 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) @@ -657,7 +638,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].get()) << std::endl; + std::cout << "scenes[target]: " << (long)(scenes[target]) << std::endl; */ if (scenes.count(target) == 0) return NULL; return scenes[target]->ui_elements; @@ -682,23 +663,9 @@ 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 f7ca4a4..36d507c 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,8 +230,6 @@ 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 a5096b3..292236d 100644 --- a/src/McRFPy_API.cpp +++ b/src/McRFPy_API.cpp @@ -111,10 +111,6 @@ 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); } @@ -148,11 +144,6 @@ 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)) { @@ -708,18 +699,28 @@ 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; @@ -730,6 +731,9 @@ 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"); @@ -810,11 +814,18 @@ 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); @@ -1261,33 +1272,6 @@ 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 a5e767a..b8276d4 100644 --- a/src/McRFPy_API.h +++ b/src/McRFPy_API.h @@ -93,9 +93,6 @@ 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 272df99..0081365 100644 --- a/src/PyAnimation.cpp +++ b/src/PyAnimation.cpp @@ -331,13 +331,6 @@ 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; @@ -397,14 +390,6 @@ 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 cc38e95..ec463f9 100644 --- a/src/PyAnimation.h +++ b/src/PyAnimation.h @@ -30,7 +30,6 @@ 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 d21634f..8d78ca5 100644 --- a/src/PySceneObject.cpp +++ b/src/PySceneObject.cpp @@ -25,57 +25,50 @@ 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; } - - // 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 + + // Store this Python object in our registry python_scenes[name] = self; - Py_INCREF(self); // python_scenes holds a reference - + 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 + self->initialized = true; - + return 0; } void PySceneClass::__dealloc(PyObject* self_obj) { PySceneObject* self = (PySceneObject*)self_obj; - - // Remove from python_scenes registry if we're the registered scene + + // Remove from registry 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); } @@ -129,15 +122,6 @@ 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); @@ -157,11 +141,17 @@ static PyObject* PySceneClass_get_children(PySceneObject* self, void* closure) // on_key property getter static PyObject* PySceneClass_get_on_key(PySceneObject* self, void* closure) { - if (!self->scene || !self->scene->key_callable) { + GameEngine* game = McRFPy_API::game; + if (!game) { Py_RETURN_NONE; } - PyObject* callable = self->scene->key_callable->borrow(); + auto scene = game->getScene(self->name); + if (!scene || !scene->key_callable) { + Py_RETURN_NONE; + } + + PyObject* callable = scene->key_callable->borrow(); if (callable && callable != Py_None) { Py_INCREF(callable); return callable; @@ -172,15 +162,22 @@ 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) { - if (!self->scene) { - PyErr_SetString(PyExc_RuntimeError, "Scene not initialized"); + 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"); return -1; } if (value == Py_None || value == NULL) { - self->scene->key_unregister(); + scene->key_unregister(); } else if (PyCallable_Check(value)) { - self->scene->key_register(value); + scene->key_register(value); } else { PyErr_SetString(PyExc_TypeError, "on_key must be callable or None"); return -1; @@ -206,14 +203,21 @@ PyObject* PySceneClass::get_active(PySceneObject* self, void* closure) // #118: Scene position getter static PyObject* PySceneClass_get_pos(PySceneObject* self, void* closure) { - if (!self->scene) { + 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) { 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)", self->scene->position.x, self->scene->position.y); + PyObject* args = Py_BuildValue("(ff)", scene->position.x, scene->position.y); PyObject* result = PyObject_CallObject((PyObject*)type, args); Py_DECREF(type); Py_DECREF(args); @@ -223,8 +227,15 @@ static PyObject* PySceneClass_get_pos(PySceneObject* self, void* closure) // #118: Scene position setter static int PySceneClass_set_pos(PySceneObject* self, PyObject* value, void* closure) { - if (!self->scene) { - PyErr_SetString(PyExc_RuntimeError, "Scene not initialized"); + 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"); return -1; } @@ -245,25 +256,38 @@ static int PySceneClass_set_pos(PySceneObject* self, PyObject* value, void* clos return -1; } - self->scene->position = sf::Vector2f(x, y); + scene->position = sf::Vector2f(x, y); return 0; } // #118: Scene visible getter static PyObject* PySceneClass_get_visible(PySceneObject* self, void* closure) { - if (!self->scene) { + GameEngine* game = McRFPy_API::game; + if (!game) { Py_RETURN_TRUE; } - return PyBool_FromLong(self->scene->visible); + auto scene = game->getScene(self->name); + if (!scene) { + Py_RETURN_TRUE; + } + + return PyBool_FromLong(scene->visible); } // #118: Scene visible setter static int PySceneClass_set_visible(PySceneObject* self, PyObject* value, void* closure) { - if (!self->scene) { - PyErr_SetString(PyExc_RuntimeError, "Scene not initialized"); + 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"); return -1; } @@ -272,25 +296,38 @@ static int PySceneClass_set_visible(PySceneObject* self, PyObject* value, void* return -1; } - self->scene->visible = PyObject_IsTrue(value); + scene->visible = PyObject_IsTrue(value); return 0; } // #118: Scene opacity getter static PyObject* PySceneClass_get_opacity(PySceneObject* self, void* closure) { - if (!self->scene) { + GameEngine* game = McRFPy_API::game; + if (!game) { return PyFloat_FromDouble(1.0); } - return PyFloat_FromDouble(self->scene->opacity); + auto scene = game->getScene(self->name); + if (!scene) { + return PyFloat_FromDouble(1.0); + } + + return PyFloat_FromDouble(scene->opacity); } // #118: Scene opacity setter static int PySceneClass_set_opacity(PySceneObject* self, PyObject* value, void* closure) { - if (!self->scene) { - PyErr_SetString(PyExc_RuntimeError, "Scene not initialized"); + 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"); return -1; } @@ -308,7 +345,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; - self->scene->opacity = opacity; + scene->opacity = opacity; return 0; } @@ -457,29 +494,12 @@ 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}, @@ -501,12 +521,19 @@ 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) { + GameEngine* game = McRFPy_API::game; + if (!game) { + PyErr_SetString(PyExc_RuntimeError, "No game engine"); + return NULL; + } + + auto scene = game->getScene(self->name); + if (!scene || !scene->ui_elements) { Py_RETURN_NONE; } // Iterate through all UI elements and realign those with alignment set - for (auto& drawable : *self->scene->ui_elements) { + for (auto& drawable : *scene->ui_elements) { if (drawable && drawable->align_type != AlignmentType::NONE) { drawable->applyAlignment(); } @@ -515,66 +542,6 @@ static PyObject* PySceneClass_realign(PySceneObject* self, PyObject* args) 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) { - PyErr_SetString(PyExc_RuntimeError, "No game engine"); - return NULL; - } - - if (!self->scene) { - PyErr_SetString(PyExc_RuntimeError, "Scene not initialized"); - return NULL; - } - - // 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; -} - // Methods PyMethodDef PySceneClass::methods[] = { {"activate", (PyCFunction)activate, METH_VARARGS | METH_KEYWORDS, @@ -594,22 +561,6 @@ 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/platform/SDL2Renderer.cpp b/src/platform/SDL2Renderer.cpp index 3ed9a13..1968ed4 100644 --- a/src/platform/SDL2Renderer.cpp +++ b/src/platform/SDL2Renderer.cpp @@ -377,9 +377,16 @@ 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) { @@ -459,6 +466,22 @@ 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 @@ -518,6 +541,8 @@ 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 @@ -580,16 +605,37 @@ 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 - glClearColor(0.2f, 0.3f, 0.4f, 1.0f); + // Initial clear to a visible color to confirm GL is working + glClearColor(0.2f, 0.3f, 0.4f, 1.0f); // Blue-gray 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() { @@ -1231,6 +1277,7 @@ bool Font::loadFromFile(const std::string& filename) { ftStroker_ = stroker; loaded_ = true; + std::cout << "Font: Loaded " << filename << " with FreeType" << std::endl; return true; } @@ -2082,6 +2129,10 @@ 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; } @@ -2183,6 +2234,10 @@ 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; } diff --git a/src/scripts_playground/game.py b/src/scripts_playground/game.py deleted file mode 100644 index e52dc29..0000000 --- a/src/scripts_playground/game.py +++ /dev/null @@ -1,25 +0,0 @@ -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