Compare commits

...

5 Commits

Author SHA1 Message Date
2d3715c5ff fix(gameengine): resolve duplicate CMake preset error from stale Conan layouts
When Conan's build output layout changes (e.g. build/generators/ vs
build/Release/generators/), CMakeUserPresets.json accumulates includes
that define the same preset names, causing CMake to fail with
"Duplicate preset". The fix detects preset name collisions across
included files and keeps only the newest, in addition to pruning
missing files.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 22:14:41 +01:00
fe44599eb4 fix(gameengine): cover all Python install methods for conan discovery
Add explicit search paths for python.org installer (%APPDATA%\Python),
Chocolatey, macOS system Python (~/Library/Python), and ~/.local/bin.
Document coverage matrix for Windows (Store/python.org/Chocolatey),
macOS (Homebrew/system/pyenv), and Linux (apt/dnf/pip --user).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 22:09:12 +01:00
46e9d5b51e fix(gameengine): robust conan discovery across platforms and install methods
Searches PATH, interpreter/user Scripts dirs, site-packages sibling
dirs, and falls back to module invocation. Covers system Python,
Homebrew, Windows Store Python, pyenv, conda, pip --user, and venvs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 22:07:13 +01:00
dd97bf621b fix(gameengine): fall back to python -m conans.conan when conan is not on PATH
Windows Store Python installs packages without adding Scripts to PATH,
causing FileNotFoundError when invoking conan directly.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 22:05:37 +01:00
c0adf86740 docs(gameengine): add README with build instructions and architecture overview
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 21:58:10 +01:00
2 changed files with 309 additions and 6 deletions

155
gameengine/README.md Normal file
View File

@@ -0,0 +1,155 @@
# GameEngine
SDL3 GPU 2D/3D game engine with a JSON-driven workflow system. Built with C++20, using Conan for dependency management and CMake + Ninja for builds.
## Quick Start
### Prerequisites
- C++20 compiler (MSVC, Clang, or GCC)
- [CMake](https://cmake.org/) 3.24+
- [Conan](https://conan.io/) 2.x
- [Ninja](https://ninja-build.org/) (recommended)
- Python 3.9+ (build helper)
### Build & Run (Recommended)
The `dev_commands.py` build helper runs the full pipeline: Conan install, CMake generate, configure, and build.
```bash
# Build and run the seed demo (auto-detects platform bootstrap)
python python/dev_commands.py all --run
# Equivalent explicit form on Windows
python python/dev_commands.py all --run --bootstrap bootstrap_windows --game seed
# macOS
python python/dev_commands.py all --run --bootstrap bootstrap_mac --game seed
# Linux
python python/dev_commands.py all --run --bootstrap bootstrap_linux --game seed
```
### Build Steps (Manual)
```bash
# 1. Install Conan dependencies
python python/dev_commands.py dependencies
# 2. Generate CMakeLists.txt from cmake_config.json
python python/dev_commands.py generate
# 3. Configure CMake
python python/dev_commands.py configure --preset conan-default
# 4. Build
python python/dev_commands.py build
# 5. Run
python python/dev_commands.py run -- --bootstrap bootstrap_windows --game seed
```
### Makefile (Unix)
```bash
make build # Build sdl3_app
make build TARGET=sdl3_app # Build specific target
make test # Build and run all tests
make rebuild # Clean + build
make list-targets # Show available targets
```
## CLI Arguments
The compiled `sdl3_app` binary accepts:
| Flag | Default | Description |
|------|---------|-------------|
| `--bootstrap` | `bootstrap_mac` | Platform bootstrap package |
| `--game` | `standalone_cubes` | Game package to load |
| `--project-root` | Current directory | Path to gameengine root |
## Packages
Game content and platform initialization are defined as JSON packages in `packages/`:
| Package | Type | Description |
|---------|------|-------------|
| `bootstrap_windows` | bootloader | Windows init, SDL3 GPU with D3D12 |
| `bootstrap_mac` | bootloader | macOS init, SDL3 GPU with Metal |
| `bootstrap_linux` | bootloader | Linux init, SDL3 GPU with Vulkan |
| `seed` | game | FPS demo: room with spinning cube, WASD movement, mouse look, jump. Bullet3 physics |
| `standalone_cubes` | game | 11x11 grid of spinning colored cubes with per-cube animation offsets |
| `quake3` | game | Quake 3 BSP map viewer with FPS controls |
| `soundboard` | game | Audio cues + GUI controls |
| `engine_tester` | game | Validation tour with teleport checkpoints, captures, and diagnostics |
| `asset_loader` | library | Universal asset loading with cross-engine unit conversion (JSON-driven) |
| `assets` | library | Shared runtime assets (audio, fonts, images) |
| `materialx` | library | MaterialX integration |
## Architecture
```
src/
main.cpp # Entry point, CLI parsing, workflow bootstrap
services/
interfaces/ # Abstract service interfaces (16 services)
i_audio_service.hpp
i_graphics_service.hpp
i_input_service.hpp
i_physics_service.hpp
i_scene_service.hpp
i_window_service.hpp
i_workflow_executor.hpp
...
impl/ # Concrete implementations
workflow/ # Workflow step implementations
diagnostics/ # Logging
app/ # CLI, platform services
packages/ # JSON-driven game content
{package}/
package.json # Package metadata, config, shader list
workflows/*.json # Workflow definitions (v2.2.0 format)
shaders/spirv/ # SPIR-V shaders (Vulkan, D3D12)
shaders/msl/ # Metal shaders (macOS)
scene/*.json # Scene definitions
assets/ # Package-specific assets
python/
dev_commands.py # Build helper CLI
generate_cmake.py # CMakeLists.txt generator from cmake_config.json
export_room_gltf.py # Seed demo room glTF exporter
```
## Dependencies (Conan)
| Library | Version | Purpose |
|---------|---------|---------|
| SDL | 3.2.20 | Windowing, input, GPU API |
| Bullet3 | 3.25 | 3D physics |
| Box2D | 3.1.1 | 2D physics |
| EnTT | 3.16.0 | Entity Component System |
| Assimp | 6.0.2 | 3D model import |
| glm | 1.0.1 | Math library |
| nlohmann_json | 3.11.3 | JSON parsing |
| RapidJSON | cci.20230929 | JSON parsing (performance) |
| FFmpeg | 8.0.1 | Audio/video decoding |
| FreeType | 2.13.2 | Font rendering |
| Cairo | 1.18.0 | 2D vector graphics |
| stb | cci.20230920 | Image loading |
| LunaSVG | 3.0.1 | SVG rendering |
| libzip | 1.10.1 | ZIP archive support |
| cpptrace | 1.0.4 | Stack traces |
| CLI11 | 2.6.0 | Command-line parsing |
| GTest | 1.17.0 | Testing framework |
## Testing
```bash
# Via Makefile
make test
# Via dev_commands.py
python python/dev_commands.py build --target test_exit_step
```

View File

@@ -34,11 +34,123 @@ import argparse
import os
import platform
import subprocess
import sys
from pathlib import Path
from typing import Iterable, Sequence
IS_WINDOWS = platform.system() == "Windows"
def _conan_cmd() -> list[str]:
"""Return the command prefix for invoking Conan.
Searches for the ``conan`` executable using several strategies so the
script works regardless of how Python/Conan were installed.
**Windows coverage**:
- Windows Store Python (``pip install --user``) — ``nt_user`` sysconfig
- python.org installer (global or per-user) — interpreter scripts dir
- python.org / Chocolatey ``pip install --user`` — ``%APPDATA%\\Python``
- Chocolatey (global) — typically on PATH or interpreter scripts dir
**macOS coverage**:
- Homebrew Python — on PATH (``/opt/homebrew/bin`` or ``/usr/local/bin``)
- System Python ``pip install --user`` — ``~/Library/Python/X.Y/bin``
- pyenv / conda / virtualenv — on PATH when activated, scripts dir otherwise
**Linux coverage**:
- apt / dnf system Python — on PATH (``/usr/bin`` or ``/usr/local/bin``)
- ``pip install --user`` — ``~/.local/bin``
- pyenv / conda / virtualenv — on PATH when activated, scripts dir otherwise
Strategy order:
1. ``shutil.which`` — anything already on PATH.
2. Python-adjacent Scripts/bin dirs — interpreter-level, user-level,
``%APPDATA%\\Python`` (Windows), ``~/Library/Python`` (macOS),
``~/.local/bin`` (Linux), and site-packages siblings.
3. ``sys.executable -m conans.conan`` — package importable but no script.
Raises ``FileNotFoundError`` with install instructions if nothing works.
"""
import shutil
import sysconfig
import site
# 1. Already on PATH (system install, brew, pyenv, conda, activated venv,
# choco with PATH, apt/dnf)
if shutil.which("conan"):
return ["conan"]
# 2. Search Scripts/bin dirs adjacent to the running interpreter
exe_name = "conan.exe" if IS_WINDOWS else "conan"
bin_dir = "Scripts" if IS_WINDOWS else "bin"
search_dirs: list[str] = []
# Interpreter-level scripts (venv/Scripts, C:\PythonXXX\Scripts,
# C:\Users\X\AppData\Local\Programs\Python\PythonXXX\Scripts,
# /usr/local/bin, /opt/homebrew/bin)
interp_scripts = sysconfig.get_path("scripts")
if interp_scripts:
search_dirs.append(interp_scripts)
# User-level scripts via sysconfig (Windows Store Python, ~/.local/bin)
scheme = "nt_user" if IS_WINDOWS else "posix_user"
try:
user_scripts = sysconfig.get_path("scripts", scheme)
if user_scripts:
search_dirs.append(user_scripts)
except KeyError:
pass
if IS_WINDOWS:
# python.org / Chocolatey "pip install --user" puts scripts under
# %APPDATA%\Python\PythonXYZ\Scripts — not covered by nt_user when
# running from Windows Store Python
appdata = os.environ.get("APPDATA", "")
if appdata:
ver = f"Python{sys.version_info.major}{sys.version_info.minor}"
search_dirs.append(os.path.join(appdata, "Python", ver, "Scripts"))
# Also check unversioned (older pip layouts)
search_dirs.append(os.path.join(appdata, "Python", "Scripts"))
else:
# macOS system Python: ~/Library/Python/X.Y/bin
if platform.system() == "Darwin":
ver = f"{sys.version_info.major}.{sys.version_info.minor}"
search_dirs.append(
os.path.expanduser(f"~/Library/Python/{ver}/bin")
)
# Linux/macOS: ~/.local/bin (pip install --user default)
search_dirs.append(os.path.expanduser("~/.local/bin"))
# Site-packages sibling dirs (covers additional layouts)
try:
for sp in site.getsitepackages():
search_dirs.append(os.path.join(os.path.dirname(sp), bin_dir))
except AttributeError:
pass
for d in dict.fromkeys(search_dirs): # dedupe, preserve order
candidate = os.path.join(d, exe_name)
if os.path.isfile(candidate):
return [candidate]
# 3. Module invocation (conan is importable but no script on disk)
try:
from importlib.metadata import distribution
distribution("conan") # raises PackageNotFoundError if absent
return [sys.executable, "-m", "conans.conan"]
except Exception:
pass
raise FileNotFoundError(
"Could not find Conan. Install it with: pip install conan"
)
DEFAULT_GENERATOR = "ninja-msvc" if IS_WINDOWS else "ninja"
GENERATOR_DEFAULT_DIR = {
"vs": "build",
@@ -196,8 +308,9 @@ def _has_cmake_cache(build_dir: str) -> bool:
def dependencies(args: argparse.Namespace) -> None:
"""Run Conan profile detection and install dependencies with C++20."""
cmd_detect = ["conan", "profile", "detect", "-f"]
cmd_install = ["conan", "install", ".", "-of", "build-ninja", "-b", "missing",
conan = _conan_cmd()
cmd_detect = [*conan, "profile", "detect", "-f"]
cmd_install = [*conan, "install", ".", "-of", "build-ninja", "-b", "missing",
"-s", "compiler.cppstd=20"]
conan_install_args = _strip_leading_double_dash(args.conan_install_args)
if conan_install_args:
@@ -225,7 +338,14 @@ def generate(args: argparse.Namespace) -> None:
def _fix_cmake_user_presets() -> None:
"""Ensure CMakeUserPresets.json only includes existing preset files."""
"""Ensure CMakeUserPresets.json only includes existing, non-conflicting preset files.
Conan regenerates preset files when the build layout changes (e.g.
``build-ninja/build/generators/`` vs ``build-ninja/build/Release/generators/``).
If the old file still exists, CMake fails with "Duplicate preset". This
function removes missing includes *and* detects preset name collisions,
keeping only the newest file when duplicates are found.
"""
import json as json_mod
presets_path = Path("CMakeUserPresets.json")
if not presets_path.exists():
@@ -233,11 +353,39 @@ def _fix_cmake_user_presets() -> None:
try:
data = json_mod.loads(presets_path.read_text())
includes = data.get("include", [])
# Drop missing files
valid = [p for p in includes if Path(p).exists()]
if len(valid) != len(includes):
data["include"] = valid
# Detect and resolve duplicate preset names across included files
seen_names: dict[str, str] = {} # preset_name -> include_path
duplicates: set[str] = set()
for inc_path in valid:
try:
inc_data = json_mod.loads(Path(inc_path).read_text())
except (json_mod.JSONDecodeError, OSError):
continue
for key in ("configurePresets", "buildPresets", "testPresets"):
for preset in inc_data.get(key, []):
name = preset.get("name", "")
if name in seen_names and seen_names[name] != inc_path:
# Keep the newer file, drop the older one
older = seen_names[name]
newer = inc_path
if Path(older).stat().st_mtime > Path(newer).stat().st_mtime:
older, newer = newer, older
duplicates.add(older)
seen_names[name] = newer
else:
seen_names[name] = inc_path
deduped = [p for p in valid if p not in duplicates]
if deduped != includes:
data["include"] = deduped
presets_path.write_text(json_mod.dumps(data, indent=4) + "\n")
print(f" Fixed CMakeUserPresets.json: kept {len(valid)}/{len(includes)} includes")
removed = len(includes) - len(deduped)
print(f" Fixed CMakeUserPresets.json: removed {removed} stale/duplicate include(s)")
except (json_mod.JSONDecodeError, OSError):
pass