Compare commits

..

2 commits

12 changed files with 326 additions and 211 deletions

View file

@ -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) # 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) 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 # Emscripten builds: use SDL2 if specified, otherwise fall back to headless
if(EMSCRIPTEN) if(EMSCRIPTEN)
if(MCRF_SDL2) if(MCRF_SDL2)
@ -29,6 +32,10 @@ if(MCRF_SDL2)
message(STATUS "Building with SDL2 backend - SDL2+OpenGL ES 2") message(STATUS "Building with SDL2 backend - SDL2+OpenGL ES 2")
endif() endif()
if(MCRF_PLAYGROUND)
message(STATUS "Building in PLAYGROUND mode - minimal scripts for web REPL")
endif()
if(MCRF_HEADLESS) if(MCRF_HEADLESS)
message(STATUS "Building in HEADLESS mode - no SFML/ImGui dependencies") message(STATUS "Building in HEADLESS mode - no SFML/ImGui dependencies")
endif() endif()
@ -266,8 +273,8 @@ if(EMSCRIPTEN)
-sALLOW_UNIMPLEMENTED_SYSCALLS=1 -sALLOW_UNIMPLEMENTED_SYSCALLS=1
# Preload Python stdlib into virtual filesystem at /lib/python3.14 # Preload Python stdlib into virtual filesystem at /lib/python3.14
--preload-file=${CMAKE_SOURCE_DIR}/wasm_stdlib/lib@/lib --preload-file=${CMAKE_SOURCE_DIR}/wasm_stdlib/lib@/lib
# Preload game scripts into /scripts # Preload game scripts into /scripts (use playground scripts if MCRF_PLAYGROUND is set)
--preload-file=${CMAKE_SOURCE_DIR}/src/scripts@/scripts --preload-file=${CMAKE_SOURCE_DIR}/src/$<IF:$<BOOL:${MCRF_PLAYGROUND}>,scripts_playground,scripts>@/scripts
# Preload assets # Preload assets
--preload-file=${CMAKE_SOURCE_DIR}/assets@/assets --preload-file=${CMAKE_SOURCE_DIR}/assets@/assets
# Use custom HTML shell for crisp pixel rendering # Use custom HTML shell for crisp pixel rendering

View file

@ -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) { 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 // Try to lock weak_ptr to get shared_ptr
std::shared_ptr<UIDrawable> target = targetWeak.lock(); std::shared_ptr<UIDrawable> target = targetWeak.lock();
std::shared_ptr<UIEntity> entity = entityTargetWeak.lock(); std::shared_ptr<UIEntity> entity = entityTargetWeak.lock();

View file

@ -62,6 +62,9 @@ public:
// Complete the animation immediately (jump to final value) // Complete the animation immediately (jump to final value)
void complete(); void complete();
// Stop the animation without completing (no final value applied, no callback)
void stop();
// Update animation (called each frame) // Update animation (called each frame)
// Returns true if animation is still running, false if complete // Returns true if animation is still running, false if complete
bool update(float deltaTime); bool update(float deltaTime);
@ -79,7 +82,8 @@ public:
std::string getTargetProperty() const { return targetProperty; } std::string getTargetProperty() const { return targetProperty; }
float getDuration() const { return duration; } float getDuration() const { return duration; }
float getElapsed() const { return elapsed; } 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; } bool isDelta() const { return delta; }
// Get raw target pointer for property locking (#120) // Get raw target pointer for property locking (#120)
@ -97,6 +101,7 @@ private:
float elapsed = 0.0f; // Elapsed time float elapsed = 0.0f; // Elapsed time
EasingFunction easingFunc; // Easing function to use EasingFunction easingFunc; // Easing function to use
bool delta; // If true, targetValue is relative to start 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 // RAII: Use weak_ptr for safe target tracking
std::weak_ptr<UIDrawable> targetWeak; std::weak_ptr<UIDrawable> targetWeak;
@ -196,6 +201,9 @@ public:
// Get active animation count (for debugging/testing) // Get active animation count (for debugging/testing)
size_t getActiveAnimationCount() const { return activeAnimations.size(); } size_t getActiveAnimationCount() const { return activeAnimations.size(); }
// Get all active animations (for mcrfpy.animations)
const std::vector<std::shared_ptr<Animation>>& getActiveAnimations() const { return activeAnimations; }
private: private:
AnimationManager() = default; AnimationManager() = default;
std::vector<std::shared_ptr<Animation>> activeAnimations; std::vector<std::shared_ptr<Animation>> activeAnimations;

View file

@ -107,7 +107,7 @@ GameEngine::GameEngine(const McRogueFaceConfig& cfg)
gameView.setCenter(std::floor(gameResolution.x / 2.0f), std::floor(gameResolution.y / 2.0f)); gameView.setCenter(std::floor(gameResolution.x / 2.0f), std::floor(gameResolution.y / 2.0f));
updateViewport(); updateViewport();
scene = "uitest"; scene = "uitest";
scenes["uitest"] = new UITestScene(this); scenes["uitest"] = std::make_shared<UITestScene>(this);
McRFPy_API::game = this; McRFPy_API::game = this;
@ -123,21 +123,11 @@ GameEngine::GameEngine(const McRogueFaceConfig& cfg)
!config.python_mode; !config.python_mode;
if (should_load_game) { if (should_load_game) {
std::cerr << "[DEBUG] GameEngine: loading default game.py" << std::endl;
std::cerr.flush();
if (!Py_IsInitialized()) { if (!Py_IsInitialized()) {
std::cerr << "[DEBUG] GameEngine: initializing Python API" << std::endl;
std::cerr.flush();
McRFPy_API::api_init(); McRFPy_API::api_init();
} }
std::cerr << "[DEBUG] GameEngine: importing mcrfpy" << std::endl;
std::cerr.flush();
McRFPy_API::executePyString("import mcrfpy"); McRFPy_API::executePyString("import mcrfpy");
std::cerr << "[DEBUG] GameEngine: executing scripts/game.py" << std::endl;
std::cerr.flush();
McRFPy_API::executeScript("scripts/game.py"); 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. // Note: --exec scripts are NOT executed here.
@ -169,9 +159,8 @@ void GameEngine::executeStartupScripts()
GameEngine::~GameEngine() GameEngine::~GameEngine()
{ {
cleanup(); cleanup();
for (auto& [name, scene] : scenes) { // scenes map uses shared_ptr, will clean up automatically
delete scene; scenes.clear();
}
delete profilerOverlay; delete profilerOverlay;
} }
@ -208,10 +197,10 @@ void GameEngine::cleanup()
#endif #endif
} }
Scene* GameEngine::currentScene() { return scenes[scene]; } Scene* GameEngine::currentScene() { return scenes[scene].get(); }
Scene* GameEngine::getScene(const std::string& name) { Scene* GameEngine::getScene(const std::string& name) {
auto it = scenes.find(name); auto it = scenes.find(name);
return (it != scenes.end()) ? it->second : nullptr; return (it != scenes.end()) ? it->second.get() : nullptr;
} }
std::vector<std::string> GameEngine::getSceneNames() const { std::vector<std::string> GameEngine::getSceneNames() const {
@ -286,7 +275,29 @@ sf::RenderTarget & GameEngine::getRenderTarget() {
return *render_target; 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<PyScene>(this); }
void GameEngine::registerScene(const std::string& name, std::shared_ptr<Scene> 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) void GameEngine::setWindowScale(float multiplier)
{ {
@ -320,8 +331,16 @@ void GameEngine::run()
#ifdef __EMSCRIPTEN__ #ifdef __EMSCRIPTEN__
// Browser: use callback-based loop (non-blocking) // 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); 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 #else
// Desktop: traditional blocking loop // Desktop: traditional blocking loop
while (running) while (running)
@ -638,7 +657,7 @@ std::shared_ptr<std::vector<std::shared_ptr<UIDrawable>>> GameEngine::scene_ui(s
std::cout << "iterators: " << std::distance(scenes.begin(), scenes.begin()) << " " << std::cout << "iterators: " << std::distance(scenes.begin(), scenes.begin()) << " " <<
std::distance(scenes.begin(), scenes.end()) << std::endl; std::distance(scenes.begin(), scenes.end()) << std::endl;
std::cout << "scenes.contains(target): " << scenes.contains(target) << 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; if (scenes.count(target) == 0) return NULL;
return scenes[target]->ui_elements; return scenes[target]->ui_elements;
@ -663,9 +682,23 @@ void GameEngine::setVSync(bool enabled)
void GameEngine::setFramerateLimit(unsigned int limit) void GameEngine::setFramerateLimit(unsigned int limit)
{ {
framerate_limit = 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) { if (!headless && window) {
window->setFramerateLimit(limit); window->setFramerateLimit(limit);
} }
#endif
} }
void GameEngine::setGameResolution(unsigned int width, unsigned int height) { void GameEngine::setGameResolution(unsigned int width, unsigned int height) {

View file

@ -159,7 +159,7 @@ private:
sf::RenderTarget* render_target; sf::RenderTarget* render_target;
sf::Font font; sf::Font font;
std::map<std::string, Scene*> scenes; std::map<std::string, std::shared_ptr<Scene>> scenes;
bool running = true; bool running = true;
bool paused = false; bool paused = false;
int currentFrame = 0; int currentFrame = 0;
@ -230,6 +230,8 @@ public:
void changeScene(std::string); void changeScene(std::string);
void changeScene(std::string sceneName, TransitionType transitionType, float duration); void changeScene(std::string sceneName, TransitionType transitionType, float duration);
void createScene(std::string); void createScene(std::string);
void registerScene(const std::string& name, std::shared_ptr<Scene> scene);
void unregisterScene(const std::string& name);
void quit(); void quit();
void setPause(bool); void setPause(bool);
sf::Font & getFont(); sf::Font & getFont();

View file

@ -111,6 +111,10 @@ static PyObject* mcrfpy_module_getattr(PyObject* self, PyObject* args)
return McRFPy_API::api_get_timers(); return McRFPy_API::api_get_timers();
} }
if (strcmp(name, "animations") == 0) {
return McRFPy_API::api_get_animations();
}
if (strcmp(name, "default_transition") == 0) { if (strcmp(name, "default_transition") == 0) {
return PyTransition::to_python(PyTransition::default_transition); return PyTransition::to_python(PyTransition::default_transition);
} }
@ -144,6 +148,11 @@ static int mcrfpy_module_setattro(PyObject* self, PyObject* name, PyObject* valu
return -1; 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) { if (strcmp(name_str, "default_transition") == 0) {
TransitionType trans; TransitionType trans;
if (!PyTransition::from_arg(value, &trans, nullptr)) { if (!PyTransition::from_arg(value, &trans, nullptr)) {
@ -699,28 +708,18 @@ PyObject* PyInit_mcrfpy()
// init_python - configure interpreter details here // init_python - configure interpreter details here
PyStatus init_python(const char *program_name) PyStatus init_python(const char *program_name)
{ {
std::cerr << "[DEBUG] api_init: starting" << std::endl;
std::cerr.flush();
PyStatus status; PyStatus status;
//**preconfig to establish locale** // Preconfig to establish locale
PyPreConfig preconfig; PyPreConfig preconfig;
PyPreConfig_InitIsolatedConfig(&preconfig); PyPreConfig_InitIsolatedConfig(&preconfig);
preconfig.utf8_mode = 1; preconfig.utf8_mode = 1;
std::cerr << "[DEBUG] api_init: Py_PreInitialize" << std::endl;
std::cerr.flush();
status = Py_PreInitialize(&preconfig); status = Py_PreInitialize(&preconfig);
if (PyStatus_Exception(status)) { if (PyStatus_Exception(status)) {
std::cerr << "[DEBUG] api_init: PreInit failed" << std::endl;
Py_ExitStatusException(status); Py_ExitStatusException(status);
} }
std::cerr << "[DEBUG] api_init: PyConfig setup" << std::endl;
std::cerr.flush();
PyConfig config; PyConfig config;
PyConfig_InitIsolatedConfig(&config); PyConfig_InitIsolatedConfig(&config);
config.dev_mode = 0; config.dev_mode = 0;
@ -731,9 +730,6 @@ PyStatus init_python(const char *program_name)
config.configure_c_stdio = 1; config.configure_c_stdio = 1;
#ifdef __EMSCRIPTEN__ #ifdef __EMSCRIPTEN__
std::cerr << "[DEBUG] api_init: WASM path config" << std::endl;
std::cerr.flush();
// WASM: Use absolute paths in virtual filesystem // WASM: Use absolute paths in virtual filesystem
PyConfig_SetString(&config, &config.executable, L"/mcrogueface"); PyConfig_SetString(&config, &config.executable, L"/mcrogueface");
PyConfig_SetString(&config, &config.home, L"/lib/python3.14"); PyConfig_SetString(&config, &config.home, L"/lib/python3.14");
@ -814,18 +810,11 @@ PyStatus init_python(const char *program_name)
PyStatus McRFPy_API::init_python_with_config(const McRogueFaceConfig& config) 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 Python is already initialized, just return success
if (Py_IsInitialized()) { if (Py_IsInitialized()) {
std::cerr << "[DEBUG] init_python_with_config: already initialized" << std::endl;
return PyStatus_Ok(); return PyStatus_Ok();
} }
std::cerr << "[DEBUG] init_python_with_config: PyConfig_InitIsolatedConfig" << std::endl;
std::cerr.flush();
PyStatus status; PyStatus status;
PyConfig pyconfig; PyConfig pyconfig;
PyConfig_InitIsolatedConfig(&pyconfig); PyConfig_InitIsolatedConfig(&pyconfig);
@ -1272,6 +1261,33 @@ PyObject* McRFPy_API::api_get_timers()
return tuple; 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 // #153 - Headless simulation control
PyObject* McRFPy_API::_step(PyObject* self, PyObject* args) { PyObject* McRFPy_API::_step(PyObject* self, PyObject* args) {
PyObject* dt_obj = Py_None; PyObject* dt_obj = Py_None;

View file

@ -93,6 +93,9 @@ public:
// #173: Module-level timer collection accessor // #173: Module-level timer collection accessor
static PyObject* api_get_timers(); 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 // Exception handling - signal game loop to exit on unhandled Python exceptions
static std::atomic<bool> exception_occurred; static std::atomic<bool> exception_occurred;
static std::atomic<int> exit_code; static std::atomic<int> exit_code;

View file

@ -331,6 +331,13 @@ PyObject* PyAnimation::complete(PyAnimationObject* self, PyObject* args) {
Py_RETURN_NONE; 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) { PyObject* PyAnimation::has_valid_target(PyAnimationObject* self, PyObject* args) {
if (self->data && self->data->hasValidTarget()) { if (self->data && self->data->hasValidTarget()) {
Py_RETURN_TRUE; Py_RETURN_TRUE;
@ -390,6 +397,14 @@ PyMethodDef PyAnimation::methods[] = {
MCRF_RETURNS("None") MCRF_RETURNS("None")
MCRF_NOTE("Sets elapsed = duration and applies target value immediately. Completion callback will be called if set.") 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, {"hasValidTarget", (PyCFunction)has_valid_target, METH_NOARGS,
MCRF_METHOD(Animation, hasValidTarget, MCRF_METHOD(Animation, hasValidTarget,
MCRF_SIG("()", "bool"), MCRF_SIG("()", "bool"),

View file

@ -30,6 +30,7 @@ public:
static PyObject* update(PyAnimationObject* self, PyObject* args); static PyObject* update(PyAnimationObject* self, PyObject* args);
static PyObject* get_current_value(PyAnimationObject* self, PyObject* args); static PyObject* get_current_value(PyAnimationObject* self, PyObject* args);
static PyObject* complete(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 PyObject* has_valid_target(PyAnimationObject* self, PyObject* args);
static PyGetSetDef getsetters[]; static PyGetSetDef getsetters[];

View file

@ -30,30 +30,32 @@ int PySceneClass::__init__(PySceneObject* self, PyObject* args, PyObject* kwds)
return -1; 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; GameEngine* game = McRFPy_API::game;
if (!game) { if (!game) {
PyErr_SetString(PyExc_RuntimeError, "No game engine initialized"); PyErr_SetString(PyExc_RuntimeError, "No game engine initialized");
return -1; return -1;
} }
// Store this Python object in our registry // If scene with this name already exists in python_scenes, unregister the old one
python_scenes[name] = self; if (python_scenes.count(name) > 0) {
Py_INCREF(self); // Keep a reference 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);
}
// Create a Python function that routes to on_keypress self->name = name;
// We'll register this after the object is fully initialized
// Create the C++ PyScene with shared ownership
self->scene = std::make_shared<PyScene>(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); // python_scenes holds a reference
self->initialized = true; self->initialized = true;
@ -64,11 +66,16 @@ void PySceneClass::__dealloc(PyObject* self_obj)
{ {
PySceneObject* self = (PySceneObject*)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) { if (python_scenes.count(self->name) > 0 && python_scenes[self->name] == self) {
python_scenes.erase(self->name); 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 // Call Python object destructor
Py_TYPE(self)->tp_free(self); Py_TYPE(self)->tp_free(self);
} }
@ -122,6 +129,15 @@ PyObject* PySceneClass::activate(PySceneObject* self, PyObject* args, PyObject*
return NULL; 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 // Call game->changeScene directly with proper transition
game->changeScene(self->name, transition_type, duration); game->changeScene(self->name, transition_type, duration);
@ -141,17 +157,11 @@ static PyObject* PySceneClass_get_children(PySceneObject* self, void* closure)
// on_key property getter // on_key property getter
static PyObject* PySceneClass_get_on_key(PySceneObject* self, void* closure) static PyObject* PySceneClass_get_on_key(PySceneObject* self, void* closure)
{ {
GameEngine* game = McRFPy_API::game; if (!self->scene || !self->scene->key_callable) {
if (!game) {
Py_RETURN_NONE; Py_RETURN_NONE;
} }
auto scene = game->getScene(self->name); PyObject* callable = self->scene->key_callable->borrow();
if (!scene || !scene->key_callable) {
Py_RETURN_NONE;
}
PyObject* callable = scene->key_callable->borrow();
if (callable && callable != Py_None) { if (callable && callable != Py_None) {
Py_INCREF(callable); Py_INCREF(callable);
return callable; return callable;
@ -162,22 +172,15 @@ static PyObject* PySceneClass_get_on_key(PySceneObject* self, void* closure)
// on_key property setter // on_key property setter
static int PySceneClass_set_on_key(PySceneObject* self, PyObject* value, void* closure) static int PySceneClass_set_on_key(PySceneObject* self, PyObject* value, void* closure)
{ {
GameEngine* game = McRFPy_API::game; if (!self->scene) {
if (!game) { PyErr_SetString(PyExc_RuntimeError, "Scene not initialized");
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; return -1;
} }
if (value == Py_None || value == NULL) { if (value == Py_None || value == NULL) {
scene->key_unregister(); self->scene->key_unregister();
} else if (PyCallable_Check(value)) { } else if (PyCallable_Check(value)) {
scene->key_register(value); self->scene->key_register(value);
} else { } else {
PyErr_SetString(PyExc_TypeError, "on_key must be callable or None"); PyErr_SetString(PyExc_TypeError, "on_key must be callable or None");
return -1; return -1;
@ -203,21 +206,14 @@ PyObject* PySceneClass::get_active(PySceneObject* self, void* closure)
// #118: Scene position getter // #118: Scene position getter
static PyObject* PySceneClass_get_pos(PySceneObject* self, void* closure) static PyObject* PySceneClass_get_pos(PySceneObject* self, void* closure)
{ {
GameEngine* game = McRFPy_API::game; if (!self->scene) {
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; Py_RETURN_NONE;
} }
// Create a Vector object // Create a Vector object
auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Vector"); auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Vector");
if (!type) return NULL; 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); PyObject* result = PyObject_CallObject((PyObject*)type, args);
Py_DECREF(type); Py_DECREF(type);
Py_DECREF(args); Py_DECREF(args);
@ -227,15 +223,8 @@ static PyObject* PySceneClass_get_pos(PySceneObject* self, void* closure)
// #118: Scene position setter // #118: Scene position setter
static int PySceneClass_set_pos(PySceneObject* self, PyObject* value, void* closure) static int PySceneClass_set_pos(PySceneObject* self, PyObject* value, void* closure)
{ {
GameEngine* game = McRFPy_API::game; if (!self->scene) {
if (!game) { PyErr_SetString(PyExc_RuntimeError, "Scene not initialized");
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; return -1;
} }
@ -256,38 +245,25 @@ static int PySceneClass_set_pos(PySceneObject* self, PyObject* value, void* clos
return -1; return -1;
} }
scene->position = sf::Vector2f(x, y); self->scene->position = sf::Vector2f(x, y);
return 0; return 0;
} }
// #118: Scene visible getter // #118: Scene visible getter
static PyObject* PySceneClass_get_visible(PySceneObject* self, void* closure) static PyObject* PySceneClass_get_visible(PySceneObject* self, void* closure)
{ {
GameEngine* game = McRFPy_API::game; if (!self->scene) {
if (!game) {
Py_RETURN_TRUE; Py_RETURN_TRUE;
} }
auto scene = game->getScene(self->name); return PyBool_FromLong(self->scene->visible);
if (!scene) {
Py_RETURN_TRUE;
}
return PyBool_FromLong(scene->visible);
} }
// #118: Scene visible setter // #118: Scene visible setter
static int PySceneClass_set_visible(PySceneObject* self, PyObject* value, void* closure) static int PySceneClass_set_visible(PySceneObject* self, PyObject* value, void* closure)
{ {
GameEngine* game = McRFPy_API::game; if (!self->scene) {
if (!game) { PyErr_SetString(PyExc_RuntimeError, "Scene not initialized");
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; return -1;
} }
@ -296,38 +272,25 @@ static int PySceneClass_set_visible(PySceneObject* self, PyObject* value, void*
return -1; return -1;
} }
scene->visible = PyObject_IsTrue(value); self->scene->visible = PyObject_IsTrue(value);
return 0; return 0;
} }
// #118: Scene opacity getter // #118: Scene opacity getter
static PyObject* PySceneClass_get_opacity(PySceneObject* self, void* closure) static PyObject* PySceneClass_get_opacity(PySceneObject* self, void* closure)
{ {
GameEngine* game = McRFPy_API::game; if (!self->scene) {
if (!game) {
return PyFloat_FromDouble(1.0); return PyFloat_FromDouble(1.0);
} }
auto scene = game->getScene(self->name); return PyFloat_FromDouble(self->scene->opacity);
if (!scene) {
return PyFloat_FromDouble(1.0);
}
return PyFloat_FromDouble(scene->opacity);
} }
// #118: Scene opacity setter // #118: Scene opacity setter
static int PySceneClass_set_opacity(PySceneObject* self, PyObject* value, void* closure) static int PySceneClass_set_opacity(PySceneObject* self, PyObject* value, void* closure)
{ {
GameEngine* game = McRFPy_API::game; if (!self->scene) {
if (!game) { PyErr_SetString(PyExc_RuntimeError, "Scene not initialized");
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; 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 < 0.0) opacity = 0.0;
if (opacity > 1.0) opacity = 1.0; if (opacity > 1.0) opacity = 1.0;
scene->opacity = opacity; self->scene->opacity = opacity;
return 0; 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 // Properties
PyGetSetDef PySceneClass::getsetters[] = { PyGetSetDef PySceneClass::getsetters[] = {
{"name", (getter)get_name, NULL, {"name", (getter)get_name, NULL,
MCRF_PROPERTY(name, "Scene name (str, read-only). Unique identifier for this scene."), NULL}, MCRF_PROPERTY(name, "Scene name (str, read-only). Unique identifier for this scene."), NULL},
{"active", (getter)get_active, 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}, 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 // #118: Scene-level UIDrawable-like properties
{"pos", (getter)PySceneClass_get_pos, (setter)PySceneClass_set_pos, {"pos", (getter)PySceneClass_get_pos, (setter)PySceneClass_set_pos,
MCRF_PROPERTY(pos, "Scene position offset (Vector). Applied to all UI elements during rendering."), NULL}, 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 // Scene.realign() - recalculate alignment for all children
static PyObject* PySceneClass_realign(PySceneObject* self, PyObject* args) 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; GameEngine* game = McRFPy_API::game;
if (!game) { if (!game) {
@ -527,18 +524,54 @@ static PyObject* PySceneClass_realign(PySceneObject* self, PyObject* args)
return NULL; return NULL;
} }
auto scene = game->getScene(self->name); if (!self->scene) {
if (!scene || !scene->ui_elements) { PyErr_SetString(PyExc_RuntimeError, "Scene not initialized");
Py_RETURN_NONE; return NULL;
} }
// Iterate through all UI elements and realign those with alignment set // If another scene with this name is registered, unregister it first
for (auto& drawable : *scene->ui_elements) { if (python_scenes.count(self->name) > 0 && python_scenes[self->name] != self) {
if (drawable && drawable->align_type != AlignmentType::NONE) { PySceneObject* old = python_scenes[self->name];
drawable->applyAlignment(); 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; Py_RETURN_NONE;
} }
@ -561,6 +594,22 @@ PyMethodDef PySceneClass::methods[] = {
MCRF_NOTE("Call this after window resize or when game_resolution changes. " MCRF_NOTE("Call this after window resize or when game_resolution changes. "
"For responsive layouts, connect this to on_resize callback.") "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} {NULL}
}; };

View file

@ -377,16 +377,9 @@ void SDL2Renderer::setProjection(float left, float right, float bottom, float to
projectionMatrix_[15] = 1.0f; projectionMatrix_[15] = 1.0f;
} }
static int clearCount = 0;
void SDL2Renderer::clear(float r, float g, float b, float a) { void SDL2Renderer::clear(float r, float g, float b, float a) {
glClearColor(r, g, b, a); glClearColor(r, g, b, a);
glClear(GL_COLOR_BUFFER_BIT); 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) { 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); glBindTexture(GL_TEXTURE_2D, textureId);
int texLoc = glGetUniformLocation(program, "u_texture"); int texLoc = glGetUniformLocation(program, "u_texture");
glUniform1i(texLoc, 0); 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 // 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 // Set the canvas size explicitly before creating the window
emscripten_set_canvas_element_size("#canvas", mode.width, mode.height); emscripten_set_canvas_element_size("#canvas", mode.width, mode.height);
std::cout << "Emscripten: Setting canvas to " << mode.width << "x" << mode.height << std::endl;
#endif #endif
// Create window // Create window
@ -605,37 +580,16 @@ void RenderWindow::create(VideoMode mode, const std::string& title, uint32_t sty
// Set up OpenGL state // Set up OpenGL state
glViewport(0, 0, mode.width, mode.height); 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); SDL2Renderer::getInstance().setProjection(0, mode.width, mode.height, 0);
// Enable blending for transparency // Enable blending for transparency
glEnable(GL_BLEND); glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
// Initial clear to a visible color to confirm GL is working // Initial clear
glClearColor(0.2f, 0.3f, 0.4f, 1.0f); // Blue-gray glClearColor(0.2f, 0.3f, 0.4f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT); 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); 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() { void RenderWindow::close() {
@ -1277,7 +1231,6 @@ bool Font::loadFromFile(const std::string& filename) {
ftStroker_ = stroker; ftStroker_ = stroker;
loaded_ = true; loaded_ = true;
std::cout << "Font: Loaded " << filename << " with FreeType" << std::endl;
return true; return true;
} }
@ -2129,10 +2082,6 @@ bool FontAtlas::load(const Font* font, float fontSize) {
} }
textureId_ = SDL2Renderer::getInstance().createTexture(ATLAS_SIZE, ATLAS_SIZE, rgbaPixels.data()); 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; 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()); 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; return true;
} }

View file

@ -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