From 9176dca0551793b1f4b06a27d8e357737a4d4b66 Mon Sep 17 00:00:00 2001 From: John McCardle Date: Fri, 20 Feb 2026 23:17:59 -0500 Subject: [PATCH] Add mcrf-init.sh: game project scaffolding without engine recompilation New workflow for game developers: run mcrf-init to create a project directory with symlinks to a pre-built engine, then just write Python scripts and assets. Games package for distribution (Linux/Windows/WASM) without ever rebuilding the engine. mcrf-init.sh creates: - build/ with symlinked binary and libs, game content in assets/ + scripts/ - build-windows/ (if engine has a Windows build) - Makefile with run, wasm, dist-linux, dist-windows, dist-wasm targets - Starter game.py, .gitignore, pyrightconfig.json, VERSION file CMakeLists.txt: WASM preload paths (assets, scripts) are now configurable via MCRF_ASSETS_DIR / MCRF_SCRIPTS_DIR cache variables, so game project Makefiles can point WASM builds at their own content without modifying the engine. Also adds pyrightconfig.json for the engine repo itself (IDE support via stubs/). Co-Authored-By: Claude Opus 4.6 --- CMakeLists.txt | 9 +- mcrf-init.sh | 422 +++++++++++++++++++++++++++++++++++++++++++++ pyrightconfig.json | 8 + 3 files changed, 437 insertions(+), 2 deletions(-) create mode 100755 mcrf-init.sh create mode 100644 pyrightconfig.json diff --git a/CMakeLists.txt b/CMakeLists.txt index dc2a63d..21f79d3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -266,6 +266,11 @@ if(MCRF_SDL2) target_compile_definitions(mcrogueface PRIVATE MCRF_SDL2) endif() +# Asset/script directories for WASM preloading (game projects override these) +set(MCRF_ASSETS_DIR "${CMAKE_SOURCE_DIR}/assets" CACHE PATH "Assets directory for WASM preloading") +set(MCRF_SCRIPTS_DIR "${CMAKE_SOURCE_DIR}/src/scripts" CACHE PATH "Scripts directory for WASM preloading") +set(MCRF_SCRIPTS_PLAYGROUND_DIR "${CMAKE_SOURCE_DIR}/src/scripts_playground" CACHE PATH "Playground scripts for WASM") + # Emscripten-specific link options (use ports for zlib, bzip2, sqlite3) if(EMSCRIPTEN) # Base Emscripten options @@ -287,9 +292,9 @@ if(EMSCRIPTEN) # Preload Python stdlib into virtual filesystem at /lib/python3.14 --preload-file=${CMAKE_SOURCE_DIR}/wasm_stdlib/lib@/lib # Preload game scripts into /scripts (use playground scripts if MCRF_PLAYGROUND is set) - --preload-file=${CMAKE_SOURCE_DIR}/src/$,scripts_playground,scripts>@/scripts + --preload-file=$,${MCRF_SCRIPTS_PLAYGROUND_DIR},${MCRF_SCRIPTS_DIR}>@/scripts # Preload assets - --preload-file=${CMAKE_SOURCE_DIR}/assets@/assets + --preload-file=${MCRF_ASSETS_DIR}@/assets # Use custom HTML shell - game shell (fullscreen) or playground shell (REPL) --shell-file=${CMAKE_SOURCE_DIR}/src/$,shell_game.html,shell.html> # Pre-JS to fix browser zoom causing undefined values in events diff --git a/mcrf-init.sh b/mcrf-init.sh new file mode 100755 index 0000000..d33a7d8 --- /dev/null +++ b/mcrf-init.sh @@ -0,0 +1,422 @@ +#!/bin/bash +# +# mcrf-init.sh - Initialize a McRogueFace game project +# +# Creates a game project directory with symlinks to a pre-built engine, +# so game developers only write Python + assets, never rebuild the engine. +# +# Usage: +# mcrf-init # initialize current directory +# mcrf-init my-game # create and initialize my-game/ +# +# Setup (add to ~/.bashrc): +# alias mcrf-init='/path/to/McRogueFace/mcrf-init.sh' +# # or: export PATH="$PATH:/path/to/McRogueFace" +# +# After init: +# make run # run the game +# make dist # package for distribution + +set -e + +# --- Resolve engine root (where this script lives) --- +ENGINE_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# --- Determine game directory --- +if [ -n "$1" ]; then + GAME_DIR="$(pwd)/$1" + mkdir -p "$GAME_DIR" +else + GAME_DIR="$(pwd)" +fi + +# --- Colors --- +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +RED='\033[0;31m' +BOLD='\033[1m' +NC='\033[0m' + +info() { echo -e "${GREEN}[mcrf]${NC} $1"; } +warn() { echo -e "${YELLOW}[mcrf]${NC} $1"; } +error() { echo -e "${RED}[mcrf]${NC} $1"; } + +# --- Safety checks --- +if [ "$GAME_DIR" = "$ENGINE_ROOT" ]; then + error "Cannot init inside the engine directory itself." + exit 1 +fi + +if [ ! -f "$ENGINE_ROOT/build/mcrogueface" ]; then + error "Engine not built. Run 'make' in $ENGINE_ROOT first." + exit 1 +fi + +# Engine version for packaging +ENGINE_VERSION=$(grep 'MCRFPY_VERSION' "$ENGINE_ROOT/src/McRogueFaceVersion.h" \ + | sed 's/.*"\(.*\)"/\1/') + +info "Initializing McRogueFace game project" +info " Engine: $ENGINE_ROOT (v${ENGINE_VERSION})" +info " Game: $GAME_DIR" +echo + +# --- Create game directories --- +mkdir -p "$GAME_DIR/assets" +mkdir -p "$GAME_DIR/scripts" + +# --- Set up build/ (Linux) --- +info "Setting up build/ (Linux)..." +mkdir -p "$GAME_DIR/build" + +# Binary +ln -sfn "$ENGINE_ROOT/build/mcrogueface" "$GAME_DIR/build/mcrogueface" + +# Shared libraries (entire lib/ tree: .so files + Python stdlib + extensions) +ln -sfn "$ENGINE_ROOT/build/lib" "$GAME_DIR/build/lib" + +# Game content: symlink to project's own directories +ln -sfn ../assets "$GAME_DIR/build/assets" +ln -sfn ../scripts "$GAME_DIR/build/scripts" + +# --- Set up build-windows/ (if engine has it) --- +if [ -f "$ENGINE_ROOT/build-windows/mcrogueface.exe" ]; then + info "Setting up build-windows/..." + mkdir -p "$GAME_DIR/build-windows" + + # Executable + ln -sfn "$ENGINE_ROOT/build-windows/mcrogueface.exe" "$GAME_DIR/build-windows/mcrogueface.exe" + + # DLLs + for dll in "$ENGINE_ROOT/build-windows/"*.dll; do + [ -f "$dll" ] && ln -sfn "$dll" "$GAME_DIR/build-windows/$(basename "$dll")" + done + + # Python stdlib zip + [ -f "$ENGINE_ROOT/build-windows/python314.zip" ] && \ + ln -sfn "$ENGINE_ROOT/build-windows/python314.zip" "$GAME_DIR/build-windows/python314.zip" + + # Python lib directory + [ -d "$ENGINE_ROOT/build-windows/lib" ] && \ + ln -sfn "$ENGINE_ROOT/build-windows/lib" "$GAME_DIR/build-windows/lib" + + # Game content + ln -sfn ../assets "$GAME_DIR/build-windows/assets" + ln -sfn ../scripts "$GAME_DIR/build-windows/scripts" +else + warn "No Windows build found (skipping build-windows/)" +fi + +# --- WASM build info --- +info "WASM support: use 'make wasm' to build (requires emsdk)" + +# --- Starter game.py --- +if [ ! -f "$GAME_DIR/scripts/game.py" ]; then + info "Creating starter scripts/game.py..." + cat > "$GAME_DIR/scripts/game.py" << 'PYTHON' +"""McRogueFace Game - created by mcrf-init""" +import mcrfpy + +scene = mcrfpy.Scene("title") +ui = scene.children + +ui.append(mcrfpy.Caption( + text="My McRogueFace Game", + pos=(512, 300), + font=mcrfpy.Font("assets/JetbrainsMono.ttf"), + fill_color=(255, 255, 255), + font_size=48 +)) + +ui.append(mcrfpy.Caption( + text="Press ESC to quit", + pos=(512, 400), + font=mcrfpy.Font("assets/JetbrainsMono.ttf"), + fill_color=(180, 180, 180), + font_size=24 +)) + +def on_key(key, state): + if key == mcrfpy.Key.ESCAPE and state == mcrfpy.InputState.PRESSED: + mcrfpy.exit() + +scene.on_key = on_key +mcrfpy.current_scene = scene +PYTHON +fi + +# --- Copy a default font if game assets/ is empty --- +if [ -z "$(ls -A "$GAME_DIR/assets/" 2>/dev/null)" ]; then + if [ -f "$ENGINE_ROOT/assets/JetbrainsMono.ttf" ]; then + info "Copying default font to assets/..." + cp "$ENGINE_ROOT/assets/JetbrainsMono.ttf" "$GAME_DIR/assets/" + fi +fi + +# --- .gitignore --- +if [ ! -f "$GAME_DIR/.gitignore" ]; then + info "Creating .gitignore..." + cat > "$GAME_DIR/.gitignore" << 'GITIGNORE' +# McRogueFace game project +build/ +build-windows/ +build-wasm/ +dist/ +__pycache__/ +*.pyc +*.png.bak +GITIGNORE +fi + +# --- pyrightconfig.json (for Vim/LSP autocomplete) --- +if [ ! -f "$GAME_DIR/pyrightconfig.json" ]; then + info "Creating pyrightconfig.json (IDE autocomplete)..." + cat > "$GAME_DIR/pyrightconfig.json" << PYRIGHT +{ + "include": ["scripts"], + "extraPaths": ["${ENGINE_ROOT}/stubs"], + "pythonVersion": "3.14", + "pythonPlatform": "Linux", + "typeCheckingMode": "basic", + "reportMissingModuleSource": false +} +PYRIGHT +fi + +# --- Makefile --- +info "Creating Makefile..." +cat > "$GAME_DIR/Makefile" << MAKEFILE +# McRogueFace Game Project Makefile +# Generated by mcrf-init +# +# Engine: ${ENGINE_ROOT} + +ENGINE_ROOT := ${ENGINE_ROOT} +GAME_NAME := $(basename "$GAME_DIR") +GAME_DIR := \$(shell pwd) + +# Game version - edit this or create a VERSION file +VERSION := \$(shell cat VERSION 2>/dev/null || echo "0.1.0") + +# Engine version (from the linked build) +ENGINE_VERSION := \$(shell grep 'MCRFPY_VERSION' "\$(ENGINE_ROOT)/src/McRogueFaceVersion.h" \\ + | sed 's/.*"\\(.*\\)"/\\1/' 2>/dev/null || echo "unknown") + +.PHONY: run run-windows wasm serve-wasm dist dist-linux dist-windows dist-wasm clean-dist clean-wasm info + +# ============================================================ +# Run targets +# ============================================================ + +run: + @cd build && ./mcrogueface + +run-headless: + @cd build && ./mcrogueface --headless --exec ../scripts/game.py + +run-windows: + @echo "Launch build-windows/mcrogueface.exe on a Windows machine or via Wine" + +serve-wasm: + @if [ ! -f build-wasm/mcrogueface.html ]; then \\ + echo "No WASM build found. Run 'make wasm' first."; \\ + exit 1; \\ + fi + @echo "Serving WASM build at http://localhost:8080" + @cd build-wasm && python3 -m http.server 8080 + +# ============================================================ +# WASM build (requires emsdk) +# ============================================================ + +wasm: + @if ! command -v emcmake >/dev/null 2>&1; then \\ + echo "Error: emcmake not found. Activate emsdk first:"; \\ + echo " source ~/emsdk/emsdk_env.sh"; \\ + exit 1; \\ + fi + @echo "Building WASM with game assets..." + @emcmake cmake -S "\$(ENGINE_ROOT)" -B build-wasm \\ + -DMCRF_SDL2=ON \\ + -DMCRF_GAME_SHELL=ON \\ + -DMCRF_ASSETS_DIR="\$(GAME_DIR)/assets" \\ + -DMCRF_SCRIPTS_DIR="\$(GAME_DIR)/scripts" + @emmake make -C build-wasm -j\$\$(nproc) + @echo "" + @echo "WASM build complete: build-wasm/" + @echo " make serve-wasm - test in browser" + @echo " make dist-wasm - package for distribution" + +clean-wasm: + @rm -rf build-wasm + @echo "Cleaned build-wasm/" + +# ============================================================ +# Distribution packaging +# ============================================================ + +dist: dist-linux dist-windows dist-wasm + @echo "" + @echo "=== Packages ===" + @ls -lh dist/ 2>/dev/null + +dist-linux: + @if [ ! -f build/mcrogueface ]; then \\ + echo "Error: build/mcrogueface not found. Run mcrf-init first."; \\ + exit 1; \\ + fi + @echo "Packaging \$(GAME_NAME) for Linux..." + @mkdir -p dist + \$(eval PKG := dist/\$(GAME_NAME)-\$(VERSION)-Linux) + @rm -rf \$(PKG) + @mkdir -p \$(PKG)/lib + @# Binary + @cp "\$(ENGINE_ROOT)/build/mcrogueface" \$(PKG)/ + @# Shared libraries + @for lib in libpython3.14.so.1.0 \\ + libsfml-graphics.so.2.6.1 libsfml-window.so.2.6.1 \\ + libsfml-system.so.2.6.1 libsfml-audio.so.2.6.1 \\ + libtcod.so; do \\ + [ -f "\$(ENGINE_ROOT)/__lib/\$\$lib" ] && \\ + cp "\$(ENGINE_ROOT)/__lib/\$\$lib" \$(PKG)/lib/; \\ + done + @# Library symlinks + @cd \$(PKG)/lib && \\ + ln -sf libpython3.14.so.1.0 libpython3.14.so && \\ + ln -sf libsfml-graphics.so.2.6.1 libsfml-graphics.so.2.6 && \\ + ln -sf libsfml-graphics.so.2.6.1 libsfml-graphics.so && \\ + ln -sf libsfml-window.so.2.6.1 libsfml-window.so.2.6 && \\ + ln -sf libsfml-window.so.2.6.1 libsfml-window.so && \\ + ln -sf libsfml-system.so.2.6.1 libsfml-system.so.2.6 && \\ + ln -sf libsfml-system.so.2.6.1 libsfml-system.so && \\ + ln -sf libsfml-audio.so.2.6.1 libsfml-audio.so.2.6 && \\ + ln -sf libsfml-audio.so.2.6.1 libsfml-audio.so && \\ + ln -sf libtcod.so libtcod.so.2 + @# Python extension modules + @if [ -d "\$(ENGINE_ROOT)/__lib/Python/lib.linux-x86_64-3.14" ]; then \\ + mkdir -p \$(PKG)/lib/Python/lib.linux-x86_64-3.14; \\ + for so in "\$(ENGINE_ROOT)/__lib/Python/lib.linux-x86_64-3.14"/*.so; do \\ + case "\$\$(basename \$\$so)" in \\ + *test*|xxlimited*|_ctypes_test*|_xxtestfuzz*) continue ;; \\ + *) cp "\$\$so" \$(PKG)/lib/Python/lib.linux-x86_64-3.14/ ;; \\ + esac; \\ + done; \\ + fi + @# Python stdlib + @if [ -d "\$(ENGINE_ROOT)/__lib/Python/Lib" ]; then \\ + mkdir -p \$(PKG)/lib/Python; \\ + cp -r "\$(ENGINE_ROOT)/__lib/Python/Lib" \$(PKG)/lib/Python/; \\ + rm -rf \$(PKG)/lib/Python/Lib/test \$(PKG)/lib/Python/Lib/tests \\ + \$(PKG)/lib/Python/Lib/idlelib \$(PKG)/lib/Python/Lib/tkinter \\ + \$(PKG)/lib/Python/Lib/turtledemo \$(PKG)/lib/Python/Lib/pydoc_data \\ + \$(PKG)/lib/Python/Lib/lib2to3 \$(PKG)/lib/Python/Lib/ensurepip \\ + \$(PKG)/lib/Python/Lib/_pyrepl; \\ + find \$(PKG)/lib/Python/Lib -name "__pycache__" -type d -exec rm -rf {} + 2>/dev/null || true; \\ + find \$(PKG)/lib/Python/Lib -name "*.pyc" -delete 2>/dev/null || true; \\ + find \$(PKG)/lib/Python/Lib -name "test_*.py" -delete 2>/dev/null || true; \\ + fi + @# Game content (real copies, not symlinks) + @cp -r assets \$(PKG)/assets + @cp -r scripts \$(PKG)/scripts + @# Run script + @printf '#!/bin/bash\\nDIR="\$\$(cd "\$\$(dirname "\$\$0")" && pwd)"\\nexport LD_LIBRARY_PATH="\$\$DIR/lib:\$\$LD_LIBRARY_PATH"\\nexec "\$\$DIR/mcrogueface" "\$\$@"\\n' > \$(PKG)/run.sh + @chmod +x \$(PKG)/run.sh + @# Archive + @cd dist && tar -czf "\$(GAME_NAME)-\$(VERSION)-Linux.tar.gz" "\$(GAME_NAME)-\$(VERSION)-Linux" + @rm -rf \$(PKG) + @echo "Created: dist/\$(GAME_NAME)-\$(VERSION)-Linux.tar.gz" + +dist-windows: + @if [ ! -f "\$(ENGINE_ROOT)/build-windows/mcrogueface.exe" ]; then \\ + echo "No Windows build available. Skipping."; \\ + exit 0; \\ + fi + @echo "Packaging \$(GAME_NAME) for Windows..." + @mkdir -p dist + \$(eval PKG := dist/\$(GAME_NAME)-\$(VERSION)-Windows) + @rm -rf \$(PKG) + @mkdir -p \$(PKG) + @# Executable and DLLs + @cp "\$(ENGINE_ROOT)/build-windows/mcrogueface.exe" \$(PKG)/ + @for dll in "\$(ENGINE_ROOT)/build-windows/"*.dll; do \\ + [ -f "\$\$dll" ] && cp "\$\$dll" \$(PKG)/; \\ + done + @# Python stdlib zip + @[ -f "\$(ENGINE_ROOT)/build-windows/python314.zip" ] && \\ + cp "\$(ENGINE_ROOT)/build-windows/python314.zip" \$(PKG)/ || true + @# Python lib directory + @if [ -d "\$(ENGINE_ROOT)/build-windows/lib" ]; then \\ + cp -r "\$(ENGINE_ROOT)/build-windows/lib" \$(PKG)/lib; \\ + fi + @# Game content (real copies) + @cp -r assets \$(PKG)/assets + @cp -r scripts \$(PKG)/scripts + @# Archive + @cd dist && zip -qr "\$(GAME_NAME)-\$(VERSION)-Windows.zip" "\$(GAME_NAME)-\$(VERSION)-Windows" + @rm -rf \$(PKG) + @echo "Created: dist/\$(GAME_NAME)-\$(VERSION)-Windows.zip" + +dist-wasm: + @if ! command -v emcmake >/dev/null 2>&1; then \\ + echo "WASM: emsdk not activated. Skipping WASM package."; \\ + echo " To include WASM: source ~/emsdk/emsdk_env.sh && make dist-wasm"; \\ + exit 0; \\ + fi; \\ + if [ ! -f build-wasm/mcrogueface.html ]; then \\ + echo "Building WASM first..."; \\ + \$(MAKE) wasm || exit 1; \\ + fi; \\ + echo "Packaging \$(GAME_NAME) for Web..."; \\ + mkdir -p dist; \\ + PKG="dist/\$(GAME_NAME)-\$(VERSION)-Web"; \\ + rm -rf "\$\$PKG"; \\ + mkdir -p "\$\$PKG"; \\ + cp build-wasm/mcrogueface.html "\$\$PKG/index.html"; \\ + cp build-wasm/mcrogueface.js "\$\$PKG/"; \\ + cp build-wasm/mcrogueface.wasm "\$\$PKG/"; \\ + cp build-wasm/mcrogueface.data "\$\$PKG/"; \\ + cd dist && zip -qr "\$(GAME_NAME)-\$(VERSION)-Web.zip" "\$(GAME_NAME)-\$(VERSION)-Web"; \\ + rm -rf "\$\$PKG"; \\ + echo "Created: dist/\$(GAME_NAME)-\$(VERSION)-Web.zip" + +clean-dist: + @rm -rf dist + @echo "Cleaned dist/" + +# ============================================================ +# Info +# ============================================================ + +info: + @echo "Game: \$(GAME_NAME) v\$(VERSION)" + @echo "Engine: McRogueFace v\$(ENGINE_VERSION)" + @echo "Root: \$(ENGINE_ROOT)" + @echo "" + @echo "Targets:" + @echo " make run Run the game" + @echo " make run-headless Run headless (for testing)" + @echo " make wasm Build for web (requires emsdk)" + @echo " make serve-wasm Test WASM build in browser" + @echo " make dist Package for all platforms" + @echo " make dist-linux Package for Linux only" + @echo " make dist-windows Package for Windows only" + @echo " make dist-wasm Package for web only" + @echo " make clean-dist Remove dist/" + @echo " make clean-wasm Remove build-wasm/" +MAKEFILE + +# --- VERSION file --- +if [ ! -f "$GAME_DIR/VERSION" ]; then + echo "0.1.0" > "$GAME_DIR/VERSION" +fi + +# --- Done --- +echo +info "Project ready!" +echo +echo -e " ${BOLD}make run${NC} - play the game" +echo -e " ${BOLD}make dist${NC} - package for distribution" +echo -e " ${BOLD}make info${NC} - show project info" +echo +echo -e " Edit ${BOLD}scripts/game.py${NC} and ${BOLD}assets/${NC} to build your game." +echo -e " Edit ${BOLD}VERSION${NC} to set your game version." diff --git a/pyrightconfig.json b/pyrightconfig.json new file mode 100644 index 0000000..adfcbf4 --- /dev/null +++ b/pyrightconfig.json @@ -0,0 +1,8 @@ +{ + "include": ["src/scripts", "shade_sprite", "tests"], + "extraPaths": ["stubs"], + "pythonVersion": "3.14", + "pythonPlatform": "Linux", + "typeCheckingMode": "basic", + "reportMissingModuleSource": false +}