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
|
# 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 python312)
|
list(APPEND LINK_LIBS python314)
|
||||||
# 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.12 m dl util pthread)
|
list(APPEND LINK_LIBS python3.14 m dl util pthread)
|
||||||
include_directories(${CMAKE_SOURCE_DIR}/deps/platform/linux)
|
include_directories(${CMAKE_SOURCE_DIR}/deps/platform/linux)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -176,5 +176,5 @@ void CommandLineParser::print_help() {
|
||||||
}
|
}
|
||||||
|
|
||||||
void CommandLineParser::print_version() {
|
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();
|
auto it = timers.begin();
|
||||||
while (it != timers.end())
|
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,
|
||||||
// Remove timers that have been cancelled or are one-shot and fired
|
// 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.
|
||||||
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);
|
||||||
|
|
|
||||||
|
|
@ -154,6 +154,7 @@ 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
|
||||||
|
|
|
||||||
|
|
@ -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.12",
|
L"/lib/Python/lib.linux-x86_64-3.14",
|
||||||
L"/lib/Python",
|
L"/lib/Python",
|
||||||
L"/lib/Python/Lib",
|
L"/lib/Python/Lib",
|
||||||
L"/venv/lib/python3.12/site-packages"
|
L"/venv/lib/python3.14/site-packages"
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -419,61 +419,107 @@ PyStatus init_python(const char *program_name)
|
||||||
return status;
|
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 Python is already initialized, just return success
|
||||||
if (Py_IsInitialized()) {
|
if (Py_IsInitialized()) {
|
||||||
return PyStatus_Ok();
|
return PyStatus_Ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
PyStatus status;
|
PyStatus status;
|
||||||
PyConfig pyconfig;
|
PyConfig pyconfig;
|
||||||
PyConfig_InitIsolatedConfig(&pyconfig);
|
PyConfig_InitIsolatedConfig(&pyconfig);
|
||||||
|
|
||||||
// Configure UTF-8 for stdio
|
// Configure UTF-8 for stdio
|
||||||
PyConfig_SetString(&pyconfig, &pyconfig.stdio_encoding, L"UTF-8");
|
PyConfig_SetString(&pyconfig, &pyconfig.stdio_encoding, L"UTF-8");
|
||||||
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;
|
||||||
|
|
||||||
// CRITICAL: Pass actual command line arguments to Python
|
// Set interactive mode (replaces deprecated Py_InspectFlag)
|
||||||
status = PyConfig_SetBytesArgv(&pyconfig, argc, argv);
|
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)) {
|
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_path = std::filesystem::path(argv[0]);
|
auto exe_wpath = executable_filename();
|
||||||
auto exe_dir = exe_path.parent_path();
|
auto exe_path_fs = std::filesystem::path(exe_wpath);
|
||||||
|
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.12" / "site-packages";
|
auto site_packages = venv_root / "lib" / "python3.14" / "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;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set Python home to our bundled Python
|
// Set Python home to our bundled Python
|
||||||
auto python_home = executable_path() + L"/lib/Python";
|
auto python_home = executable_path() + L"/lib/Python";
|
||||||
PyConfig_SetString(&pyconfig, &pyconfig.home, python_home.c_str());
|
PyConfig_SetString(&pyconfig, &pyconfig.home, python_home.c_str());
|
||||||
|
|
||||||
// Set up module search paths
|
// Set up module search paths
|
||||||
#if __PLATFORM_SET_PYTHON_SEARCH_PATHS == 1
|
#if __PLATFORM_SET_PYTHON_SEARCH_PATHS == 1
|
||||||
if (!pyconfig.module_search_paths_set) {
|
if (!pyconfig.module_search_paths_set) {
|
||||||
pyconfig.module_search_paths_set = 1;
|
pyconfig.module_search_paths_set = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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.12",
|
L"/lib/Python/lib.linux-x86_64-3.14",
|
||||||
L"/lib/Python",
|
L"/lib/Python",
|
||||||
L"/lib/Python/Lib",
|
L"/lib/Python/Lib",
|
||||||
L"/venv/lib/python3.12/site-packages"
|
L"/venv/lib/python3.14/site-packages"
|
||||||
};
|
};
|
||||||
|
|
||||||
for(auto s : str_arr) {
|
for(auto s : str_arr) {
|
||||||
status = PyWideStringList_Append(&pyconfig.module_search_paths, (executable_path() + s).c_str());
|
status = PyWideStringList_Append(&pyconfig.module_search_paths, (executable_path() + s).c_str());
|
||||||
if (PyStatus_Exception(status)) {
|
if (PyStatus_Exception(status)) {
|
||||||
|
|
@ -481,15 +527,13 @@ PyStatus McRFPy_API::init_python_with_config(const McRogueFaceConfig& config, in
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// Register mcrfpy module before initialization
|
// Register mcrfpy module before initialization
|
||||||
if (!Py_IsInitialized()) {
|
PyImport_AppendInittab("mcrfpy", &PyInit_mcrfpy);
|
||||||
PyImport_AppendInittab("mcrfpy", &PyInit_mcrfpy);
|
|
||||||
}
|
|
||||||
|
|
||||||
status = Py_InitializeFromConfig(&pyconfig);
|
status = Py_InitializeFromConfig(&pyconfig);
|
||||||
PyConfig_Clear(&pyconfig);
|
PyConfig_Clear(&pyconfig);
|
||||||
|
|
||||||
return status;
|
return status;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -535,9 +579,9 @@ void McRFPy_API::api_init() {
|
||||||
//setSpriteTexture(0);
|
//setSpriteTexture(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
void McRFPy_API::api_init(const McRogueFaceConfig& config, int argc, char** argv) {
|
void McRFPy_API::api_init(const McRogueFaceConfig& config) {
|
||||||
// Initialize Python with proper argv - this is CRITICAL
|
// Initialize Python with proper argv constructed from config
|
||||||
PyStatus status = init_python_with_config(config, argc, argv);
|
PyStatus status = init_python_with_config(config);
|
||||||
if (PyStatus_Exception(status)) {
|
if (PyStatus_Exception(status)) {
|
||||||
Py_ExitStatusException(status);
|
Py_ExitStatusException(status);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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, int argc, char** argv);
|
static void api_init(const McRogueFaceConfig& config);
|
||||||
static PyStatus init_python_with_config(const McRogueFaceConfig& config, int argc, char** argv);
|
static PyStatus init_python_with_config(const McRogueFaceConfig& config);
|
||||||
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*);
|
||||||
|
|
|
||||||
|
|
@ -236,6 +236,7 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
#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[] = {
|
||||||
|
|
@ -261,35 +262,46 @@ int PyVector::set_member(PyObject* obj, PyObject* value, void* closure)
|
||||||
|
|
||||||
PyVectorObject* PyVector::from_arg(PyObject* args)
|
PyVectorObject* PyVector::from_arg(PyObject* args)
|
||||||
{
|
{
|
||||||
auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Vector");
|
// Use RAII for type reference management
|
||||||
if (PyObject_IsInstance(args, (PyObject*)type)) return (PyVectorObject*)args;
|
PyRAII::PyTypeRef type("Vector", McRFPy_API::mcrf_module);
|
||||||
|
if (!type) {
|
||||||
auto obj = (PyVectorObject*)type->tp_alloc(type, 0);
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
// 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(obj, args, NULL);
|
int err = init((PyVectorObject*)obj.get(), args, NULL);
|
||||||
if (err) {
|
if (err) {
|
||||||
Py_DECREF(obj);
|
// obj will be automatically cleaned up when it goes out of scope
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Wrap single argument in a tuple for init
|
// Wrap single argument in a tuple for init
|
||||||
PyObject* tuple = PyTuple_Pack(1, args);
|
PyRAII::PyObjectRef tuple(PyTuple_Pack(1, args), true);
|
||||||
if (!tuple) {
|
if (!tuple) {
|
||||||
Py_DECREF(obj);
|
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
int err = init(obj, tuple, NULL);
|
int err = init((PyVectorObject*)obj.get(), tuple.get(), NULL);
|
||||||
Py_DECREF(tuple);
|
|
||||||
if (err) {
|
if (err) {
|
||||||
Py_DECREF(obj);
|
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return obj;
|
// Release ownership and return
|
||||||
|
return (PyVectorObject*)obj.release();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Arithmetic operations
|
// Arithmetic operations
|
||||||
|
|
|
||||||
65
src/main.cpp
65
src/main.cpp
|
|
@ -11,27 +11,27 @@
|
||||||
|
|
||||||
// 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 argc, char* argv[]);
|
int run_python_interpreter(const McRogueFaceConfig& config);
|
||||||
|
|
||||||
int main(int argc, char* argv[])
|
int main(int argc, char* argv[])
|
||||||
{
|
{
|
||||||
McRogueFaceConfig config;
|
McRogueFaceConfig config;
|
||||||
CommandLineParser parser(argc, argv);
|
CommandLineParser parser(argc, argv);
|
||||||
|
|
||||||
// Parse arguments
|
// Parse arguments
|
||||||
auto parse_result = parser.parse(config);
|
auto parse_result = parser.parse(config);
|
||||||
if (parse_result.should_exit) {
|
if (parse_result.should_exit) {
|
||||||
return parse_result.exit_code;
|
return parse_result.exit_code;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Special handling for -m module: let Python handle modules properly
|
// Special handling for -m module: let Python handle modules properly
|
||||||
if (!config.python_module.empty()) {
|
if (!config.python_module.empty()) {
|
||||||
config.python_mode = true;
|
config.python_mode = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize based on configuration
|
// Initialize based on configuration
|
||||||
if (config.python_mode) {
|
if (config.python_mode) {
|
||||||
return run_python_interpreter(config, argc, argv);
|
return run_python_interpreter(config);
|
||||||
} 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 argc, char* argv[])
|
int run_python_interpreter(const McRogueFaceConfig& config)
|
||||||
{
|
{
|
||||||
// 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
|
// Initialize Python with configuration (argv is constructed from config)
|
||||||
McRFPy_API::init_python_with_config(config, argc, argv);
|
McRFPy_API::init_python_with_config(config);
|
||||||
|
|
||||||
// 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,49 +116,28 @@ int run_python_interpreter(const McRogueFaceConfig& config, int argc, char* argv
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (!config.python_module.empty()) {
|
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 =
|
std::string run_module_code =
|
||||||
"import sys\n"
|
|
||||||
"import runpy\n"
|
"import runpy\n"
|
||||||
"sys.argv = ['" + config.python_module + "'";
|
"runpy.run_module('" + config.python_module + "', run_name='__main__', alter_sys=True)\n";
|
||||||
|
|
||||||
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();
|
||||||
delete engine;
|
delete engine;
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
else if (!config.script_path.empty()) {
|
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");
|
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 << "': ";
|
||||||
std::cerr << "[Errno " << errno << "] " << strerror(errno) << std::endl;
|
std::cerr << "[Errno " << errno << "] " << strerror(errno) << std::endl;
|
||||||
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) {
|
||||||
|
|
@ -197,22 +176,18 @@ int run_python_interpreter(const McRogueFaceConfig& config, int argc, char* argv
|
||||||
}
|
}
|
||||||
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)
|
||||||
Py_InspectFlag = 1;
|
// Note: pyconfig.inspect is set at init time based on config.interactive_mode
|
||||||
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()) {
|
||||||
// With --exec, run the game engine after scripts execute
|
// With --exec, scripts were already executed by the first GameEngine constructor.
|
||||||
// In headless mode, auto-exit when no timers remain
|
// Just configure auto-exit and run the existing engine to preserve timers/state.
|
||||||
McRogueFaceConfig mutable_config = config;
|
if (config.headless) {
|
||||||
if (mutable_config.headless) {
|
engine->setAutoExitAfterExec(true);
|
||||||
mutable_config.auto_exit_after_exec = true;
|
|
||||||
}
|
}
|
||||||
delete engine;
|
|
||||||
engine = new GameEngine(mutable_config);
|
|
||||||
McRFPy_API::game = engine;
|
|
||||||
engine->run();
|
engine->run();
|
||||||
McRFPy_API::api_shutdown();
|
McRFPy_API::api_shutdown();
|
||||||
delete engine;
|
delete engine;
|
||||||
|
|
|
||||||
|
|
@ -2,133 +2,132 @@
|
||||||
"""Test UIFrame clipping functionality"""
|
"""Test UIFrame clipping functionality"""
|
||||||
|
|
||||||
import mcrfpy
|
import mcrfpy
|
||||||
from mcrfpy import Color, Frame, Caption, Vector
|
from mcrfpy import Color, Frame, Caption
|
||||||
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(50, 50, 200, 150,
|
parent1 = Frame(pos=(50, 50), size=(200, 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)
|
||||||
parent1.name = "parent1"
|
parent1.name = "parent1"
|
||||||
scene.append(parent1)
|
scene.append(parent1)
|
||||||
|
|
||||||
# Create parent frame with clipping enabled
|
# 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),
|
fill_color=Color(200, 100, 100),
|
||||||
outline_color=Color(255, 255, 255),
|
outline_color=Color(255, 255, 255),
|
||||||
outline=2)
|
outline=2)
|
||||||
parent2.name = "parent2"
|
parent2.name = "parent2"
|
||||||
parent2.clip_children = True
|
parent2.clip_children = True
|
||||||
scene.append(parent2)
|
scene.append(parent2)
|
||||||
|
|
||||||
# Add captions to both frames
|
# 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.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(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.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(150, 100, 100, 100,
|
child1 = Frame(pos=(150, 100), size=(100, 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(150, 100, 100, 100,
|
child2 = Frame(pos=(150, 100), size=(100, 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
|
# Add caption to show clip state
|
||||||
status = Caption(50, 250,
|
status = Caption(text=f"Left frame: clip_children={parent1.clip_children}\n"
|
||||||
f"Left frame: clip_children={parent1.clip_children}\n"
|
f"Right frame: clip_children={parent2.clip_children}",
|
||||||
f"Right frame: clip_children={parent2.clip_children}")
|
pos=(50, 250))
|
||||||
status.font_size = 14
|
status.font_size = 14
|
||||||
status.fill_color = Color(255, 255, 255)
|
status.fill_color = Color(255, 255, 255)
|
||||||
scene.append(status)
|
scene.append(status)
|
||||||
|
|
||||||
# Add instructions
|
# Add instructions
|
||||||
instructions = Caption(50, 300,
|
instructions = Caption(text="Left: Children should overflow (no clipping)\n"
|
||||||
"Left: Children should overflow (no clipping)\n"
|
"Right: Children should be clipped to frame bounds\n"
|
||||||
"Right: Children should be clipped to frame bounds\n"
|
"Press 'c' to toggle clipping on left frame",
|
||||||
"Press 'c' to toggle clipping on left frame")
|
pos=(50, 300))
|
||||||
instructions.font_size = 12
|
instructions.font_size = 12
|
||||||
instructions.fill_color = Color(200, 200, 200)
|
instructions.fill_color = Color(200, 200, 200)
|
||||||
scene.append(instructions)
|
scene.append(instructions)
|
||||||
|
|
||||||
# Take screenshot
|
# Take screenshot
|
||||||
from mcrfpy import Window, automation
|
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}")
|
||||||
print(f"Parent2 clip_children: {parent2.clip_children}")
|
print(f"Parent2 clip_children: {parent2.clip_children}")
|
||||||
|
|
||||||
# Test toggling clip_children
|
# Test toggling clip_children
|
||||||
parent1.clip_children = True
|
parent1.clip_children = True
|
||||||
print(f"After toggle - Parent1 clip_children: {parent1.clip_children}")
|
print(f"After toggle - Parent1 clip_children: {parent1.clip_children}")
|
||||||
|
|
||||||
# Verify the property setter works
|
# Verify the property setter works
|
||||||
try:
|
try:
|
||||||
parent1.clip_children = "not a bool" # Should raise TypeError
|
parent1.clip_children = "not a bool"
|
||||||
print("ERROR: clip_children accepted non-boolean value")
|
print("ERROR: clip_children accepted non-boolean value")
|
||||||
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}")
|
||||||
|
|
||||||
# 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
|
# Start animation after a short delay
|
||||||
mcrfpy.setTimer("animate", animate_frames, 100)
|
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
|
# Main execution
|
||||||
print("Creating test scene...")
|
print("Creating test scene...")
|
||||||
mcrfpy.createScene("test")
|
mcrfpy.createScene("test")
|
||||||
mcrfpy.setScene("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)
|
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...")
|
|
||||||
|
|
|
||||||
|
|
@ -15,16 +15,16 @@ 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(50, 50, 400, 300,
|
outer = Frame(pos=(50, 50), size=(400, 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)
|
||||||
outer.name = "outer"
|
outer.name = "outer"
|
||||||
outer.clip_children = True
|
outer.clip_children = True
|
||||||
scene.append(outer)
|
scene.append(outer)
|
||||||
|
|
||||||
# Create inner frame that extends beyond outer bounds
|
# 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),
|
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(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.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(250, 100, 200, 150,
|
deeply_nested = Frame(pos=(250, 100), size=(200, 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)
|
||||||
|
|
@ -48,11 +48,11 @@ def test_nested_clipping(runtime):
|
||||||
inner.children.append(deeply_nested)
|
inner.children.append(deeply_nested)
|
||||||
|
|
||||||
# Add status text
|
# Add status text
|
||||||
status = Caption(50, 380,
|
status = Caption(text="Nested clipping test:\n"
|
||||||
"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))
|
||||||
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)
|
||||||
|
|
|
||||||
|
|
@ -10,17 +10,17 @@ call_count = 0
|
||||||
pause_test_count = 0
|
pause_test_count = 0
|
||||||
cancel_test_count = 0
|
cancel_test_count = 0
|
||||||
|
|
||||||
def timer_callback(elapsed_ms):
|
def timer_callback(timer, runtime):
|
||||||
global call_count
|
global call_count
|
||||||
call_count += 1
|
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
|
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(elapsed_ms):
|
def cancel_test_callback(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!")
|
||||||
|
|
@ -50,14 +50,14 @@ def run_tests(runtime):
|
||||||
timer2.pause()
|
timer2.pause()
|
||||||
print(f" Timer2 paused: {timer2.paused}")
|
print(f" Timer2 paused: {timer2.paused}")
|
||||||
print(f" Timer2 active: {timer2.active}")
|
print(f" Timer2 active: {timer2.active}")
|
||||||
|
|
||||||
# Schedule resume after another 400ms
|
# Schedule resume after another 400ms
|
||||||
def resume_timer2(runtime):
|
def resume_timer2(runtime):
|
||||||
print(" Resuming timer2...")
|
print(" Resuming timer2...")
|
||||||
timer2.resume()
|
timer2.resume()
|
||||||
print(f" Timer2 paused: {timer2.paused}")
|
print(f" Timer2 paused: {timer2.paused}")
|
||||||
print(f" Timer2 active: {timer2.active}")
|
print(f" Timer2 active: {timer2.active}")
|
||||||
|
|
||||||
mcrfpy.setTimer("resume_timer2", resume_timer2, 400)
|
mcrfpy.setTimer("resume_timer2", resume_timer2, 400)
|
||||||
|
|
||||||
mcrfpy.setTimer("pause_timer2", pause_timer2, 250)
|
mcrfpy.setTimer("pause_timer2", pause_timer2, 250)
|
||||||
|
|
@ -76,7 +76,7 @@ def run_tests(runtime):
|
||||||
|
|
||||||
# Test 4: Test interval modification
|
# Test 4: Test interval modification
|
||||||
print("\nTest 4: Testing 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")
|
print(f" Interval test fired at {runtime}ms")
|
||||||
|
|
||||||
timer4 = mcrfpy.Timer("interval_test", interval_test, 1000)
|
timer4 = mcrfpy.Timer("interval_test", interval_test, 1000)
|
||||||
|
|
@ -98,12 +98,12 @@ def run_tests(runtime):
|
||||||
print("\nTest 6: Testing restart functionality")
|
print("\nTest 6: Testing restart functionality")
|
||||||
restart_count = [0]
|
restart_count = [0]
|
||||||
|
|
||||||
def restart_test(runtime):
|
def restart_test(timer, runtime):
|
||||||
restart_count[0] += 1
|
restart_count[0] += 1
|
||||||
print(f" Restart test: {restart_count[0]}")
|
print(f" Restart test: {restart_count[0]}")
|
||||||
if restart_count[0] == 2:
|
if restart_count[0] == 2:
|
||||||
print(" Restarting timer...")
|
print(" Restarting timer...")
|
||||||
timer5.restart()
|
timer.restart()
|
||||||
|
|
||||||
timer5 = mcrfpy.Timer("restart_test", restart_test, 400)
|
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
|
# Create a frame that fills the game resolution to show boundaries
|
||||||
game_res = window.game_resolution
|
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),
|
fill_color=Color(50, 50, 100),
|
||||||
outline_color=Color(255, 255, 255),
|
outline_color=Color(255, 255, 255),
|
||||||
outline=2)
|
outline=2)
|
||||||
|
|
@ -41,13 +41,13 @@ def test_viewport_modes(runtime):
|
||||||
]
|
]
|
||||||
|
|
||||||
for x, y, label in corners:
|
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),
|
fill_color=Color(255, 100, 100),
|
||||||
outline_color=Color(255, 255, 255),
|
outline_color=Color(255, 255, 255),
|
||||||
outline=1)
|
outline=1)
|
||||||
scene.append(corner)
|
scene.append(corner)
|
||||||
|
|
||||||
text = Caption(x + 5, y + 5, label)
|
text = Caption(text=label, pos=(x + 5, y + 5))
|
||||||
text.font_size = 20
|
text.font_size = 20
|
||||||
text.fill_color = Color(255, 255, 255)
|
text.fill_color = Color(255, 255, 255)
|
||||||
scene.append(text)
|
scene.append(text)
|
||||||
|
|
@ -55,28 +55,28 @@ def test_viewport_modes(runtime):
|
||||||
# Add center crosshair
|
# Add center crosshair
|
||||||
center_x = game_res[0] // 2
|
center_x = game_res[0] // 2
|
||||||
center_y = game_res[1] // 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))
|
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))
|
fill_color=Color(255, 255, 0))
|
||||||
scene.append(h_line)
|
scene.append(h_line)
|
||||||
scene.append(v_line)
|
scene.append(v_line)
|
||||||
|
|
||||||
# Add mode indicator
|
# 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.font_size = 24
|
||||||
mode_text.fill_color = Color(255, 255, 255)
|
mode_text.fill_color = Color(255, 255, 255)
|
||||||
mode_text.name = "mode_text"
|
mode_text.name = "mode_text"
|
||||||
scene.append(mode_text)
|
scene.append(mode_text)
|
||||||
|
|
||||||
# Add instructions
|
# Add instructions
|
||||||
instructions = Caption(10, 40,
|
instructions = Caption(text="Press 1: Center mode (1:1 pixels)\n"
|
||||||
"Press 1: Center mode (1:1 pixels)\n"
|
"Press 2: Stretch mode (fill window)\n"
|
||||||
"Press 2: Stretch mode (fill window)\n"
|
"Press 3: Fit mode (maintain aspect ratio)\n"
|
||||||
"Press 3: Fit mode (maintain aspect ratio)\n"
|
"Press R: Change resolution\n"
|
||||||
"Press R: Change resolution\n"
|
"Press G: Change game resolution\n"
|
||||||
"Press G: Change game resolution\n"
|
"Press Esc: Exit",
|
||||||
"Press Esc: Exit")
|
pos=(10, 40))
|
||||||
instructions.font_size = 14
|
instructions.font_size = 14
|
||||||
instructions.fill_color = Color(200, 200, 200)
|
instructions.fill_color = Color(200, 200, 200)
|
||||||
scene.append(instructions)
|
scene.append(instructions)
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ def test_viewport_visual(runtime):
|
||||||
game_res = window.game_resolution
|
game_res = window.game_resolution
|
||||||
|
|
||||||
# Full boundary frame
|
# 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),
|
fill_color=Color(40, 40, 80),
|
||||||
outline_color=Color(255, 255, 0),
|
outline_color=Color(255, 255, 0),
|
||||||
outline=3)
|
outline=3)
|
||||||
|
|
@ -44,13 +44,13 @@ def test_viewport_visual(runtime):
|
||||||
labels = ["TL", "TR", "BL", "BR"]
|
labels = ["TL", "TR", "BL", "BR"]
|
||||||
|
|
||||||
for (x, y), color, label in zip(positions, colors, labels):
|
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,
|
fill_color=color,
|
||||||
outline_color=Color(255, 255, 255),
|
outline_color=Color(255, 255, 255),
|
||||||
outline=2)
|
outline=2)
|
||||||
scene.append(corner)
|
scene.append(corner)
|
||||||
|
|
||||||
text = Caption(x + 10, y + 10, label)
|
text = Caption(text=label, pos=(x + 10, y + 10))
|
||||||
text.font_size = 32
|
text.font_size = 32
|
||||||
text.fill_color = Color(0, 0, 0)
|
text.fill_color = Color(0, 0, 0)
|
||||||
scene.append(text)
|
scene.append(text)
|
||||||
|
|
@ -58,23 +58,23 @@ def test_viewport_visual(runtime):
|
||||||
# Center crosshair
|
# Center crosshair
|
||||||
center_x = game_res[0] // 2
|
center_x = game_res[0] // 2
|
||||||
center_y = game_res[1] // 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))
|
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))
|
fill_color=Color(255, 255, 255, 128))
|
||||||
scene.append(h_line)
|
scene.append(h_line)
|
||||||
scene.append(v_line)
|
scene.append(v_line)
|
||||||
|
|
||||||
# Mode text
|
# Mode text
|
||||||
mode_text = Caption(center_x - 100, center_y - 50,
|
mode_text = Caption(text=f"Mode: {window.scaling_mode}",
|
||||||
f"Mode: {window.scaling_mode}")
|
pos=(center_x - 100, center_y - 50))
|
||||||
mode_text.font_size = 36
|
mode_text.font_size = 36
|
||||||
mode_text.fill_color = Color(255, 255, 255)
|
mode_text.fill_color = Color(255, 255, 255)
|
||||||
scene.append(mode_text)
|
scene.append(mode_text)
|
||||||
|
|
||||||
# Resolution text
|
# Resolution text
|
||||||
res_text = Caption(center_x - 150, center_y + 10,
|
res_text = Caption(text=f"Game: {game_res[0]}x{game_res[1]}",
|
||||||
f"Game: {game_res[0]}x{game_res[1]}")
|
pos=(center_x - 150, center_y + 10))
|
||||||
res_text.font_size = 24
|
res_text.font_size = 24
|
||||||
res_text.fill_color = Color(200, 200, 200)
|
res_text.fill_color = Color(200, 200, 200)
|
||||||
scene.append(res_text)
|
scene.append(res_text)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue