Closes #127 Previously, `./mcrogueface --headless --exec <script>` would hang indefinitely after the script completed because the game loop ran continuously. This required external timeouts and explicit mcrfpy.exit() calls in every automation script. This commit adds automatic exit detection for headless+exec mode: - Added `auto_exit_after_exec` flag to McRogueFaceConfig - Set flag automatically when both --headless and --exec are present - Game loop exits when no timers remain (timers.empty()) Benefits: - Documentation generation scripts work without explicit exit calls - Testing scripts don't need timeout wrappers - Clean process termination for automation - Backward compatible (scripts with mcrfpy.exit() continue working) Changes: - src/McRogueFaceConfig.h: Add auto_exit_after_exec flag - src/main.cpp: Set flag and recreate engine with modified config - src/GameEngine.cpp: Check timers.empty() in game loop - ROADMAP.md: Mark Phase 7 as complete (2025-10-30) - CLAUDE.md: Add instruction about closing issues with commit messages 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
215 lines
7.6 KiB
C++
215 lines
7.6 KiB
C++
#include <SFML/Graphics.hpp>
|
|
#include "GameEngine.h"
|
|
#include "CommandLineParser.h"
|
|
#include "McRogueFaceConfig.h"
|
|
#include "McRFPy_API.h"
|
|
#include "PyFont.h"
|
|
#include "PyTexture.h"
|
|
#include <Python.h>
|
|
#include <iostream>
|
|
#include <filesystem>
|
|
|
|
// Forward declarations
|
|
int run_game_engine(const McRogueFaceConfig& config);
|
|
int run_python_interpreter(const McRogueFaceConfig& config, int argc, char* argv[]);
|
|
|
|
int main(int argc, char* argv[])
|
|
{
|
|
McRogueFaceConfig config;
|
|
CommandLineParser parser(argc, argv);
|
|
|
|
// Parse arguments
|
|
auto parse_result = parser.parse(config);
|
|
if (parse_result.should_exit) {
|
|
return parse_result.exit_code;
|
|
}
|
|
|
|
// Special handling for -m module: let Python handle modules properly
|
|
if (!config.python_module.empty()) {
|
|
config.python_mode = true;
|
|
}
|
|
|
|
// Initialize based on configuration
|
|
if (config.python_mode) {
|
|
return run_python_interpreter(config, argc, argv);
|
|
} else {
|
|
return run_game_engine(config);
|
|
}
|
|
}
|
|
|
|
int run_game_engine(const McRogueFaceConfig& config)
|
|
{
|
|
GameEngine g(config);
|
|
g.run();
|
|
if (Py_IsInitialized()) {
|
|
McRFPy_API::api_shutdown();
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
int run_python_interpreter(const McRogueFaceConfig& config, int argc, char* argv[])
|
|
{
|
|
// Create a game engine with the requested configuration
|
|
GameEngine* engine = new GameEngine(config);
|
|
|
|
// Initialize Python with configuration
|
|
McRFPy_API::init_python_with_config(config, argc, argv);
|
|
|
|
// Import mcrfpy module and store reference
|
|
McRFPy_API::mcrf_module = PyImport_ImportModule("mcrfpy");
|
|
if (!McRFPy_API::mcrf_module) {
|
|
PyErr_Print();
|
|
std::cerr << "Failed to import mcrfpy module" << std::endl;
|
|
} else {
|
|
// Set up default_font and default_texture if not already done
|
|
if (!McRFPy_API::default_font) {
|
|
McRFPy_API::default_font = std::make_shared<PyFont>("assets/JetbrainsMono.ttf");
|
|
McRFPy_API::default_texture = std::make_shared<PyTexture>("assets/kenney_tinydungeon.png", 16, 16);
|
|
}
|
|
PyObject_SetAttrString(McRFPy_API::mcrf_module, "default_font", McRFPy_API::default_font->pyObject());
|
|
PyObject_SetAttrString(McRFPy_API::mcrf_module, "default_texture", McRFPy_API::default_texture->pyObject());
|
|
}
|
|
|
|
// Handle different Python modes
|
|
if (!config.python_command.empty()) {
|
|
// Execute command from -c
|
|
if (config.interactive_mode) {
|
|
// Use PyRun_String to catch SystemExit
|
|
PyObject* main_module = PyImport_AddModule("__main__");
|
|
PyObject* main_dict = PyModule_GetDict(main_module);
|
|
PyObject* result_obj = PyRun_String(config.python_command.c_str(),
|
|
Py_file_input, main_dict, main_dict);
|
|
|
|
if (result_obj == NULL) {
|
|
// Check if it's SystemExit
|
|
if (PyErr_Occurred()) {
|
|
PyObject *type, *value, *traceback;
|
|
PyErr_Fetch(&type, &value, &traceback);
|
|
|
|
// If it's SystemExit and we're in interactive mode, clear it
|
|
if (PyErr_GivenExceptionMatches(type, PyExc_SystemExit)) {
|
|
PyErr_Clear();
|
|
} else {
|
|
// Re-raise other exceptions
|
|
PyErr_Restore(type, value, traceback);
|
|
PyErr_Print();
|
|
}
|
|
|
|
Py_XDECREF(type);
|
|
Py_XDECREF(value);
|
|
Py_XDECREF(traceback);
|
|
}
|
|
} else {
|
|
Py_DECREF(result_obj);
|
|
}
|
|
// Continue to interactive mode below
|
|
} else {
|
|
int result = PyRun_SimpleString(config.python_command.c_str());
|
|
McRFPy_API::api_shutdown();
|
|
delete engine;
|
|
return result;
|
|
}
|
|
}
|
|
else if (!config.python_module.empty()) {
|
|
// Execute module using runpy
|
|
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";
|
|
|
|
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
|
|
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) {
|
|
// Check if it was SystemExit
|
|
if (PyErr_Occurred()) {
|
|
PyObject *type, *value, *traceback;
|
|
PyErr_Fetch(&type, &value, &traceback);
|
|
|
|
if (PyErr_GivenExceptionMatches(type, PyExc_SystemExit)) {
|
|
PyErr_Clear();
|
|
result = 0; // Don't exit with error
|
|
} else {
|
|
PyErr_Restore(type, value, traceback);
|
|
PyErr_Print();
|
|
}
|
|
|
|
Py_XDECREF(type);
|
|
Py_XDECREF(value);
|
|
Py_XDECREF(traceback);
|
|
}
|
|
}
|
|
// Run interactive mode after script
|
|
PyRun_InteractiveLoop(stdin, "<stdin>");
|
|
}
|
|
|
|
// Run the game engine after script execution
|
|
engine->run();
|
|
|
|
McRFPy_API::api_shutdown();
|
|
delete engine;
|
|
return result;
|
|
}
|
|
else if (config.interactive_mode) {
|
|
// Interactive Python interpreter (only if explicitly requested with -i)
|
|
Py_InspectFlag = 1;
|
|
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;
|
|
}
|
|
delete engine;
|
|
engine = new GameEngine(mutable_config);
|
|
McRFPy_API::game = engine;
|
|
engine->run();
|
|
McRFPy_API::api_shutdown();
|
|
delete engine;
|
|
return 0;
|
|
}
|
|
|
|
delete engine;
|
|
return 0;
|
|
}
|