Compare commits
No commits in common. "2fb29a102e07ffe7dd48810b90db2f1e1319b626" and "69a59ad1e8993cfc04b43c4d0bf2e42c32682bc8" have entirely different histories.
2fb29a102e
...
69a59ad1e8
12 changed files with 212 additions and 327 deletions
|
|
@ -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)
|
# 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)
|
||||||
|
|
@ -32,10 +29,6 @@ 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()
|
||||||
|
|
@ -273,8 +266,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 (use playground scripts if MCRF_PLAYGROUND is set)
|
# Preload game scripts into /scripts
|
||||||
--preload-file=${CMAKE_SOURCE_DIR}/src/$<IF:$<BOOL:${MCRF_PLAYGROUND}>,scripts_playground,scripts>@/scripts
|
--preload-file=${CMAKE_SOURCE_DIR}/src/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
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
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();
|
||||||
|
|
|
||||||
|
|
@ -62,9 +62,6 @@ 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);
|
||||||
|
|
@ -82,8 +79,7 @@ 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 || stopped; }
|
bool isComplete() const { return elapsed >= duration; }
|
||||||
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)
|
||||||
|
|
@ -101,7 +97,6 @@ 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;
|
||||||
|
|
@ -201,9 +196,6 @@ 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;
|
||||||
|
|
|
||||||
|
|
@ -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"] = std::make_shared<UITestScene>(this);
|
scenes["uitest"] = new UITestScene(this);
|
||||||
|
|
||||||
McRFPy_API::game = this;
|
McRFPy_API::game = this;
|
||||||
|
|
||||||
|
|
@ -123,11 +123,21 @@ 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.
|
||||||
|
|
@ -159,8 +169,9 @@ void GameEngine::executeStartupScripts()
|
||||||
GameEngine::~GameEngine()
|
GameEngine::~GameEngine()
|
||||||
{
|
{
|
||||||
cleanup();
|
cleanup();
|
||||||
// scenes map uses shared_ptr, will clean up automatically
|
for (auto& [name, scene] : scenes) {
|
||||||
scenes.clear();
|
delete scene;
|
||||||
|
}
|
||||||
delete profilerOverlay;
|
delete profilerOverlay;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -197,10 +208,10 @@ void GameEngine::cleanup()
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
Scene* GameEngine::currentScene() { return scenes[scene].get(); }
|
Scene* GameEngine::currentScene() { return scenes[scene]; }
|
||||||
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.get() : nullptr;
|
return (it != scenes.end()) ? it->second : nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<std::string> GameEngine::getSceneNames() const {
|
std::vector<std::string> GameEngine::getSceneNames() const {
|
||||||
|
|
@ -275,29 +286,7 @@ sf::RenderTarget & GameEngine::getRenderTarget() {
|
||||||
return *render_target;
|
return *render_target;
|
||||||
}
|
}
|
||||||
|
|
||||||
void GameEngine::createScene(std::string s) { scenes[s] = std::make_shared<PyScene>(this); }
|
void GameEngine::createScene(std::string s) { scenes[s] = new 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)
|
||||||
{
|
{
|
||||||
|
|
@ -331,16 +320,8 @@ void GameEngine::run()
|
||||||
|
|
||||||
#ifdef __EMSCRIPTEN__
|
#ifdef __EMSCRIPTEN__
|
||||||
// Browser: use callback-based loop (non-blocking)
|
// 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);
|
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)
|
||||||
|
|
@ -657,7 +638,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].get()) << std::endl;
|
std::cout << "scenes[target]: " << (long)(scenes[target]) << 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;
|
||||||
|
|
@ -682,23 +663,9 @@ 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) {
|
||||||
|
|
|
||||||
|
|
@ -159,7 +159,7 @@ private:
|
||||||
sf::RenderTarget* render_target;
|
sf::RenderTarget* render_target;
|
||||||
|
|
||||||
sf::Font font;
|
sf::Font font;
|
||||||
std::map<std::string, std::shared_ptr<Scene>> scenes;
|
std::map<std::string, Scene*> scenes;
|
||||||
bool running = true;
|
bool running = true;
|
||||||
bool paused = false;
|
bool paused = false;
|
||||||
int currentFrame = 0;
|
int currentFrame = 0;
|
||||||
|
|
@ -230,8 +230,6 @@ 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();
|
||||||
|
|
|
||||||
|
|
@ -111,10 +111,6 @@ 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);
|
||||||
}
|
}
|
||||||
|
|
@ -148,11 +144,6 @@ 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)) {
|
||||||
|
|
@ -708,18 +699,28 @@ 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;
|
||||||
|
|
@ -730,6 +731,9 @@ 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");
|
||||||
|
|
@ -810,11 +814,18 @@ 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);
|
||||||
|
|
@ -1261,33 +1272,6 @@ 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;
|
||||||
|
|
|
||||||
|
|
@ -93,9 +93,6 @@ 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;
|
||||||
|
|
|
||||||
|
|
@ -331,13 +331,6 @@ 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;
|
||||||
|
|
@ -397,14 +390,6 @@ 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"),
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,6 @@ 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[];
|
||||||
|
|
|
||||||
|
|
@ -30,32 +30,30 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If scene with this name already exists in python_scenes, unregister the old one
|
// Store this Python object in our registry
|
||||||
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<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;
|
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;
|
self->initialized = true;
|
||||||
|
|
||||||
|
|
@ -66,16 +64,11 @@ void PySceneClass::__dealloc(PyObject* self_obj)
|
||||||
{
|
{
|
||||||
PySceneObject* self = (PySceneObject*)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) {
|
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);
|
||||||
}
|
}
|
||||||
|
|
@ -129,15 +122,6 @@ 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);
|
||||||
|
|
||||||
|
|
@ -157,11 +141,17 @@ 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)
|
||||||
{
|
{
|
||||||
if (!self->scene || !self->scene->key_callable) {
|
GameEngine* game = McRFPy_API::game;
|
||||||
|
if (!game) {
|
||||||
Py_RETURN_NONE;
|
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) {
|
if (callable && callable != Py_None) {
|
||||||
Py_INCREF(callable);
|
Py_INCREF(callable);
|
||||||
return callable;
|
return callable;
|
||||||
|
|
@ -172,15 +162,22 @@ 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)
|
||||||
{
|
{
|
||||||
if (!self->scene) {
|
GameEngine* game = McRFPy_API::game;
|
||||||
PyErr_SetString(PyExc_RuntimeError, "Scene not initialized");
|
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;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (value == Py_None || value == NULL) {
|
if (value == Py_None || value == NULL) {
|
||||||
self->scene->key_unregister();
|
scene->key_unregister();
|
||||||
} else if (PyCallable_Check(value)) {
|
} else if (PyCallable_Check(value)) {
|
||||||
self->scene->key_register(value);
|
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;
|
||||||
|
|
@ -206,14 +203,21 @@ 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)
|
||||||
{
|
{
|
||||||
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;
|
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)", 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);
|
PyObject* result = PyObject_CallObject((PyObject*)type, args);
|
||||||
Py_DECREF(type);
|
Py_DECREF(type);
|
||||||
Py_DECREF(args);
|
Py_DECREF(args);
|
||||||
|
|
@ -223,8 +227,15 @@ 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)
|
||||||
{
|
{
|
||||||
if (!self->scene) {
|
GameEngine* game = McRFPy_API::game;
|
||||||
PyErr_SetString(PyExc_RuntimeError, "Scene not initialized");
|
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;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -245,25 +256,38 @@ static int PySceneClass_set_pos(PySceneObject* self, PyObject* value, void* clos
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
self->scene->position = sf::Vector2f(x, y);
|
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)
|
||||||
{
|
{
|
||||||
if (!self->scene) {
|
GameEngine* game = McRFPy_API::game;
|
||||||
|
if (!game) {
|
||||||
Py_RETURN_TRUE;
|
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
|
// #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)
|
||||||
{
|
{
|
||||||
if (!self->scene) {
|
GameEngine* game = McRFPy_API::game;
|
||||||
PyErr_SetString(PyExc_RuntimeError, "Scene not initialized");
|
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;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -272,25 +296,38 @@ static int PySceneClass_set_visible(PySceneObject* self, PyObject* value, void*
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
self->scene->visible = PyObject_IsTrue(value);
|
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)
|
||||||
{
|
{
|
||||||
if (!self->scene) {
|
GameEngine* game = McRFPy_API::game;
|
||||||
|
if (!game) {
|
||||||
return PyFloat_FromDouble(1.0);
|
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
|
// #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)
|
||||||
{
|
{
|
||||||
if (!self->scene) {
|
GameEngine* game = McRFPy_API::game;
|
||||||
PyErr_SetString(PyExc_RuntimeError, "Scene not initialized");
|
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;
|
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 < 0.0) opacity = 0.0;
|
||||||
if (opacity > 1.0) opacity = 1.0;
|
if (opacity > 1.0) opacity = 1.0;
|
||||||
|
|
||||||
self->scene->opacity = opacity;
|
scene->opacity = opacity;
|
||||||
return 0;
|
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
|
// 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},
|
||||||
|
|
@ -501,12 +521,19 @@ 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) {
|
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;
|
Py_RETURN_NONE;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Iterate through all UI elements and realign those with alignment set
|
// 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) {
|
if (drawable && drawable->align_type != AlignmentType::NONE) {
|
||||||
drawable->applyAlignment();
|
drawable->applyAlignment();
|
||||||
}
|
}
|
||||||
|
|
@ -515,66 +542,6 @@ static PyObject* PySceneClass_realign(PySceneObject* self, PyObject* args)
|
||||||
Py_RETURN_NONE;
|
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
|
// Methods
|
||||||
PyMethodDef PySceneClass::methods[] = {
|
PyMethodDef PySceneClass::methods[] = {
|
||||||
{"activate", (PyCFunction)activate, METH_VARARGS | METH_KEYWORDS,
|
{"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. "
|
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}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -377,9 +377,16 @@ 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) {
|
||||||
|
|
@ -459,6 +466,22 @@ 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
|
||||||
|
|
@ -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
|
// 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
|
||||||
|
|
@ -580,16 +605,37 @@ 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
|
// Initial clear to a visible color to confirm GL is working
|
||||||
glClearColor(0.2f, 0.3f, 0.4f, 1.0f);
|
glClearColor(0.2f, 0.3f, 0.4f, 1.0f); // Blue-gray
|
||||||
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() {
|
||||||
|
|
@ -1231,6 +1277,7 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2082,6 +2129,10 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -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());
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue