Compare commits

..

No commits in common. "a703bce196ea94baba78469ed02f03290cbe3b24" and "ce0be78b73d8fe4166943ffe4a1075c2024cee8e" have entirely different histories.

13 changed files with 178 additions and 413 deletions

View file

@ -31,7 +31,7 @@ set(LINK_LIBS
# On Windows, add any additional libs and include directories # On Windows, add any additional libs and include directories
if(WIN32) if(WIN32)
# Windows-specific Python library name (no dots) # 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 # Add the necessary Windows-specific libraries and include directories
# include_directories(path_to_additional_includes) # include_directories(path_to_additional_includes)
# link_directories(path_to_additional_libs) # link_directories(path_to_additional_libs)
@ -39,7 +39,7 @@ if(WIN32)
include_directories(${CMAKE_SOURCE_DIR}/deps/platform/windows) include_directories(${CMAKE_SOURCE_DIR}/deps/platform/windows)
else() else()
# Unix/Linux specific libraries # 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) include_directories(${CMAKE_SOURCE_DIR}/deps/platform/linux)
endif() endif()

View file

@ -176,5 +176,5 @@ void CommandLineParser::print_help() {
} }
void CommandLineParser::print_version() { void CommandLineParser::print_version() {
std::cout << "Python 3.14.0 (McRogueFace embedded)\n"; std::cout << "Python 3.12.0 (McRogueFace embedded)\n";
} }

View file

@ -344,14 +344,9 @@ void GameEngine::testTimers()
auto it = timers.begin(); auto it = timers.begin();
while (it != timers.end()) while (it != timers.end())
{ {
// Keep a local copy of the timer to prevent use-after-free. it->second->test(now);
// 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) if (!it->second->getCallback() || it->second->getCallback() == Py_None)
{ {
it = timers.erase(it); it = timers.erase(it);

View file

@ -155,7 +155,6 @@ public:
void setWindowScale(float); void setWindowScale(float);
bool isHeadless() const { return headless; } bool isHeadless() const { return headless; }
const McRogueFaceConfig& getConfig() const { return config; } const McRogueFaceConfig& getConfig() const { return config; }
void setAutoExitAfterExec(bool enabled) { config.auto_exit_after_exec = enabled; }
void processEvent(const sf::Event& event); void processEvent(const sf::Event& event);
// Window property accessors // Window property accessors

View file

@ -397,10 +397,10 @@ PyStatus init_python(const char *program_name)
// search paths for python libs/modules/scripts // search paths for python libs/modules/scripts
const wchar_t* str_arr[] = { const wchar_t* str_arr[] = {
L"/scripts", 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",
L"/lib/Python/Lib", L"/lib/Python/Lib",
L"/venv/lib/python3.14/site-packages" L"/venv/lib/python3.12/site-packages"
}; };
@ -419,7 +419,7 @@ PyStatus init_python(const char *program_name)
return status; 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 Python is already initialized, just return success
if (Py_IsInitialized()) { if (Py_IsInitialized()) {
@ -435,67 +435,21 @@ PyStatus McRFPy_API::init_python_with_config(const McRogueFaceConfig& config)
PyConfig_SetString(&pyconfig, &pyconfig.stdio_errors, L"surrogateescape"); PyConfig_SetString(&pyconfig, &pyconfig.stdio_errors, L"surrogateescape");
pyconfig.configure_c_stdio = 1; pyconfig.configure_c_stdio = 1;
// Set interactive mode (replaces deprecated Py_InspectFlag) // CRITICAL: Pass actual command line arguments to Python
if (config.interactive_mode) { status = PyConfig_SetBytesArgv(&pyconfig, argc, argv);
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)) { if (PyStatus_Exception(status)) {
return status; return status;
} }
// Check if we're in a virtual environment // Check if we're in a virtual environment
auto exe_wpath = executable_filename(); auto exe_path = std::filesystem::path(argv[0]);
auto exe_path_fs = std::filesystem::path(exe_wpath); auto exe_dir = exe_path.parent_path();
auto exe_dir = exe_path_fs.parent_path();
auto venv_root = exe_dir.parent_path(); auto venv_root = exe_dir.parent_path();
if (std::filesystem::exists(venv_root / "pyvenv.cfg")) { if (std::filesystem::exists(venv_root / "pyvenv.cfg")) {
// We're running from within a venv! // We're running from within a venv!
// Add venv's site-packages to module search paths // 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, PyWideStringList_Append(&pyconfig.module_search_paths,
site_packages.wstring().c_str()); site_packages.wstring().c_str());
pyconfig.module_search_paths_set = 1; pyconfig.module_search_paths_set = 1;
@ -514,10 +468,10 @@ PyStatus McRFPy_API::init_python_with_config(const McRogueFaceConfig& config)
// search paths for python libs/modules/scripts // search paths for python libs/modules/scripts
const wchar_t* str_arr[] = { const wchar_t* str_arr[] = {
L"/scripts", 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",
L"/lib/Python/Lib", L"/lib/Python/Lib",
L"/venv/lib/python3.14/site-packages" L"/venv/lib/python3.12/site-packages"
}; };
for(auto s : str_arr) { for(auto s : str_arr) {
@ -529,7 +483,9 @@ PyStatus McRFPy_API::init_python_with_config(const McRogueFaceConfig& config)
#endif #endif
// Register mcrfpy module before initialization // Register mcrfpy module before initialization
PyImport_AppendInittab("mcrfpy", &PyInit_mcrfpy); if (!Py_IsInitialized()) {
PyImport_AppendInittab("mcrfpy", &PyInit_mcrfpy);
}
status = Py_InitializeFromConfig(&pyconfig); status = Py_InitializeFromConfig(&pyconfig);
PyConfig_Clear(&pyconfig); PyConfig_Clear(&pyconfig);
@ -579,9 +535,9 @@ void McRFPy_API::api_init() {
//setSpriteTexture(0); //setSpriteTexture(0);
} }
void McRFPy_API::api_init(const McRogueFaceConfig& config) { void McRFPy_API::api_init(const McRogueFaceConfig& config, int argc, char** argv) {
// Initialize Python with proper argv constructed from config // Initialize Python with proper argv - this is CRITICAL
PyStatus status = init_python_with_config(config); PyStatus status = init_python_with_config(config, argc, argv);
if (PyStatus_Exception(status)) { if (PyStatus_Exception(status)) {
Py_ExitStatusException(status); Py_ExitStatusException(status);
} }

View file

@ -29,8 +29,8 @@ public:
//static void setSpriteTexture(int); //static void setSpriteTexture(int);
inline static GameEngine* game; inline static GameEngine* game;
static void api_init(); static void api_init();
static void api_init(const McRogueFaceConfig& config); static void api_init(const McRogueFaceConfig& config, int argc, char** argv);
static PyStatus init_python_with_config(const McRogueFaceConfig& config); static PyStatus init_python_with_config(const McRogueFaceConfig& config, int argc, char** argv);
static void api_shutdown(); static void api_shutdown();
// Python API functionality - use mcrfpy.* in scripts // Python API functionality - use mcrfpy.* in scripts
//static PyObject* _drawSprite(PyObject*, PyObject*); //static PyObject* _drawSprite(PyObject*, PyObject*);

View file

@ -236,7 +236,6 @@ PyColorObject* PyColor::from_arg(PyObject* args)
// Check if args is already a Color instance // Check if args is already a Color instance
if (PyObject_IsInstance(args, (PyObject*)type.get())) { if (PyObject_IsInstance(args, (PyObject*)type.get())) {
Py_INCREF(args); // Return new reference so caller can safely DECREF
return (PyColorObject*)args; return (PyColorObject*)args;
} }

View file

@ -1,7 +1,6 @@
#include "PyVector.h" #include "PyVector.h"
#include "PyObjectUtils.h" #include "PyObjectUtils.h"
#include "McRFPy_Doc.h" #include "McRFPy_Doc.h"
#include "PyRAII.h"
#include <cmath> #include <cmath>
PyGetSetDef PyVector::getsetters[] = { PyGetSetDef PyVector::getsetters[] = {
@ -262,46 +261,35 @@ int PyVector::set_member(PyObject* obj, PyObject* value, void* closure)
PyVectorObject* PyVector::from_arg(PyObject* args) PyVectorObject* PyVector::from_arg(PyObject* args)
{ {
// Use RAII for type reference management auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Vector");
PyRAII::PyTypeRef type("Vector", McRFPy_API::mcrf_module); if (PyObject_IsInstance(args, (PyObject*)type)) return (PyVectorObject*)args;
if (!type) {
return NULL;
}
// Check if args is already a Vector instance auto obj = (PyVectorObject*)type->tp_alloc(type, 0);
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 // Handle different input types
if (PyTuple_Check(args)) { if (PyTuple_Check(args)) {
// It's already a tuple, pass it directly to init // 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) { if (err) {
// obj will be automatically cleaned up when it goes out of scope Py_DECREF(obj);
return NULL; return NULL;
} }
} else { } else {
// Wrap single argument in a tuple for init // Wrap single argument in a tuple for init
PyRAII::PyObjectRef tuple(PyTuple_Pack(1, args), true); PyObject* tuple = PyTuple_Pack(1, args);
if (!tuple) { if (!tuple) {
Py_DECREF(obj);
return NULL; return NULL;
} }
int err = init((PyVectorObject*)obj.get(), tuple.get(), NULL); int err = init(obj, tuple, NULL);
Py_DECREF(tuple);
if (err) { if (err) {
Py_DECREF(obj);
return NULL; return NULL;
} }
} }
// Release ownership and return return obj;
return (PyVectorObject*)obj.release();
} }
// Arithmetic operations // Arithmetic operations

View file

@ -11,7 +11,7 @@
// Forward declarations // Forward declarations
int run_game_engine(const McRogueFaceConfig& config); 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[]) int main(int argc, char* argv[])
{ {
@ -31,7 +31,7 @@ int main(int argc, char* argv[])
// Initialize based on configuration // Initialize based on configuration
if (config.python_mode) { if (config.python_mode) {
return run_python_interpreter(config); return run_python_interpreter(config, argc, argv);
} else { } else {
return run_game_engine(config); return run_game_engine(config);
} }
@ -52,13 +52,13 @@ int run_game_engine(const McRogueFaceConfig& config)
return 0; 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 // Create a game engine with the requested configuration
GameEngine* engine = new GameEngine(config); GameEngine* engine = new GameEngine(config);
// Initialize Python with configuration (argv is constructed from config) // Initialize Python with configuration
McRFPy_API::init_python_with_config(config); McRFPy_API::init_python_with_config(config, argc, argv);
// Import mcrfpy module and store reference // Import mcrfpy module and store reference
McRFPy_API::mcrf_module = PyImport_ImportModule("mcrfpy"); McRFPy_API::mcrf_module = PyImport_ImportModule("mcrfpy");
@ -116,10 +116,17 @@ int run_python_interpreter(const McRogueFaceConfig& config)
} }
} }
else if (!config.python_module.empty()) { else if (!config.python_module.empty()) {
// Execute module using runpy (sys.argv already set at init time) // Execute module using runpy
std::string run_module_code = std::string run_module_code =
"import sys\n"
"import runpy\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()); int result = PyRun_SimpleString(run_module_code.c_str());
McRFPy_API::api_shutdown(); McRFPy_API::api_shutdown();
@ -127,7 +134,7 @@ int run_python_interpreter(const McRogueFaceConfig& config)
return result; return result;
} }
else if (!config.script_path.empty()) { 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"); FILE* fp = fopen(config.script_path.string().c_str(), "r");
if (!fp) { if (!fp) {
std::cerr << "mcrogueface: can't open file '" << config.script_path << "': "; std::cerr << "mcrogueface: can't open file '" << config.script_path << "': ";
@ -135,9 +142,23 @@ int run_python_interpreter(const McRogueFaceConfig& config)
return 1; 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()); int result = PyRun_SimpleFile(fp, config.script_path.string().c_str());
fclose(fp); 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) { if (config.interactive_mode) {
// Even if script had SystemExit, continue to interactive mode // Even if script had SystemExit, continue to interactive mode
if (result != 0) { if (result != 0) {
@ -176,18 +197,23 @@ int run_python_interpreter(const McRogueFaceConfig& config)
} }
else if (config.interactive_mode) { else if (config.interactive_mode) {
// Interactive Python interpreter (only if explicitly requested with -i) // 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, "<stdin>"); PyRun_InteractiveLoop(stdin, "<stdin>");
McRFPy_API::api_shutdown(); McRFPy_API::api_shutdown();
delete engine; delete engine;
return 0; return 0;
} }
else if (!config.exec_scripts.empty()) { else if (!config.exec_scripts.empty()) {
// Execute startup scripts on the existing engine (not in constructor to prevent double-execution) // With --exec, run the game engine after scripts execute
engine->executeStartupScripts(); // In headless mode, auto-exit when no timers remain
if (config.headless) { McRogueFaceConfig mutable_config = config;
engine->setAutoExitAfterExec(true); 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(); engine->run();
McRFPy_API::api_shutdown(); McRFPy_API::api_shutdown();
delete engine; delete engine;

View file

@ -3,43 +3,20 @@
import mcrfpy import mcrfpy
from mcrfpy import Color, Frame, Caption from mcrfpy import Color, Frame, Caption
from mcrfpy import automation
import sys 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): def test_clipping(runtime):
"""Test that clip_children property works correctly""" """Test that clip_children property works correctly"""
mcrfpy.delTimer("test_clipping") mcrfpy.delTimer("test_clipping")
print("Testing UIFrame clipping functionality...") print("Testing UIFrame clipping functionality...")
# Create test scene
scene = mcrfpy.sceneUI("test") scene = mcrfpy.sceneUI("test")
# Create parent frame with clipping disabled (default) # 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), fill_color=Color(100, 100, 200),
outline_color=Color(255, 255, 255), outline_color=Color(255, 255, 255),
outline=2) outline=2)
@ -47,7 +24,7 @@ def test_clipping(runtime):
scene.append(parent1) scene.append(parent1)
# Create parent frame with clipping enabled # 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), fill_color=Color(200, 100, 100),
outline_color=Color(255, 255, 255), outline_color=Color(255, 255, 255),
outline=2) outline=2)
@ -56,48 +33,30 @@ def test_clipping(runtime):
scene.append(parent2) scene.append(parent2)
# Add captions to both frames # 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.font_size = 16
caption1.fill_color = Color(255, 255, 255) caption1.fill_color = Color(255, 255, 255)
parent1.children.append(caption1) 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.font_size = 16
caption2.fill_color = Color(255, 255, 255) caption2.fill_color = Color(255, 255, 255)
parent2.children.append(caption2) parent2.children.append(caption2)
# Add child frames that extend beyond parent bounds # 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), fill_color=Color(50, 255, 50),
outline_color=Color(0, 0, 0), outline_color=Color(0, 0, 0),
outline=1) outline=1)
parent1.children.append(child1) 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), fill_color=Color(50, 255, 50),
outline_color=Color(0, 0, 0), outline_color=Color(0, 0, 0),
outline=1) outline=1)
parent2.children.append(child2) 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 # Take screenshot
from mcrfpy import automation
automation.screenshot("frame_clipping_test.png") automation.screenshot("frame_clipping_test.png")
print(f"Parent1 clip_children: {parent1.clip_children}") print(f"Parent1 clip_children: {parent1.clip_children}")
@ -109,25 +68,21 @@ def test_clipping(runtime):
# Verify the property setter works # Verify the property setter works
try: try:
parent1.clip_children = "not a bool" parent1.clip_children = "not a bool" # Should raise TypeError
print("ERROR: clip_children accepted non-boolean value") print("ERROR: clip_children accepted non-boolean value")
sys.exit(1)
except TypeError as e: except TypeError as e:
print(f"PASS: clip_children correctly rejected non-boolean: {e}") print(f"PASS: clip_children correctly rejected non-boolean: {e}")
# Start animation after a short delay print("\nTest completed successfully!")
mcrfpy.setTimer("animate", animate_frames, 100) sys.exit(0)
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 # Main execution
print("Creating test scene...") print("Creating test scene...")
mcrfpy.createScene("test") mcrfpy.createScene("test")
mcrfpy.setScene("test") mcrfpy.setScene("test")
mcrfpy.keypressScene(handle_keypress)
# Schedule the test
mcrfpy.setTimer("test_clipping", test_clipping, 100) mcrfpy.setTimer("test_clipping", test_clipping, 100)
print("Test scheduled, running...") print("Test scheduled, running...")

View file

@ -15,7 +15,7 @@ def test_nested_clipping(runtime):
scene = mcrfpy.sceneUI("test") scene = mcrfpy.sceneUI("test")
# Create outer frame with clipping enabled # 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), fill_color=Color(50, 50, 150),
outline_color=Color(255, 255, 255), outline_color=Color(255, 255, 255),
outline=3) outline=3)
@ -24,7 +24,7 @@ def test_nested_clipping(runtime):
scene.append(outer) scene.append(outer)
# Create inner frame that extends beyond outer bounds # 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), fill_color=Color(150, 50, 50),
outline_color=Color(255, 255, 0), outline_color=Color(255, 255, 0),
outline=2) outline=2)
@ -34,13 +34,13 @@ def test_nested_clipping(runtime):
# Add content to inner frame that extends beyond its bounds # Add content to inner frame that extends beyond its bounds
for i in range(5): 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.font_size = 14
caption.fill_color = Color(255, 255, 255) caption.fill_color = Color(255, 255, 255)
inner.children.append(caption) inner.children.append(caption)
# Add a child frame to inner that extends way out # 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), fill_color=Color(50, 150, 50),
outline_color=Color(255, 0, 255), outline_color=Color(255, 0, 255),
outline=2) outline=2)
@ -49,10 +49,10 @@ def test_nested_clipping(runtime):
# Add status text # Add status text
status = Caption(text="Nested clipping test:\n" status = Caption(text="Nested clipping test:\n"
"- Blue outer frame clips red inner frame\n" "- Blue outer frame clips red inner frame\n"
"- Red inner frame clips green deeply nested frame\n" "- Red inner frame clips green deeply nested frame\n"
"- All text should be clipped to frame bounds", "- All text should be clipped to frame bounds",
pos=(50, 380)) x=50, y=380)
status.font_size = 12 status.font_size = 12
status.fill_color = Color(200, 200, 200) status.fill_color = Color(200, 200, 200)
scene.append(status) scene.append(status)

View file

@ -11,16 +11,19 @@ pause_test_count = 0
cancel_test_count = 0 cancel_test_count = 0
def timer_callback(timer, runtime): def timer_callback(timer, runtime):
"""Timer object callbacks receive (timer, runtime)"""
global call_count global call_count
call_count += 1 call_count += 1
print(f"Timer fired! Count: {call_count}, Runtime: {runtime}ms") print(f"Timer fired! Count: {call_count}, Runtime: {runtime}ms")
def pause_test_callback(timer, runtime): def pause_test_callback(timer, runtime):
"""Timer object callbacks receive (timer, runtime)"""
global pause_test_count global pause_test_count
pause_test_count += 1 pause_test_count += 1
print(f"Pause test timer: {pause_test_count}") print(f"Pause test timer: {pause_test_count}")
def cancel_test_callback(timer, runtime): def cancel_test_callback(timer, runtime):
"""Timer object callbacks receive (timer, runtime)"""
global cancel_test_count global cancel_test_count
cancel_test_count += 1 cancel_test_count += 1
print(f"Cancel test timer: {cancel_test_count} - This should only print once!") 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 # Schedule pause after 250ms
def pause_timer2(runtime): def pause_timer2(runtime):
mcrfpy.delTimer("pause_timer2") # Prevent re-entry
print(" Pausing timer2...") print(" Pausing timer2...")
timer2.pause() timer2.pause()
print(f" Timer2 paused: {timer2.paused}") print(f" Timer2 paused: {timer2.paused}")
@ -53,6 +57,7 @@ def run_tests(runtime):
# Schedule resume after another 400ms # Schedule resume after another 400ms
def resume_timer2(runtime): def resume_timer2(runtime):
mcrfpy.delTimer("resume_timer2") # Prevent re-entry
print(" Resuming timer2...") print(" Resuming timer2...")
timer2.resume() timer2.resume()
print(f" Timer2 paused: {timer2.paused}") print(f" Timer2 paused: {timer2.paused}")
@ -68,6 +73,7 @@ def run_tests(runtime):
# Cancel after 350ms (should fire once) # Cancel after 350ms (should fire once)
def cancel_timer3(runtime): def cancel_timer3(runtime):
mcrfpy.delTimer("cancel_timer3") # Prevent re-entry
print(" Canceling timer3...") print(" Canceling timer3...")
timer3.cancel() timer3.cancel()
print(" Timer3 canceled") print(" Timer3 canceled")
@ -84,13 +90,16 @@ def run_tests(runtime):
timer4.interval = 500 timer4.interval = 500
print(f" Modified interval: {timer4.interval}ms") 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") print("\nTest 5: Testing remaining time")
def check_remaining(runtime): def check_remaining(runtime):
if timer1.active: try:
print(f" Timer1 remaining: {timer1.remaining}ms") if timer1.active:
if timer2.active or timer2.paused: print(f" Timer1 remaining: {timer1.remaining}ms")
print(f" Timer2 remaining: {timer2.remaining}ms (paused: {timer2.paused})") 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) mcrfpy.setTimer("check_remaining", check_remaining, 150)

View file

@ -2,7 +2,8 @@
"""Test viewport scaling modes""" """Test viewport scaling modes"""
import mcrfpy import mcrfpy
from mcrfpy import Window, Frame, Caption, Color, Vector from mcrfpy import Window, Frame, Caption, Color
from mcrfpy import automation
import sys import sys
def test_viewport_modes(runtime): def test_viewport_modes(runtime):
@ -19,219 +20,56 @@ def test_viewport_modes(runtime):
print(f"Initial scaling mode: {window.scaling_mode}") print(f"Initial scaling mode: {window.scaling_mode}")
print(f"Window resolution: {window.resolution}") print(f"Window resolution: {window.resolution}")
# Create test scene with visual elements # Get scene
scene = mcrfpy.sceneUI("test") 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 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), fill_color=Color(50, 50, 100),
outline_color=Color(255, 255, 255), outline_color=Color(255, 255, 255),
outline=2) outline=2)
boundary.name = "boundary"
scene.append(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 # 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.font_size = 24
mode_text.fill_color = Color(255, 255, 255) mode_text.fill_color = Color(255, 255, 255)
mode_text.name = "mode_text"
scene.append(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 # Test changing modes
def test_mode_changes(runtime): print("\nTesting scaling modes:")
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"
automation.screenshot("viewport_center_mode.png")
# Test center mode # Test stretch mode
window.scaling_mode = "center" window.scaling_mode = "stretch"
print(f"Set to center mode: {window.scaling_mode}") print(f"Set to stretch mode: {window.scaling_mode}")
mode_text.text = f"Mode: center (1:1 pixels)" mode_text.text = f"Mode: stretch"
automation.screenshot("viewport_center_mode.png") automation.screenshot("viewport_stretch_mode.png")
# Schedule next mode test # Test fit mode
mcrfpy.setTimer("test_stretch", test_stretch_mode, 1000) 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")
def test_stretch_mode(runtime): # Note: Cannot change window resolution in headless mode
mcrfpy.delTimer("test_stretch") # Just verify the scaling mode properties work
from mcrfpy import automation print("\nScaling mode property tests passed!")
print("\nTest completed!")
window.scaling_mode = "stretch" sys.exit(0)
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)
# 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)
# Main execution # Main execution
print("Creating viewport test scene...") print("Creating viewport test scene...")
mcrfpy.createScene("test") mcrfpy.createScene("test")
mcrfpy.setScene("test") mcrfpy.setScene("test")
mcrfpy.keypressScene(handle_keypress)
# Schedule the test # Schedule the test
mcrfpy.setTimer("test_viewport", test_viewport_modes, 100) mcrfpy.setTimer("test_viewport", test_viewport_modes, 100)
print("Viewport test running...") print("Viewport test running...")
print("Use number keys to switch modes, R to resize window, G to change game resolution")