diff --git a/CMakeLists.txt b/CMakeLists.txt index 4ddd923..db78272 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -31,7 +31,7 @@ set(LINK_LIBS # On Windows, add any additional libs and include directories if(WIN32) # Windows-specific Python library name (no dots) - list(APPEND LINK_LIBS python314) + list(APPEND LINK_LIBS python312) # Add the necessary Windows-specific libraries and include directories # include_directories(path_to_additional_includes) # link_directories(path_to_additional_libs) @@ -39,7 +39,7 @@ if(WIN32) include_directories(${CMAKE_SOURCE_DIR}/deps/platform/windows) else() # Unix/Linux specific libraries - list(APPEND LINK_LIBS python3.14 m dl util pthread) + list(APPEND LINK_LIBS python3.12 m dl util pthread) include_directories(${CMAKE_SOURCE_DIR}/deps/platform/linux) endif() diff --git a/src/CommandLineParser.cpp b/src/CommandLineParser.cpp index 24be3e9..cad5398 100644 --- a/src/CommandLineParser.cpp +++ b/src/CommandLineParser.cpp @@ -176,5 +176,5 @@ void CommandLineParser::print_help() { } void CommandLineParser::print_version() { - std::cout << "Python 3.14.0 (McRogueFace embedded)\n"; + std::cout << "Python 3.12.0 (McRogueFace embedded)\n"; } \ No newline at end of file diff --git a/src/GameEngine.cpp b/src/GameEngine.cpp index f82cd99..392cda1 100644 --- a/src/GameEngine.cpp +++ b/src/GameEngine.cpp @@ -344,14 +344,9 @@ void GameEngine::testTimers() auto it = timers.begin(); while (it != timers.end()) { - // Keep a local copy of the timer to prevent use-after-free. - // If the callback calls delTimer(), the map entry gets replaced, - // but we need the Timer object to survive until test() returns. - auto timer = it->second; - timer->test(now); - - // Remove timers that have been cancelled or are one-shot and fired. - // Note: Check it->second (current map value) in case callback replaced it. + it->second->test(now); + + // Remove timers that have been cancelled or are one-shot and fired if (!it->second->getCallback() || it->second->getCallback() == Py_None) { it = timers.erase(it); diff --git a/src/GameEngine.h b/src/GameEngine.h index e212743..557f6de 100644 --- a/src/GameEngine.h +++ b/src/GameEngine.h @@ -155,7 +155,6 @@ public: void setWindowScale(float); bool isHeadless() const { return headless; } const McRogueFaceConfig& getConfig() const { return config; } - void setAutoExitAfterExec(bool enabled) { config.auto_exit_after_exec = enabled; } void processEvent(const sf::Event& event); // Window property accessors diff --git a/src/McRFPy_API.cpp b/src/McRFPy_API.cpp index 0fb8e43..b58df75 100644 --- a/src/McRFPy_API.cpp +++ b/src/McRFPy_API.cpp @@ -397,10 +397,10 @@ PyStatus init_python(const char *program_name) // search paths for python libs/modules/scripts const wchar_t* str_arr[] = { L"/scripts", - L"/lib/Python/lib.linux-x86_64-3.14", + L"/lib/Python/lib.linux-x86_64-3.12", L"/lib/Python", L"/lib/Python/Lib", - L"/venv/lib/python3.14/site-packages" + L"/venv/lib/python3.12/site-packages" }; @@ -419,107 +419,61 @@ PyStatus init_python(const char *program_name) return status; } -PyStatus McRFPy_API::init_python_with_config(const McRogueFaceConfig& config) +PyStatus McRFPy_API::init_python_with_config(const McRogueFaceConfig& config, int argc, char** argv) { // If Python is already initialized, just return success if (Py_IsInitialized()) { return PyStatus_Ok(); } - + PyStatus status; PyConfig pyconfig; PyConfig_InitIsolatedConfig(&pyconfig); - + // Configure UTF-8 for stdio PyConfig_SetString(&pyconfig, &pyconfig.stdio_encoding, L"UTF-8"); PyConfig_SetString(&pyconfig, &pyconfig.stdio_errors, L"surrogateescape"); pyconfig.configure_c_stdio = 1; - - // Set interactive mode (replaces deprecated Py_InspectFlag) - if (config.interactive_mode) { - pyconfig.inspect = 1; - } - - // Don't modify sys.path based on script location (replaces PySys_SetArgvEx updatepath=0) - pyconfig.safe_path = 1; - - // Construct Python argv from config (replaces deprecated PySys_SetArgvEx) - // Python convention: - // - Script mode: argv[0] = script_path, argv[1:] = script_args - // - -c mode: argv[0] = "-c" - // - -m mode: argv[0] = module_name, argv[1:] = script_args - // - Interactive only: argv[0] = "" - std::vector argv_storage; - - if (!config.script_path.empty()) { - // Script execution: argv[0] = script path - argv_storage.push_back(config.script_path.wstring()); - for (const auto& arg : config.script_args) { - std::wstring warg(arg.begin(), arg.end()); - argv_storage.push_back(warg); - } - } else if (!config.python_command.empty()) { - // -c command: argv[0] = "-c" - argv_storage.push_back(L"-c"); - } else if (!config.python_module.empty()) { - // -m module: argv[0] = module name - std::wstring wmodule(config.python_module.begin(), config.python_module.end()); - argv_storage.push_back(wmodule); - for (const auto& arg : config.script_args) { - std::wstring warg(arg.begin(), arg.end()); - argv_storage.push_back(warg); - } - } else { - // Interactive mode or no script: argv[0] = "" - argv_storage.push_back(L""); - } - - // Build wchar_t* array for PyConfig - std::vector argv_ptrs; - for (auto& ws : argv_storage) { - argv_ptrs.push_back(const_cast(ws.c_str())); - } - - status = PyConfig_SetWideStringList(&pyconfig, &pyconfig.argv, - argv_ptrs.size(), argv_ptrs.data()); + + // CRITICAL: Pass actual command line arguments to Python + status = PyConfig_SetBytesArgv(&pyconfig, argc, argv); if (PyStatus_Exception(status)) { return status; } - + // Check if we're in a virtual environment - auto exe_wpath = executable_filename(); - auto exe_path_fs = std::filesystem::path(exe_wpath); - auto exe_dir = exe_path_fs.parent_path(); + auto exe_path = std::filesystem::path(argv[0]); + auto exe_dir = exe_path.parent_path(); auto venv_root = exe_dir.parent_path(); - + if (std::filesystem::exists(venv_root / "pyvenv.cfg")) { // We're running from within a venv! // Add venv's site-packages to module search paths - auto site_packages = venv_root / "lib" / "python3.14" / "site-packages"; + auto site_packages = venv_root / "lib" / "python3.12" / "site-packages"; PyWideStringList_Append(&pyconfig.module_search_paths, site_packages.wstring().c_str()); pyconfig.module_search_paths_set = 1; } - + // Set Python home to our bundled Python auto python_home = executable_path() + L"/lib/Python"; PyConfig_SetString(&pyconfig, &pyconfig.home, python_home.c_str()); - + // Set up module search paths #if __PLATFORM_SET_PYTHON_SEARCH_PATHS == 1 if (!pyconfig.module_search_paths_set) { pyconfig.module_search_paths_set = 1; } - + // search paths for python libs/modules/scripts const wchar_t* str_arr[] = { L"/scripts", - L"/lib/Python/lib.linux-x86_64-3.14", + L"/lib/Python/lib.linux-x86_64-3.12", L"/lib/Python", L"/lib/Python/Lib", - L"/venv/lib/python3.14/site-packages" + L"/venv/lib/python3.12/site-packages" }; - + for(auto s : str_arr) { status = PyWideStringList_Append(&pyconfig.module_search_paths, (executable_path() + s).c_str()); if (PyStatus_Exception(status)) { @@ -527,13 +481,15 @@ PyStatus McRFPy_API::init_python_with_config(const McRogueFaceConfig& config) } } #endif - + // Register mcrfpy module before initialization - PyImport_AppendInittab("mcrfpy", &PyInit_mcrfpy); - + if (!Py_IsInitialized()) { + PyImport_AppendInittab("mcrfpy", &PyInit_mcrfpy); + } + status = Py_InitializeFromConfig(&pyconfig); PyConfig_Clear(&pyconfig); - + return status; } @@ -579,9 +535,9 @@ void McRFPy_API::api_init() { //setSpriteTexture(0); } -void McRFPy_API::api_init(const McRogueFaceConfig& config) { - // Initialize Python with proper argv constructed from config - PyStatus status = init_python_with_config(config); +void McRFPy_API::api_init(const McRogueFaceConfig& config, int argc, char** argv) { + // Initialize Python with proper argv - this is CRITICAL + PyStatus status = init_python_with_config(config, argc, argv); if (PyStatus_Exception(status)) { Py_ExitStatusException(status); } diff --git a/src/McRFPy_API.h b/src/McRFPy_API.h index 81ba540..6841fd2 100644 --- a/src/McRFPy_API.h +++ b/src/McRFPy_API.h @@ -29,8 +29,8 @@ public: //static void setSpriteTexture(int); inline static GameEngine* game; static void api_init(); - static void api_init(const McRogueFaceConfig& config); - static PyStatus init_python_with_config(const McRogueFaceConfig& config); + static void api_init(const McRogueFaceConfig& config, int argc, char** argv); + static PyStatus init_python_with_config(const McRogueFaceConfig& config, int argc, char** argv); static void api_shutdown(); // Python API functionality - use mcrfpy.* in scripts //static PyObject* _drawSprite(PyObject*, PyObject*); diff --git a/src/PyColor.cpp b/src/PyColor.cpp index ef6ca2b..4fd2154 100644 --- a/src/PyColor.cpp +++ b/src/PyColor.cpp @@ -236,7 +236,6 @@ PyColorObject* PyColor::from_arg(PyObject* args) // Check if args is already a Color instance if (PyObject_IsInstance(args, (PyObject*)type.get())) { - Py_INCREF(args); // Return new reference so caller can safely DECREF return (PyColorObject*)args; } diff --git a/src/PyVector.cpp b/src/PyVector.cpp index 1625106..c8e92c6 100644 --- a/src/PyVector.cpp +++ b/src/PyVector.cpp @@ -1,7 +1,6 @@ #include "PyVector.h" #include "PyObjectUtils.h" #include "McRFPy_Doc.h" -#include "PyRAII.h" #include PyGetSetDef PyVector::getsetters[] = { @@ -262,46 +261,35 @@ int PyVector::set_member(PyObject* obj, PyObject* value, void* closure) PyVectorObject* PyVector::from_arg(PyObject* args) { - // Use RAII for type reference management - PyRAII::PyTypeRef type("Vector", McRFPy_API::mcrf_module); - if (!type) { - return NULL; - } - - // Check if args is already a Vector instance - if (PyObject_IsInstance(args, (PyObject*)type.get())) { - Py_INCREF(args); // Return new reference so caller can safely DECREF - return (PyVectorObject*)args; - } - - // Create new Vector object using RAII - PyRAII::PyObjectRef obj(type->tp_alloc(type.get(), 0), true); - if (!obj) { - return NULL; - } - + auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Vector"); + if (PyObject_IsInstance(args, (PyObject*)type)) return (PyVectorObject*)args; + + auto obj = (PyVectorObject*)type->tp_alloc(type, 0); + // Handle different input types if (PyTuple_Check(args)) { // It's already a tuple, pass it directly to init - int err = init((PyVectorObject*)obj.get(), args, NULL); + int err = init(obj, args, NULL); if (err) { - // obj will be automatically cleaned up when it goes out of scope + Py_DECREF(obj); return NULL; } } else { // Wrap single argument in a tuple for init - PyRAII::PyObjectRef tuple(PyTuple_Pack(1, args), true); + PyObject* tuple = PyTuple_Pack(1, args); if (!tuple) { + Py_DECREF(obj); return NULL; } - int err = init((PyVectorObject*)obj.get(), tuple.get(), NULL); + int err = init(obj, tuple, NULL); + Py_DECREF(tuple); if (err) { + Py_DECREF(obj); return NULL; } } - - // Release ownership and return - return (PyVectorObject*)obj.release(); + + return obj; } // Arithmetic operations diff --git a/src/main.cpp b/src/main.cpp index b0e9599..7ec4e14 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -11,27 +11,27 @@ // Forward declarations int run_game_engine(const McRogueFaceConfig& config); -int run_python_interpreter(const McRogueFaceConfig& config); +int run_python_interpreter(const McRogueFaceConfig& config, int argc, char* argv[]); int main(int argc, char* argv[]) { McRogueFaceConfig config; CommandLineParser parser(argc, argv); - + // Parse arguments auto parse_result = parser.parse(config); if (parse_result.should_exit) { return parse_result.exit_code; } - + // Special handling for -m module: let Python handle modules properly if (!config.python_module.empty()) { config.python_mode = true; } - + // Initialize based on configuration if (config.python_mode) { - return run_python_interpreter(config); + return run_python_interpreter(config, argc, argv); } else { return run_game_engine(config); } @@ -52,13 +52,13 @@ int run_game_engine(const McRogueFaceConfig& config) return 0; } -int run_python_interpreter(const McRogueFaceConfig& config) +int run_python_interpreter(const McRogueFaceConfig& config, int argc, char* argv[]) { // Create a game engine with the requested configuration GameEngine* engine = new GameEngine(config); - - // Initialize Python with configuration (argv is constructed from config) - McRFPy_API::init_python_with_config(config); + + // Initialize Python with configuration + McRFPy_API::init_python_with_config(config, argc, argv); // Import mcrfpy module and store reference McRFPy_API::mcrf_module = PyImport_ImportModule("mcrfpy"); @@ -116,28 +116,49 @@ int run_python_interpreter(const McRogueFaceConfig& config) } } else if (!config.python_module.empty()) { - // Execute module using runpy (sys.argv already set at init time) - std::string run_module_code = + // Execute module using runpy + std::string run_module_code = + "import sys\n" "import runpy\n" - "runpy.run_module('" + config.python_module + "', run_name='__main__', alter_sys=True)\n"; - + "sys.argv = ['" + config.python_module + "'"; + + for (const auto& arg : config.script_args) { + run_module_code += ", '" + arg + "'"; + } + run_module_code += "]\n"; + run_module_code += "runpy.run_module('" + config.python_module + "', run_name='__main__', alter_sys=True)\n"; + int result = PyRun_SimpleString(run_module_code.c_str()); McRFPy_API::api_shutdown(); delete engine; return result; } else if (!config.script_path.empty()) { - // Execute script file (sys.argv already set at init time) + // Execute script file FILE* fp = fopen(config.script_path.string().c_str(), "r"); if (!fp) { std::cerr << "mcrogueface: can't open file '" << config.script_path << "': "; std::cerr << "[Errno " << errno << "] " << strerror(errno) << std::endl; return 1; } - + + // Set up sys.argv + wchar_t** python_argv = new wchar_t*[config.script_args.size() + 1]; + python_argv[0] = Py_DecodeLocale(config.script_path.string().c_str(), nullptr); + for (size_t i = 0; i < config.script_args.size(); i++) { + python_argv[i + 1] = Py_DecodeLocale(config.script_args[i].c_str(), nullptr); + } + PySys_SetArgvEx(config.script_args.size() + 1, python_argv, 0); + int result = PyRun_SimpleFile(fp, config.script_path.string().c_str()); fclose(fp); + // Clean up + for (size_t i = 0; i <= config.script_args.size(); i++) { + PyMem_RawFree(python_argv[i]); + } + delete[] python_argv; + if (config.interactive_mode) { // Even if script had SystemExit, continue to interactive mode if (result != 0) { @@ -176,18 +197,23 @@ int run_python_interpreter(const McRogueFaceConfig& config) } else if (config.interactive_mode) { // Interactive Python interpreter (only if explicitly requested with -i) - // Note: pyconfig.inspect is set at init time based on config.interactive_mode + Py_InspectFlag = 1; PyRun_InteractiveLoop(stdin, ""); McRFPy_API::api_shutdown(); delete engine; return 0; } else if (!config.exec_scripts.empty()) { - // Execute startup scripts on the existing engine (not in constructor to prevent double-execution) - engine->executeStartupScripts(); - if (config.headless) { - engine->setAutoExitAfterExec(true); + // With --exec, run the game engine after scripts execute + // In headless mode, auto-exit when no timers remain + McRogueFaceConfig mutable_config = config; + if (mutable_config.headless) { + mutable_config.auto_exit_after_exec = true; } + delete engine; + engine = new GameEngine(mutable_config); + McRFPy_API::game = engine; + engine->executeStartupScripts(); // Execute --exec scripts ONCE here engine->run(); McRFPy_API::api_shutdown(); delete engine; diff --git a/tests/unit/test_frame_clipping.py b/tests/unit/test_frame_clipping.py index 5917aca..ba77152 100644 --- a/tests/unit/test_frame_clipping.py +++ b/tests/unit/test_frame_clipping.py @@ -3,43 +3,20 @@ import mcrfpy from mcrfpy import Color, Frame, Caption +from mcrfpy import automation import sys -# Module-level state to avoid closures -_test_state = {} - -def take_second_screenshot(runtime): - """Take final screenshot and exit""" - mcrfpy.delTimer("screenshot2") - from mcrfpy import automation - automation.screenshot("frame_clipping_animated.png") - print("\nTest completed successfully!") - print("Screenshots saved:") - print(" - frame_clipping_test.png (initial state)") - print(" - frame_clipping_animated.png (with animation)") - sys.exit(0) - -def animate_frames(runtime): - """Animate frames to demonstrate clipping""" - mcrfpy.delTimer("animate") - scene = mcrfpy.sceneUI("test") - # Move child frames - parent1 = scene[0] - parent2 = scene[1] - parent1.children[1].x = 50 - parent2.children[1].x = 50 - mcrfpy.setTimer("screenshot2", take_second_screenshot, 500) - def test_clipping(runtime): """Test that clip_children property works correctly""" mcrfpy.delTimer("test_clipping") print("Testing UIFrame clipping functionality...") + # Create test scene scene = mcrfpy.sceneUI("test") # Create parent frame with clipping disabled (default) - parent1 = Frame(pos=(50, 50), size=(200, 150), + parent1 = Frame(x=50, y=50, w=200, h=150, fill_color=Color(100, 100, 200), outline_color=Color(255, 255, 255), outline=2) @@ -47,7 +24,7 @@ def test_clipping(runtime): scene.append(parent1) # Create parent frame with clipping enabled - parent2 = Frame(pos=(300, 50), size=(200, 150), + parent2 = Frame(x=300, y=50, w=200, h=150, fill_color=Color(200, 100, 100), outline_color=Color(255, 255, 255), outline=2) @@ -56,48 +33,30 @@ def test_clipping(runtime): scene.append(parent2) # Add captions to both frames - caption1 = Caption(text="This text should overflow the frame bounds", pos=(10, 10)) + caption1 = Caption(text="This text should overflow", x=10, y=10) caption1.font_size = 16 caption1.fill_color = Color(255, 255, 255) parent1.children.append(caption1) - caption2 = Caption(text="This text should be clipped to frame bounds", pos=(10, 10)) + caption2 = Caption(text="This text should be clipped", x=10, y=10) caption2.font_size = 16 caption2.fill_color = Color(255, 255, 255) parent2.children.append(caption2) # Add child frames that extend beyond parent bounds - child1 = Frame(pos=(150, 100), size=(100, 100), + child1 = Frame(x=150, y=100, w=100, h=100, fill_color=Color(50, 255, 50), outline_color=Color(0, 0, 0), outline=1) parent1.children.append(child1) - child2 = Frame(pos=(150, 100), size=(100, 100), + child2 = Frame(x=150, y=100, w=100, h=100, fill_color=Color(50, 255, 50), outline_color=Color(0, 0, 0), outline=1) parent2.children.append(child2) - # Add caption to show clip state - status = Caption(text=f"Left frame: clip_children={parent1.clip_children}\n" - f"Right frame: clip_children={parent2.clip_children}", - pos=(50, 250)) - status.font_size = 14 - status.fill_color = Color(255, 255, 255) - scene.append(status) - - # Add instructions - instructions = Caption(text="Left: Children should overflow (no clipping)\n" - "Right: Children should be clipped to frame bounds\n" - "Press 'c' to toggle clipping on left frame", - pos=(50, 300)) - instructions.font_size = 12 - instructions.fill_color = Color(200, 200, 200) - scene.append(instructions) - # Take screenshot - from mcrfpy import automation automation.screenshot("frame_clipping_test.png") print(f"Parent1 clip_children: {parent1.clip_children}") @@ -109,25 +68,21 @@ def test_clipping(runtime): # Verify the property setter works try: - parent1.clip_children = "not a bool" + parent1.clip_children = "not a bool" # Should raise TypeError print("ERROR: clip_children accepted non-boolean value") + sys.exit(1) except TypeError as e: print(f"PASS: clip_children correctly rejected non-boolean: {e}") - # Start animation after a short delay - mcrfpy.setTimer("animate", animate_frames, 100) - -def handle_keypress(key, modifiers): - if key == "c": - scene = mcrfpy.sceneUI("test") - parent1 = scene[0] - parent1.clip_children = not parent1.clip_children - print(f"Toggled parent1 clip_children to: {parent1.clip_children}") + print("\nTest completed successfully!") + sys.exit(0) # Main execution print("Creating test scene...") mcrfpy.createScene("test") mcrfpy.setScene("test") -mcrfpy.keypressScene(handle_keypress) + +# Schedule the test mcrfpy.setTimer("test_clipping", test_clipping, 100) + print("Test scheduled, running...") diff --git a/tests/unit/test_frame_clipping_advanced.py b/tests/unit/test_frame_clipping_advanced.py index b7e9a33..b4d89d3 100644 --- a/tests/unit/test_frame_clipping_advanced.py +++ b/tests/unit/test_frame_clipping_advanced.py @@ -15,7 +15,7 @@ def test_nested_clipping(runtime): scene = mcrfpy.sceneUI("test") # Create outer frame with clipping enabled - outer = Frame(pos=(50, 50), size=(400, 300), + outer = Frame(x=50, y=50, w=400, h=300, fill_color=Color(50, 50, 150), outline_color=Color(255, 255, 255), outline=3) @@ -24,35 +24,35 @@ def test_nested_clipping(runtime): scene.append(outer) # Create inner frame that extends beyond outer bounds - inner = Frame(pos=(200, 150), size=(300, 200), + inner = Frame(x=200, y=150, w=300, h=200, fill_color=Color(150, 50, 50), outline_color=Color(255, 255, 0), outline=2) inner.name = "inner" inner.clip_children = True # Also enable clipping on inner frame outer.children.append(inner) - + # Add content to inner frame that extends beyond its bounds for i in range(5): - caption = Caption(text=f"Line {i+1}: This text should be double-clipped", pos=(10, 30 * i)) + caption = Caption(text=f"Line {i+1}: This text should be double-clipped", x=10, y=30 * i) caption.font_size = 14 caption.fill_color = Color(255, 255, 255) inner.children.append(caption) - + # Add a child frame to inner that extends way out - deeply_nested = Frame(pos=(250, 100), size=(200, 150), + deeply_nested = Frame(x=250, y=100, w=200, h=150, fill_color=Color(50, 150, 50), outline_color=Color(255, 0, 255), outline=2) deeply_nested.name = "deeply_nested" inner.children.append(deeply_nested) - + # Add status text status = Caption(text="Nested clipping test:\n" - "- Blue outer frame clips red inner frame\n" - "- Red inner frame clips green deeply nested frame\n" - "- All text should be clipped to frame bounds", - pos=(50, 380)) + "- Blue outer frame clips red inner frame\n" + "- Red inner frame clips green deeply nested frame\n" + "- All text should be clipped to frame bounds", + x=50, y=380) status.font_size = 12 status.fill_color = Color(200, 200, 200) scene.append(status) diff --git a/tests/unit/test_timer_object.py b/tests/unit/test_timer_object.py index dfc3b88..9929dfd 100644 --- a/tests/unit/test_timer_object.py +++ b/tests/unit/test_timer_object.py @@ -11,16 +11,19 @@ pause_test_count = 0 cancel_test_count = 0 def timer_callback(timer, runtime): + """Timer object callbacks receive (timer, runtime)""" global call_count call_count += 1 print(f"Timer fired! Count: {call_count}, Runtime: {runtime}ms") def pause_test_callback(timer, runtime): + """Timer object callbacks receive (timer, runtime)""" global pause_test_count pause_test_count += 1 print(f"Pause test timer: {pause_test_count}") def cancel_test_callback(timer, runtime): + """Timer object callbacks receive (timer, runtime)""" global cancel_test_count cancel_test_count += 1 print(f"Cancel test timer: {cancel_test_count} - This should only print once!") @@ -46,6 +49,7 @@ def run_tests(runtime): # Schedule pause after 250ms def pause_timer2(runtime): + mcrfpy.delTimer("pause_timer2") # Prevent re-entry print(" Pausing timer2...") timer2.pause() print(f" Timer2 paused: {timer2.paused}") @@ -53,13 +57,14 @@ def run_tests(runtime): # Schedule resume after another 400ms def resume_timer2(runtime): + mcrfpy.delTimer("resume_timer2") # Prevent re-entry print(" Resuming timer2...") timer2.resume() print(f" Timer2 paused: {timer2.paused}") print(f" Timer2 active: {timer2.active}") mcrfpy.setTimer("resume_timer2", resume_timer2, 400) - + mcrfpy.setTimer("pause_timer2", pause_timer2, 250) # Test 3: Test cancel @@ -68,43 +73,47 @@ def run_tests(runtime): # Cancel after 350ms (should fire once) def cancel_timer3(runtime): + mcrfpy.delTimer("cancel_timer3") # Prevent re-entry print(" Canceling timer3...") timer3.cancel() print(" Timer3 canceled") - + mcrfpy.setTimer("cancel_timer3", cancel_timer3, 350) # Test 4: Test interval modification print("\nTest 4: Testing interval modification") def interval_test(timer, runtime): print(f" Interval test fired at {runtime}ms") - + timer4 = mcrfpy.Timer("interval_test", interval_test, 1000) print(f" Original interval: {timer4.interval}ms") timer4.interval = 500 print(f" Modified interval: {timer4.interval}ms") - # Test 5: Test remaining time + # Test 5: Test remaining time (periodic check - no delTimer, runs multiple times) print("\nTest 5: Testing remaining time") def check_remaining(runtime): - if timer1.active: - print(f" Timer1 remaining: {timer1.remaining}ms") - if timer2.active or timer2.paused: - print(f" Timer2 remaining: {timer2.remaining}ms (paused: {timer2.paused})") - + try: + if timer1.active: + print(f" Timer1 remaining: {timer1.remaining}ms") + if timer2.active or timer2.paused: + print(f" Timer2 remaining: {timer2.remaining}ms (paused: {timer2.paused})") + except RuntimeError: + pass # Timer may have been cancelled + mcrfpy.setTimer("check_remaining", check_remaining, 150) # Test 6: Test restart print("\nTest 6: Testing restart functionality") restart_count = [0] - + def restart_test(timer, runtime): restart_count[0] += 1 print(f" Restart test: {restart_count[0]}") if restart_count[0] == 2: print(" Restarting timer...") timer.restart() - + timer5 = mcrfpy.Timer("restart_test", restart_test, 400) # Final verification after 2 seconds diff --git a/tests/unit/test_viewport_scaling.py b/tests/unit/test_viewport_scaling.py index 148416c..f7a7e3a 100644 --- a/tests/unit/test_viewport_scaling.py +++ b/tests/unit/test_viewport_scaling.py @@ -2,236 +2,74 @@ """Test viewport scaling modes""" import mcrfpy -from mcrfpy import Window, Frame, Caption, Color, Vector +from mcrfpy import Window, Frame, Caption, Color +from mcrfpy import automation import sys def test_viewport_modes(runtime): """Test all three viewport scaling modes""" mcrfpy.delTimer("test_viewport") - + print("Testing viewport scaling modes...") - + # Get window singleton window = Window.get() - + # Test initial state print(f"Initial game resolution: {window.game_resolution}") print(f"Initial scaling mode: {window.scaling_mode}") print(f"Window resolution: {window.resolution}") - - # Create test scene with visual elements + + # Get scene scene = mcrfpy.sceneUI("test") - - # Create a frame that fills the game resolution to show boundaries + + # Create a simple frame to show boundaries game_res = window.game_resolution - boundary = Frame(pos=(0, 0), size=(game_res[0], game_res[1]), + boundary = Frame(x=0, y=0, w=game_res[0], h=game_res[1], fill_color=Color(50, 50, 100), outline_color=Color(255, 255, 255), outline=2) - boundary.name = "boundary" scene.append(boundary) - - # Add corner markers - corner_size = 50 - corners = [ - (0, 0, "TL"), # Top-left - (game_res[0] - corner_size, 0, "TR"), # Top-right - (0, game_res[1] - corner_size, "BL"), # Bottom-left - (game_res[0] - corner_size, game_res[1] - corner_size, "BR") # Bottom-right - ] - - for x, y, label in corners: - corner = Frame(pos=(x, y), size=(corner_size, corner_size), - fill_color=Color(255, 100, 100), - outline_color=Color(255, 255, 255), - outline=1) - scene.append(corner) - - text = Caption(text=label, pos=(x + 5, y + 5)) - text.font_size = 20 - text.fill_color = Color(255, 255, 255) - scene.append(text) - - # Add center crosshair - center_x = game_res[0] // 2 - center_y = game_res[1] // 2 - h_line = Frame(pos=(center_x - 50, center_y - 1), size=(100, 2), - fill_color=Color(255, 255, 0)) - v_line = Frame(pos=(center_x - 1, center_y - 50), size=(2, 100), - fill_color=Color(255, 255, 0)) - scene.append(h_line) - scene.append(v_line) - + # Add mode indicator - mode_text = Caption(text=f"Mode: {window.scaling_mode}", pos=(10, 10)) + mode_text = Caption(text=f"Mode: {window.scaling_mode}", x=10, y=10) mode_text.font_size = 24 mode_text.fill_color = Color(255, 255, 255) - mode_text.name = "mode_text" scene.append(mode_text) - # Add instructions - instructions = Caption(text="Press 1: Center mode (1:1 pixels)\n" - "Press 2: Stretch mode (fill window)\n" - "Press 3: Fit mode (maintain aspect ratio)\n" - "Press R: Change resolution\n" - "Press G: Change game resolution\n" - "Press Esc: Exit", - pos=(10, 40)) - instructions.font_size = 14 - instructions.fill_color = Color(200, 200, 200) - scene.append(instructions) - # Test changing modes - def test_mode_changes(runtime): - mcrfpy.delTimer("test_modes") - from mcrfpy import automation - - print("\nTesting scaling modes:") - - # Test center mode - window.scaling_mode = "center" - print(f"Set to center mode: {window.scaling_mode}") - mode_text.text = f"Mode: center (1:1 pixels)" - automation.screenshot("viewport_center_mode.png") - - # Schedule next mode test - mcrfpy.setTimer("test_stretch", test_stretch_mode, 1000) - - def test_stretch_mode(runtime): - mcrfpy.delTimer("test_stretch") - from mcrfpy import automation - - window.scaling_mode = "stretch" - print(f"Set to stretch mode: {window.scaling_mode}") - mode_text.text = f"Mode: stretch (fill window)" - automation.screenshot("viewport_stretch_mode.png") - - # Schedule next mode test - mcrfpy.setTimer("test_fit", test_fit_mode, 1000) - - def test_fit_mode(runtime): - mcrfpy.delTimer("test_fit") - from mcrfpy import automation - - window.scaling_mode = "fit" - print(f"Set to fit mode: {window.scaling_mode}") - mode_text.text = f"Mode: fit (aspect ratio maintained)" - automation.screenshot("viewport_fit_mode.png") - - # Test different window sizes - mcrfpy.setTimer("test_resize", test_window_resize, 1000) - - def test_window_resize(runtime): - mcrfpy.delTimer("test_resize") - from mcrfpy import automation - - print("\nTesting window resize with fit mode:") - - # Make window wider - window.resolution = (1280, 720) - print(f"Window resized to: {window.resolution}") - automation.screenshot("viewport_fit_wide.png") - - # Make window taller - mcrfpy.setTimer("test_tall", test_tall_window, 1000) - - def test_tall_window(runtime): - mcrfpy.delTimer("test_tall") - from mcrfpy import automation - - window.resolution = (800, 1000) - print(f"Window resized to: {window.resolution}") - automation.screenshot("viewport_fit_tall.png") - - # Test game resolution change - mcrfpy.setTimer("test_game_res", test_game_resolution, 1000) - - def test_game_resolution(runtime): - mcrfpy.delTimer("test_game_res") - - print("\nTesting game resolution change:") - window.game_resolution = (800, 600) - print(f"Game resolution changed to: {window.game_resolution}") - - # Note: UI elements won't automatically reposition, but viewport will adjust - - print("\nTest completed!") - print("Screenshots saved:") - print(" - viewport_center_mode.png") - print(" - viewport_stretch_mode.png") - print(" - viewport_fit_mode.png") - print(" - viewport_fit_wide.png") - print(" - viewport_fit_tall.png") - - # Restore original settings - window.resolution = (1024, 768) - window.game_resolution = (1024, 768) - window.scaling_mode = "fit" - - sys.exit(0) - - # Start test sequence - mcrfpy.setTimer("test_modes", test_mode_changes, 500) + print("\nTesting scaling modes:") -# Set up keyboard handler for manual testing -def handle_keypress(key, state): - if state != "start": - return - - window = Window.get() - scene = mcrfpy.sceneUI("test") - mode_text = None - for elem in scene: - if hasattr(elem, 'name') and elem.name == "mode_text": - mode_text = elem - break - - if key == "1": - window.scaling_mode = "center" - if mode_text: - mode_text.text = f"Mode: center (1:1 pixels)" - print(f"Switched to center mode") - elif key == "2": - window.scaling_mode = "stretch" - if mode_text: - mode_text.text = f"Mode: stretch (fill window)" - print(f"Switched to stretch mode") - elif key == "3": - window.scaling_mode = "fit" - if mode_text: - mode_text.text = f"Mode: fit (aspect ratio maintained)" - print(f"Switched to fit mode") - elif key == "r": - # Cycle through some resolutions - current = window.resolution - if current == (1024, 768): - window.resolution = (1280, 720) - elif current == (1280, 720): - window.resolution = (800, 600) - else: - window.resolution = (1024, 768) - print(f"Window resolution: {window.resolution}") - elif key == "g": - # Cycle game resolutions - current = window.game_resolution - if current == (1024, 768): - window.game_resolution = (800, 600) - elif current == (800, 600): - window.game_resolution = (640, 480) - else: - window.game_resolution = (1024, 768) - print(f"Game resolution: {window.game_resolution}") - elif key == "escape": - sys.exit(0) + # Test center mode + window.scaling_mode = "center" + print(f"Set to center mode: {window.scaling_mode}") + mode_text.text = f"Mode: center" + automation.screenshot("viewport_center_mode.png") + + # Test stretch mode + window.scaling_mode = "stretch" + print(f"Set to stretch mode: {window.scaling_mode}") + mode_text.text = f"Mode: stretch" + automation.screenshot("viewport_stretch_mode.png") + + # Test fit mode + window.scaling_mode = "fit" + print(f"Set to fit mode: {window.scaling_mode}") + mode_text.text = f"Mode: fit" + automation.screenshot("viewport_fit_mode.png") + + # Note: Cannot change window resolution in headless mode + # Just verify the scaling mode properties work + print("\nScaling mode property tests passed!") + print("\nTest completed!") + sys.exit(0) # Main execution print("Creating viewport test scene...") mcrfpy.createScene("test") mcrfpy.setScene("test") -mcrfpy.keypressScene(handle_keypress) # Schedule the test mcrfpy.setTimer("test_viewport", test_viewport_modes, 100) print("Viewport test running...") -print("Use number keys to switch modes, R to resize window, G to change game resolution") \ No newline at end of file