Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
9 changes: 9 additions & 0 deletions .cursor/mcp.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"mcpServers": {
"caffeine-docs": {
"command": "node",
"args": ["packages/docs-mcp/src/index.js"],
"cwd": "${workspaceFolder}/caffeine-website"
}
}
}
10 changes: 10 additions & 0 deletions .cursor/rules/engine-docs-policy.mdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
description: Engine repo has no Markdown docs; use caffeine-website submodule and MCP
alwaysApply: true
---

# Documentation

- Do not add `docs/` or `planning/` Markdown to the Caffeine engine repo.
- Read `AGENTS.md` for layout and implementation rules.
- User-facing docs: `caffeine-website` submodule (Svelte/HTML + TeX). Agents use `caffeine-docs` MCP from that repo.
9 changes: 9 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,12 @@ CTestTestfile.cmake

# Git worktrees
.worktrees/

# Dependencies and local tooling
**/node_modules/
.doppio-plugins/

# Documentation lives in caffeine-website submodule
docs/
planning/
.planning/
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,6 @@
[submodule "WaveShaper"]
path = WaveShaper
url = git@github.com:devscafecommunity/waveshaper.git
[submodule "caffeine-website"]
path = caffeine-website
url = git@github.com:devscafecommunity/caffeine-website.git
55 changes: 55 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# AGENTS.md — Caffeine Engine

Rules for humans and coding agents working in **this repository** (the C++ engine). User-facing documentation lives in the **`caffeine-website`** submodule, not here.

## Repository layout

| Path | Purpose |
|------|---------|
| `src/` | Engine runtime, editor, render, script bridge |
| `apps/` | Executables (runtime, editor shells, plugins) |
| `include/` | Public C++ headers shipped to games/tools |
| `tests/` | GTest suites |
| `cmake/` | CMake modules |
| `assets/` | Sample content (no `node_modules` in git) |
| `caf-pack/`, `Convoy/`, `WaveShaper/` | Git submodules |
| `caffeine-website/` | Docs site (Svelte/HTML), TeX/PDF, docs MCP |

Do not add `docs/`, `planning/`, or `.planning/` back to this repo.

## Documentation policy

- **No Markdown documentation** in the engine tree (no `docs/**/*.md`, no planning RFCs here).
- **Specs and manuals**: Svelte/HTML in `caffeine-website/packages/docs-site`, or LaTeX under `caffeine-website/tex/` for PDFs.
- **Agents**: Use the **caffeine-docs MCP** from `caffeine-website` (`search_docs`, `get_doc`). Do not scrape removed paths from git history unless the user asks.
- **README.md** here is only a short entrypoint with build commands and a link to the site submodule.

## Implementation rules

1. **C++20**, CMake 3.20+, SDL3 — match existing style in the file you edit.
2. **Minimal diffs** — one concern per change; no drive-by refactors.
3. **Tests** — add or update tests under `tests/` when behavior changes; run `ctest` from the build dir.
4. **Submodules** — after clone, `git submodule update --init --recursive`.
5. **Editor vs runtime** — keep play-mode-only code out of editor-only paths unless shared APIs are intentional.
6. **Scripting / native modules** — follow public headers in `include/caffeine/`; document API changes in the website repo, not here.
7. **Commits** — only when the user requests; never commit secrets or local build artifacts.

## Build (quick)

```bash
git submodule update --init --recursive
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build -j
cd build && ctest --output-on-failure
```

Cross-platform helpers: `scripts/build.sh`, `scripts/build.bat`, `scripts/build.fish`.

## Submodule: caffeine-website

```bash
git submodule update --init caffeine-website
cd caffeine-website && npm install && npm run dev
```

Publish doc changes in that repository; bump the submodule pointer in the engine when the user wants the engine repo to pin a new docs revision.
183 changes: 155 additions & 28 deletions CMakeLists.txt

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion Convoy
Submodule Convoy updated 53 files
+62 −1 CMakeLists.txt
+14 −1 build.sh
+34 −14 convoy_layout.ini
+22 −0 docs/EXPORT_TO_CAFPACK.md
+8 −5 src/cmake/FindGLAD.cmake
+3 −3 src/cmake/FindGLFW3.cmake
+10 −2 src/core/input/input_handler.cpp
+223 −0 src/core/ui/convoy_assets.cpp
+33 −0 src/core/ui/convoy_assets.hpp
+12 −3 src/core/ui/main_menu_bar.cpp
+1 −0 src/core/ui/main_menu_bar.h
+39 −11 src/core/ui/theme_manager.cpp
+13 −20 src/core/ui/theme_manager.h
+155 −0 src/core/ui/tool_icons.cpp
+27 −0 src/core/ui/tool_icons.hpp
+18 −1 src/core/window_manager.cpp
+3 −1 src/core/window_manager.h
+387 −133 src/modules/mod_architect/architect_ui.cpp
+39 −5 src/modules/mod_architect/architect_ui.h
+23 −1 src/modules/mod_architect/canvas.cpp
+1 −0 src/modules/mod_architect/canvas.h
+110 −0 src/modules/mod_architect/selection.cpp
+41 −0 src/modules/mod_architect/selection.h
+2 −1 src/modules/mod_architect/tools/bucket_tool.cpp
+0 −4 src/modules/mod_architect/tools/bucket_tool.h
+19 −0 src/modules/mod_architect/tools/color_picker_tool.cpp
+24 −0 src/modules/mod_architect/tools/color_picker_tool.h
+42 −0 src/modules/mod_architect/tools/ellipse_tool.cpp
+24 −0 src/modules/mod_architect/tools/ellipse_tool.h
+7 −18 src/modules/mod_architect/tools/eraser_tool.cpp
+42 −0 src/modules/mod_architect/tools/line_tool.cpp
+24 −0 src/modules/mod_architect/tools/line_tool.h
+8 −16 src/modules/mod_architect/tools/pencil_tool.cpp
+146 −0 src/modules/mod_architect/tools/pixel_ops.cpp
+22 −0 src/modules/mod_architect/tools/pixel_ops.h
+42 −0 src/modules/mod_architect/tools/rect_tool.cpp
+24 −0 src/modules/mod_architect/tools/rect_tool.h
+24 −0 src/modules/mod_architect/tools/tool_base.h
+17 −0 src/modules/mod_architect/tools/tool_settings.h
+26 −16 src/modules/mod_architect/ui/color_palette.cpp
+3 −1 src/modules/mod_architect/ui/color_palette.h
+1 −1 src/modules/mod_architect/viewport.cpp
+138 −0 src/modules/mod_modeler/math3d.hpp
+152 −0 src/modules/mod_modeler/mesh_primitives.cpp
+25 −0 src/modules/mod_modeler/mesh_primitives.hpp
+120 −0 src/modules/mod_modeler/model_scene.cpp
+46 −0 src/modules/mod_modeler/model_scene.hpp
+233 −0 src/modules/mod_modeler/modeler_renderer.cpp
+59 −0 src/modules/mod_modeler/modeler_renderer.hpp
+288 −0 src/modules/mod_modeler/modeler_ui.cpp
+45 −0 src/modules/mod_modeler/modeler_ui.hpp
+4 −0 src/shared/types.h
+11 −1 tests/CMakeLists.txt
2 changes: 1 addition & 1 deletion WaveShaper
Binary file added _test_hero.caf
Binary file not shown.
Binary file added _test_multi.caf
Binary file not shown.
182 changes: 182 additions & 0 deletions apps/plugins/ai_development_plugin/main.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
#include "caffeine/navigation/NavigationApi.hpp"
#include "editor/EditorContext.hpp"
#include "editor/PluginAPI.hpp"

#include <filesystem>
#include <fstream>

#ifdef CF_HAS_IMGUI
#include <imgui.h>
#endif

namespace {

const char* kIdleScript = R"(function onCreate(entity)
caffeine.navigation.setMode(entity, "idle")
end

function onUpdate(entity, dt)
end
)";

const char* kPatrolScript = R"(function onCreate(entity)
caffeine.navigation.setMode(entity, "patrol")
end

function onUpdate(entity, dt)
end
)";

const char* kFollowScript = R"(-- otherEntityId: id of the entity this NPC should follow.
function onCreate(entity)
caffeine.navigation.setMode(entity, "follow")
end

function onUpdate(entity, dt)
end
)";

class AiDevelopmentPlugin : public Caffeine::Editor::IPlugin {
public:
explicit AiDevelopmentPlugin(const Caffeine::Editor::PluginHostApi* host) : m_host(host) {}

void OnLoad() override {
if (!m_host || !m_host->registerPanel) return;
m_host->registerPanel(m_host->editorContext, GetName(), "AI Development",
&AiDevelopmentPlugin::renderPanel, this);
}

void OnUnload() override {}
void OnUpdate(float) override {}

const char* GetName() const override { return "AI Development"; }
const char* GetVersion() const override { return "0.1.0"; }
const char* GetDescription() const override {
return "Author NPC behaviour: idle, patrol, follow, or a Lua script. Not limited to combat.";
}

private:
static Caffeine::Editor::EditorContext* editorCtx(const Caffeine::Editor::PluginHostApi* host) {
return host ? static_cast<Caffeine::Editor::EditorContext*>(host->editorContext) : nullptr;
}

static bool writeTemplate(const std::filesystem::path& path, const char* source) {
std::error_code ec;
std::filesystem::create_directories(path.parent_path(), ec);
std::ofstream out(path);
if (!out) return false;
out << source;
return static_cast<bool>(out);
}

static void renderPanel(void* userData) {
#ifdef CF_HAS_IMGUI
auto* self = static_cast<AiDevelopmentPlugin*>(userData);
auto* ctx = editorCtx(self->m_host);
if (!ctx || !ctx->activeWorld) {
ImGui::TextDisabled("No active scene.");
return;
}
Caffeine::ECS::World& world = *ctx->activeWorld;
ImGui::TextWrapped(
"Program any NPC: a villager, a guide, a creature, or an opponent. The grid pathfinder "
"lives in the engine; this panel only authors behaviour.");

if (!ctx->selectedEntity.isValid()) {
ImGui::TextDisabled("Select an entity to add a nav volume or an agent.");
return;
}
Caffeine::ECS::Entity entity = ctx->selectedEntity;
ImGui::Text("Target: %s (%u)", Caffeine::Editor::getEntityName(world, entity), entity.id());

if (!world.has<Caffeine::Navigation::NavVolume>(entity)) {
if (ImGui::Button("Add Nav Volume")) {
Caffeine::Navigation::NavVolume volume;
volume.resize(32, 32);
world.add<Caffeine::Navigation::NavVolume>(entity, volume);
ctx->isDirty = true;
}
} else if (auto* volume = world.get<Caffeine::Navigation::NavVolume>(entity)) {
ImGui::SeparatorText("Nav volume");
int size[2] = {volume->width, volume->height};
if (ImGui::DragInt2("Cells", size, 1.0f, 2, 256)) {
volume->resize(size[0], size[1]);
ctx->isDirty = true;
}
if (ImGui::DragFloat("Cell size", &volume->cellSize, 0.05f, 0.1f, 100.0f)) ctx->isDirty = true;
if (ImGui::DragFloat3("Origin", &volume->origin.x, 0.1f)) ctx->isDirty = true;
}

ImGui::Separator();
if (!world.has<Caffeine::Navigation::NavAgent>(entity)) {
if (ImGui::Button("Add Nav Agent")) {
world.add<Caffeine::Navigation::NavAgent>(entity);
ctx->isDirty = true;
}
} else if (auto* agent = world.get<Caffeine::Navigation::NavAgent>(entity)) {
ImGui::SeparatorText("Agent");
int mode = static_cast<int>(agent->mode);
const char* modes[] = {"Idle", "Patrol", "Follow", "Scripted"};
if (ImGui::Combo("Behaviour", &mode, modes, 4)) {
agent->mode = static_cast<Caffeine::Navigation::NavMode>(mode);
agent->planDirty = true;
ctx->isDirty = true;
}
if (ImGui::DragFloat("Speed", &agent->speed, 0.05f, 0.0f, 100.0f)) ctx->isDirty = true;
if (ImGui::DragFloat3("Destination", &agent->destination.x, 0.1f)) {
agent->hasDestination = true;
agent->planDirty = true;
ctx->isDirty = true;
}
if (ImGui::Button("Add patrol point from destination")) {
Caffeine::Navigation::addPatrolPoint(world, entity, agent->destination);
ctx->isDirty = true;
}
ImGui::TextDisabled("Patrol points: %u", agent->patrolCount);
if (ImGui::Button("Clear patrol")) {
agent->patrolCount = 0;
agent->patrolIndex = 0;
ctx->isDirty = true;
}

ImGui::SeparatorText("Behaviour scripts");
const std::filesystem::path root = ctx->projectRootPath.empty()
? std::filesystem::path("scripts/ai")
: ctx->projectRootPath / "scripts" / "ai";
auto save = [&](const char* file, const char* source) {
const std::filesystem::path path = root / file;
if (!writeTemplate(path, source)) return;
Caffeine::Navigation::setBehaviorScript(world, entity, path.generic_string());
ctx->assetBrowserDirty = true;
ctx->isDirty = true;
};
if (ImGui::Button("Write idle.lua")) save("idle.lua", kIdleScript);
ImGui::SameLine();
if (ImGui::Button("Write patrol.lua")) save("patrol.lua", kPatrolScript);
ImGui::SameLine();
if (ImGui::Button("Write follow.lua")) save("follow.lua", kFollowScript);
if (agent->behaviorScript[0] != '\0') {
ImGui::TextWrapped("Script: %s", agent->behaviorScript);
}
}
#else
(void)userData;
#endif
}

const Caffeine::Editor::PluginHostApi* m_host = nullptr;
};

AiDevelopmentPlugin* g_plugin = nullptr;

} // namespace

extern "C" Caffeine::Editor::IPlugin* CreatePlugin(const Caffeine::Editor::PluginHostApi* host) {
if (!g_plugin) g_plugin = new AiDevelopmentPlugin(host);
return g_plugin;
}

extern "C" void DestroyPlugin(Caffeine::Editor::IPlugin* plugin) {
delete plugin;
g_plugin = nullptr;
}
4 changes: 2 additions & 2 deletions apps/plugins/post_processing_plugin/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,8 @@ class PostProcessingPlugin : public Caffeine::Editor::IPlugin {
if (!fx) return;

ImGui::TextWrapped(
"Modular effect stack — enable and tune each module independently. GPU passes are "
"planned; editor preview uses approximate overlays.");
"The same stack the viewport and gameplay preview run on the GPU. Enable each module "
"on its own; cinematic, horror and arcade are optional looks, not game types.");

if (ImGui::CollapsingHeader("Benchmark looks (optional)", ImGuiTreeNodeFlags_DefaultOpen)) {
if (Caffeine::PostProcess::EditorUI::drawBenchmarkPresets(*fx)) ctx->isDirty = true;
Expand Down
40 changes: 38 additions & 2 deletions apps/runtime/main.cpp
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
#include "animation/AnimationSystem.hpp"
#include "effects/EffectSystem.hpp"
#include "navigation/NavigationSystem.hpp"
#include "debug/LogSystem.hpp"
#include "events/EventBus.hpp"
#include "events/Events.hpp"
Expand All @@ -15,12 +17,19 @@
#include "ecs/World.hpp"
#include "ecs/MeshComponents.hpp"
#include "ecs/CameraComponents.hpp"
#include "ecs/Components3D.hpp"
#include "ecs/ComponentQuery.hpp"
#include "scene/SceneComponents.hpp"
#include "scene/HierarchySystem.hpp"
#include "ui/UISystem.hpp"
#include "ui/UIRenderer.hpp"
#include "render/Overlay2D.hpp"
#include "render/Camera2D.hpp"
#include "scene/PickQuery.hpp"
#include "scene/PlayMode2D.hpp"
#include "runtime/PlayClock.hpp"
#include "scene/EnvironmentSystem.hpp"
#include "scene/EnvironmentEffectsSystem.hpp"
#include "procedural/ProceduralWorldSystem.hpp"
#include "render/SkyboxRenderer.hpp"
#include "math/Mat4.hpp"
Expand Down Expand Up @@ -330,6 +339,18 @@ void renderGameView(Caffeine::ECS::World& world, Caffeine::Editor::EditorContext
renderer.render(world, ctx, dl, vp, camPos, origin, panelSize, cameraEntity, projectRoot,
&gpuCam);

Caffeine::Render::Rect2D vpRect{{origin.x, origin.y}, {panelSize.x, panelSize.y}};
Caffeine::Render::Camera2D cam2d;
cam2d.setViewport(vpRect);
cam2d.setPosition({camPos.x, camPos.y});
cam2d.setZoom(1.0f);
const Caffeine::ECS::Entity cam2dEntity = Caffeine::Scene::findActiveCamera2DEntity(world);
if (cam2dEntity.isValid()) {
cam2d = Caffeine::Scene::buildCamera2DFromEntity(world, cam2dEntity, vpRect);
}
Caffeine::Scene::PlayRenderContext::instance().set2D(vpRect, cam2d);
Caffeine::Render::Overlay2D::draw(dl, cam2d, origin, panelSize);

Caffeine::UI::drawWidgets(world, dl, origin, panelSize);

ImGui::End();
Expand Down Expand Up @@ -439,15 +460,16 @@ int main(int argc, char** argv) {
Caffeine::Events::EventBus eventBus;
Caffeine::Physics2D::PhysicsSystem2D physics(&eventBus);
Caffeine::Animation::AnimationSystem animation;
Caffeine::UI::UISystem uiSystem(&eventBus);
Caffeine::Script::ScriptEngine scriptEngine;
Caffeine::Script::ScriptEngine::InitParams scriptParams;
scriptParams.world = &world;
scriptParams.input = &input;
scriptParams.events = &eventBus;
scriptParams.ui = &uiSystem;
const bool scriptsReady = scriptEngine.init(scriptParams);
scriptEngine.setSearchRoot(buildRoot.string());
Caffeine::Script::ScriptSystem scriptSystem(scriptsReady ? &scriptEngine : nullptr);
Caffeine::UI::UISystem uiSystem(&eventBus);
Caffeine::Script::ScriptSystem scriptSystem(scriptsReady ? &scriptEngine : nullptr, &eventBus);
if (scriptsReady) {
scriptSystem.resetPlayState();
eventBus.subscribe<Caffeine::Events::OnCollision2D>(
Expand Down Expand Up @@ -506,6 +528,20 @@ int main(int argc, char** argv) {
pumpRuntimeInput(input);
if (sceneLoaded) {
animation.onUpdate(world, dt);
Caffeine::Vec3 effectCamera{};
{
Caffeine::ECS::ComponentQuery cameraQuery;
cameraQuery.with<Caffeine::ECS::Camera3DComponent>();
cameraQuery.with<Caffeine::ECS::Position3D>();
world.forEach<Caffeine::ECS::Camera3DComponent, Caffeine::ECS::Position3D>(
cameraQuery, [&](Caffeine::ECS::Entity, Caffeine::ECS::Camera3DComponent&,
Caffeine::ECS::Position3D& position) {
effectCamera = position.position;
});
}
Caffeine::Effects::tickEffects(world, dt, effectCamera);
Caffeine::Scene::tickEnvironmentEffects(world, dt, effectCamera);
Caffeine::Navigation::updateNavigation(world, dt);
physics.onUpdate(world, dt);
if (scriptsReady) {
scriptEngine.setWorld(&world);
Expand Down
Binary file added assets/benchmarks/dense_outdoor.caf
Binary file not shown.
Loading
Loading