This commit enables McRogueFace to compile without SFML dependencies when built with -DMCRF_HEADLESS, a prerequisite for Emscripten/WebAssembly support. Changes: - Add src/platform/HeadlessTypes.h (~900 lines of SFML type stubs) - Consolidate all SFML includes through src/Common.h (15 files fixed) - Wrap ImGui-SFML with #ifndef MCRF_HEADLESS guards - Disable debug console/explorer in headless builds - Add comprehensive research document: docs/EMSCRIPTEN_RESEARCH.md The headless build compiles successfully but uses stub implementations that return failure/no-op. This proves the abstraction boundary is clean and enables future work on alternative backends (VRSFML, Emscripten). What still works in headless mode: - Python interpreter and script execution - libtcod integrations (pathfinding, FOV, noise, BSP, heightmaps) - Timer system and scene management - All game logic and data structures Build commands: Normal: make Headless: cmake .. -DCMAKE_CXX_FLAGS="-DMCRF_HEADLESS" && make Addresses #158 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
233 lines
8.3 KiB
C++
233 lines
8.3 KiB
C++
#include "Common.h"
|
|
#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 <fstream>
|
|
#include <filesystem>
|
|
|
|
// Forward declarations
|
|
int run_game_engine(const McRogueFaceConfig& config);
|
|
int run_python_interpreter(const McRogueFaceConfig& config);
|
|
|
|
int main(int argc, char* argv[])
|
|
{
|
|
McRogueFaceConfig config;
|
|
CommandLineParser parser(argc, argv);
|
|
|
|
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);
|
|
} 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 exception exit code if a Python exception signaled exit
|
|
if (McRFPy_API::shouldExit()) {
|
|
return McRFPy_API::exit_code.load();
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
int run_python_interpreter(const McRogueFaceConfig& config)
|
|
{
|
|
// Create a game engine with the requested configuration
|
|
GameEngine* engine = new GameEngine(config);
|
|
|
|
// 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");
|
|
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 (sys.argv already set at init time)
|
|
std::string run_module_code =
|
|
"import runpy\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 (sys.argv already set at init time)
|
|
// Note: Using PyRun_SimpleString instead of PyRun_SimpleFile for better
|
|
// cross-platform compatibility (PyRun_SimpleFile has issues with MinGW/Wine)
|
|
|
|
// Read file contents
|
|
std::ifstream file(config.script_path);
|
|
if (!file.is_open()) {
|
|
std::cerr << "mcrogueface: can't open file '" << config.script_path << "': ";
|
|
std::cerr << "[Errno " << errno << "] " << strerror(errno) << std::endl;
|
|
delete engine;
|
|
return 1;
|
|
}
|
|
std::string script_content((std::istreambuf_iterator<char>(file)),
|
|
std::istreambuf_iterator<char>());
|
|
file.close();
|
|
|
|
// Set __file__ before execution
|
|
PyObject* main_module = PyImport_AddModule("__main__");
|
|
PyObject* main_dict = PyModule_GetDict(main_module);
|
|
PyObject* py_filename = PyUnicode_FromString(config.script_path.string().c_str());
|
|
PyDict_SetItemString(main_dict, "__file__", py_filename);
|
|
Py_DECREF(py_filename);
|
|
|
|
int result = PyRun_SimpleString(script_content.c_str());
|
|
|
|
// Flush Python stdout/stderr to ensure output appears before engine->run() blocks
|
|
PyObject* sys_module = PyImport_ImportModule("sys");
|
|
if (sys_module) {
|
|
PyObject* stdout_obj = PyObject_GetAttrString(sys_module, "stdout");
|
|
if (stdout_obj) {
|
|
PyObject_CallMethod(stdout_obj, "flush", NULL);
|
|
Py_DECREF(stdout_obj);
|
|
}
|
|
PyObject* stderr_obj = PyObject_GetAttrString(sys_module, "stderr");
|
|
if (stderr_obj) {
|
|
PyObject_CallMethod(stderr_obj, "flush", NULL);
|
|
Py_DECREF(stderr_obj);
|
|
}
|
|
Py_DECREF(sys_module);
|
|
}
|
|
|
|
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 exception exit code if signaled
|
|
if (McRFPy_API::shouldExit()) {
|
|
return McRFPy_API::exit_code.load();
|
|
}
|
|
return result;
|
|
}
|
|
else if (config.interactive_mode) {
|
|
// Interactive Python interpreter (only if explicitly requested with -i)
|
|
// 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()) {
|
|
// Execute startup scripts on the existing engine (not in constructor to prevent double-execution)
|
|
engine->executeStartupScripts();
|
|
if (config.headless) {
|
|
engine->setAutoExitAfterExec(true);
|
|
}
|
|
engine->run();
|
|
McRFPy_API::api_shutdown();
|
|
delete engine;
|
|
// Return exception exit code if signaled
|
|
if (McRFPy_API::shouldExit()) {
|
|
return McRFPy_API::exit_code.load();
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
delete engine;
|
|
return 0;
|
|
}
|