McRogueFace/src/main.cpp
John McCardle 8f8b72da4a feat: auto-exit in --headless --exec mode when script completes
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>
2025-10-30 22:52:52 -04:00

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;
}