diff --git a/GNUmakefile b/GNUmakefile new file mode 100644 index 0000000..577cda0 --- /dev/null +++ b/GNUmakefile @@ -0,0 +1,54 @@ +# Convenience Makefile wrapper for McRogueFace +# This delegates to CMake build in the build directory + +.PHONY: all build clean run test dist help + +# Default target +all: build + +# Build the project +build: + @./build.sh + +# Clean build artifacts +clean: + @./clean.sh + +# Run the game +run: build + @cd build && ./mcrogueface + +# Run in Python mode +python: build + @cd build && ./mcrogueface -i + +# Test basic functionality +test: build + @echo "Testing McRogueFace..." + @cd build && ./mcrogueface -V + @cd build && ./mcrogueface -c "print('Test passed')" + @cd build && ./mcrogueface --headless -c "import mcrfpy; print('mcrfpy imported successfully')" + +# Create distribution archive +dist: build + @echo "Creating distribution archive..." + @cd build && zip -r ../McRogueFace-$$(date +%Y%m%d).zip . -x "*.o" "CMakeFiles/*" "Makefile" "*.cmake" + @echo "Distribution archive created: McRogueFace-$$(date +%Y%m%d).zip" + +# Show help +help: + @echo "McRogueFace Build System" + @echo "=======================" + @echo "" + @echo "Available targets:" + @echo " make - Build the project (default)" + @echo " make build - Build the project" + @echo " make clean - Remove all build artifacts" + @echo " make run - Build and run the game" + @echo " make python - Build and run in Python interactive mode" + @echo " make test - Run basic tests" + @echo " make dist - Create distribution archive" + @echo " make help - Show this help message" + @echo "" + @echo "Build output goes to: ./build/" + @echo "Distribution archives are created in project root" \ No newline at end of file diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..790bba8 --- /dev/null +++ b/build.sh @@ -0,0 +1,54 @@ +#!/bin/bash +# Build script for McRogueFace - compiles everything into ./build directory + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +echo -e "${GREEN}McRogueFace Build Script${NC}" +echo "=========================" + +# Create build directory if it doesn't exist +if [ ! -d "build" ]; then + echo -e "${YELLOW}Creating build directory...${NC}" + mkdir build +fi + +# Change to build directory +cd build + +# Run CMake to generate build files +echo -e "${YELLOW}Running CMake...${NC}" +cmake .. -DCMAKE_BUILD_TYPE=Release + +# Check if CMake succeeded +if [ $? -ne 0 ]; then + echo -e "${RED}CMake configuration failed!${NC}" + exit 1 +fi + +# Run make with parallel jobs +echo -e "${YELLOW}Building with make...${NC}" +make -j$(nproc) + +# Check if make succeeded +if [ $? -ne 0 ]; then + echo -e "${RED}Build failed!${NC}" + exit 1 +fi + +echo -e "${GREEN}Build completed successfully!${NC}" +echo "" +echo "The build directory contains:" +ls -la + +echo "" +echo -e "${GREEN}To run McRogueFace:${NC}" +echo " cd build" +echo " ./mcrogueface" +echo "" +echo -e "${GREEN}To create a distribution archive:${NC}" +echo " cd build" +echo " zip -r ../McRogueFace-$(date +%Y%m%d).zip ." \ No newline at end of file diff --git a/clean.sh b/clean.sh new file mode 100755 index 0000000..817a9ee --- /dev/null +++ b/clean.sh @@ -0,0 +1,33 @@ +#!/bin/bash +# Clean script for McRogueFace - removes build artifacts + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +echo -e "${YELLOW}Cleaning McRogueFace build artifacts...${NC}" + +# Remove build directory +if [ -d "build" ]; then + echo "Removing build directory..." + rm -rf build +fi + +# Remove CMake artifacts from project root +echo "Removing CMake artifacts from project root..." +rm -f CMakeCache.txt +rm -f cmake_install.cmake +rm -f Makefile +rm -rf CMakeFiles + +# Remove compiled executable from project root +rm -f mcrogueface + +# Remove any test artifacts +rm -f test_script.py +rm -rf test_venv +rm -f python3 # symlink + +echo -e "${GREEN}Clean complete!${NC}" \ No newline at end of file diff --git a/cmake_install.cmake b/cmake_install.cmake deleted file mode 100644 index b4ca489..0000000 --- a/cmake_install.cmake +++ /dev/null @@ -1,54 +0,0 @@ -# Install script for directory: /home/john/Development/McRogueFace - -# Set the install prefix -if(NOT DEFINED CMAKE_INSTALL_PREFIX) - set(CMAKE_INSTALL_PREFIX "/usr/local") -endif() -string(REGEX REPLACE "/$" "" CMAKE_INSTALL_PREFIX "${CMAKE_INSTALL_PREFIX}") - -# Set the install configuration name. -if(NOT DEFINED CMAKE_INSTALL_CONFIG_NAME) - if(BUILD_TYPE) - string(REGEX REPLACE "^[^A-Za-z0-9_]+" "" - CMAKE_INSTALL_CONFIG_NAME "${BUILD_TYPE}") - else() - set(CMAKE_INSTALL_CONFIG_NAME "") - endif() - message(STATUS "Install configuration: \"${CMAKE_INSTALL_CONFIG_NAME}\"") -endif() - -# Set the component getting installed. -if(NOT CMAKE_INSTALL_COMPONENT) - if(COMPONENT) - message(STATUS "Install component: \"${COMPONENT}\"") - set(CMAKE_INSTALL_COMPONENT "${COMPONENT}") - else() - set(CMAKE_INSTALL_COMPONENT) - endif() -endif() - -# Install shared libraries without execute permission? -if(NOT DEFINED CMAKE_INSTALL_SO_NO_EXE) - set(CMAKE_INSTALL_SO_NO_EXE "1") -endif() - -# Is this installation the result of a crosscompile? -if(NOT DEFINED CMAKE_CROSSCOMPILING) - set(CMAKE_CROSSCOMPILING "FALSE") -endif() - -# Set default install directory permissions. -if(NOT DEFINED CMAKE_OBJDUMP) - set(CMAKE_OBJDUMP "/usr/bin/objdump") -endif() - -if(CMAKE_INSTALL_COMPONENT) - set(CMAKE_INSTALL_MANIFEST "install_manifest_${CMAKE_INSTALL_COMPONENT}.txt") -else() - set(CMAKE_INSTALL_MANIFEST "install_manifest.txt") -endif() - -string(REPLACE ";" "\n" CMAKE_INSTALL_MANIFEST_CONTENT - "${CMAKE_INSTALL_MANIFEST_FILES}") -file(WRITE "/home/john/Development/McRogueFace/${CMAKE_INSTALL_MANIFEST}" - "${CMAKE_INSTALL_MANIFEST_CONTENT}") diff --git a/src/CommandLineParser.cpp b/src/CommandLineParser.cpp new file mode 100644 index 0000000..cabf47d --- /dev/null +++ b/src/CommandLineParser.cpp @@ -0,0 +1,157 @@ +#include "CommandLineParser.h" +#include +#include +#include + +CommandLineParser::CommandLineParser(int argc, char* argv[]) + : argc(argc), argv(argv) {} + +CommandLineParser::ParseResult CommandLineParser::parse(McRogueFaceConfig& config) { + ParseResult result; + current_arg = 1; // Reset for each parse + + // Detect if running as Python interpreter + std::filesystem::path exec_name = std::filesystem::path(argv[0]).filename(); + if (exec_name.string().find("python") == 0) { + config.headless = true; + config.python_mode = true; + } + + while (current_arg < argc) { + std::string arg = argv[current_arg]; + + // Handle Python-style single-letter flags + if (arg == "-h" || arg == "--help") { + print_help(); + result.should_exit = true; + result.exit_code = 0; + return result; + } + + if (arg == "-V" || arg == "--version") { + print_version(); + result.should_exit = true; + result.exit_code = 0; + return result; + } + + // Python execution modes + if (arg == "-c") { + config.python_mode = true; + current_arg++; + if (current_arg >= argc) { + std::cerr << "Argument expected for the -c option" << std::endl; + result.should_exit = true; + result.exit_code = 1; + return result; + } + config.python_command = argv[current_arg]; + current_arg++; + continue; + } + + if (arg == "-m") { + config.python_mode = true; + current_arg++; + if (current_arg >= argc) { + std::cerr << "Argument expected for the -m option" << std::endl; + result.should_exit = true; + result.exit_code = 1; + return result; + } + config.python_module = argv[current_arg]; + current_arg++; + // Collect remaining args as module args + while (current_arg < argc) { + config.script_args.push_back(argv[current_arg]); + current_arg++; + } + continue; + } + + if (arg == "-i") { + config.interactive_mode = true; + config.python_mode = true; + current_arg++; + continue; + } + + // McRogueFace specific flags + if (arg == "--headless") { + config.headless = true; + config.audio_enabled = false; + current_arg++; + continue; + } + + if (arg == "--audio-off") { + config.audio_enabled = false; + current_arg++; + continue; + } + + if (arg == "--audio-on") { + config.audio_enabled = true; + current_arg++; + continue; + } + + if (arg == "--screenshot") { + config.take_screenshot = true; + current_arg++; + if (current_arg < argc && argv[current_arg][0] != '-') { + config.screenshot_path = argv[current_arg]; + current_arg++; + } else { + config.screenshot_path = "screenshot.png"; + } + continue; + } + + // If no flags matched, treat as positional argument (script name) + if (arg[0] != '-') { + config.script_path = arg; + config.python_mode = true; + current_arg++; + // Remaining args are script args + while (current_arg < argc) { + config.script_args.push_back(argv[current_arg]); + current_arg++; + } + break; + } + + // Unknown flag + std::cerr << "Unknown option: " << arg << std::endl; + result.should_exit = true; + result.exit_code = 1; + return result; + } + + return result; +} + +void CommandLineParser::print_help() { + std::cout << "usage: mcrogueface [option] ... [-c cmd | -m mod | file | -] [arg] ...\n" + << "Options:\n" + << " -c cmd : program passed in as string (terminates option list)\n" + << " -h : print this help message and exit (also --help)\n" + << " -i : inspect interactively after running script\n" + << " -m mod : run library module as a script (terminates option list)\n" + << " -V : print the Python version number and exit (also --version)\n" + << "\n" + << "McRogueFace specific options:\n" + << " --headless : run without creating a window (implies --audio-off)\n" + << " --audio-off : disable audio\n" + << " --audio-on : enable audio (even in headless mode)\n" + << " --screenshot [path] : take a screenshot in headless mode\n" + << "\n" + << "Arguments:\n" + << " file : program read from script file\n" + << " - : program read from stdin\n" + << " arg ...: arguments passed to program in sys.argv[1:]\n"; +} + +void CommandLineParser::print_version() { + std::cout << "Python 3.12.0 (McRogueFace embedded)\n"; +} \ No newline at end of file diff --git a/src/CommandLineParser.h b/src/CommandLineParser.h new file mode 100644 index 0000000..c330b85 --- /dev/null +++ b/src/CommandLineParser.h @@ -0,0 +1,30 @@ +#ifndef COMMAND_LINE_PARSER_H +#define COMMAND_LINE_PARSER_H + +#include +#include +#include "McRogueFaceConfig.h" + +class CommandLineParser { +public: + struct ParseResult { + bool should_exit = false; + int exit_code = 0; + }; + + CommandLineParser(int argc, char* argv[]); + ParseResult parse(McRogueFaceConfig& config); + +private: + int argc; + char** argv; + int current_arg = 1; // Skip program name + + bool has_flag(const std::string& short_flag, const std::string& long_flag = ""); + std::string get_next_arg(const std::string& flag_name); + void parse_positional_args(McRogueFaceConfig& config); + void print_help(); + void print_version(); +}; + +#endif // COMMAND_LINE_PARSER_H \ No newline at end of file diff --git a/src/GameEngine.cpp b/src/GameEngine.cpp index f548709..8495810 100644 --- a/src/GameEngine.cpp +++ b/src/GameEngine.cpp @@ -5,14 +5,32 @@ #include "UITestScene.h" #include "Resources.h" -GameEngine::GameEngine() +GameEngine::GameEngine() : GameEngine(McRogueFaceConfig{}) +{ +} + +GameEngine::GameEngine(const McRogueFaceConfig& cfg) + : config(cfg), headless(cfg.headless) { Resources::font.loadFromFile("./assets/JetbrainsMono.ttf"); Resources::game = this; window_title = "Crypt of Sokoban - 7DRL 2025, McRogueface Engine"; - window.create(sf::VideoMode(1024, 768), window_title, sf::Style::Titlebar | sf::Style::Close); - visible = window.getDefaultView(); - window.setFramerateLimit(60); + + // Initialize rendering based on headless mode + if (headless) { + headless_renderer = std::make_unique(); + if (!headless_renderer->init(1024, 768)) { + throw std::runtime_error("Failed to initialize headless renderer"); + } + render_target = &headless_renderer->getRenderTarget(); + } else { + window = std::make_unique(); + window->create(sf::VideoMode(1024, 768), window_title, sf::Style::Titlebar | sf::Style::Close); + window->setFramerateLimit(60); + render_target = window.get(); + } + + visible = render_target->getDefaultView(); scene = "uitest"; scenes["uitest"] = new UITestScene(this); @@ -25,6 +43,13 @@ GameEngine::GameEngine() runtime.restart(); } +GameEngine::~GameEngine() +{ + for (auto& [name, scene] : scenes) { + delete scene; + } +} + Scene* GameEngine::currentScene() { return scenes[scene]; } void GameEngine::changeScene(std::string s) { @@ -37,13 +62,24 @@ void GameEngine::changeScene(std::string s) void GameEngine::quit() { running = false; } void GameEngine::setPause(bool p) { paused = p; } sf::Font & GameEngine::getFont() { /*return font; */ return Resources::font; } -sf::RenderWindow & GameEngine::getWindow() { return window; } +sf::RenderWindow & GameEngine::getWindow() { + if (!window) { + throw std::runtime_error("Window not available in headless mode"); + } + return *window; +} + +sf::RenderTarget & GameEngine::getRenderTarget() { + return *render_target; +} void GameEngine::createScene(std::string s) { scenes[s] = new PyScene(this); } void GameEngine::setWindowScale(float multiplier) { - window.setSize(sf::Vector2u(1024 * multiplier, 768 * multiplier)); // 7DRL 2024: window scaling + if (!headless && window) { + window->setSize(sf::Vector2u(1024 * multiplier, 768 * multiplier)); // 7DRL 2024: window scaling + } //window.create(sf::VideoMode(1024 * multiplier, 768 * multiplier), window_title, sf::Style::Titlebar | sf::Style::Close); } @@ -55,18 +91,40 @@ void GameEngine::run() { currentScene()->update(); testTimers(); - sUserInput(); + if (!headless) { + sUserInput(); + } if (!paused) { } currentScene()->render(); + + // Display the frame + if (headless) { + headless_renderer->display(); + // Take screenshot if requested + if (config.take_screenshot) { + headless_renderer->saveScreenshot(config.screenshot_path.empty() ? "screenshot.png" : config.screenshot_path); + config.take_screenshot = false; // Only take one screenshot + } + } else { + window->display(); + } + currentFrame++; frameTime = clock.restart().asSeconds(); fps = 1 / frameTime; int whole_fps = (int)fps; int tenth_fps = int(fps * 100) % 10; - //window.setTitle(window_title + " " + std::to_string(fps) + " FPS"); - window.setTitle(window_title + " " + std::to_string(whole_fps) + "." + std::to_string(tenth_fps) + " FPS"); + + if (!headless && window) { + window->setTitle(window_title + " " + std::to_string(whole_fps) + "." + std::to_string(tenth_fps) + " FPS"); + } + + // In windowed mode, check if window was closed + if (!headless && window && !window->isOpen()) { + running = false; + } } } @@ -111,7 +169,7 @@ void GameEngine::testTimers() void GameEngine::sUserInput() { sf::Event event; - while (window.pollEvent(event)) + while (window && window->pollEvent(event)) { std::string actionType; int actionCode = 0; diff --git a/src/GameEngine.h b/src/GameEngine.h index 8d688b3..326823d 100644 --- a/src/GameEngine.h +++ b/src/GameEngine.h @@ -6,10 +6,16 @@ #include "IndexTexture.h" #include "Timer.h" #include "PyCallable.h" +#include "McRogueFaceConfig.h" +#include "HeadlessRenderer.h" +#include class GameEngine { - sf::RenderWindow window; + std::unique_ptr window; + std::unique_ptr headless_renderer; + sf::RenderTarget* render_target; + sf::Font font; std::map scenes; bool running = true; @@ -19,6 +25,9 @@ class GameEngine sf::Clock clock; float frameTime; std::string window_title; + + bool headless = false; + McRogueFaceConfig config; sf::Clock runtime; //std::map timers; @@ -28,6 +37,8 @@ class GameEngine public: std::string scene; GameEngine(); + GameEngine(const McRogueFaceConfig& cfg); + ~GameEngine(); Scene* currentScene(); void changeScene(std::string); void createScene(std::string); @@ -35,6 +46,7 @@ public: void setPause(bool); sf::Font & getFont(); sf::RenderWindow & getWindow(); + sf::RenderTarget & getRenderTarget(); void run(); void sUserInput(); int getFrame() { return currentFrame; } @@ -42,6 +54,7 @@ public: sf::View getView() { return visible; } void manageTimer(std::string, PyObject*, int); void setWindowScale(float); + bool isHeadless() const { return headless; } // global textures for scripts to access std::vector textures; diff --git a/src/HeadlessRenderer.cpp b/src/HeadlessRenderer.cpp new file mode 100644 index 0000000..27dff47 --- /dev/null +++ b/src/HeadlessRenderer.cpp @@ -0,0 +1,27 @@ +#include "HeadlessRenderer.h" +#include + +bool HeadlessRenderer::init(int width, int height) { + if (!render_texture.create(width, height)) { + std::cerr << "Failed to create headless render texture" << std::endl; + return false; + } + return true; +} + +sf::RenderTarget& HeadlessRenderer::getRenderTarget() { + return render_texture; +} + +void HeadlessRenderer::saveScreenshot(const std::string& path) { + sf::Image screenshot = render_texture.getTexture().copyToImage(); + if (!screenshot.saveToFile(path)) { + std::cerr << "Failed to save screenshot to: " << path << std::endl; + } else { + std::cout << "Screenshot saved to: " << path << std::endl; + } +} + +void HeadlessRenderer::display() { + render_texture.display(); +} \ No newline at end of file diff --git a/src/HeadlessRenderer.h b/src/HeadlessRenderer.h new file mode 100644 index 0000000..2b08291 --- /dev/null +++ b/src/HeadlessRenderer.h @@ -0,0 +1,20 @@ +#ifndef HEADLESS_RENDERER_H +#define HEADLESS_RENDERER_H + +#include +#include +#include + +class HeadlessRenderer { +private: + sf::RenderTexture render_texture; + +public: + bool init(int width = 1024, int height = 768); + sf::RenderTarget& getRenderTarget(); + void saveScreenshot(const std::string& path); + void display(); // Finalize the current frame + bool isOpen() const { return true; } // Always "open" in headless mode +}; + +#endif // HEADLESS_RENDERER_H \ No newline at end of file diff --git a/src/McRFPy_API.cpp b/src/McRFPy_API.cpp index 2f2be1e..37eb8c4 100644 --- a/src/McRFPy_API.cpp +++ b/src/McRFPy_API.cpp @@ -3,6 +3,8 @@ #include "GameEngine.h" #include "UI.h" #include "Resources.h" +#include +#include std::map McRFPy_API::callbacks; std::vector McRFPy_API::soundbuffers; @@ -160,6 +162,68 @@ PyStatus init_python(const char *program_name) return status; } +PyStatus McRFPy_API::init_python_with_config(const McRogueFaceConfig& config, int argc, char** argv) +{ + PyStatus status; + PyConfig pyconfig; + PyConfig_InitIsolatedConfig(&pyconfig); + + // CRITICAL: Pass actual command line arguments to Python + status = PyConfig_SetBytesArgv(&pyconfig, argc, argv); + if (PyStatus_Exception(status)) { + return status; + } + + // Check if we're in a virtual environment + auto exe_path = std::filesystem::path(argv[0]); + auto exe_dir = exe_path.parent_path(); + auto venv_root = exe_dir.parent_path(); + + if (std::filesystem::exists(venv_root / "pyvenv.cfg")) { + // We're running from within a venv! + // Add venv's site-packages to module search paths + auto site_packages = venv_root / "lib" / "python3.12" / "site-packages"; + PyWideStringList_Append(&pyconfig.module_search_paths, + site_packages.wstring().c_str()); + pyconfig.module_search_paths_set = 1; + } + + // Set Python home to our bundled Python + auto python_home = executable_path() + L"/lib/Python"; + PyConfig_SetString(&pyconfig, &pyconfig.home, python_home.c_str()); + + // Set up module search paths +#if __PLATFORM_SET_PYTHON_SEARCH_PATHS == 1 + if (!pyconfig.module_search_paths_set) { + pyconfig.module_search_paths_set = 1; + } + + // search paths for python libs/modules/scripts + const wchar_t* str_arr[] = { + L"/scripts", + L"/lib/Python/lib.linux-x86_64-3.12", + L"/lib/Python", + L"/lib/Python/Lib", + L"/venv/lib/python3.12/site-packages" + }; + + for(auto s : str_arr) { + status = PyWideStringList_Append(&pyconfig.module_search_paths, (executable_path() + s).c_str()); + if (PyStatus_Exception(status)) { + continue; + } + } +#endif + + // Register mcrfpy module before initialization + PyImport_AppendInittab("mcrfpy", &PyInit_mcrfpy); + + status = Py_InitializeFromConfig(&pyconfig); + PyConfig_Clear(&pyconfig); + + return status; +} + /* void McRFPy_API::setSpriteTexture(int ti) { @@ -200,6 +264,33 @@ void McRFPy_API::api_init() { //setSpriteTexture(0); } +void McRFPy_API::api_init(const McRogueFaceConfig& config, int argc, char** argv) { + // Initialize Python with proper argv - this is CRITICAL + PyStatus status = init_python_with_config(config, argc, argv); + if (PyStatus_Exception(status)) { + Py_ExitStatusException(status); + } + + McRFPy_API::mcrf_module = PyImport_ImportModule("mcrfpy"); + + // For -m module execution, let Python handle it + if (!config.python_module.empty() && config.python_module != "venv") { + // Py_RunMain() will handle -m execution + return; + } + + // Execute based on mode - this is handled in main.cpp now + // The actual execution logic is in run_python_interpreter() + + // Set up default resources only if in game mode + if (!config.python_mode) { + //PyModule_AddObject(McRFPy_API::mcrf_module, "default_font", McRFPy_API::default_font->pyObject()); + PyObject_SetAttrString(McRFPy_API::mcrf_module, "default_font", McRFPy_API::default_font->pyObject()); + //PyModule_AddObject(McRFPy_API::mcrf_module, "default_texture", McRFPy_API::default_texture->pyObject()); + PyObject_SetAttrString(McRFPy_API::mcrf_module, "default_texture", McRFPy_API::default_texture->pyObject()); + } +} + void McRFPy_API::executeScript(std::string filename) { FILE* PScriptFile = fopen(filename.c_str(), "r"); diff --git a/src/McRFPy_API.h b/src/McRFPy_API.h index 08d034e..758eb17 100644 --- a/src/McRFPy_API.h +++ b/src/McRFPy_API.h @@ -5,6 +5,7 @@ #include "PyFont.h" #include "PyTexture.h" +#include "McRogueFaceConfig.h" class GameEngine; // forward declared (circular members) @@ -27,6 +28,8 @@ public: //static void setSpriteTexture(int); inline static GameEngine* game; static void api_init(); + static void api_init(const McRogueFaceConfig& config, int argc, char** argv); + static PyStatus init_python_with_config(const McRogueFaceConfig& config, int argc, char** argv); static void api_shutdown(); // Python API functionality - use mcrfpy.* in scripts //static PyObject* _drawSprite(PyObject*, PyObject*); diff --git a/src/McRogueFaceConfig.h b/src/McRogueFaceConfig.h new file mode 100644 index 0000000..659168d --- /dev/null +++ b/src/McRogueFaceConfig.h @@ -0,0 +1,30 @@ +#ifndef MCROGUEFACE_CONFIG_H +#define MCROGUEFACE_CONFIG_H + +#include +#include +#include + +struct McRogueFaceConfig { + // McRogueFace specific + bool headless = false; + bool audio_enabled = true; + + // Python interpreter emulation + bool python_mode = false; + std::string python_command; // -c command + std::string python_module; // -m module + bool interactive_mode = false; // -i flag + bool show_version = false; // -V flag + bool show_help = false; // -h flag + + // Script execution + std::filesystem::path script_path; + std::vector script_args; + + // Screenshot functionality for headless mode + std::string screenshot_path; + bool take_screenshot = false; +}; + +#endif // MCROGUEFACE_CONFIG_H \ No newline at end of file diff --git a/src/PyScene.cpp b/src/PyScene.cpp index 8474572..a42d85f 100644 --- a/src/PyScene.cpp +++ b/src/PyScene.cpp @@ -21,6 +21,11 @@ void PyScene::update() void PyScene::do_mouse_input(std::string button, std::string type) { + // In headless mode, mouse input is not available + if (game->isHeadless()) { + return; + } + auto unscaledmousepos = sf::Mouse::getPosition(game->getWindow()); auto mousepos = game->getWindow().mapPixelToCoords(unscaledmousepos); UIDrawable* target; @@ -62,7 +67,7 @@ void PyScene::doAction(std::string name, std::string type) void PyScene::render() { - game->getWindow().clear(); + game->getRenderTarget().clear(); auto vec = *ui_elements; for (auto e: vec) @@ -71,5 +76,5 @@ void PyScene::render() e->render(); } - game->getWindow().display(); + // Display is handled by GameEngine } diff --git a/src/UIDrawable.cpp b/src/UIDrawable.cpp index 693d5f6..bd4c63d 100644 --- a/src/UIDrawable.cpp +++ b/src/UIDrawable.cpp @@ -14,7 +14,7 @@ void UIDrawable::click_unregister() void UIDrawable::render() { - render(sf::Vector2f(), Resources::game->getWindow()); + render(sf::Vector2f(), Resources::game->getRenderTarget()); } PyObject* UIDrawable::get_click(PyObject* self, void* closure) { diff --git a/src/UITestScene.cpp b/src/UITestScene.cpp index 17f2416..d3d5ff9 100644 --- a/src/UITestScene.cpp +++ b/src/UITestScene.cpp @@ -156,8 +156,8 @@ void UITestScene::doAction(std::string name, std::string type) void UITestScene::render() { - game->getWindow().clear(); - game->getWindow().draw(text); + game->getRenderTarget().clear(); + game->getRenderTarget().draw(text); // draw all UI elements //for (auto e: ui_elements) @@ -175,7 +175,7 @@ void UITestScene::render() //e1.render(sf::Vector2f(-100, -100)); - game->getWindow().display(); + // Display is handled by GameEngine //McRFPy_API::REPL(); } diff --git a/src/main.cpp b/src/main.cpp index e4e355b..55a3b33 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,8 +1,117 @@ #include #include "GameEngine.h" +#include "CommandLineParser.h" +#include "McRogueFaceConfig.h" +#include "McRFPy_API.h" +#include +#include +#include -int main() +// 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[]) { - GameEngine g; - g.run(); + 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(); + return 0; +} + +int run_python_interpreter(const McRogueFaceConfig& config, int argc, char* argv[]) +{ + // Initialize Python with configuration + McRFPy_API::init_python_with_config(config, argc, argv); + + // Handle different Python modes + if (!config.python_command.empty()) { + // Execute command from -c + int result = PyRun_SimpleString(config.python_command.c_str()); + Py_Finalize(); + 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()); + Py_Finalize(); + 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 && result == 0) { + // Run interactive mode after script + PyRun_InteractiveLoop(stdin, ""); + } + + Py_Finalize(); + return result; + } + else if (config.interactive_mode || config.python_mode) { + // Interactive Python interpreter + Py_InspectFlag = 1; + PyRun_InteractiveLoop(stdin, ""); + Py_Finalize(); + return 0; + } + + return 0; }