openage is a free-software RTS engine recreating Age of Empires II: libopenage/ is the C++20 engine, openage/ the Python package (converter, codegen, nyan integration), glued together by Cython
Read the docs of the subsystem you touch; link to them instead of duplicating their content
- doc/project_structure.md — folder map
- doc/building.md — dependencies (flags C/R/A/S/O) and build; per-platform setup incl. Nix in doc/build_instructions/
- doc/buildsystem.md — codegen → pxdgen → cythonize pipeline
- doc/code/architecture.md — subsystems and data flow, details in its sibling dirs (
event/,game_simulation/,input/,renderer/,pathfinding/,converter/) - doc/code/ — curves, exceptions, logger, pyinterface, testing
- doc/code_style/ — C++ by example in mom.h and mom.cpp, tabs indent and spaces align; Python is a PEP8 subset
- doc/nyan/, doc/convert/, doc/media_convert.md — game data and converter
- doc/development.md — code philosophy; doc/contributing.md — PR workflow
./configure creates the build dir in .bin/, symlinked as bin/; the root Makefile forwards there
./configure # e.g. --compiler=clang --mode=release --ccache --download-nyan
make -j$(nproc) # build
make run # run the game
make tests # all C++/Python tests and doctests, as CI runs them
make checkmerge # compliance checks, as CI runs them
make doc # doxygen → bin/doc/html/index.html- Single test:
bin/run test NAME, e.g.openage::curve::tests::container;-llists all,-d NAME ARGSruns a demo,-b NAMEa benchmark,-a --have-assetsincludes tests needing game assets - Checks on changed files only:
make checkuncommited(sic; vsHEAD, new files count oncegit added) ormake checkchanged(vsorigin/master) checkfast: authors, cppstyle, cython, headerguards, legal, filemodes, textfiles;checkmergeadds ruff (lint + format) and ty (type check), run throughuv run(venv from pyproject.toml/uv.lock);checkalladds clang-tidy- Memory or threading changes: also run the tests in a build configured with
--sanitize=yes(ASan+UBSan) or--sanitize=thread - CI also builds on macOS and Windows (.github/workflows/): keep code portable
- No ticks: the simulation is event-driven and all gamestate lives in time-indexed curves (curves.md); schedule events (
event/) instead of adding per-frame updates - Presenter, simulation and time run in separate threads and communicate only via defined interfaces; renderer and input are optional, the simulation must run without them
- Subsystems talk only through their interfaces, never through each other's internals, e.g. gamestate reaches the renderer only via render entities, never OpenGL
- Simulation time and world coordinates are fixed-point (
time::time_t,coord::phys_t); don't model them asfloat/double - Game data is nyan, converted from the original game files by
bin/run convertintoassets/converted/(git-ignored, never built in) - The nyan API is mirrored in doc/nyan/api_reference/,
openage/convert/service/read/nyan_api_loader.pyandlibopenage/gamestate/api/: change them together, then reconvert - Rendering: OpenGL via libepoxy (Vulkan in progress); GUI: Qt6 Quick
Non-obvious traps; add new ones here, verified and in one line
- In-source builds are forbidden and poison the tree;
make cleaninsourcebuildremoves the traces - Run Python only via
bin/run ...ormake run, neverpython3 -m openagein the source tree: compiled Cython modules andopenage/config.pylive inbin/ - Never edit files in
bin/, they are generated:config.*from*.in,*.gen.{h,cpp}fromopenage/codegen/and*.template,libopenage.pxdfiles frompxd:annotations in C++ headers; codegen trouble:make cleancodegen - nyan is a separate project: build it and point to it with
-Dnyan_DIR=, or./configure --download-nyan - C++20, but the minimum compilers are gcc/clang 10 and MSVC 16.8 (
buildsystem/CheckCompilerFeatures.cmake): check support before using newer library features likestd::format
- Read the surrounding code first and follow its patterns instead of inventing parallel ones
- Run a read-only command only if its result could change your next action; otherwise decide from what you have and act (edit + test)
- Do things properly, not quickly (development.md): prefer the principled fix; if it needs broad changes, propose the approach and confirm the scope first
- Broken or missing upstream (nyan, Cython, compilers, libraries): fix or report it there; an unavoidable hack gets a
TODO:explaining it - Internal interfaces may change: update all callers instead of adding compatibility shims; ask before changing file formats (nyan API, modpacks, cfg files)
- Ask when requirements are unclear instead of guessing
- Keep diffs focused: no drive-by refactors, reformatting, renames or "improvements" outside the task
- Fail hard: throw/raise on unexpected state instead of skipping, logging and continuing, or returning a default
- Trust the code over comments and docs; fix stale comments in code you touch
- Done means it builds, the relevant tests pass and
make checkmergeis clean; state what you could not verify, e.g. without game assets or GPU - Scratch files go to
/tmp, never into the tree
- Use the first that applies: YAGNI, stdlib, existing utilities (
libopenage/util/,openage/util/), an existing dependency, minimal new code - One source of truth: reuse, adapt or extend existing code instead of duplicating it, e.g. share converter logic between game editions instead of copying it per edition
- New dependencies need approval and must be added to doc/building.md, doc/build_instructions/, CMake,
nix/andpackaging/ - Don't hardcode what belongs in a config file, cvar or CLI option
- No dead code, commented-out code, debug output or unexplained
TODOs - Format only the lines you change:
clang-formatfor C++, never whole files (many aren't clang-format clean); useruff formatfor Python - Don't pad consecutive lines into columns and never realign existing ones; aligning continuation lines is fine (tabs_n_spaces.md)
- Separate logical blocks with empty lines
- Comment why, never what
- Never mention development phases, "scaffolding" or AI assistance in code, docs or commits
- Style by example (mom.h, mom.cpp):
this->for members,{}initialization, braces on every block, Doxygen API docs in the header, forward declarations in headers, sorted include groups - Every
.hhas a.cppthat includes it first, even if otherwise empty; header guard#pragma once - Register new
.cppfiles and directories in the localCMakeLists.txt(add_sources(libopenage ...),add_subdirectory) - Integer types per inttypes.md
- RAII and smart pointers, no owning raw
new/delete - Log with
log::log(INFO << ...), throwError{MSG(err) << ...}(exceptions.md); nostd::cout/printf, no barestd::exceptions
- Target
PYTHON_MIN_VERSIONfrom CMakeLists.txt - Register new files in the local
CMakeLists.txt(add_py_modules,add_cython_modules,add_pxds); cmake fails on unlisted.pyfiles - Declare C++ functions
except +in theirpxd:annotation unless they arenoexcept, otherwise a C++ exception crashes the interpreter (pyinterface.md) - Diagnostics via
openage.log(dbg,info,warn, ...), notprint - Type-annotate new functions like the surrounding code
- Keep ruff clean; a
# noqa: ...needs a reason and the narrowest scope
Write tests where they catch real bugs; the right kind depends on the subject:
- Data structures, algorithms, math, curves, events, pathfinding: unit tests of invariants and edge cases (empty, boundaries, overflow, equal timestamps, queries exactly at, before and after keyframes)
- Parsers and converter logic: small hand-made inputs; gate tests needing game assets on
has_assetsin the test list - Small pure Python functions: doctests, which double as documentation
- Rendering, GUI, input wiring and other interactive code: usually no automated test; verify with a demo or
make runand say so - Performance claims: measure with a benchmark (
bin/run test -b NAME) - Bug fixes: a regression test that fails without the fix, where the subject is testable as above
- Test observable behavior and contracts, not implementation details; no tests that restate the code or depend on timing, network or user interaction
- Never weaken, skip or delete a test to make it pass; fix the code or ask
Mechanics (testing.md):
- Register every test, demo and benchmark in
openage/testing/testlist.py - C++:
voidfunction in namespaceopenage, not declared in a header, failing viaTESTFAIL,TESTEQUALS,TESTTHROWS, ... fromlibopenage/testing/testing.h - Python: argument-less function raising
openage.testing.testing.TestError, e.g. viaassert_valueorassert_raises
- Update
doc/when behavior, interfaces or file formats change - Write for the reader, not as a record of decisions or attempts
- Markdown: one sentence per line
- Message
subsystem: summary, e.g.convert: ...,renderer: ...; the body explains what and why, concisely - One logical change per commit; squash fixups before review (contributing.md)
- New source files start with
// Copyright <year> the openage authors. See copying.md for legal info.(#in Python and CMake) and have mode 0644; don't bump years in existing headers - Commit authors must be listed in copying.md (authors check)
- Never commit generated files, build output, converted assets or scratch files
- Never push, force-push, open or comment on PRs without explicit consent