feat: Migrate to Python 3.14 (closes #135)
Replace deprecated Python C API calls with modern PyConfig-based initialization: - PySys_SetArgvEx() -> PyConfig.argv (deprecated since 3.11) - Py_InspectFlag -> PyConfig.inspect (deprecated since 3.12) Fix critical memory safety bugs discovered during migration: - PyColor::from_arg() and PyVector::from_arg() now return new references instead of borrowed references, preventing use-after-free when callers call Py_DECREF on the result - GameEngine::testTimers() now holds a local shared_ptr copy during callback execution, preventing use-after-free when timer callbacks call delTimer() on themselves Fix double script execution bug with --exec flag: - Scripts were running twice because GameEngine constructor executed them, then main.cpp deleted and recreated the engine - Now reuses existing engine and just sets auto_exit_after_exec flag Update test syntax to use keyword arguments for Frame/Caption constructors. Test results: 127/130 passing (97.7%) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
b173f59f22
commit
28396b65c9
|
|
@ -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 python312)
|
||||
list(APPEND LINK_LIBS python314)
|
||||
# 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.12 m dl util pthread)
|
||||
list(APPEND LINK_LIBS python3.14 m dl util pthread)
|
||||
include_directories(${CMAKE_SOURCE_DIR}/deps/platform/linux)
|
||||
endif()
|
||||
|
||||
|
|
|
|||
|
|
@ -176,5 +176,5 @@ void CommandLineParser::print_help() {
|
|||
}
|
||||
|
||||
void CommandLineParser::print_version() {
|
||||
std::cout << "Python 3.12.0 (McRogueFace embedded)\n";
|
||||
std::cout << "Python 3.14.0 (McRogueFace embedded)\n";
|
||||
}
|
||||
|
|
@ -336,9 +336,14 @@ void GameEngine::testTimers()
|
|||
auto it = timers.begin();
|
||||
while (it != timers.end())
|
||||
{
|
||||
it->second->test(now);
|
||||
// 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
|
||||
// Remove timers that have been cancelled or are one-shot and fired.
|
||||
// Note: Check it->second (current map value) in case callback replaced it.
|
||||
if (!it->second->getCallback() || it->second->getCallback() == Py_None)
|
||||
{
|
||||
it = timers.erase(it);
|
||||
|
|
|
|||
|
|
@ -154,6 +154,7 @@ 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
|
||||
|
|
|
|||
|
|
@ -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.12",
|
||||
L"/lib/Python/lib.linux-x86_64-3.14",
|
||||
L"/lib/Python",
|
||||
L"/lib/Python/Lib",
|
||||
L"/venv/lib/python3.12/site-packages"
|
||||
L"/venv/lib/python3.14/site-packages"
|
||||
};
|
||||
|
||||
|
||||
|
|
@ -419,7 +419,7 @@ PyStatus init_python(const char *program_name)
|
|||
return status;
|
||||
}
|
||||
|
||||
PyStatus McRFPy_API::init_python_with_config(const McRogueFaceConfig& config, int argc, char** argv)
|
||||
PyStatus McRFPy_API::init_python_with_config(const McRogueFaceConfig& config)
|
||||
{
|
||||
// If Python is already initialized, just return success
|
||||
if (Py_IsInitialized()) {
|
||||
|
|
@ -435,21 +435,67 @@ PyStatus McRFPy_API::init_python_with_config(const McRogueFaceConfig& config, in
|
|||
PyConfig_SetString(&pyconfig, &pyconfig.stdio_errors, L"surrogateescape");
|
||||
pyconfig.configure_c_stdio = 1;
|
||||
|
||||
// CRITICAL: Pass actual command line arguments to Python
|
||||
status = PyConfig_SetBytesArgv(&pyconfig, argc, argv);
|
||||
// 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<std::wstring> 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<wchar_t*> argv_ptrs;
|
||||
for (auto& ws : argv_storage) {
|
||||
argv_ptrs.push_back(const_cast<wchar_t*>(ws.c_str()));
|
||||
}
|
||||
|
||||
status = PyConfig_SetWideStringList(&pyconfig, &pyconfig.argv,
|
||||
argv_ptrs.size(), argv_ptrs.data());
|
||||
if (PyStatus_Exception(status)) {
|
||||
return status;
|
||||
}
|
||||
|
||||
// Check if we're in a virtual environment
|
||||
auto exe_path = std::filesystem::path(argv[0]);
|
||||
auto exe_dir = exe_path.parent_path();
|
||||
auto exe_wpath = executable_filename();
|
||||
auto exe_path_fs = std::filesystem::path(exe_wpath);
|
||||
auto exe_dir = exe_path_fs.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.12" / "site-packages";
|
||||
auto site_packages = venv_root / "lib" / "python3.14" / "site-packages";
|
||||
PyWideStringList_Append(&pyconfig.module_search_paths,
|
||||
site_packages.wstring().c_str());
|
||||
pyconfig.module_search_paths_set = 1;
|
||||
|
|
@ -468,10 +514,10 @@ PyStatus McRFPy_API::init_python_with_config(const McRogueFaceConfig& config, in
|
|||
// search paths for python libs/modules/scripts
|
||||
const wchar_t* str_arr[] = {
|
||||
L"/scripts",
|
||||
L"/lib/Python/lib.linux-x86_64-3.12",
|
||||
L"/lib/Python/lib.linux-x86_64-3.14",
|
||||
L"/lib/Python",
|
||||
L"/lib/Python/Lib",
|
||||
L"/venv/lib/python3.12/site-packages"
|
||||
L"/venv/lib/python3.14/site-packages"
|
||||
};
|
||||
|
||||
for(auto s : str_arr) {
|
||||
|
|
@ -483,9 +529,7 @@ PyStatus McRFPy_API::init_python_with_config(const McRogueFaceConfig& config, in
|
|||
#endif
|
||||
|
||||
// Register mcrfpy module before initialization
|
||||
if (!Py_IsInitialized()) {
|
||||
PyImport_AppendInittab("mcrfpy", &PyInit_mcrfpy);
|
||||
}
|
||||
PyImport_AppendInittab("mcrfpy", &PyInit_mcrfpy);
|
||||
|
||||
status = Py_InitializeFromConfig(&pyconfig);
|
||||
PyConfig_Clear(&pyconfig);
|
||||
|
|
@ -535,9 +579,9 @@ void McRFPy_API::api_init() {
|
|||
//setSpriteTexture(0);
|
||||
}
|
||||
|
||||
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);
|
||||
void McRFPy_API::api_init(const McRogueFaceConfig& config) {
|
||||
// Initialize Python with proper argv constructed from config
|
||||
PyStatus status = init_python_with_config(config);
|
||||
if (PyStatus_Exception(status)) {
|
||||
Py_ExitStatusException(status);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,8 +29,8 @@ public:
|
|||
//static void setSpriteTexture(int);
|
||||
inline static GameEngine* game;
|
||||
static void api_init();
|
||||
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_init(const McRogueFaceConfig& config);
|
||||
static PyStatus init_python_with_config(const McRogueFaceConfig& config);
|
||||
static void api_shutdown();
|
||||
// Python API functionality - use mcrfpy.* in scripts
|
||||
//static PyObject* _drawSprite(PyObject*, PyObject*);
|
||||
|
|
|
|||
|
|
@ -236,6 +236,7 @@ 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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
#include "PyVector.h"
|
||||
#include "PyObjectUtils.h"
|
||||
#include "McRFPy_Doc.h"
|
||||
#include "PyRAII.h"
|
||||
#include <cmath>
|
||||
|
||||
PyGetSetDef PyVector::getsetters[] = {
|
||||
|
|
@ -261,35 +262,46 @@ int PyVector::set_member(PyObject* obj, PyObject* value, void* closure)
|
|||
|
||||
PyVectorObject* PyVector::from_arg(PyObject* args)
|
||||
{
|
||||
auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Vector");
|
||||
if (PyObject_IsInstance(args, (PyObject*)type)) return (PyVectorObject*)args;
|
||||
// Use RAII for type reference management
|
||||
PyRAII::PyTypeRef type("Vector", McRFPy_API::mcrf_module);
|
||||
if (!type) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
auto obj = (PyVectorObject*)type->tp_alloc(type, 0);
|
||||
// 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;
|
||||
}
|
||||
|
||||
// Handle different input types
|
||||
if (PyTuple_Check(args)) {
|
||||
// It's already a tuple, pass it directly to init
|
||||
int err = init(obj, args, NULL);
|
||||
int err = init((PyVectorObject*)obj.get(), args, NULL);
|
||||
if (err) {
|
||||
Py_DECREF(obj);
|
||||
// obj will be automatically cleaned up when it goes out of scope
|
||||
return NULL;
|
||||
}
|
||||
} else {
|
||||
// Wrap single argument in a tuple for init
|
||||
PyObject* tuple = PyTuple_Pack(1, args);
|
||||
PyRAII::PyObjectRef tuple(PyTuple_Pack(1, args), true);
|
||||
if (!tuple) {
|
||||
Py_DECREF(obj);
|
||||
return NULL;
|
||||
}
|
||||
int err = init(obj, tuple, NULL);
|
||||
Py_DECREF(tuple);
|
||||
int err = init((PyVectorObject*)obj.get(), tuple.get(), NULL);
|
||||
if (err) {
|
||||
Py_DECREF(obj);
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
return obj;
|
||||
// Release ownership and return
|
||||
return (PyVectorObject*)obj.release();
|
||||
}
|
||||
|
||||
// Arithmetic operations
|
||||
|
|
|
|||
51
src/main.cpp
51
src/main.cpp
|
|
@ -11,7 +11,7 @@
|
|||
|
||||
// Forward declarations
|
||||
int run_game_engine(const McRogueFaceConfig& config);
|
||||
int run_python_interpreter(const McRogueFaceConfig& config, int argc, char* argv[]);
|
||||
int run_python_interpreter(const McRogueFaceConfig& config);
|
||||
|
||||
int main(int argc, char* argv[])
|
||||
{
|
||||
|
|
@ -31,7 +31,7 @@ int main(int argc, char* argv[])
|
|||
|
||||
// Initialize based on configuration
|
||||
if (config.python_mode) {
|
||||
return run_python_interpreter(config, argc, argv);
|
||||
return run_python_interpreter(config);
|
||||
} 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 argc, char* argv[])
|
||||
int run_python_interpreter(const McRogueFaceConfig& config)
|
||||
{
|
||||
// Create a game engine with the requested configuration
|
||||
GameEngine* engine = new GameEngine(config);
|
||||
|
||||
// Initialize Python with configuration
|
||||
McRFPy_API::init_python_with_config(config, argc, argv);
|
||||
// Initialize Python with configuration (argv is constructed from config)
|
||||
McRFPy_API::init_python_with_config(config);
|
||||
|
||||
// Import mcrfpy module and store reference
|
||||
McRFPy_API::mcrf_module = PyImport_ImportModule("mcrfpy");
|
||||
|
|
@ -116,17 +116,10 @@ int run_python_interpreter(const McRogueFaceConfig& config, int argc, char* argv
|
|||
}
|
||||
}
|
||||
else if (!config.python_module.empty()) {
|
||||
// Execute module using runpy
|
||||
// Execute module using runpy (sys.argv already set at init time)
|
||||
std::string run_module_code =
|
||||
"import sys\n"
|
||||
"import runpy\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";
|
||||
"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();
|
||||
|
|
@ -134,7 +127,7 @@ int run_python_interpreter(const McRogueFaceConfig& config, int argc, char* argv
|
|||
return result;
|
||||
}
|
||||
else if (!config.script_path.empty()) {
|
||||
// Execute script file
|
||||
// Execute script file (sys.argv already set at init time)
|
||||
FILE* fp = fopen(config.script_path.string().c_str(), "r");
|
||||
if (!fp) {
|
||||
std::cerr << "mcrogueface: can't open file '" << config.script_path << "': ";
|
||||
|
|
@ -142,23 +135,9 @@ int run_python_interpreter(const McRogueFaceConfig& config, int argc, char* argv
|
|||
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) {
|
||||
|
|
@ -197,22 +176,18 @@ int run_python_interpreter(const McRogueFaceConfig& config, int argc, char* argv
|
|||
}
|
||||
else if (config.interactive_mode) {
|
||||
// Interactive Python interpreter (only if explicitly requested with -i)
|
||||
Py_InspectFlag = 1;
|
||||
// Note: pyconfig.inspect is set at init time based on config.interactive_mode
|
||||
PyRun_InteractiveLoop(stdin, "<stdin>");
|
||||
McRFPy_API::api_shutdown();
|
||||
delete engine;
|
||||
return 0;
|
||||
}
|
||||
else if (!config.exec_scripts.empty()) {
|
||||
// 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;
|
||||
// With --exec, scripts were already executed by the first GameEngine constructor.
|
||||
// Just configure auto-exit and run the existing engine to preserve timers/state.
|
||||
if (config.headless) {
|
||||
engine->setAutoExitAfterExec(true);
|
||||
}
|
||||
delete engine;
|
||||
engine = new GameEngine(mutable_config);
|
||||
McRFPy_API::game = engine;
|
||||
engine->run();
|
||||
McRFPy_API::api_shutdown();
|
||||
delete engine;
|
||||
|
|
|
|||
|
|
@ -2,20 +2,44 @@
|
|||
"""Test UIFrame clipping functionality"""
|
||||
|
||||
import mcrfpy
|
||||
from mcrfpy import Color, Frame, Caption, Vector
|
||||
from mcrfpy import Color, Frame, Caption
|
||||
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(50, 50, 200, 150,
|
||||
parent1 = Frame(pos=(50, 50), size=(200, 150),
|
||||
fill_color=Color(100, 100, 200),
|
||||
outline_color=Color(255, 255, 255),
|
||||
outline=2)
|
||||
|
|
@ -23,7 +47,7 @@ def test_clipping(runtime):
|
|||
scene.append(parent1)
|
||||
|
||||
# Create parent frame with clipping enabled
|
||||
parent2 = Frame(300, 50, 200, 150,
|
||||
parent2 = Frame(pos=(300, 50), size=(200, 150),
|
||||
fill_color=Color(200, 100, 100),
|
||||
outline_color=Color(255, 255, 255),
|
||||
outline=2)
|
||||
|
|
@ -32,48 +56,48 @@ def test_clipping(runtime):
|
|||
scene.append(parent2)
|
||||
|
||||
# Add captions to both frames
|
||||
caption1 = Caption(10, 10, "This text should overflow the frame bounds")
|
||||
caption1 = Caption(text="This text should overflow the frame bounds", pos=(10, 10))
|
||||
caption1.font_size = 16
|
||||
caption1.fill_color = Color(255, 255, 255)
|
||||
parent1.children.append(caption1)
|
||||
|
||||
caption2 = Caption(10, 10, "This text should be clipped to frame bounds")
|
||||
caption2 = Caption(text="This text should be clipped to frame bounds", pos=(10, 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(150, 100, 100, 100,
|
||||
child1 = Frame(pos=(150, 100), size=(100, 100),
|
||||
fill_color=Color(50, 255, 50),
|
||||
outline_color=Color(0, 0, 0),
|
||||
outline=1)
|
||||
parent1.children.append(child1)
|
||||
|
||||
child2 = Frame(150, 100, 100, 100,
|
||||
child2 = Frame(pos=(150, 100), size=(100, 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(50, 250,
|
||||
f"Left frame: clip_children={parent1.clip_children}\n"
|
||||
f"Right frame: clip_children={parent2.clip_children}")
|
||||
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(50, 300,
|
||||
"Left: Children should overflow (no clipping)\n"
|
||||
"Right: Children should be clipped to frame bounds\n"
|
||||
"Press 'c' to toggle clipping on left frame")
|
||||
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 Window, automation
|
||||
from mcrfpy import automation
|
||||
automation.screenshot("frame_clipping_test.png")
|
||||
|
||||
print(f"Parent1 clip_children: {parent1.clip_children}")
|
||||
|
|
@ -85,50 +109,25 @@ def test_clipping(runtime):
|
|||
|
||||
# Verify the property setter works
|
||||
try:
|
||||
parent1.clip_children = "not a bool" # Should raise TypeError
|
||||
parent1.clip_children = "not a bool"
|
||||
print("ERROR: clip_children accepted non-boolean value")
|
||||
except TypeError as e:
|
||||
print(f"PASS: clip_children correctly rejected non-boolean: {e}")
|
||||
|
||||
# Test with animations
|
||||
def animate_frames(runtime):
|
||||
mcrfpy.delTimer("animate")
|
||||
# Animate child frames to show clipping in action
|
||||
# Note: For now, just move the frames manually to demonstrate clipping
|
||||
parent1.children[1].x = 50 # Move child frame
|
||||
parent2.children[1].x = 50 # Move child frame
|
||||
|
||||
# Take another screenshot after starting animation
|
||||
mcrfpy.setTimer("screenshot2", take_second_screenshot, 500)
|
||||
|
||||
def take_second_screenshot(runtime):
|
||||
mcrfpy.delTimer("screenshot2")
|
||||
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)
|
||||
|
||||
# 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}")
|
||||
|
||||
# Main execution
|
||||
print("Creating test scene...")
|
||||
mcrfpy.createScene("test")
|
||||
mcrfpy.setScene("test")
|
||||
|
||||
# Set up keyboard handler to toggle clipping
|
||||
def handle_keypress(key, modifiers):
|
||||
if key == "c":
|
||||
scene = mcrfpy.sceneUI("test")
|
||||
parent1 = scene[0] # First frame
|
||||
parent1.clip_children = not parent1.clip_children
|
||||
print(f"Toggled parent1 clip_children to: {parent1.clip_children}")
|
||||
|
||||
mcrfpy.keypressScene(handle_keypress)
|
||||
|
||||
# Schedule the test
|
||||
mcrfpy.setTimer("test_clipping", test_clipping, 100)
|
||||
|
||||
print("Test scheduled, running...")
|
||||
|
|
@ -15,7 +15,7 @@ def test_nested_clipping(runtime):
|
|||
scene = mcrfpy.sceneUI("test")
|
||||
|
||||
# Create outer frame with clipping enabled
|
||||
outer = Frame(50, 50, 400, 300,
|
||||
outer = Frame(pos=(50, 50), size=(400, 300),
|
||||
fill_color=Color(50, 50, 150),
|
||||
outline_color=Color(255, 255, 255),
|
||||
outline=3)
|
||||
|
|
@ -24,7 +24,7 @@ def test_nested_clipping(runtime):
|
|||
scene.append(outer)
|
||||
|
||||
# Create inner frame that extends beyond outer bounds
|
||||
inner = Frame(200, 150, 300, 200,
|
||||
inner = Frame(pos=(200, 150), size=(300, 200),
|
||||
fill_color=Color(150, 50, 50),
|
||||
outline_color=Color(255, 255, 0),
|
||||
outline=2)
|
||||
|
|
@ -34,13 +34,13 @@ def test_nested_clipping(runtime):
|
|||
|
||||
# Add content to inner frame that extends beyond its bounds
|
||||
for i in range(5):
|
||||
caption = Caption(10, 30 * i, f"Line {i+1}: This text should be double-clipped")
|
||||
caption = Caption(text=f"Line {i+1}: This text should be double-clipped", pos=(10, 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(250, 100, 200, 150,
|
||||
deeply_nested = Frame(pos=(250, 100), size=(200, 150),
|
||||
fill_color=Color(50, 150, 50),
|
||||
outline_color=Color(255, 0, 255),
|
||||
outline=2)
|
||||
|
|
@ -48,11 +48,11 @@ def test_nested_clipping(runtime):
|
|||
inner.children.append(deeply_nested)
|
||||
|
||||
# Add status text
|
||||
status = Caption(50, 380,
|
||||
"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")
|
||||
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))
|
||||
status.font_size = 12
|
||||
status.fill_color = Color(200, 200, 200)
|
||||
scene.append(status)
|
||||
|
|
|
|||
|
|
@ -10,17 +10,17 @@ call_count = 0
|
|||
pause_test_count = 0
|
||||
cancel_test_count = 0
|
||||
|
||||
def timer_callback(elapsed_ms):
|
||||
def timer_callback(timer, runtime):
|
||||
global call_count
|
||||
call_count += 1
|
||||
print(f"Timer fired! Count: {call_count}, Elapsed: {elapsed_ms}ms")
|
||||
print(f"Timer fired! Count: {call_count}, Runtime: {runtime}ms")
|
||||
|
||||
def pause_test_callback(elapsed_ms):
|
||||
def pause_test_callback(timer, runtime):
|
||||
global pause_test_count
|
||||
pause_test_count += 1
|
||||
print(f"Pause test timer: {pause_test_count}")
|
||||
|
||||
def cancel_test_callback(elapsed_ms):
|
||||
def cancel_test_callback(timer, runtime):
|
||||
global cancel_test_count
|
||||
cancel_test_count += 1
|
||||
print(f"Cancel test timer: {cancel_test_count} - This should only print once!")
|
||||
|
|
@ -76,7 +76,7 @@ def run_tests(runtime):
|
|||
|
||||
# Test 4: Test interval modification
|
||||
print("\nTest 4: Testing interval modification")
|
||||
def interval_test(runtime):
|
||||
def interval_test(timer, runtime):
|
||||
print(f" Interval test fired at {runtime}ms")
|
||||
|
||||
timer4 = mcrfpy.Timer("interval_test", interval_test, 1000)
|
||||
|
|
@ -98,12 +98,12 @@ def run_tests(runtime):
|
|||
print("\nTest 6: Testing restart functionality")
|
||||
restart_count = [0]
|
||||
|
||||
def restart_test(runtime):
|
||||
def restart_test(timer, runtime):
|
||||
restart_count[0] += 1
|
||||
print(f" Restart test: {restart_count[0]}")
|
||||
if restart_count[0] == 2:
|
||||
print(" Restarting timer...")
|
||||
timer5.restart()
|
||||
timer.restart()
|
||||
|
||||
timer5 = mcrfpy.Timer("restart_test", restart_test, 400)
|
||||
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ def test_viewport_modes(runtime):
|
|||
|
||||
# Create a frame that fills the game resolution to show boundaries
|
||||
game_res = window.game_resolution
|
||||
boundary = Frame(0, 0, game_res[0], game_res[1],
|
||||
boundary = Frame(pos=(0, 0), size=(game_res[0], game_res[1]),
|
||||
fill_color=Color(50, 50, 100),
|
||||
outline_color=Color(255, 255, 255),
|
||||
outline=2)
|
||||
|
|
@ -41,13 +41,13 @@ def test_viewport_modes(runtime):
|
|||
]
|
||||
|
||||
for x, y, label in corners:
|
||||
corner = Frame(x, y, corner_size, corner_size,
|
||||
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(x + 5, y + 5, label)
|
||||
text = Caption(text=label, pos=(x + 5, y + 5))
|
||||
text.font_size = 20
|
||||
text.fill_color = Color(255, 255, 255)
|
||||
scene.append(text)
|
||||
|
|
@ -55,28 +55,28 @@ def test_viewport_modes(runtime):
|
|||
# Add center crosshair
|
||||
center_x = game_res[0] // 2
|
||||
center_y = game_res[1] // 2
|
||||
h_line = Frame(center_x - 50, center_y - 1, 100, 2,
|
||||
h_line = Frame(pos=(center_x - 50, center_y - 1), size=(100, 2),
|
||||
fill_color=Color(255, 255, 0))
|
||||
v_line = Frame(center_x - 1, center_y - 50, 2, 100,
|
||||
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(10, 10, f"Mode: {window.scaling_mode}")
|
||||
mode_text = Caption(text=f"Mode: {window.scaling_mode}", pos=(10, 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(10, 40,
|
||||
"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")
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ def test_viewport_visual(runtime):
|
|||
game_res = window.game_resolution
|
||||
|
||||
# Full boundary frame
|
||||
boundary = Frame(0, 0, game_res[0], game_res[1],
|
||||
boundary = Frame(pos=(0, 0), size=(game_res[0], game_res[1]),
|
||||
fill_color=Color(40, 40, 80),
|
||||
outline_color=Color(255, 255, 0),
|
||||
outline=3)
|
||||
|
|
@ -44,13 +44,13 @@ def test_viewport_visual(runtime):
|
|||
labels = ["TL", "TR", "BL", "BR"]
|
||||
|
||||
for (x, y), color, label in zip(positions, colors, labels):
|
||||
corner = Frame(x, y, corner_size, corner_size,
|
||||
corner = Frame(pos=(x, y), size=(corner_size, corner_size),
|
||||
fill_color=color,
|
||||
outline_color=Color(255, 255, 255),
|
||||
outline=2)
|
||||
scene.append(corner)
|
||||
|
||||
text = Caption(x + 10, y + 10, label)
|
||||
text = Caption(text=label, pos=(x + 10, y + 10))
|
||||
text.font_size = 32
|
||||
text.fill_color = Color(0, 0, 0)
|
||||
scene.append(text)
|
||||
|
|
@ -58,23 +58,23 @@ def test_viewport_visual(runtime):
|
|||
# Center crosshair
|
||||
center_x = game_res[0] // 2
|
||||
center_y = game_res[1] // 2
|
||||
h_line = Frame(0, center_y - 1, game_res[0], 2,
|
||||
h_line = Frame(pos=(0, center_y - 1), size=(game_res[0], 2),
|
||||
fill_color=Color(255, 255, 255, 128))
|
||||
v_line = Frame(center_x - 1, 0, 2, game_res[1],
|
||||
v_line = Frame(pos=(center_x - 1, 0), size=(2, game_res[1]),
|
||||
fill_color=Color(255, 255, 255, 128))
|
||||
scene.append(h_line)
|
||||
scene.append(v_line)
|
||||
|
||||
# Mode text
|
||||
mode_text = Caption(center_x - 100, center_y - 50,
|
||||
f"Mode: {window.scaling_mode}")
|
||||
mode_text = Caption(text=f"Mode: {window.scaling_mode}",
|
||||
pos=(center_x - 100, center_y - 50))
|
||||
mode_text.font_size = 36
|
||||
mode_text.fill_color = Color(255, 255, 255)
|
||||
scene.append(mode_text)
|
||||
|
||||
# Resolution text
|
||||
res_text = Caption(center_x - 150, center_y + 10,
|
||||
f"Game: {game_res[0]}x{game_res[1]}")
|
||||
res_text = Caption(text=f"Game: {game_res[0]}x{game_res[1]}",
|
||||
pos=(center_x - 150, center_y + 10))
|
||||
res_text.font_size = 24
|
||||
res_text.fill_color = Color(200, 200, 200)
|
||||
scene.append(res_text)
|
||||
|
|
|
|||
Loading…
Reference in New Issue