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
14 changed files with 240 additions and 203 deletions
65
src/main.cpp
65
src/main.cpp
|
|
@ -11,27 +11,27 @@
|
|||
|
||||
// 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[])
|
||||
{
|
||||
McRogueFaceConfig config;
|
||||
CommandLineParser parser(argc, argv);
|
||||
|
||||
|
||||
// Parse arguments
|
||||
auto parse_result = parser.parse(config);
|
||||
if (parse_result.should_exit) {
|
||||
return parse_result.exit_code;
|
||||
}
|
||||
|
||||
|
||||
// Special handling for -m module: let Python handle modules properly
|
||||
if (!config.python_module.empty()) {
|
||||
config.python_mode = true;
|
||||
}
|
||||
|
||||
|
||||
// Initialize based on configuration
|
||||
if (config.python_mode) {
|
||||
return run_python_interpreter(config, 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,49 +116,28 @@ int run_python_interpreter(const McRogueFaceConfig& config, int argc, char* argv
|
|||
}
|
||||
}
|
||||
else if (!config.python_module.empty()) {
|
||||
// Execute module using runpy
|
||||
std::string run_module_code =
|
||||
"import sys\n"
|
||||
// Execute module using runpy (sys.argv already set at init time)
|
||||
std::string run_module_code =
|
||||
"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();
|
||||
delete engine;
|
||||
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 << "': ";
|
||||
std::cerr << "[Errno " << errno << "] " << strerror(errno) << std::endl;
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Set up sys.argv
|
||||
wchar_t** python_argv = new wchar_t*[config.script_args.size() + 1];
|
||||
python_argv[0] = Py_DecodeLocale(config.script_path.string().c_str(), nullptr);
|
||||
for (size_t i = 0; i < config.script_args.size(); i++) {
|
||||
python_argv[i + 1] = Py_DecodeLocale(config.script_args[i].c_str(), nullptr);
|
||||
}
|
||||
PySys_SetArgvEx(config.script_args.size() + 1, python_argv, 0);
|
||||
|
||||
|
||||
int result = PyRun_SimpleFile(fp, config.script_path.string().c_str());
|
||||
fclose(fp);
|
||||
|
||||
// Clean up
|
||||
for (size_t i = 0; i <= config.script_args.size(); i++) {
|
||||
PyMem_RawFree(python_argv[i]);
|
||||
}
|
||||
delete[] python_argv;
|
||||
|
||||
if (config.interactive_mode) {
|
||||
// Even if script had SystemExit, continue to interactive mode
|
||||
if (result != 0) {
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue