diff --git a/src/McRFPy_API.cpp b/src/McRFPy_API.cpp index 5db31fc..dcbcb74 100644 --- a/src/McRFPy_API.cpp +++ b/src/McRFPy_API.cpp @@ -9,6 +9,7 @@ #include "PyWindow.h" #include "PySceneObject.h" #include "PyFOV.h" +#include "PyTransition.h" #include "PySound.h" #include "PyMusic.h" #include "PyKeyboard.h" @@ -51,6 +52,14 @@ static PyObject* mcrfpy_module_getattr(PyObject* self, PyObject* args) return McRFPy_API::api_get_scenes(); } + if (strcmp(name, "default_transition") == 0) { + return PyTransition::to_python(PyTransition::default_transition); + } + + if (strcmp(name, "default_transition_duration") == 0) { + return PyFloat_FromDouble(PyTransition::default_duration); + } + // Attribute not found - raise AttributeError PyErr_Format(PyExc_AttributeError, "module 'mcrfpy' has no attribute '%s'", name); return NULL; @@ -71,6 +80,33 @@ static int mcrfpy_module_setattro(PyObject* self, PyObject* name, PyObject* valu return -1; } + if (strcmp(name_str, "default_transition") == 0) { + TransitionType trans; + if (!PyTransition::from_arg(value, &trans, nullptr)) { + return -1; + } + PyTransition::default_transition = trans; + return 0; + } + + if (strcmp(name_str, "default_transition_duration") == 0) { + double duration; + if (PyFloat_Check(value)) { + duration = PyFloat_AsDouble(value); + } else if (PyLong_Check(value)) { + duration = PyLong_AsDouble(value); + } else { + PyErr_SetString(PyExc_TypeError, "default_transition_duration must be a number"); + return -1; + } + if (duration < 0.0) { + PyErr_SetString(PyExc_ValueError, "default_transition_duration must be non-negative"); + return -1; + } + PyTransition::default_duration = static_cast(duration); + return 0; + } + // For other attributes, use default module setattr return PyObject_GenericSetAttr(self, name, value); } @@ -392,7 +428,16 @@ PyObject* PyInit_mcrfpy() // Fallback to integer if enum failed PyModule_AddIntConstant(m, "default_fov", FOV_BASIC); } - + + // Add Transition enum class (uses Python's IntEnum) + PyObject* transition_class = PyTransition::create_enum_class(m); + if (!transition_class) { + // If enum creation fails, continue without it (non-fatal) + PyErr_Clear(); + } + // Note: default_transition and default_transition_duration are handled via + // mcrfpy_module_getattr/setattro using PyTransition::default_transition/default_duration + // Add automation submodule PyObject* automation_module = McRFPy_Automation::init_automation_module(); if (automation_module != NULL) { diff --git a/src/PySceneObject.cpp b/src/PySceneObject.cpp index 04711f0..3ccfc1c 100644 --- a/src/PySceneObject.cpp +++ b/src/PySceneObject.cpp @@ -3,6 +3,7 @@ #include "GameEngine.h" #include "McRFPy_API.h" #include "McRFPy_Doc.h" +#include "PyTransition.h" #include // Static map to store Python scene objects by name @@ -75,13 +76,54 @@ PyObject* PySceneClass::__repr__(PySceneObject* self) return PyUnicode_FromFormat("", self->name.c_str()); } -PyObject* PySceneClass::activate(PySceneObject* self, PyObject* args) +PyObject* PySceneClass::activate(PySceneObject* self, PyObject* args, PyObject* kwds) { - // Call the static method from McRFPy_API - PyObject* py_args = Py_BuildValue("(s)", self->name.c_str()); - PyObject* result = McRFPy_API::_setScene(NULL, py_args); - Py_DECREF(py_args); - return result; + static const char* keywords[] = {"transition", "duration", nullptr}; + PyObject* transition_arg = nullptr; + PyObject* duration_arg = nullptr; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OO", const_cast(keywords), + &transition_arg, &duration_arg)) { + return NULL; + } + + // Get transition type (use default if not provided) + TransitionType transition_type; + bool trans_was_none = false; + if (transition_arg) { + if (!PyTransition::from_arg(transition_arg, &transition_type, &trans_was_none)) { + return NULL; + } + } else { + transition_type = PyTransition::default_transition; + } + + // Get duration (use default if not provided) + float duration; + if (duration_arg && duration_arg != Py_None) { + if (PyFloat_Check(duration_arg)) { + duration = static_cast(PyFloat_AsDouble(duration_arg)); + } else if (PyLong_Check(duration_arg)) { + duration = static_cast(PyLong_AsLong(duration_arg)); + } else { + PyErr_SetString(PyExc_TypeError, "duration must be a number"); + return NULL; + } + } else { + duration = PyTransition::default_duration; + } + + // Build transition string for _setScene (or call game->changeScene directly) + GameEngine* game = McRFPy_API::game; + if (!game) { + PyErr_SetString(PyExc_RuntimeError, "No game engine"); + return NULL; + } + + // Call game->changeScene directly with proper transition + game->changeScene(self->name, transition_type, duration); + + Py_RETURN_NONE; } // children property getter (replaces get_ui method) @@ -455,12 +497,15 @@ PyGetSetDef PySceneClass::getsetters[] = { // Methods PyMethodDef PySceneClass::methods[] = { - {"activate", (PyCFunction)activate, METH_NOARGS, + {"activate", (PyCFunction)activate, METH_VARARGS | METH_KEYWORDS, MCRF_METHOD(SceneClass, activate, - MCRF_SIG("()", "None"), - MCRF_DESC("Make this the active scene."), + MCRF_SIG("(transition: Transition = None, duration: float = None)", "None"), + MCRF_DESC("Make this the active scene with optional transition effect."), + MCRF_ARGS_START + MCRF_ARG("transition", "Transition type (mcrfpy.Transition enum). Defaults to mcrfpy.default_transition") + MCRF_ARG("duration", "Transition duration in seconds. Defaults to mcrfpy.default_transition_duration") MCRF_RETURNS("None") - MCRF_NOTE("Deactivates the current scene and activates this one. Scene transitions and lifecycle callbacks are triggered.") + MCRF_NOTE("Deactivates the current scene and activates this one. Lifecycle callbacks (on_exit, on_enter) are triggered.") )}, {"register_keyboard", (PyCFunction)register_keyboard, METH_VARARGS, MCRF_METHOD(SceneClass, register_keyboard, @@ -575,9 +620,8 @@ int McRFPy_API::api_set_current_scene(PyObject* value) return -1; } - std::string old_scene = game->scene; - game->scene = scene_name; - McRFPy_API::triggerSceneChange(old_scene, scene_name); + // Use changeScene with default transition settings + game->changeScene(scene_name, PyTransition::default_transition, PyTransition::default_duration); return 0; } diff --git a/src/PySceneObject.h b/src/PySceneObject.h index 22ef8ab..fdba708 100644 --- a/src/PySceneObject.h +++ b/src/PySceneObject.h @@ -26,7 +26,7 @@ public: static PyObject* __repr__(PySceneObject* self); // Scene methods - static PyObject* activate(PySceneObject* self, PyObject* args); + static PyObject* activate(PySceneObject* self, PyObject* args, PyObject* kwds); static PyObject* register_keyboard(PySceneObject* self, PyObject* args); // Properties diff --git a/src/PyTransition.cpp b/src/PyTransition.cpp new file mode 100644 index 0000000..7a328e1 --- /dev/null +++ b/src/PyTransition.cpp @@ -0,0 +1,158 @@ +#include "PyTransition.h" +#include "McRFPy_API.h" + +// Static storage +PyObject* PyTransition::transition_enum_class = nullptr; +TransitionType PyTransition::default_transition = TransitionType::None; +float PyTransition::default_duration = 1.0f; + +PyObject* PyTransition::create_enum_class(PyObject* module) { + // Import IntEnum from enum module + PyObject* enum_module = PyImport_ImportModule("enum"); + if (!enum_module) { + return NULL; + } + + PyObject* int_enum = PyObject_GetAttrString(enum_module, "IntEnum"); + Py_DECREF(enum_module); + if (!int_enum) { + return NULL; + } + + // Create dict of enum members + PyObject* members = PyDict_New(); + if (!members) { + Py_DECREF(int_enum); + return NULL; + } + + // Add all Transition type members + // Values match the C++ TransitionType enum + struct { + const char* name; + int value; + } transition_members[] = { + {"NONE", static_cast(TransitionType::None)}, + {"FADE", static_cast(TransitionType::Fade)}, + {"SLIDE_LEFT", static_cast(TransitionType::SlideLeft)}, + {"SLIDE_RIGHT", static_cast(TransitionType::SlideRight)}, + {"SLIDE_UP", static_cast(TransitionType::SlideUp)}, + {"SLIDE_DOWN", static_cast(TransitionType::SlideDown)}, + }; + + for (const auto& m : transition_members) { + PyObject* value = PyLong_FromLong(m.value); + if (!value) { + Py_DECREF(members); + Py_DECREF(int_enum); + return NULL; + } + if (PyDict_SetItemString(members, m.name, value) < 0) { + Py_DECREF(value); + Py_DECREF(members); + Py_DECREF(int_enum); + return NULL; + } + Py_DECREF(value); + } + + // Call IntEnum("Transition", members) to create the enum class + PyObject* name = PyUnicode_FromString("Transition"); + if (!name) { + Py_DECREF(members); + Py_DECREF(int_enum); + return NULL; + } + + // IntEnum(name, members) using functional API + PyObject* args = PyTuple_Pack(2, name, members); + Py_DECREF(name); + Py_DECREF(members); + if (!args) { + Py_DECREF(int_enum); + return NULL; + } + + PyObject* transition_class = PyObject_Call(int_enum, args, NULL); + Py_DECREF(args); + Py_DECREF(int_enum); + + if (!transition_class) { + return NULL; + } + + // Cache the reference for fast type checking + transition_enum_class = transition_class; + Py_INCREF(transition_enum_class); + + // Add to module + if (PyModule_AddObject(module, "Transition", transition_class) < 0) { + Py_DECREF(transition_class); + transition_enum_class = nullptr; + return NULL; + } + + return transition_class; +} + +int PyTransition::from_arg(PyObject* arg, TransitionType* out_type, bool* was_none) { + if (was_none) *was_none = false; + + // Accept None -> caller should use default + if (arg == Py_None || arg == NULL) { + if (was_none) *was_none = true; + *out_type = default_transition; + return 1; + } + + // Accept Transition enum member (check if it's an instance of our enum) + if (transition_enum_class && PyObject_IsInstance(arg, transition_enum_class)) { + // IntEnum members have a 'value' attribute + PyObject* value = PyObject_GetAttrString(arg, "value"); + if (!value) { + return 0; + } + long val = PyLong_AsLong(value); + Py_DECREF(value); + if (val == -1 && PyErr_Occurred()) { + return 0; + } + *out_type = static_cast(val); + return 1; + } + + // Accept int (for flexibility) + if (PyLong_Check(arg)) { + long val = PyLong_AsLong(arg); + if (val == -1 && PyErr_Occurred()) { + return 0; + } + if (val < 0 || val > static_cast(TransitionType::SlideDown)) { + PyErr_Format(PyExc_ValueError, + "Invalid Transition value: %ld. Must be 0-5 or use mcrfpy.Transition enum.", + val); + return 0; + } + *out_type = static_cast(val); + return 1; + } + + PyErr_SetString(PyExc_TypeError, + "transition must be mcrfpy.Transition enum member, int, or None"); + return 0; +} + +PyObject* PyTransition::to_python(TransitionType type) { + if (!transition_enum_class) { + PyErr_SetString(PyExc_RuntimeError, "Transition enum not initialized"); + return NULL; + } + + // Get the enum member by value + PyObject* value = PyLong_FromLong(static_cast(type)); + if (!value) return NULL; + + PyObject* result = PyObject_CallFunctionObjArgs(transition_enum_class, value, NULL); + Py_DECREF(value); + return result; +} diff --git a/src/PyTransition.h b/src/PyTransition.h new file mode 100644 index 0000000..2218305 --- /dev/null +++ b/src/PyTransition.h @@ -0,0 +1,29 @@ +#pragma once +#include "Common.h" +#include "Python.h" +#include "SceneTransition.h" + +// Module-level Transition enum class (created at runtime using Python's IntEnum) +// Stored as a module attribute: mcrfpy.Transition + +class PyTransition { +public: + // Create the Transition enum class and add to module + // Returns the enum class (new reference), or NULL on error + static PyObject* create_enum_class(PyObject* module); + + // Helper to extract transition type from Python arg (accepts Transition enum, int, or None) + // Returns 1 on success, 0 on error (with exception set) + // If arg is None, sets *out_type to the default and sets *was_none to true + static int from_arg(PyObject* arg, TransitionType* out_type, bool* was_none = nullptr); + + // Convert TransitionType to Python enum member + static PyObject* to_python(TransitionType type); + + // Cached reference to the Transition enum class for fast type checking + static PyObject* transition_enum_class; + + // Module-level defaults + static TransitionType default_transition; + static float default_duration; +}; diff --git a/tests/unit/test_scene_transitions.py b/tests/unit/test_scene_transitions.py index efd23c7..03dae42 100644 --- a/tests/unit/test_scene_transitions.py +++ b/tests/unit/test_scene_transitions.py @@ -63,7 +63,7 @@ def create_test_scenes(): print("Created test scenes: red_scene, blue_scene, green_scene, menu_scene") # Track current transition type -current_transition = "fade" +current_transition = mcrfpy.Transition.FADE transition_duration = 1.0 def handle_key(key, action): @@ -76,24 +76,35 @@ def handle_key(key, action): current_scene = (mcrfpy.current_scene.name if mcrfpy.current_scene else None) # Number keys set transition type - if key == "Num1": - current_transition = "fade" - print("Transition set to: fade") - elif key == "Num2": - current_transition = "slide_left" - print("Transition set to: slide_left") - elif key == "Num3": - current_transition = "slide_right" - print("Transition set to: slide_right") - elif key == "Num4": - current_transition = "slide_up" - print("Transition set to: slide_up") - elif key == "Num5": - current_transition = "slide_down" - print("Transition set to: slide_down") - elif key == "Num6": - current_transition = None # Instant - print("Transition set to: instant") + keyselections = { + "Num1": mcrfpy.Transition.FADE, + "Num2": mcrfpy.Transition.SLIDE_LEFT, + "Num3": mcrfpy.Transition.SLIDE_RIGHT, + "Num4": mcrfpy.Transition.SLIDE_UP, + "Num5": mcrfpy.Transition.SLIDE_DOWN, + "Num6": mcrfpy.Transition.NONE + } + if key in keyselections: + current_transition = keyselections[key] + print(f"Transition set to: {current_transition}") + #if key == "Num1": + # current_transition = "fade" + # print("Transition set to: fade") + #elif key == "Num2": + # current_transition = "slide_left" + # print("Transition set to: slide_left") + #elif key == "Num3": + # current_transition = "slide_right" + # print("Transition set to: slide_right") + #elif key == "Num4": + # current_transition = "slide_up" + # print("Transition set to: slide_up") + #elif key == "Num5": + # current_transition = "slide_down" + # print("Transition set to: slide_down") + #elif key == "Num6": + # current_transition = None # Instant + # print("Transition set to: instant") # Letter keys change scene keytransitions = { @@ -104,7 +115,7 @@ def handle_key(key, action): } if key in keytransitions: if mcrfpy.current_scene != keytransitions[key]: - keytransitions[key].activate() + keytransitions[key].activate(current_transition, transition_duration) #elif key == "R": # if current_scene != "red_scene": # print(f"Transitioning to red_scene with {current_transition}")