Codex instructions for this repository. Match existing subsystem patterns, fix root causes, and keep changes aligned with production architecture.
Apply guidance in this order:
- Explicit user instructions
- Root
AGENTS.md - Any nested
AGENTS.mdorAGENTS.override.mdthat applies to touched paths - Existing local subsystem patterns
- General style preferences
- Codex is launched from the project root, so only this root file is guaranteed to load automatically. Before editing a path covered by nested guidance, read the matching nested file explicitly.
- This is repo-level guidance. Narrower nested
AGENTS.mdfiles add subtree-specific rules and override broader guidance for their subtree. - Keep this file durable, concise, and repo-specific. Move subsystem-only detail into nested agent files when needed.
- If the user names a specific file, work in that file only unless they approve spillover.
Read these files before editing matching paths:
include/ai/AGENTS.mdfor AI public contracts.src/ai/AGENTS.mdfor AI implementation code.include/controllers/ui/AGENTS.mdfor UI controller public contracts.src/controllers/ui/AGENTS.mdfor UI controller implementation code.include/managers/AGENTS.mdfor manager public contracts.src/managers/AGENTS.mdfor manager implementation code.tests/AGENTS.mdfor tests, plus narrower test guidance when present.tests/ai/AGENTS.mdfor tests undertests/ai/.tests/managers/AGENTS.mdfor tests undertests/managers/.
- Performance-oriented, data-oriented, and minimal abstraction overhead.
- Prioritize memory safety, cache efficiency, low latency, minimal allocations, and deterministic behavior.
- Read the exact code path before editing.
- Search the same subsystem for the established pattern before inventing one.
- Do not assume system ownership, hot paths, or participating managers. Trace them in code first.
- Prefer targeted fixes over cleanup or opportunistic refactors.
- Do not add compatibility overloads, ad-hoc safety layers, or new abstractions unless the task requires them.
- Keep production and test updates in the same change when behavior changes.
- Before finishing, run the most targeted build or test feasible and state exactly what you verified.
Build:
cmake -B build/ -G Ninja -DCMAKE_BUILD_TYPE=Debug && ninja -C build
cmake -B build/ -G Ninja -DCMAKE_BUILD_TYPE=Release && ninja -C buildSanitizers:
- ASan and TSan are mutually exclusive.
- Remove
build/CMakeCache.txtwhen switching sanitizers or major build options.
cmake -B build/ -G Ninja -DCMAKE_BUILD_TYPE=Debug -DCMAKE_CXX_FLAGS="-D_GLIBCXX_DEBUG -fsanitize=address -fno-omit-frame-pointer -g" -DCMAKE_EXE_LINKER_FLAGS="-fsanitize=address" -DUSE_MOLD_LINKER=OFF && ninja -C build
cmake -B build/ -G Ninja -DCMAKE_BUILD_TYPE=Debug -DCMAKE_CXX_FLAGS="-D_GLIBCXX_DEBUG -fsanitize=thread -fno-omit-frame-pointer -g" -DCMAKE_EXE_LINKER_FLAGS="-fsanitize=thread" -DUSE_MOLD_LINKER=OFF && ninja -C build
export TSAN_OPTIONS="suppressions=$(pwd)/tests/tsan_suppressions.txt"Reconfigure:
rm build/CMakeCache.txt && cmake -B build/ ...Run:
./bin/debug/VoidLight_Template
./bin/release/VoidLight_TemplateTests:
./bin/debug/<test_executable>
./bin/debug/<test_executable> --list_content
./bin/debug/<test_executable> --run_test="TestCase*"
./bin/debug/entity_data_manager_tests
./bin/debug/ai_manager_edm_integration_tests
./bin/debug/behavior_functionality_tests --run_test="FleeFromAttacker*"Slow scripts:
./tests/test_scripts/run_all_tests.sh --core-only --errors-only
./tests/test_scripts/run_controller_tests.sh --verboseBoost.Test notes:
- Test names use the
BOOST_AUTO_TEST_CASEname directly. - Suite prefixes are optional.
- Use
--list_contentto confirm the exact test name before filtering.
See tests/TESTING.md for broader test documentation.
- Source:
src/{core,managers,controllers,gameStates,entities,events,ai,collisions,utils,world,gpu} - Headers mirror source under
include/ - Other important dirs:
tests/,docs/,res/,res/shaders/ - Dependency direction:
Core -> Managers -> GameStates -> Entities/Controllers - Managers serve the states: states own screen lifecycle and policy; domain managers provide services states call.
GameStateManageris state-stack infrastructure (not a domain manager). Full-screen transitions use exit-then-enter; UI is cleared in the transition and rebuilt inenter(). Seedocs/ARCHITECTURE.md. - Common architectural anchors:
GameEngine,ThreadSystem,EntityDataManager,AIManager,EventManager,ControllerRegistry,GPURenderer,GPUSceneRecorder,SpriteBatch
- C++20, 4-space indent, Allman braces.
- Naming: UpperCamelCase types, lowerCamelCase functions/vars,
m_/mp_members, ALL_CAPS constants. - Prefer RAII, smart pointers, forward declarations, and non-trivial logic in
.cpp. Use.hppfor C++ headers and.hfor C headers. - Use
const T&for read-only non-trivial inputs,T&for mutation, and value for primitives. - Prefer
std::span,std::string_view,std::optional, and explicit read/mutate APIs. - Use
const std::string&for map lookups. Avoidstring_view -> stringchurn. - Do not introduce raw-pointer ownership, nullable raw-pointer parameters/returns, or raw-pointer optional out-parameters. Use references, values,
std::optional, handles, and smart pointers instead. - Avoid raw arrays,
const char*constants, C-string APIs, and new legacy compatibility overloads in C++ code. Usestd::string_viewfor non-owning text andstd::stringwhen storage or lifetime is needed. - Keep new code idiomatic C++. Add C-style code only when required by SDL or another unavoidable C API, and isolate it at the final boundary.
- Use
std::format()for logs. Never concatenate log strings with+. UseAI_INFO_IF(cond, msg)when only logging is conditional. - Use
VOIDLIGHT_DEBUG_ONLY(...)for debug-only code. Do not use raw#ifdef DEBUG. - Remove unused parameter names entirely, for example
void foo(float). Do not use(void)paramor commented names. Avoid[[maybe_unused]]in production except on empty virtual base defaults; in tests prefer real assertions over unused probes. - Check important
[[nodiscard]]bool returns such asinit(),load(), andcreate(). UseBOOST_REQUIRE()in tests. - Preserve the project copyright header:
/* Copyright (c) 2025 Hammer Forged Games ... MIT License */- Avoid per-frame allocations. Reuse member buffers, call
reserve()when size is known, preserve capacity withclear(), neverswap()away reusable capacity, and prefer reusable ref-based buffer APIs over return-by-value patterns. - Keep reusable scratch state thread-local when used from worker code.
- Main thread owns SDL events and rendering. Worker threads process batches only.
- Use
ThreadSystem, not raw threads. - Use
WorkerBudgetto decide threading and batch sizing, and report execution after work completes. - Futures must complete before dependent operations.
- Avoid non-
thread_localstatic state in threaded code. - Align hot atomics with
alignas(64)when contention matters. - Use
include/utils/SIMDMath.hppfor SIMD work. Process 4 elements per iteration plus a scalar tail.
EntityDataManageris storage only. AI decision logic belongs inBehaviors::andBehaviorExecutors.EDM::recordCombatEvent()records stats and memory only; emotion math belongs outside EDM in AI/behavior code.- Witnessed combat/death memories are behavior-consumed state; EDM stores memory records only.
AIManager::update()commits command-bus changes and caches world/player data on the main thread before worker batches; behavior execution and emotional decay run in the AI batch path.- Cross-frame state such as paths and timers belongs in EDM, not local temporaries.
- Controllers must never mutate AI behavior state directly in EDM.
- Use
Behaviors::queueBehaviorMessage()from the main thread andBehaviors::deferBehaviorMessage()from worker threads. Behaviors::switchBehavior()enqueues behavior transitions.AIManager::commitQueuedBehaviorTransitions()clears behavior data beforeinit(); set new behavior state after the transition commit, not before.- EDM render data stores atlas coordinates and frame metadata, not texture ownership. Resolve manager-owned GPU textures at render submission and call
.get()only at the final GPU API boundary.
- Call
prepareForStateTransition()on active managers before cleanup. - In AI-heavy states, clean up in this order when initialized:
AIManager,ProjectileManager,BackgroundSimulationManager,WorldManager,WorldResourceManager,EventManager,CollisionManager,PathfinderManager,EntityDataManager,WorkerBudgetManager,ParticleManager. - Demo states may skip managers they never initialized.
ControllerRegistry::clear()must be called inGamePlayState::exit(), not justunsubscribeAll().EventManagersupports persistent and transient handlers. Persistent manager-level handlers register ininit()withregisterPersistentHandler[WithToken]()and survive transitions. State-level handlers register inenter()withregisterHandler[WithToken]()and are cleared byclearTransientHandlers().clearAllHandlers()is for shutdown only.- Collision callbacks are manager-owned infrastructure; projectile collisions use the persistent projectile hit sink rather than state-owned callbacks.
- Do not manually unsubscribe and resubscribe persistent manager handlers across transitions.
- World-geometry caches, spatial indices, and reverse lookups must be cleared by transition cleanup or unload handling. Do not rely only on deferred
WorldUnloadedafter transition cleanup has begun. - No game state should register collision callbacks directly.
- Use deferred transitions: set intent in
enter(), then transition inupdate().
- Exactly one present per frame.
GameEngine::render()performs scene and UI rendering;GameEngine::present()performs the actual present. Never clear, end, submit command buffers, or present inside a game state. - GPU flow is
beginFrame(), staterecordGPUVertices(), scene pass, composite to swapchain, UI pass, thenendFrame()fromGameEngine::present(). States implementrecordGPUVertices(),renderGPUScene(), andrenderGPUUI(). - GPU scene textures stay at viewport dimensions. Zoom and sub-pixel offset belong in the composite shader, not tile scaling.
- GPU atlas interpretation is authoritative for atlas-backed EDM render data.
- For SDL3 GPU UI text, use
TTF_GetGPUTextDrawData()only. Do not add UV flips, half-texel offsets, or shader hacks. Snap integer UI text placement to whole pixels before emitting vertices. - Trace camera updates, interpolation, rounding, sub-pixel offsets, and draw submission before proposing jitter or flicker fixes. Do not apply speculative fixes for jitter, shimmer, or flicker.
DayNightControllerrequiresupdate(dt)every frame. The GPU path already feeds it throughGPURenderer::setDayNightParams().- Use
LoadingStateplus asyncThreadSystemwork for loading instead of blocking manual rendering. - Call
setComponentPositioning()after creating UI components. Prefer existing UI helpers such ascreateTitleAtTop(),createButtonAtBottom(),createCenteredButton(), andcreateCenteredDialog(). - For UI/controller layout work, prefer
UIManagerpublic sizing, positioning, and relayout APIs. Do not reach back intoGameEnginefrom controllers just to query window size or force UI relayout. - Use
mp_stateManager->changeState()for transitions.GameEngine::Instance()remains valid for non-transition engine access. - Prefer local references over cached manager or controller members. Add controllers with
m_controllers.add<T>()inenter()and do not keep cachedmp_*Ctrlmembers.
- Prefer direct test executables over slow wrapper scripts.
- Never relax test expectations to hide a production bug unless the user explicitly asks.
- For
EventManagerregressions, first distinguish missing state-owned handler wiring in tests from a production defect. - Delete dead code and unused parameters. Do not comment them out.
FrameProfilerusesF3. Prefer RAII timers:ScopedPhaseTimer,ScopedManagerTimer,ScopedRenderTimer,ScopedRenderTimerGPU. Profiling is a no-op in release builds; hitch detection starts above 20 ms.- Demo states are test and showcase code.
GamePlayStatestays production-clean. - Use
SettingsMenuStateandMainMenuStateas menu references. WorldResourceManageris a spatial index over EDM, not a quantity store.CollisionManager::subscribeWorldEvents()is persistent manager infrastructure registered frominit(); do not rewire it during state transitions.