Документація › Огляд
v7.0 · M46+AI-Ext-1–20+BT VM+IMP-1–9 · C++17 · SDL3 + SDL_GPU (Vulkan) · EnTT · Recast/Detour · AVX2
monkey_dust
Ізометричний RPG-sandbox з відкритим світом. C++17 без ігрового рушія — тільки бібліотеки. Рендер-пайплайн натхненний Flare Engine. Бекенд: SDL3 + SDL_GPU (Vulkan), виключний — OpenGL/Raylib повністю видалено (FIX-8). AI архітектура натхненна механіками Alien: Isolation. 9 нових покращень з аналізу механік OGRE/Echo/Cocos2d-x. Цільове залізо: Intel HD 520 (AVX2).
⚡
SDL_GPU Exclusive
Vulkan через SDL_GPU HAL. SPIR-V шейдери (#version 460). MD_OPENGL43_ENABLED видалено повністю (FIX-8: −1449 рядків).
🗺️
Isometric Flare
Flare-сумісні мапи: TileMapRenderer (3D ізометрія) + TileMap2DRenderer (2D SDL_GPU). Multi-atlas, billboard, FRINGE.
🧩
ECS (EnTT)
500+ NPC, всі компоненти — чистий POD. Collect→apply патерн. Split-ready engine/include/monkey_dust/.
🧠
AI Systems
Stackless BT VM + BTSystem + BTJsonLoader + DirectorSystem + FlowGraph + SenseSystem. Extended AI patterns (C1–C20).
🔒
Security Audit
10 fixes: Lua 8MB sandbox, CRC32 SaveHeader v8, POSIX mkdir, _mm_pause spinlock, MAX_BONES 128→6.
👾
Extended AI Patterns
20 Extended AI patterns — MotivationType, RoleRegistry, AwarenessState, FlowDurableTrigger, BTJsonLoader, BTNodePool.
🤖
BT VM повний
BTNodePool 32KB arena · BTSystem ECS driver · BTJsonLoader strstr parser · 30+ node types · data/bt/ JSON trees.
🔬
9 Improvements
ACES tonemapping · Live BT reload · NpcMemory · Dirty TINST · Dirty SSBO · WorldSim 1Hz · MaterialDesc · UtilityScorer · md::fs
🏆 SDL_GPU Exclusive — стан після FIX-8
Поточний стан: SDL_GPU виключний (2026-05-16)
Raylib повністю відв'язаний.
MD_OPENGL43_ENABLED видалено з усіх 32 файлів (FIX-8: −1449 рядків мертвого коду). Єдиний активний бекенд — SDL_GPU (Vulkan). SPIR-V шейдери
#version 460. Standalone editor компілюється з
-UMD_SDL_GPU і використовує GL fallback через
if(dev==null).
| Milestone | Зміст | Дата |
| M0–M5 | Platform abstraction: input.h, audio.h, window.h; SSBO→glad; miniaudio | 2026-05-02 |
| M6 | Monorepo: engine/ + game/ + tools/; split-ready; MdCamera unified | 2026-05-02 |
| M7.1–M7.27 | Flare parser, TileMetaRegistry, billboard, multi-atlas, animation, FRINGE, x_off | 2026-04-30 |
| M11–M12 | math_types.h; MdDraw2D; standalone editor SDL3; Raylib removed from engine | 2026-05-02 |
| SDL_GPU A1 | SPIR-V toolchain: compile_shaders.sh; 21 шейдерів Vulkan 1.1 | 2026-05-02 |
| SDL_GPU A2–A7 | GpuRingBuffer, GPU HAL, GpuTexture/StaticBuffer/ComputePipeline/RenderPass/DrawIndexedIndirect | 2026-05-02 |
| М_SPLIT | ECS компоненти + combat типи → engine/include/monkey_dust/ | 2026-05-06 |
| П.1–П.3 | Raylib повністю видалено з SDL3 build path; MD_SDL_GPU авто з USE_SDL3 | 2026-05-07 |
| FIX-8 | MD_OPENGL43_ENABLED: видалено 1449 рядків з 32 файлів (scripts/remove_gl43_guards.py); CMakeLists.txt очищено; DrawBuildPreview/DrawDebugGrid — stub | ✅ 2026-05-13 |
🚀 Швидкий старт
Залежності
cmake ninja-build clang libvulkan-dev glslc ccache + SDL3 як субмодуль або системний пакет.
1
Клонувати зі субмодулями
git clone --recurse-submodules https://github.com/rdga1bot/monkey_dust.git
cd monkey_dust
2
Зібрати (USE_SDL3=ON — єдиний активний шлях)
# Основна збірка:
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DUSE_SDL3=ON
ninja -C build monkey_dust # гра → build/game/monkey_dust
ninja -C build monkey_dust_editor # редактор → build/tools/monkey_dust_editor
ninja -C build md_flare_demo # viewer → build/tools/md_flare_demo
ninja -C build md_flare_convert # конвертер → build/tools/md_flare_convert
# З wicked-style editor в грі:
cmake -S . -B build -G Ninja -DUSE_SDL3=ON -DMONKEY_DUST_EDITOR=ON
ninja -C build monkey_dust
3
Тести та SPIR-V шейдери
# Запустити 131 GTests:
ninja -C build md_tests && build/tests/md_tests
# Перекомпілювати SPIR-V після зміни .vert/.frag/.comp:
bash scripts/compile_shaders.sh
4
Запустити (з кореня репо)
./build/game/monkey_dust
./build/tools/md_flare_demo
./build/tools/monkey_dust_editor
Flare demo: WASD/↑↓←→ pan · Q/E scroll zoom · R reset · Esc вийти
📁 Monorepo структура (M46 + AI-Ext-1–20 + BT VM + IMP-1–9)
monkey_dust/
├── engine/ ← Static lib: libmonkey_dust_engine.a
│ ├── include/monkey_dust/ ← Public headers (<monkey_dust/...>)
│ │ ├── platform/ input.h · audio.h · window.h (Rule M-A)
│ │ │ └── md_hints.h MD_HOT · MD_UNLIKELY · AVX2 (M46)
│ │ │ └── md_fs.h fs_read_all/alloc/free/exists — thin FS abstraction (IMP-9)
│ │ │ └── math_types.h · md_log.h
│ │ ├── render/
│ │ │ ├── gpu_hal.h GpuPipeline·VB·CB·StaticBuffer·Texture·Compute·Depth·RenderPass·DrawIndirect
│ │ │ ├── gpu_device.h · gpu_ring_buffer.h · ssbo.h
│ │ │ ├── animation_soa.h (MAX_BONES=6) · light_system.h · shadow_system.h
│ │ │ ├── ssao_system.h · particle_soa.h · instancer.h · md_camera.h
│ │ │ ├── gpu_profiler.h CPU BeginPass/EndPass (M41)
│ │ │ └── material_desc.h OGRE-style JSON → GpuPipeline::Desc auto-builder (IMP-7)
│ │ ├── scripting/ lua_system.h (sandbox 8MB) · lua_event_bus.h · flow_graph.h
│ │ ├── world/ spatial_grid.h · chunk_manager.h · transform_soa.h (dirty faction range)
│ │ │ └── world_simulation.h 1Hz economy: FactionState[8]+TradeRoute[32] (IMP-6)
│ │ ├── ai/ director_system.h · sense_registry.h · npc_config.h (bt_stage[4])
│ │ │ ├── fnv.h fnv1a + fnv_combine + fnv_path (C7)
│ │ │ ├── behavior_tree.h Stackless BT VM · BTNode(24B) · 30+ BTNodeType · C1–C20 · MemoryCheck/MemoryForget
│ │ │ ├── bt_node_pool.h Flat 32KB bump arena · AllocTree() · Reset() (sizeof(BT)=4616)
│ │ │ ├── bt_system.h BTSystem::Tick — frame_flags+hint expiry+BT tick · ConnectRegistry()
│ │ │ ├── bt_json_loader.h strstr recursive parser · LoadFromFile/String/ReadName
│ │ │ ├── bt_action_registry.h md::BTActionRegistry · MAX_ACTIONS=64 · Clear()
│ │ │ ├── role_registry.h RoleRegistry singleton · 8 NpcRoles · claim/release/query (C18)
│ │ │ └── utility_scorer.h Echo-style goal utility: FillCandidates+Bias(current,10)+BestFrom (IMP-8)
│ │ ├── components/ 14 POD компонентів; agent_state.h: timers×26, lcflags×64, gauges, motivation, awareness, alertness, mood, withdraw, entity_state; bt_components.h; npc_memory.h: SpatialMemory[8]+events[8] (IMP-3)
│ │ ├── combat/ damage_calc · hit_zones · power_def · power_manager
│ │ ├── nav/path_cache.h LRU 64 · _mm_pause spinlock (FIX-7)
│ │ ├── save/save_system.h async · CRC32 v8 · char[256] async_path (FIX-4/9)
│ │ └── flare/ tile_map.h · tile_map_renderer.h · flare_anim_system.h
│ └── src/ Engine .cpp files
│ ├── ai/behavior_tree.cpp · bt_json_loader.cpp
│ ├── ai/utility_scorer.cpp FillCandidates+Bias+BestFrom (IMP-8)
│ ├── render/material_desc.cpp strstr JSON → GpuPipeline::Desc (IMP-7)
│ ├── tools/md_fs.cpp fopen/fread/malloc impl (IMP-9)
│ └── world/world_simulation.cpp 1Hz economy tick (IMP-6)
│
├── game/ ← Executable: monkey_dust
│ └── src/
│ ├── main.cpp 10 TPS logic / 60 FPS render; SDL_GPU game loop
│ ├── ai/bt_loader.h JSON BT templates + WatchFile/PollReload live reload (IMP-2)
│ ├── ai/ · combat/ · building/ · nav/ · systems/
│ └── save/world_serializer.h v8 + CRC32 (FIX-4); POSIX mkdir (FIX-1)
│
├── tools/
│ ├── flare_demo/ md_flare_demo — SDL_GPU Vulkan Flare tile viewer
│ ├── editor/ monkey_dust_editor — wicked-style JSON + map editor
│ │ ├── editor_core · editor_hierarchy · editor_inspector · editor_map_view
│ │ ├── editor_viewcone_panel.h SenseComponent activation bars (M38)
│ │ ├── editor_flowgraph_panel.h FlowGraph nodes/vars/pending (M39)
│ │ ├── editor_director_panel.h menace + stage + manual override (M40)
│ │ └── editor_gpu_profiler_panel.h pass budget bars (M41)
│ └── flare_convert/ md_flare_convert — FLARE INI→JSON
│
├── shaders/ #version 460 GLSL + SPIR-V (compile_shaders.sh)
│ ├── pbr.frag · npc_instanced.frag ACES filmic tonemapping replaces Reinhard (IMP-1)
│ ├── animated.vert · skinning.comp MAX_BONES=6 (FIX-6)
│ ├── cull.comp · shadow_cull.comp (#version 460, FIX-5) · shadow_csm.vert/frag
│ ├── deferred_point.vert/frag · deferred_strip.vert/frag
│ ├── ssao.comp · smaa_edge/blend/final.vert/frag
│ ├── tile_map.vert/frag · tile_map_2d.vert/frag · particle.vert/frag
│ └── spirv/ *.spv — gitignored (compile_shaders.sh)
│
├── scripts/
│ ├── compile_shaders.sh glslc → SPIR-V Vulkan 1.1
│ └── remove_gl43_guards.py one-time migration tool (FIX-8, збережено для довідки)
│
├── data/bt/ systematic_search.bt.json — SystematicSearch BT (branches/motiv/role/timer)
├── tests/ Google Test suites (1252 тестів, 180 suites)
└── CLAUDE.md ← Конституція v12.0 (читати першою)
Split-readiness правило
engine/ не має жодного
#include з
game/ або
tools/. Нові компоненти →
engine/include/monkey_dust/. Game файли = forwarding wrappers.
⚙️ Збірка
| Target | Команда | Результат |
| Основна гра | ninja -C build monkey_dust | build/game/monkey_dust |
| Flare viewer | ninja -C build md_flare_demo | build/tools/md_flare_demo |
| Standalone editor | ninja -C build monkey_dust_editor | build/tools/monkey_dust_editor |
| Flare конвертер | ninja -C build md_flare_convert | build/tools/md_flare_convert |
| Test suite | ninja -C build md_tests | build/tests/md_tests |
| SPIR-V шейдери | bash scripts/compile_shaders.sh | shaders/spirv/*.spv |
CMake варіанти
# Стандартна збірка (USE_SDL3=ON — єдиний активний шлях):
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DUSE_SDL3=ON
# З wicked-style in-game editor:
cmake -S . -B build -G Ninja -DUSE_SDL3=ON -DMONKEY_DUST_EDITOR=ON
# Debug (ImGui overlay, DEBUG defines):
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Debug -DUSE_SDL3=ON
Ключові CMake defines
USE_SDL3=ON — SDL3+SDL_GPU шлях; авто-додає MD_SDL_GPU PUBLIC до engine
USE_GLM=ON — GLM замість власного math_types (автоматично при USE_SDL3)
MONKEY_DUST_EDITOR=ON — компілює editor panels у monkey_dust binary
Compile definitions per target
| Target | Defines | Примітка |
| monkey_dust_engine | USE_SDL3 USE_GLM MD_LOG_NO_RAYLIB MD_SDL_GPU | PUBLIC → propagate до всіх consumers |
| monkey_dust (game) | inherits engine PUBLIC | + DEBUG у Debug build |
| md_flare_demo | MD_SDL_GPU USE_SDL3 USE_GLM | SDL_GPU Vulkan рендер |
| monkey_dust_editor | MONKEY_DUST_STANDALONE_EDITOR USE_SDL3 -UMD_SDL_GPU | OpenGL контекст; SDL_GPU device не ініціалізується |
| md_tests | inherits engine PUBLIC | Google Test runner |
🗺️ Flare Rendering Pipeline
TileMapRenderer — 3D ізометричний (game runtime)
goblin_camp.txt
└─[LoadFlareMap]──► FlareMap { layers[], meta(TileMetaRegistry), spawns[] }
│
[ParseTilesetDefFile]
│ img= (ПЕРШИЙ рядок → atlas[0]; наступні → atlas[1..3])
▼
TileMetaRegistry { 229 entries, atlas_idx, offset_y, anim }
│
[TileMapRenderer::Render()]
PASS 1: CollectLayer (bg + fringe + object → vbuf[MAX_TILES])
PASS 2: qsort depth=(col+row)*3+prio + x_off sub-tile corr
PASS 3: UV v=1−y/H → ibuf[stride=36]; tile anim frames
PASS 4: batch draw per atlas_idx (≤4 draw calls)
│
glBufferSubData → glDrawArraysInstanced → tile_map.vert/frag
TileMap2DRenderer — 2D (SDL_GPU demo / standalone editor)
[TileMap2DRenderer::Init()]
├── GpuDevice::IsReady() == true → SDL_GPU path (Vulkan pipeline + GpuVertexBuffer)
└── GpuDevice::IsReady() == false → OpenGL fallback (VAO/VBO + GLSL shaders)
Stride = 44 bytes: a_corner(vec2) · a_screen_tl(vec2) · a_screen_size(vec2)
a_uv_rect(vec4) · a_atlas_idx(float)
📐 UV Convention — КРИТИЧНО
НЕ ПОРУШУВАТИ — призводить до невидимих тайлів
Будь-яка зміна UV у
tile_map_renderer.cpp,
tile_map_2d_renderer.cpp або
md_texture.cpp МУСИТЬ зберігати цю конвенцію.
ПРИЧИНА: MdLoadTexturePixelArt → stbi_set_flip_vertically_on_load(1)
GL texture v=0 → низ файлу (y_file = atlas_h)
GL texture v=1 → верх файлу (y_file = 0)
✅ ПРАВИЛЬНО: v_gl = 1.0f - y_file / atlas_h
❌ НЕПРАВИЛЬНО: v_gl = y_file / atlas_h ← тайли невидимі
MdLoadTexturePixelArt — фільтрація (НЕ СПРОЩУВАТИ)
| Parameter | Value | Навіщо |
| MIN_FILTER | GL_LINEAR_MIPMAP_LINEAR | Без mipmap GL_NEAREST = прозорий піксель на zoom-out |
| MAG_FILTER | GL_NEAREST | Crisp pixel-art на zoom-in |
| WRAP | GL_CLAMP_TO_EDGE | Без seam bleeding |
| flip_v | true | stbi flip — частина UV конвенції |
| gen_mipmap | true | Потрібен для LINEAR_MIPMAP |
🖼️ Atlas Selection Rule
Тільки ПЕРШИЙ img= рядок → atlas[0]
Кожен наступний
img= в tilesetdef →
atlas_idx++. Кожен тайл зберігає свій
atlas_idx в
TileMeta.
# tileset_grassland.txt:
[tileset]
img=images/tilesets/tileset_grassland.png ← atlas[0]
tile=16,1061,2698,192,96,96,48
...
img=images/tilesets/tileset_grassland_water.png ← atlas[1]
tile=301,...
🏔️ Tile Classification: Flat / Billboard
| Тип | Умова | Геометрія | Приклад |
| Flat | offset_y ≤ h/2 | XZ-ромб на y=0 (y_bot=y_top=0) | Трава, вода, скелі, будівлі |
| Billboard | offset_y > h/2 | Вертикальний quad | Дерева, гриби, NPC-спрайти |
float y_top = (float)tm->offset_y / 96.0f * tile_world_size;
float y_bot = -(float)(tm->h - tm->offset_y) / 96.0f * tile_world_size;
// Flat sentinel: y_bot = y_top = 0.0f
// Horizontal offset (M7.27):
float x_off = (float)(tm->w/2 - tm->offset_x) * tile_world_size / 192.0f;
// FRINGE depth:
int prio = (layer.type == LT::FRINGE) ? 1 : (layer.type == LT::OBJECT) ? 2 : 0;
// depth = (col + row) * 3 + prio
✨ Шейдер tile_map (3D renderer)
Per-instance layout (stride = 36 bytes, TINST_STRIDE)
| Attrib | Тип | Offset | Зміст |
| 0 | vec2 | per-vertex | a_corner — quad corner [0,1]×[0,1] |
| 1 | vec2 | 0 | a_tile_pos — grid (col, row) |
| 2 | vec4 | 8 | a_uv_rect — (u0, v0, u1, v1) |
| 3 | float | 24 | a_y_bot — world Y основи billboard (≤0); 0 = flat |
| 4 | float | 28 | a_y_top — world Y верхівки billboard (>0); 0 = flat |
| 5 | float | 32 | a_x_off — screen-right зміщення (M7.27) |
TINST_STRIDE = 36 — НЕ змінювати без синхронізації з tile_map.vert
Stride, offsets і attrib locations чітко прив'язані між
tile_map_renderer.cpp і
tile_map.vert.
⚡ GPU HAL
Абстракція поверх SDL_GPU. Після FIX-8 — тільки SDL_GPU шлях у HAL impl файлах. Весь OpenGL fallback code видалено з game/engine (залишається тільки у standalone editor через if(dev==null) guards).
| Клас | SDL_GPU impl | Призначення |
GpuPipeline | SDL_CreateGPUGraphicsPipeline (SPIR-V) | Графічний пайплайн + VAO layout |
GpuVertexBuffer | SDL_CreateGPUBuffer + TransferBuffer | Per-frame ring-buffered upload |
GpuCommandBuffer | SDL_AcquireGPUCommandBuffer | Frame command recording |
GpuStaticBuffer | SDL_CreateGPUBuffer (static) | Vertex/index дані |
GpuTexture | SDL_CreateGPUTexture | Textures + samplers |
GpuComputePipeline | SDL_CreateGPUComputePipeline | Compute shaders (cull, skin) |
GpuComputePass | SDL_BeginGPUComputePass | Dispatch + bindings |
GpuDepthTexture | SDL_CreateGPUTexture (depth) | CSM cascades, depth buffer |
GpuRenderPass | SDL_BeginGPURenderPass | Scoped render pass |
GpuDrawIndexedIndirect | SDL_DrawGPUIndexedPrimitivesIndirect | GPU-driven draw |
GpuRingBuffer | N=3 + MapWriteSDL / Upload(cmd) | Zero-stall per-frame SSBO upload |
GpuRingBuffer: zero-stall upload
cycle=true у SDL_GPU transfer: GPU автоматично обирає безпечний слот без CPU fence. TransformSoA (32KB) + AnimNpcState (8KB) завантажуються кожен кадр без блокування.
🔺 SPIR-V шейдери
bash scripts/compile_shaders.sh
# glslc --target-env=vulkan1.1 -DSPIRV_COMPILE=1 -o shaders/spirv/X.spv shaders/X.glsl
SPIRV_COMPILE guards
Всі шейдери мають
#ifdef SPIRV_COMPILE блоки для SDL_GPU (set/binding layout) та GL fallback.
#version 460 core обов'язковий для
gl_BaseInstance.
| Шейдер | Тип | Призначення |
| animated.vert + npc_instanced.frag | Graphics | GPU skeletal animation (MAX_BONES=6) |
| skinning.comp | Compute | Bone transform (local_size=64, procedural) |
| cull.comp | Compute | Main frustum + distance cull |
| shadow_cull.comp | Compute | Shadow frustum cull (#version 460, FIX-5) |
| shadow_csm.vert/frag | Graphics | 3-cascade CSM depth pass |
| deferred_point.vert/frag | Graphics | Point light icosphere volumes (M29) |
| deferred_strip.vert/frag | Graphics | Strip (capsule) lights AABB (M31) |
| ssao.comp | Compute | SSAO half-res R8_UNORM 16-tap (M30) |
| smaa_edge/blend/final.vert/frag | Graphics | 3-pass SMAA post-process (M32) |
| tile_map.vert/frag | Graphics | Isometric tile map 3D (stride=36) |
| tile_map_2d.vert/frag | Graphics | 2D SDL_GPU tile map (4-atlas) |
| particle.vert/frag | Graphics | Point sprite particles |
🎨 Rendering Stack
Кадр SDL_GPU (порядок pass)
GpuProfiler::BeginFrame()
│
├── [Upload] GpuProfiler::BeginPass("Upload")
│ AnimationSoA::UploadSDLGPU() // AnimNpcState → SSBO 5
│ TransformSoA::UploadSDL() // xzyr → SSBO 0
│ GpuProfiler::EndPass()
│
├── [Skinning compute] GpuProfiler::BeginPass("Skinning")
│ AnimationSoA::DispatchSkinning() // skinning.comp → FinalBones SSBO 4
│ GpuProfiler::EndPass()
│
├── [Shadow CSM] GpuProfiler::BeginPass("ShadowCSM")
│ shadow_cull.comp → shadow_visible[] SSBO 6
│ ShadowSystem::RenderShadowPass() // depth-only pass × 3 cascades
│ GpuProfiler::EndPass()
│
├── [Cull] cull.comp → visible_indices[] SSBO 1 + draw_cmd SSBO 2
│
├── [Scene] GpuProfiler::BeginPass("Scene")
│ NPC instanced draw (animated.vert + npc_instanced.frag)
│ Flare billboards (BillboardRenderer)
│ GpuProfiler::EndPass()
│
├── [Deferred lights] Point volumes → additive blend (SSBO 1+2 reused)
│
├── [SSAO] ssao.comp → half-res R8_UNORM
│
└── [SMAA] 3-pass fullscreen: edge → blend weight → final blend
Deferred Lighting (M29–M31)
| Система | Геометрія | Шейдер |
| PointLightSystem | Icosphere 80 трикутників | deferred_point.vert/frag (additive) |
| StripLightSystem | AABB volume (capsule SDF) | deferred_strip.vert/frag (additive) |
| SSAOSystem | Half-res compute dispatch | ssao.comp (R8_UNORM, 16-tap rotated disk) |
| SMAASystem | 3× fullscreen triangle | smaa_edge/blend/final.vert/frag |
GPU Profiler (M41)
// engine/include/monkey_dust/render/gpu_profiler.h
md::GpuProfiler::Get().BeginFrame();
md::GpuProfiler::Get().BeginPass("Skinning");
AnimationSoA::Get().DispatchSkinning(...);
md::GpuProfiler::Get().EndPass();
// → EditorGpuProfilerPanel показує budget bars у ms
🧠 AI — BehaviorTree + LOD
Stackless VM, 24-byte ноди (cache-friendly). Data-driven через BTLoader JSON. 3-tier LOD: HIGH (повний BT), LOW (спрощений), FROZEN (тільки позиція).
// Загальний flow:
RegisterAllBTBindings(); // ОБОВ'ЯЗКОВО перед LoadFromFile
BTLoader::Get().LoadFromFile("data/ai/bt_templates.json");
// ...
// У game loop:
AISystem::Get().Tick(reg, dt); // 10 TPS
ScheduleSystem::Get().Tick(reg, game_time); // day/night switch
| BT leaf | Файл | Опис |
| actPickWanderTarget | ai_goal.h | Вибрати випадкову ціль у радіусі |
| actMoveToTarget | ai_goal.h | PathCache-first QueryPath → nav waypoints |
| actChaseTarget | ai_goal.h | Chase ворога з SpatialGrid lookup |
| actAttackTarget | ai_goal.h | Атака в радіусі → CombatSystem event |
| actFlee | ai_goal.h | Втеча в протилежному напрямку |
| actPatrol | ai_goal.h | 4-waypoint patrol loop (static table, без malloc) |
| actSetMotivationIdle/Attack/Flee/Search | ai_goal.h | Записує MotivationType в AgentState (C1) |
| actLuaScript | lua_bt_bridge.h | Виконати Lua функцію як BT leaf |
| actSenseCheck | bt_extensions.h | Перевірка SenseActivation (M21) |
| actFlagCheck/Set | bt_extensions.h | LogicCharacterFlags uint64 bit-index (C4) |
| actTimerStart/Check | bt_extensions.h | AgentTimerSlot[26] (C5) |
BT Node Types (behavior_tree.h)
| Тип | Клас | Примітка |
| Selector, Sequence | Composite | Стандартні |
| Action, Condition | Leaf | Функція-покажчик |
| Inverter, Repeat, Wait | Decorator | Стандартні |
| TimerStart, TimerCheck | Decorator | AgentTimerSlot індекс у data |
| FlagCheck, FlagSet | Leaf | lcf bit-index (0–39) у data — НЕ bitmask (C4) |
| SenseCheck | Leaf | ViewConeSet activation_level (M19) |
| Branch | Decorator | BranchType + ShutdownSpeed у data (C2) |
| MotivationCheck | Leaf | as->motivation == (MotivationType)data (C1) |
| SetMotivation | Leaf | as->motivation = (MotivationType)data; Success (C1) |
| Reference | Decorator | ptr у nd._padding → sub-tree tick; null = Failure (C3) |
| GaugeCheck | Leaf | as->gauges.get(gauge) >= threshold (C6) |
| GaugeSet | Leaf | as->gauges.set(gauge, value) (C6) |
🎭 Director / FlowGraph / Sense (M18–M25)
DirectorSystem (M20)
// engine/include/monkey_dust/ai/director_system.h
DirectorSystem::Get().Tick(reg, dt); // збільшує/зменшує menace
float m = DirectorSystem::Get().menace(); // [0..1]
DirectorStage s = DirectorSystem::Get().stage(); // CALM/TENSE/ALERT/CRISIS
// DirectorProfile з JSON (data/ai/director_profile.json):
// { "gauge_fill_rate": 0.05, "gauge_decay_rate": 0.025, ... }
AgentState (M18 + AI patterns C1,C4,C5,C6,C8)
// engine/include/monkey_dust/components/agent_state.h
// C5: 26 named timer slots
enum class AgentTimerSlot : uint8_t {
SuspectTargetResponse=0, HeightenedSenses=1, ThreatAwareTimeout=2,
VocalResponseDelay=3, AlertResponseDelay=4, GiveUpSearchDelay=5,
HuntFailSafeTimeout=6, StalkTimeout=7, AmbushDelay=8,
RepeatedPathfindFail=24, Generic=25
};
static constexpr int MAX_AGENT_TIMERS = 26;
// C4: 40 named bit indices
namespace lcf {
static constexpr uint8_t DONE_BREAKOUT=0, SHOULD_RESET=1,
SHOULD_ATTACK=10, SHOULD_BREAKOUT=9, HAS_PATH_FAIL=39;
// ... (40 total)
}
struct LogicCharacterFlags {
uint64_t bits = 0;
bool test(uint8_t idx) const noexcept;
void set(uint8_t idx) noexcept;
void clear(uint8_t idx) noexcept;
};
// C6: Gauge system
enum class GaugeType : uint8_t { Retreat=0, StunDamage=1, COUNT };
struct AgentGauges { float val[2] = {}; /* get/set/add */ };
// C8: EntityState 23-flag bitmask
enum class EntityStateFlag : uint32_t {
None=0, Activate=0x1, Spawn=0x2, Enable=0x80, Frozen=0x400000, /* ... */
};
inline bool esf_test(uint32_t state, EntityStateFlag f);
// C1: 14-value motivation
enum class MotivationType : uint8_t {
None=0, Idle=1, Stalk=2, Attack=3, ThreatAware=4, Search=5,
Flee=6, Script=7, Despawn=8, Suspicious=9, BackstageStalk=10,
Ambush=11, Breakout=12, PlayerHiding=13, COUNT
};
struct AgentState {
uint64_t timers[MAX_AGENT_TIMERS]; // C5: 26 named slots (ms)
LogicCharacterFlags lcflags; // C4: uint64 + 40 named bits
uint32_t entity_state; // C8: EntityStateFlag bitmask
AgentGauges gauges; // C6: retreat + stun
MotivationType motivation; // C1: active motivation
uint8_t _pad[3];
int bb_count;
BlackboardEntry bb[MAX_BB_ENTRIES]; // [24] FNV-1a keyed
};
FlowGraph (M23 + M45)
// engine/include/monkey_dust/scripting/flow_graph.h
// FNV-1a uint32 для node IDs; MAX 64 nodes + 128 conns; MAX_PENDING=32
FlowGraph::Get().TriggerNode(fnv1a("on_player_enter"));
FlowGraph::Get().Tick(game_time_s);
// FlowVar typed union (M45):
FlowGraph::Get().SetVarFloat("menace_threshold", 0.7f);
FlowGraph::Get().SetVarBool("boss_alive", true);
bool alive = FlowGraph::Get().GetVarBool("boss_alive");
SenseComponent (M19)
// ViewConeSet: Close (3m/120°) / Focused (10m/30°) / Normal (6m/90°) / Peripheral (8m/180°)
// SenseActivation[2]: time_since_detected_s + activation_level [0..1]
SenseRegistry::Get().LoadViewConeSets("data/ai/view_cone_sets.json");
// Tick: FlareAnimSystem + DirectorSystem berechnen activation
NpcConfig (M22 + C9)
// engine/include/monkey_dust/ai/npc_config.h
struct NpcConfig {
char name[32];
char parent[32]; // $inherit — один рівень
char cone_set[24]; // SenseRegistry key
char director_profile[24];
char bt_day[32];
char bt_night[32];
char bt_stage[4][32]; // C9: per-DirectorStage override (Unaware/Suspicious/Hunting/Intense)
// empty string = fall back to bt_day/bt_night
int max_health;
float move_speed;
float wander_radius;
};
// JSON (data/ai/npcs/guard.json):
{ "id": "guard", "$inherit": "humanoid_base",
"health": 120, "speed": 3.5, "bt_day": "guard_patrol",
"bt_stage_2": "guard_hunting" } // Hunting stage override
🧠 Extended AI Patterns (C1–C20)
Extended AI Patterns — розширена система AI поведінки
20 архітектурних паттернів AI, реалізованих у monkey_dust незалежно. Підтверджено 1252 GTests.
C1–C10 (базові структури)
| Pattern | Pattern | monkey_dust реалізація | Файл |
| C1 MotivationGate | Priority selector with motivation gate | MotivationType (14 values) + MotivationCheck/SetMotivation BT nodes; BuildMotivationGateBT() | agent_state.h, behavior_tree.h |
| C2 BranchType | DecoratorBranch + named interrupt categories | BranchType enum (17 values) + ShutdownSpeed; addBranch(cond, btype, speed) | behavior_tree.h |
| C3 Reference node | ReferencedBehavior (sub-tree delegation) | BTNodeType::Reference + addReference(BehaviorTree*); ptr у nd._padding[2]; null → Failure | behavior_tree.h |
| C4 LogicCharacterFlags | LOGIC_CHARACTER_FLAGS 40-bit bitmask | LogicCharacterFlags{uint64_t} + namespace lcf, 40 named bits; FlagCheck/FlagSet — bit-index uint8 (НЕ bitmask) | agent_state.h |
| C5 TimerSlots | LOGIC_CHARACTER_TIMER_TYPE (26 named) | AgentTimerSlot enum; MAX_AGENT_TIMERS=26; timers[] uint64 (ms) | agent_state.h |
| C6 Gauge system | Retreat/StunDamage gauges | GaugeType + AgentGauges{val[2]} + GaugeCheck/GaugeSet | agent_state.h, behavior_tree.h |
| C7 Hierarchical IDs | ShortGuid::combine() SHA1-based | fnv_combine(parent, child) + fnv_path(a,b,c) — pure FNV, без SHA1 | fnv.h |
| C8 EntityState | EntityStateFlag 23-flag bitmask | EntityStateFlag enum (23 flags) + entity_state uint32 + esf_test() | agent_state.h |
| C9 Per-stage BT | Per-stage BT override in NPC config | NpcConfig::bt_stage[4][32] — per-DirectorStage override; empty = fall back to bt_day/bt_night | npc_config.h |
| C10 LogicTickContext | TriggerProcessingContext::begin_cycle() | LogicTickContext{dt, frame_index, active}; active guard re-entrant RunLogicTick | logic_tick.h |
C11–C20 (розширені AI примітиви)
| Pattern | Pattern | monkey_dust реалізація | Файл |
| C11 SequenceStateless | Stateless sequence — restarts from child 0 on re-entry | BTNodeType::SequenceStateless; на Running скидає currentChild=0 (на відміну від Sequence) | behavior_tree.h/.cpp |
| C12 TimerOnlyIncrease | TIMER_ONLY_INCREASE flag у DecoratorTimer | addTimerStartOnlyIncrease(slot, dur); flags bit0=1; не вкорочує активний таймер | behavior_tree.h/.cpp |
| C13 FrameFlag | Frame-scoped signal flags | frame_flags uint64 в AgentState; FrameFlagCheck/Set; BTSystem скидає на початку кожного tick | agent_state.h, bt_system.h |
| C14 WeightedSelector | Weighted random child selector | 4 ваги packed у data uint32; LCG RNG = entity ^ frame_index; max 4 дочірніх | behavior_tree.h/.cpp |
| C15 AwarenessState | Awareness state enum | AwarenessState (Dead…Aware, 7 рівнів) + поле в AgentState + AwarenessCheck | agent_state.h, behavior_tree.h |
| C16 AlertnessState | ALERTNESS_STATE enum (Relaxed→Combat) | AlertnessState (4 рівні) + поле + AlertnessCheck | agent_state.h, behavior_tree.h |
| C17 NpcMood | NPC_MOOD (Neutral/Curious/Panicked...) | NpcMood (6 значень) + поле mood в AgentState + MoodCheck | agent_state.h, behavior_tree.h |
| C18 RoleSystem | RoleClaim/RoleCheck для multi-NPC | NpcRole enum (8 ролей) + RoleRegistry singleton + RoleCheck/RoleClaim/RoleRelease BT nodes | role_registry.h, behavior_tree.h |
| C19 WithdrawState | WITHDRAW_STATE у NPC_RETREAT.XML | WithdrawState (3 стани) + withdraw_state поле + WithdrawCheck/SetWithdraw | agent_state.h, behavior_tree.h |
| C20 FlowDurableTrigger | TriggerInfo refcounted у FlowGraph | FlowDurableTrigger{node_id, duration, ref_count}; MAX=16; AcquireDurable/AddRef/Release; Tick() decay | flow_graph.h/.cpp |
C1 — MotivationGate BT патерн
// BuildMotivationGateBT (ai_goal.h) — Priority-selector з 4 motivation-gated branches:
// DirectorSystem/FlowGraph пише motivation; BT читає через MotivationCheck — нуль coupling.
uint16_t rep = bt.addRepeat(0);
uint16_t sel = bt.addSelector();
uint16_t seq_atk = bt.addSequence();
bt.addChild(seq_atk, bt.addMotivationCheck(MotivationType::Attack));
bt.addChild(seq_atk, bt.addSetMotivation(MotivationType::Idle)); // споживає мотивацію
bt.addChild(seq_atk, bt.addAction(actChaseTarget));
bt.addChild(sel, seq_atk);
// ... (Flee, Search, Idle wander branches)
bt.setRoot(rep);
C13 — frame_flags (один-тік сигнали)
// BTSystem::Tick() — фаза 1: скидаємо frame_flags ПЕРЕД BT::tick()
as.frame_flags = 0; // C13 invariant — per-frame signals reset
// Встановити сигнал (напр. з SenseSystem або FlowGraph):
as.frame_flags |= (1ULL << lcf::HAS_RECEIVED_DOT);
// BT читає через FrameFlagCheck (зникне автоматично наступного тіку):
bt.addFrameFlagCheck(lcf::HAS_RECEIVED_DOT, true);
C18 — RoleSystem (координація NPC)
// RoleRegistry: один SearchLead на всю сцену
// BT-дерево першого NPC що може стати SearchLead:
uint16_t seq = bt.addSequence();
bt.addChild(seq, bt.addRoleCheck(NpcRole::SearchLead, /*could_perform=*/true));
bt.addChild(seq, bt.addRoleClaim(NpcRole::SearchLead, query_id));
bt.addChild(seq, bt.addAction(actSystematicSearch));
// Другий NPC перевіряє чи він вже виконує роль:
bt.addRoleCheck(NpcRole::SearchLead, /*could_perform=*/false); // is_performing
// Звільнення при виході з ролі:
bt.addRoleRelease(NpcRole::SearchLead);
C4 — LogicCharacterFlags (bit-index, не bitmask)
// ПРАВИЛЬНО — передаємо bit-index (0..39):
bt.addFlagCheck(lcf::SHOULD_ATTACK, true); // перевірити що bit 10 = 1
bt.addFlagSet (lcf::SHOULD_ATTACK, false); // скинути bit 10
as.lcflags.set (lcf::HAS_PATH_FAIL); // bit 39
as.lcflags.test (lcf::SHOULD_ATTACK); // → bool
// НЕПРАВИЛЬНО: bt.addFlagCheck(0x400) ← bitmask більше не підтримується
C7 — Hierarchical FNV IDs
constexpr uint32_t K_AI_NPC_HUNT = fnv_path("ai", "npc", "hunt");
uint32_t node_id = fnv_combine(fnv1a_rt(zone_name), fnv1a("on_enter"));
🤖 BT VM — BTNodePool · BTSystem · BTJsonLoader
Повний BT runtime (2026-05-13)
Stackless VM із JSON-завантаженням дерев, ECS driver і bump-allocator для шаблонів. 26 GTests.
BTNodePool — flat 32KB arena
// engine/include/monkey_dust/ai/bt_node_pool.h
// sizeof(BehaviorTree) = 4616 bytes > BLOCK_SIZE=4096 → single flat bump ptr
BTNodePool pool; // 8 блоки × 4096 = 32 KB BSS
BehaviorTree* tpl = pool.AllocTree(); // placement-new у арені, O(1)
// ... будуємо tpl ...
pool.TreeCount(); // → кількість живих BT
pool.BytesUsed(); // → байти використано
pool.Reset(); // ~BehaviorTree() на всіх, offset=0 (не free())
Важливо: 2D масив — плоска арена
m_data[MAX_BLOCKS][BLOCK_SIZE] є contiguous в пам'яті. Алокатор використовує один flat bump pointer (
m_flat_offset) — тому об'єкти більші за BLOCK_SIZE (як BehaviorTree) розміщуються коректно.
BTSystem — ECS driver (3 фази)
// engine/include/monkey_dust/ai/bt_system.h
BTSystem bt_sys;
BTSystem::ConnectRegistry(reg); // підключаємо on_destroy listener
// У logic tick (10 TPS):
bt_sys.Tick(ctx, reg, nowMs);
// Фаза 1: clear frame_flags + expire stale DirectorHints (>MAX_PENDING_TICKS=10)
// Фаза 2: clear frame_flags для entity без DirectorHintComponent
// Фаза 3: tick всіх enabled BehaviorTreeComponent
// Якщо entity має owning=true BT — BTSystem::OnComponentDestroy() звільняє дерево
BTJsonLoader — strstr recursive parser
// engine/include/monkey_dust/ai/bt_json_loader.h
// JSON format:
{
"name": "systematic_search",
"root": {
"type": "SelectorLinear",
"children": [
{ "type": "DecoratorBranch", "branch_type": "Dead",
"condition": "condIsDead", "shutdown_speed": "Critical",
"children": [{ "type": "ActionNode", "action": "actDead" }] },
{ "type": "SequenceLinear", "children": [
{ "type": "MotivationCheck", "motivation": "Search" },
{ "type": "AwarenessCheck", "state": "SearchingArea" },
{ "type": "TimerStart", "timer_slot": "SearchTimeout",
"duration_ms": 30000, "only_increase": false },
{ "type": "RoleCheck", "role": "SearchLead", "mode": "could_perform" },
{ "type": "RoleClaim", "role": "SearchLead", "query_id": 1 },
{ "type": "ActionNode", "action": "actSystematicSearch" }
]}
]
}
}
// Завантаження:
md::BTActionRegistry::Get().Clear();
md::BTActionRegistry::Get().RegisterAction("actSystematicSearch", actSystematicSearch);
md::BTActionRegistry::Get().RegisterCondition("condIsDead", condIsDead);
BehaviorTree bt;
BTJsonLoader::LoadFromFile(bt, "data/bt/systematic_search.bt.json");
// Unregistered actions/conditions → MD_LOG_WARNING + s_act_failure / s_cond_false
// (дерево завантажується навіть якщо не всі функції зареєстровано)
Підтримувані типи вузлів BTJsonLoader
| Тип | JSON поля | C++ метод |
| SelectorLinear | — | addSelector() |
| SequenceLinear | — | addSequence() |
| SequenceStateless | — | addSequenceStateless() (C11) |
| WeightedSelector | weights[4] | addWeightedSelector(w) (C14) |
| DecoratorBranch | branch_type, condition, shutdown_speed | addBranch() |
| ConditionNode | condition | addCondition() |
| ActionNode | action | addAction() |
| ReferencedBehavior | tree (deferred) | addReference(nullptr) |
| TimerStart | timer_slot, duration_ms, only_increase | addTimerStart / addTimerStartOnlyIncrease (C12) |
| TimerCheck | timer_slot | addTimerCheck() |
| MotivationCheck / SetMotivation | motivation | addMotivationCheck/Set() (C1) |
| FlagCheck / FlagSet | flag, check_set|set | addFlagCheck/Set() (C4) |
| FrameFlagCheck / FrameFlagSet | flag, check_set|set | addFrameFlagCheck/Set() (C13) |
| AwarenessCheck / AlertnessCheck / MoodCheck | state / mood | addAwarenessCheck/AlertnessCheck/MoodCheck() (C15–17) |
| RoleCheck / RoleClaim / RoleRelease | role, mode, query_id | addRoleCheck/Claim/Release() (C18) |
| WithdrawCheck / SetWithdraw | state | addWithdrawCheck/SetWithdraw() (C19) |
| GaugeCheck / GaugeSet | gauge, threshold|value | addGaugeCheck/Set() (C6) |
| SenseCheck | sense, threshold | addSenseCheck() |
| Inverter / Repeat / Wait | count / duration_ms | addInverter/Repeat/Wait() |
🔬 9 Multi-Engine Improvements
Аналіз механік OGRE v1 · OGRE-Next · Echo · Banshee 3D · Cocos2d-x
9 патернів адаптовано з 6 engine'ів після порівняльного аналізу (2026-05-13). Всі реалізовані без нових зовнішніх залежностей.
| # | Джерело | Фіча | Файли |
| IMP-1 | Filmic tonemapping | ACES filmic tonemapping замінює Reinhard: clamp((c*(2.51c+0.03))/(c*(2.43c+0.59)+0.14), 0, 1) + gamma pow(c, 1/2.2) | shaders/pbr.frag, shaders/npc_instanced.frag |
| IMP-2 | Live BT reload | Live BT reload: BTLoader::WatchFile(path) → HotReload watcher; PollReload() у logic tick (main thread safety, volatile flag) | game/src/ai/bt_loader.h, game/src/logic_tick.cpp |
| IMP-3 | Echo | NpcMemoryComponent POD: SpatialMemory[8] (LRU evict, timestamp+confidence) + events[8] (FNV IDs, ring overwrite); MemoryCheck/MemoryForget BT nodes | engine/include/monkey_dust/components/npc_memory.h, engine/src/ai/behavior_tree.cpp, engine/src/ai/bt_json_loader.cpp |
| IMP-4 | OGRE-Next | Dirty TINST: TileMapRenderer кешує vbuf/ibuf між кадрами; PASS1–3 пропускаються коли !dirty_ && !anim_changed | engine/include/monkey_dust/flare/tile_map_renderer.h, engine/src/flare/tile_map_renderer.cpp |
| IMP-5 | OGRE-Next | Dirty faction SSBO: TransformSoA::MarkFactionDirty(slot) відслідковує faction_dirty_min_/max_; завантажує тільки брудний діапазон слотів | engine/include/monkey_dust/world/transform_soa.h, engine/src/world/transform_soa.cpp |
| IMP-6 | OGRE v1 | WorldSimulation 1 Hz: FactionState[8] + TradeRoute[32]; economy Tick(delta_s) накопичує з 10 TPS logic tick; торгівля впливає на gold/prosperity/population | engine/include/monkey_dust/world/world_simulation.h, engine/src/world/world_simulation.cpp, game/src/logic_tick.cpp |
| IMP-7 | OGRE v1 | MaterialDesc: OGRE-style JSON → GpuPipeline::Desc auto-builder; strstr parser (read_str/read_bool helpers); MatBlend/MatCull/GpuTopology mapping | engine/include/monkey_dust/render/material_desc.h, engine/src/render/material_desc.cpp |
| IMP-8 | Echo | UtilityScorer: Echo-style goal utility over MotivationType; FillCandidates (AwarenessState+AlertnessState+Gauges weights) + Bias(current, 10) (інерція) + BestFrom; без malloc | engine/include/monkey_dust/ai/utility_scorer.h, engine/src/ai/utility_scorer.cpp |
| IMP-9 | Cocos2d-x | md::fs: тонкий FS шар (fs_read_all/fs_read_alloc/fs_free/fs_exists); BTJsonLoader + BTLoader мігровані з raw fopen; єдина точка platform IO | engine/include/monkey_dust/platform/md_fs.h, engine/src/tools/md_fs.cpp |
IMP-1: ACES tonemapping (pbr.frag)
// Замінює Reinhard: color/(color+1)
// ACES filmic:
color = clamp((color*(2.51*color+0.03))/(color*(2.43*color+0.59)+0.14), 0.0, 1.0);
color = pow(color, vec3(1.0 / 2.2)); // gamma correction
IMP-2: Live BT Reload
// game/src/ai/bt_loader.h
BTLoader::Get().WatchFile("data/ai/bt_templates.json"); // реєструємо у HotReload
// logic_tick.cpp — щотіку (main thread):
BTLoader::Get().PollReload(); // if (needs_reload_) { RegisterAllBTBindings(); LoadFromFile(); }
// BTLoader::OnReloadCallback (background thread) — тільки ставить volatile flag:
static void OnReloadCallback(const char*) { Get().needs_reload_ = true; }
IMP-3: NpcMemoryComponent
// engine/include/monkey_dust/components/npc_memory.h
NpcMemoryComponent* mem = reg.try_get<NpcMemoryComponent>(e);
if (mem) {
mem->AddSpatial(wx, wz, now_ms, 200); // LRU: evicts oldest if full
mem->AddEvent(fnv1a("heard_footstep")); // ring overwrite if full
bool heard = mem->HasEvent(fnv1a("heard_footstep"));
mem->ExpireOlderThan(now_ms, 30000); // 30s lifetime
}
// BT JSON:
{ "type": "MemoryCheck", "mode": "has_spatial" } // або "has_event"
{ "type": "MemoryForget" } // ClearAll()
IMP-8: UtilityScorer
// engine/include/monkey_dust/ai/utility_scorer.h
// Замінює жорстко прошиту priority-selector для вибору мотивації:
MotivationType next = UtilityScorer::SelectMotivation(as, now_ms);
if (next != as.motivation) {
as.motivation = next;
// MotivationCheck у BT підхопить на наступному тіку
}
// Внутрішньо:
// FillCandidates: AwarenessState::Aware → Attack score 80; Alert → Search 60; ...
// Bias(as.motivation, 10) → +10 до поточної мотивації (інерція)
// BestFrom → max score, при рівності — менший enum index
📜 Lua Sandbox (M15 + FIX-2/3)
Sandbox після Security Audit (FIX-2/3)
8 MB memory hard cap через custom allocator. Бібліотеки
io /
os /
package /
debug та функції
dofile /
loadfile /
load nil-ed після
luaL_openlibs. Instruction hook 10 000 ops/call.
// LuaSystem::Init() (engine/src/scripting/lua_system.cpp):
lua_mem_used_ = 0;
L_ = lua_newstate(lua_alloc, &lua_mem_used_); // 8 MB custom allocator
luaL_openlibs(L_);
static const char* blocked[] = { "io", "os", "package", "debug", nullptr };
for (int i = 0; blocked[i]; ++i) { lua_pushnil(L_); lua_setglobal(L_, blocked[i]); }
static const char* blocked_fns[] = { "dofile", "loadfile", "load", "loadstring", nullptr };
for (int i = 0; blocked_fns[i]; ++i) { lua_pushnil(L_); lua_setglobal(L_, blocked_fns[i]); }
lua_sethook(L_, hook, LUA_MASKCOUNT, 10000);
LuaEventBus (M15)
// engine/include/monkey_dust/scripting/lua_event_bus.h
// MAX_HANDLERS=64 BSS — не збільшувати без профайлінгу
LuaEventBus::Get().Register("player_died", handler_fn);
LuaEventBus::Get().Fire("player_died", L);
Lua C API (lua_game_api.h)
| Функція | Підпис (Lua) | Опис |
| md_get_pos | md_get_pos(entity) → x,z | WorldTransform позиція |
| md_set_target | md_set_target(entity, x, z) | NavAgent ціль |
| md_get_health | md_get_health(entity) → hp | Health component |
| md_get_faction | md_get_faction(entity) → id | Faction ID |
| md_on_event | md_on_event(name, fn) | LuaEventBus::Register |
| md_fire_event | md_fire_event(name) | LuaEventBus::Fire |
| md_quest_active | md_quest_active(id) → bool | Quest state check |
| md_quest_complete | md_quest_complete(id) | Complete quest |
🗺️ Standalone Editor (monkey_dust_editor)
Standalone JSON + tile map editor. SDL3 + imgui_impl_sdl3/opengl3. Запускати з кореня репо: ./build/tools/monkey_dust_editor
Вікно: 85% висоти монітора, 16:9 (мін 854×480). Конфіг шрифтів: data/editor_config.json.
Вкладки редактора
| Вкладка | Функція |
| Items | CRUD для data/items/items.json: назва, вага, вартість, категорія |
| Factions | CRUD + матриця відносин для data/factions/factions.json |
| Map | Ізометричний Map Editor (M9): FBO viewport + palette + paint tools |
| Settings | Конфіг шрифтів (Label/Header/Mono): path + size, live Apply |
Гарячі клавіші (Map tab)
| Клавіша | Дія |
| Ctrl+S / F5 | Зберегти карту (якщо path відомий) |
| F6 | Відкрити діалог Load Map (введення шляху) |
| Ctrl+Z | Undo (до 256 операцій) |
| Ctrl+Y | Redo |
Viewport Map — керування мишею
| Дія | Ефект |
| LMB (mode: Tiles) | Малювати tile (brush 1×1 / 3×3 / 5×5) |
| Shift+LMB | Flood fill (з урахуванням активного шару) |
| LMB (mode: Spawns) | Додати spawn-маркер у вибраній позиції |
| RMB | Pan viewport |
| Scroll | Zoom in / out |
| LMB on minimap | Pan viewport до вибраної позиції |
Palette — вкладки
| Вкладка | Зміст |
| Tiles | TileMetaRegistry thumbnails; вибір tile → ЛКМ на viewport малює; brush size 1/3/5; Erase toggle |
| Spawns | Place/move/delete enemy spawn-маркерів; відображаються як S-маркери на viewport |
| Props | Hero spawn (H-маркер синій), title, music поля карти |
Layer visibility
Eye-toggles для кожного з 6 шарів; активний шар підсвічено зеленим. Тільки видимі шари рендеряться у viewport. LayerMask() передається до TileMap2DRenderer.
Undo/Redo архітектура
// PaintOp: записує до 25 комірок (5×5 brush) або index до FloodSnap
// FloodSnap: full-layer before/after snapshot (8 × 2 × 64KB BSS)
// Undo/Redo stack: кільцевий буфер до 256 ops
// Flood fill займає 1 undo slot (FloodSnap у snap_pool_)
File меню (menu bar)
| Пункт | Дія |
| New Map… | Діалог: tilesetdef path (відносно mod root) + W×H (max 128×128) |
| Load Map… | Діалог: шлях відносно кореня репо; поточний шлях підставляється |
| Save Map (Ctrl+S) | Зберегти за поточним шляхом |
| Save Map As… | Діалог: новий шлях → Save |
Робочий процес: редагування нової карти
1.
File → New Map… → ввести tilesetdef (напр.
tilesetdefs/tileset_grassland.txt) та розмір
2. Вкладка
Tiles → обрати tile → LMB на viewport
3. Вкладка
Spawns → LMB → розставити ворогів
4. Вкладка
Props → LMB → встановити hero spawn (H-маркер)
5.
Ctrl+S або
File → Save Map As… → вказати шлях у
data/
🖥️ Wicked-style Editor (M34 + M38–M41 + Extension Panels, MONKEY_DUST_EDITOR=ON)
Компілюється у monkey_dust з -DMONKEY_DUST_EDITOR=ON. Toggle: F3. Physics пауза активна за замовчуванням при відкритті.
Клавіатурні скорочення
| Клавіша | Дія |
| F3 | Toggle editor on/off |
| W | Gizmo: Translate (переміщення) |
| E | Gizmo: Rotate (обертання) |
| R | Gizmo: Scale (масштаб) |
| G | Toggle World ↔ Local gizmo space |
| F | Focus camera on selected entity (cam_dist=15) |
| Ctrl+Z | Undo |
| Ctrl+Y | Redo |
| Ctrl+D | Duplicate selected entity (+1.0 x offset) |
| Ctrl+A | Select all entities (max 64) |
| Del | Delete selected entities |
| Ctrl+P | Command Palette (fuzzy search всіх команд) |
Gizmo — snapping
| Режим | Snap (Ctrl hold) |
| Translate | 1.0 world unit |
| Rotate | 15° |
| Scale | 0.1 |
Камера редактора
| Режим | Керування |
| Orbit (default) | RMB + drag = rotate (yaw/pitch); MMB + drag = pan; Scroll = zoom (cam_dist 1..500) |
| Flythrough (Camera panel toggle) | RMB hold: look; WASD = forward/back/strafe; cam_speed регулюється у Camera panel. W = вперед (у напрямку погляду), S = назад. |
Toolbar кнопки
| Кнопка | Дія |
| [+] New Entity | Popup: Transform / NPC Bandit / NPC Trader / NPC Holy / Building (spawn at cam_target) |
| Translate / Rotate / Scale | Вибір gizmo режиму (W/E/R) |
| World/Local | Toggle gizmo space (G) — підсвічено якщо World |
| Physics Pause | Пауза logic tick — оранжевий коли активна |
| FPS counter | Відображає поточний fps у toolbar |
Меню Bar
| Меню | Пункт | Дія |
| File | New Scene | DeselectAll + Registry::clear() + TransformSoA::Init() |
| File | Import/Export Scene (.json)… | SceneSerializer JSON повна сцена |
| File | Save/Load Game (F5/F9) | SaveSystem::SaveAsync / Load |
| Edit | Duplicate (Ctrl+D) | Копія entity + offset +1x |
| Edit | Delete (Del) | Видалити вибрані + Free TransformSoA slot |
| Edit | Select All (Ctrl+A) | До 64 entities |
| View | Toggle panels | Hierarchy/Inspector/Assets/Console/Graphics/Camera/Animation/ViewCone/FlowGraph/Director/GPU Profiler/Node Graph/Sequencer |
| View | Reset Layout | Вмикає panels [0..5], вимикає [6..13] (всього 14 панелей) |
| Scene | Reload JSON Data | Hot-reload factions.json + buildings.json + dialogs.json + quests.json |
| Scene | Rebuild NavMesh | EnqueueRebuild у cam_target позиції (async) |
| Scene | Spawn NPC (Bandit/Trader) | Spawn у cam_target |
| Debug | Debug Overlay (F1) | DebugSystem::overlay_on toggle |
| Debug | SpatialGrid (F2) | DebugSystem::grid_on toggle |
| Debug | NavMesh Wireframe | DebugSystem::navmesh_on toggle |
| Debug | Physics Paused | EditorCore::physics_paused toggle |
Command Palette (Ctrl+P)
Fuzzy-search по всіх командах редактора. ↑↓ = навігація, Enter або подвійний клік = виконати, Esc = закрити.
Scoring (без heap allocation, без зовнішньої бібліотеки): +1 за кожен match символу; +4 за consecutive run; +2 за початок слова або після :/пробілу. Результати сортуються за score (insertion sort, max 32 видимих).
// Доступні команди з hint:
Gizmo: Translate [W] Gizmo: Rotate [E]
Gizmo: Scale [R] Gizmo: Toggle World/Local [G]
Camera: Focus [F] Select All [Ctrl+A]
Delete Selected [Del] Edit: Undo [Ctrl+Z]
Edit: Redo [Ctrl+Y] Scene: Rebuild NavMesh
Scene: Save Game [F5] Scene: Load Game [F9]
Spawn: NPC Bandit Debug: Toggle Overlay [F1]
Panel: ViewCone Inspector Panel: FlowGraph ...
Panels (14 штук)
| Idx | Панель | Стан за замовч. | Опис |
| 0 | Hierarchy | On | Entity list з фільтром; клік = select; Ctrl+клік = add to selection |
| 1 | Inspector | On | Компоненти вибраного: Transform (DragFloat x/y/z/rotY), Health (progress bar + slider), Combat, AIAgent, NavAgent, Inventory, Building |
| 2 | Assets | On | AssetBrowser: дерево data/; drag-to-place планується |
| 3 | Console | On | MD_LOG output + FPS/dt stat + Lua REPL. Кнопка Lua перемикає на Lua mode: ImGuiColorTextEdit з Lua syntax highlighting (read-only), показує лише рядки з тегом [Lua], auto-refresh при нових лог-подіях. |
| 4 | Graphics | On | Fog (near/far), sun direction, ambient color, render tier |
| 5 | Camera | On | Orbit/Flythrough toggle, cam_speed, cam_fovy, cam_dist; Reset view |
| 6 | Animation | Off | AnimationSoA: перелік кліпів вибраного entity; SetClip/Advance preview |
| 7 | Paint (stub) | Off | Зарезервовано |
| 8 | ViewCone (M38) | Off | SenseComponent activation bars (0..1) + ViewConeSet table (Close/Focused/Normal/Peripheral) |
| 9 | FlowGraph (M39) | Off | imnodes візуальний граф: вузли кольоровані по типу (Event/Action/Condition/Sequence/Custom), drag-to-connect links. Нижче — typed FlowVar таблиця + MAX_PENDING ring; "Fire Trigger" кнопка. |
| 10 | Director (M40) | Off | Menace gauge bar [0..1], stage кольором (CALM=зелений, CRISIS=червоний), profile params + manual override |
| 11 | GPU Profiler (M41) | Off | Pass budget bars: Upload / Skinning / ShadowCSM / Scene у ms; budget line 16ms. Внизу — flame graph (imgui-flame-graph): кожен pass = прямокутник з часткою від frame time, tooltip з ms. |
| 12 | Node Graph | Off | Blueprint-style material/logic граф (imgui-node-editor). Вузли: TexSample / Multiply / Add / Lerp / ConstFloat / ConstColor / MatOutput. ПКМ на canvas = "Add Node" popup; drag між пінами = link; Delete = видалення вузла/лінка. Налаштування зберігаються у data/node_graph.json. |
| 13 | Sequencer | Off | ImSequencer timeline редактор (ImGuizmo::ImSequencer). Доріжки: анімація / звук / скрипт / ефект / камера. Drag handles = зміна start/end frame; ПКМ = контекстне меню доріжки. |
FlareBrowser (Editor panel)
Відображає завантажений FlareMap: tile thumbnails по atlas_idx, meta (offset_y, anim frames), шар-фільтр. Корисний для перевірки що тайлсет завантажений коректно та для вибору tile_id для скриптів.
Типовий workflow з Wicked Editor для контенту
1.
Ctrl+P → "Reload JSON Data" → hot-reload всіх data/*.json без рестарту
2.
Ctrl+P → "Rebuild NavMesh" → оновити після Place/Demolish будівель
3.
[+] New Entity → NPC Bandit → entity спавниться у cam_target; Inspector редагує hp/faction
4. Vкладка
Director (F3 → View → Director) → тестувати поведінку по стадіях menace
5. Вкладка
FlowGraph → "Fire Trigger" → вручну тригерити FlowGraph events
6.
F5 → зберегти сцену + NPC positions у .mdsave
▶️ md_flare_demo
Standalone SDL_GPU Vulkan Flare map viewer. Без OpenGL context, без Raylib.
./build/tools/md_flare_demo [mods_root] [mod_name] [map_name]
# defaults: third_party/flare-game/mods empyrean_campaign maps/goblin_camp.txt
Plain SDL_Window → SDL_ClaimWindowForGPUDevice → Vulkan swapchain → TileMap2DRenderer SDL_GPU path → SPIR-V tile_map_2d.vert/frag.
Керування (md_flare_demo)
| Клавіша / Дія | Ефект |
| WASD / ↑↓←→ | Pan viewport |
| Q / Scroll↑ | Zoom out |
| E / Scroll↓ | Zoom in |
| R | Reset view (origin + default scale) |
| Esc | Вийти |
WorldPlacer (Debug build, F4)
In-game об'єктний редактор у DEBUG build. Активується F4. Коли активний — Tab не перемикає тип будівлі, а тип маркера.
| MarkerType | Idx |
| TREE | 0 |
| ROCK | 1 |
| CAMP | 2 |
| SPAWN_BANDIT | 3 |
| SPAWN_TRADER | 4 |
| Клавіша | Дія |
| F4 | Toggle WorldPlacer on/off |
| Tab (коли active) | Cycle MarkerType (0→4→0) |
| LMB | Place marker на y=0 plane (ray vs plane intersection) |
| RMB | Delete найближчий маркер |
| F6 | Export placement.json (до 512 маркерів) |
// Формат export:
{ "markers": [
{ "type": "TREE", "x": 12.5, "y": 0.0, "z": -8.3 },
{ "type": "SPAWN_BANDIT", "x": 5.0, "y": 0.0, "z": 3.1 }
]
}
🧪 Test Suite (M43–M58 + AI-Ext-1–20 + BT VM + Batch 3–27)
ninja -C build md_tests && build/tests/md_tests
# [==========] 1252 tests from 180 test suites ran. (all PASSED)
| Suite | Тестів | Що тестує |
| FNV | 6 | FNV-1a хеш коректність і колізії; fnv_combine/fnv_path (C7) |
| AgentBlackboard | 17 | Set/Get/Evict BB[24]; LogicCharacterFlags (C4); MotivationType (C1); GaugeSystem (C6); TimerSlots×26 (C5); EntityStateFlag (C8) |
| FlowGraph | 24 | Nodes, vars, FlowVar typed union (M45), triggers, ring buffer, FlowDurableTrigger (C20) |
| DirectorSystemTest | 11 | Menace fill/decay, stage transitions, profile params |
| PowerSlotManagerTest | 11 | Slot assign, cooldowns, use, Lua API |
| NpcConfigTest | 6 | JSON parse, $inherit, field override, bt_stage (C9) |
| HotReloadTest | 7 | POSIX stat, change detection, PollOnce |
| BTAIPatternTest | 20 | C11–C19: SequenceStateless, TimerOnlyIncrease, FrameFlag, WeightedSelector, AwarenessCheck, AlertnessCheck, MoodCheck, RoleSystem, WithdrawState |
| FlowDurableTriggerTest | 3 | C20: AcquireDurable, AddRef, Release decay |
| BTJsonLoaderTest | 13 | LoadFromString/File; ReadName; MotivationCheck; TimerStart only_increase; AwarenessCheck; WeightedSelector; RoleCheck; unknown action fallback; SystematicSearch JSON |
| BTSystemTest | 5 | frame_flags cleared; disabled tree skipped; enabled tree executes; multiple entities all ticked |
| DirectorHintTest | 3 | Stale hint expires after MAX_PENDING_TICKS; active hint not cleared early; no-pending untouched |
| BTNodePoolTest | 5 | AllocTree; TreeCount; Reset clears; alloc after reset; BytesUsed positive |
🔄 Game Loop
// game/src/main.cpp — спрощена схема:
while (running) {
// ── Logic: 10 TPS (LOGIC_TICK_S = 0.1f) ──────────────────────────
while (logic_lag >= LOGIC_TICK_S) {
ProcessInput();
LogicTick(reg, gs, 0.1f); // AI + Nav + Combat + Director + Projectiles
logic_lag -= LOGIC_TICK_S;
}
// ── Render: 60 FPS ───────────────────────────────────────────────
SDL_GPUCommandBuffer* cmd = GpuDevice::Get().AcquireCommandBuffer();
AnimationSoA::Get().Advance(frame_dt);
AnimationSoA::Get().UploadSDLGPU(cmd);
TransformSoA::Get().UploadSDL();
AnimationSoA::Get().DispatchSkinning(skin_bindings);
ShadowSystem::Get().RenderShadowPass(...);
// ... cull compute → NPC draw → lights → SSAO → SMAA ...
HudRenderer::Draw(reg, gs, camera);
GpuDevice::Get().Submit(cmd);
AnimationSoA::Get().AdvanceFrame();
}
LogicTick містить
BTLoader::Get().PollReload() · WorldSimulation::Get().Tick(dt) · NavSystem::Tick · AISystem::Tick · ScheduleSystem::Tick · CombatSystem::Tick · ProjectileSystem::Tick · DirectorSystem::Tick · FlowGraph::Tick · ChunkManager::PollIO · HotReload::PollOnce
🧩 ECS (EnTT 3.16)
// Registry singleton:
auto& reg = Registry::Get();
// Спавн NPC:
auto e = reg.create();
reg.emplace<WorldTransform>(e, x, 0.f, z);
reg.emplace<Health>(e, 100);
reg.emplace<AIAgent>(e);
reg.emplace<NavAgent>(e);
reg.emplace<AgentState>(e); // M18: timers+flags+blackboard
// Collect → Apply патерн (НЕ мутувати registry під час view.each):
std::vector<entt::entity> to_destroy;
reg.view<Health>().each([&](auto e, auto& hp) {
if (hp.current <= 0) to_destroy.push_back(e);
});
for (auto e : to_destroy) reg.destroy(e);
Компоненти (engine/include/monkey_dust/components/)
| Компонент | Розмір | Призначення |
| WorldTransform | 12B | x, z, y позиція |
| Health | 8B | current / max |
| AIAgent | ~32B | BT state, target entity |
| NavAgent | ~256B | path verts[64×3], len, target |
| FactionComponent | 4B | faction_id uint32 |
| AgentState (M18+C1–C8) | ~440B | timers[26]×uint64 + lcflags×uint64 + entity_state + gauges + motivation + blackboard[24] |
| SenseComponent (M19) | ~32B | ViewConeSet ref + SenseActivation[2] |
| ProjectileComponent (M34) | ~32B | vel + dmg + max_range + owner |
🧭 NavMesh + PathCache
// PathCache — LRU 64, TTL 5s, spinlock + _mm_pause (FIX-7):
uint32_t ks = PathCache::PosKey(start_x, start_z);
uint32_t ke = PathCache::PosKey(end_x, end_z);
float verts[MAX_PATH_LEN * 3]; int len;
if (!PathCache::Get().Get(ks, ke, now_s, verts, len)) {
NavSystem::Get().QueryPath(sx, sz, ex, ez, verts, len);
PathCache::Get().Put(ks, ke, now_s, verts, len);
}
// Waypoint path: path[i*3], path[i*3+1], path[i*3+2] — НЕ path[i]
Path індексування
X = path[i*3],
Y = path[i*3+1],
Z = path[i*3+2].
НЕ path[i].
⚔️ Combat + Powers (M4 + M33–M35)
Base Combat System
// 6 hit zones (HEAD/CHEST/ABDOMEN/LEFT_ARM/RIGHT_ARM/LEGS)
// 3 damage types: BLUNT / CUT / PIERCE
// armor_resistance[3] per zone
DamageCalc::Apply(attacker, target, reg); // collect → apply
PowerSlotManager (M35)
// 4 slots per entity; cooldowns; no heap alloc
PowerSlotManager::Get().AssignPower(entity, slot=0, power_id="fireball");
PowerSlotManager::Get().UsePower(entity, slot=0, reg);
float cd = PowerSlotManager::Get().GetCooldown(entity, slot=0);
// Lua:
// md_assign_power(entity, slot, "fireball")
// md_use_power(entity, slot)
// md_get_power_cooldown(entity, slot) → float
ProjectileSystem (M34)
// Collect → apply; MAX_PROJECTILES фіксований масив
// ProjectileComponent: vel(vec3) + dmg + max_range + ttl + owner_entity
ProjectileSystem::Get().Tick(reg, dt); // move → hit check → expire
🏗️ Building System
// BuildSystem: 200×200 grid snap; 9 building types
BuildSystem::Get().Place(def_id, wx, wz, reg);
BuildSystem::Get().Demolish(entity, reg); // async NavMesh rebuild
BuildSystem::WorldToGrid(wx, wz, gx, gz);
BuildSystem::SnapToGrid(wx, wz); // ref args
// ProductionChain: resource_in → ticks_per_cycle → resource_out
// Inventory: 16 слотів, resource costs для будівництва
БОРГ-1: BuildSystem::grid_ не відновлюється після Load
BuildSystem::OccupyGrid() потрібно викликати у
WorldSerializer::Deserialize().
💾 Save / Load (v8 + CRC32)
SaveSystem v8 (FIX-4)
SaveHeader містить
crc32 +
data_size поля. Serialize() обчислює CRC32 (poly 0xEDB88320) поверх всього файлу і пишеться назад. Deserialize() перевіряє size та CRC для v8+. Зворотна сумісність з v2–v7 (без CRC перевірки).
// SaveHeader (world_serializer.h):
struct SaveHeader {
uint32_t magic; // 0x4D445356 "MDSV"
uint32_t version; // 8
uint32_t crc32; // CRC32 всього файлу (це поле = 0 при обчисленні)
uint32_t data_size; // повний розмір файлу в байтах
uint64_t timestamp_ms;
float player_x, player_z, game_time_hours;
uint32_t npc_count, building_count;
};
// Atomic save: записати у .tmp → rename
// Async save: SaveSystem::SaveAsync(path) → char[256] async_path_ (FIX-9)
// SetCallbacks (НЕ SetContext):
SaveSystem::Get().SetCallbacks(WorldSerializer::Serialize,
WorldSerializer::Deserialize, nullptr);
NpcRecord layout (v7/v8)
struct NpcRecord {
float x, z, y; // позиція
uint32_t faction;
int8_t personal_relation;
uint8_t bt_template_id;
uint8_t has_agent_state; // _pad[0] = AgentState inline flag (M36)
uint8_t _reserved;
// AgentState tail (якщо has_agent_state):
float timers[8];
uint32_t flags;
uint32_t bb_keys[24]; float bb_vals[24]; uint8_t bb_used;
};
📜 Quest System
// QuestSystem (game/src/systems/quest_system.h)
// Types: KILL / BUILD / COLLECT
// bitmask: completed_quests[4] (128 quests max)
// Chains: prerequisite_quest + next_quest + faction_reward
// JSON (data/quests/quest_001.json):
{ "id": 1, "type": "KILL", "target_faction": 2, "count": 5,
"next_quest": 2, "faction_reward": { "faction": 0, "delta": 10 } }
🔒 Security & Integrity (FIX-1–10)
Security Audit — 10 fixes завершено (2026-05-13)
| Fix | Проблема | Рішення |
| FIX-1 | system("mkdir -p …") — shell injection | POSIX stat+mkdir ітерацією по '/' |
| FIX-2 | Lua необмежена пам'ять | lua_newstate(lua_alloc, …) з 8 MB hard cap |
| FIX-3 | Lua io/os/require/exec доступні | nil io/os/package/debug + dofile/loadfile/load після openlibs |
| FIX-4 | Save files без integrity check | SaveHeader v8: CRC32 (poly 0xEDB88320) + data_size |
| FIX-5 | shadow_cull.comp #version 430 | → #version 460 core |
| FIX-6 | MAX_BONES=128, stride помилковий | MAX_BONES=6 (root+body+4 limbs); FinalBones 4MB→192KB |
| FIX-7 | PathCache spinlock без pause | _mm_pause() у while-loop на x86_64 |
| FIX-8 | 1449 рядків мертвого GL43 коду | remove_gl43_guards.py: 32 файли очищено |
| FIX-9 | SaveAsync std::string heap alloc | char async_path_[256] член SaveSystem |
| FIX-10 | Застарілий Raylib коментар | Видалено з gpu_hal.cpp |
Performance: AVX2 (M46)
// engine/include/monkey_dust/platform/md_hints.h:
#ifdef __AVX2__
// BulkComputeDistSq: 8 floats/iter з _mm256_fmadd_ps
__m256 dx = _mm256_sub_ps(_mm256_load_ps(px + i), cx8);
__m256 dz = _mm256_sub_ps(_mm256_load_ps(pz + i), cz8);
_mm256_store_ps(dist_sq + i, _mm256_fmadd_ps(dx, dx, _mm256_mul_ps(dz, dz)));
#endif
// alignas(64) на всіх 5 SoA масивах → vmovaps замість vmovups
// MD_HOT на BulkComputeDistSq/BulkComputeLOD → __attribute__((hot))
🚫 Що НЕ робити
Абсолютні заборони
- FSM/switch-state для AI — тільки BehaviorTree.h
malloc/new[]/delete[] у hot-path — EnTT пули + фіксовані масиви
std::vector/map у hot-path — [MAX_N] масиви
- Зовнішні JSON бібліотеки — власний strstr-парсер
assert() — MD_LOG_WARNING(...) + return
- Мутація registry під час
view.each() — collect → apply
- C++20 features — C++17 maximum
- ImGui/editor поза
#ifdef MONKEY_DUST_EDITOR
- engine/ includes з game/ або tools/ — split-readiness
- MINIAUDIO_IMPLEMENTATION поза
audio_system.cpp
- Нові компоненти/combat у game/ — тільки engine/include/monkey_dust/
MD_OPENGL43_ENABLED — видалено FIX-8, не відновлювати
💡 Канонічні патерни
Collect → Apply (мутація ECS)
static std::vector<entt::entity> to_kill; // BSS, не heap
to_kill.clear();
reg.view<Health>().each([&](auto e, auto& hp){
if (hp.current <= 0) to_kill.push_back(e);
});
for (auto e : to_kill) reg.destroy(e);
SaveSystem setup
// game/src/main.cpp — ДО першого Save/Load:
SaveSystem::Get().SetCallbacks(
WorldSerializer::Serialize, // SaveWriteCallback
WorldSerializer::Deserialize, // SaveReadCallback
nullptr // userdata
);
// НЕ SetContext (видалено)
BTActionFunc signature
// Листові функції BT:
BTStatus myAction(md::EngineContext& ctx, entt::entity e) {
auto& gs = static_cast<GameState&>(ctx);
// ...
return BTStatus::SUCCESS;
}
// Реєстрація:
RegisterBTAction("myAction", myAction);
GPU compute dispatch
GpuComputePipeline::Desc d;
d.glsl_path = "shaders/my.comp";
d.num_readonly_storage_buffers = 1;
d.num_readwrite_storage_buffers = 1;
pipeline.Create(d);
// SDL_GPU dispatch:
GpuComputePass pass;
pass.Begin(&pipeline, bindings); // bindings.cmd != nullptr
pass.Dispatch(groups_x, 1u, 1u);
pass.End();
md_hints.h usage
#include <monkey_dust/platform/md_hints.h>
MD_HOT void MyHotFunction() { ... }
if (MD_UNLIKELY(error_condition)) { MD_LOG_WARNING(...); return; }
if (MD_LIKELY(common_path)) { ... }
MotivationGate: написати motivation з зовнішньої системи
// DirectorSystem або FlowGraph пише; BT читає через MotivationCheck
// Нульова пов'язаність — BT не знає хто і коли поставив motivation.
// FlowGraph node action (при срацюванні тригера):
if (AgentState* as = reg.try_get<AgentState>(npc))
as->motivation = MotivationType::Attack;
// BT автоматично підхопить на наступному тику через MotivationCheck вузол
fnv_path для ієрархічних ключів
#include <monkey_dust/ai/fnv.h>
// Compile-time ієрархічний ключ:
constexpr uint32_t K_ZONE_A_ENTER = fnv_path("zone_a", "on_enter");
constexpr uint32_t K_AI_PATROL = fnv_path("ai", "npc", "patrol");
// Blackboard key combination:
uint32_t k = fnv_combine(fnv1a_rt(npc_name), fnv1a("speed"));
bb_set_float(state, k, 3.5f);
LogicTickContext: захист від re-entrant тіків
// game/src/logic_tick.h — перед кожним RunLogicTick:
extern LogicTickContext g_logic_ctx;
if (g_logic_ctx.active) { MD_LOG_WARNING("re-entrant tick!"); return; }
g_logic_ctx.begin(dt);
RunLogicTick(now_s, fps, player, ...);
g_logic_ctx.end();
// frame_index доступний для per-frame дебагу:
MD_LOG_INFO("frame %llu dt=%.3f", g_logic_ctx.frame_index, g_logic_ctx.dt);
⚠️ Відомі pitfalls
| Pitfall | Симптом | Рішення |
| UV inversion | Тайли невидимі або перевернуті | v = 1.0f - y / atlas_h |
| Path індексування | NPC летять або телепортуються | path[i*3] not path[i] |
| TINST_STRIDE | Тайли зміщені або артефакти | Stride=36 sync між .cpp і .vert |
| img= перший рядок | Water textures на grass tiles | Тільки ПЕРШИЙ img= → atlas[0] |
| SaveHeader v8 CRC | Load fail "CRC mismatch" | crc32=0 під час обчислення, потім patch |
| MAX_BONES=6 | Неправильна анімація або сміття | boneBase = instanceIdx * 6u (не 128) |
| Lua sandbox | Script escape через os.execute | io/os nil-ed після luaL_openlibs |
| MINIAUDIO_IMPL | Linker: multiple definition | Тільки в audio_system.cpp |
| Registry мутація | Segfault під час view.each | Collect → apply |
| MD_OPENGL43_ENABLED | Компілятор видасть undef macro | Не визначати, видалено FIX-8 |
| БОРГ-1 | Будівлі після Load "floating" | BuildSystem::OccupyGrid() у Deserialize |
| БОРГ-6 | TransformSoA slot drift після Load | ✅ Виправлено — AssignSlot() при десеріалізації (БОРГ-6 / Save v5+) |
| БОРГ-9 | ChunkLoad тиха втрата при radius≥5 | MAX_STAGING=8 (знати обмеження) |
| FlagCheck bitmask vs bit-index | FlagCheck завжди Failure | addFlagCheck(lcf::SHOULD_ATTACK) — передавай bit-index uint8, НЕ bitmask (C4) |
| MotivationType не споживається | Attack branch спрацьовує кожен тік | Після MotivationCheck → SetMotivation(Idle), щоб "спожити" (C1) |
| Reference null ptr | BT sub-tree завжди Failure | addReference(bt) — bt = null → Failure; завантажуй шаблон до виклику (C3) |
| RegisterAllBTBindings порядок | unknown builder warning; шаблони не завантажуються | RegisterAllBTBindings() ПЕРЕД BTLoader::LoadFromFile() |
| LogicTickContext re-entrant | Подвійний AI tick за кадр | Перевіряй ltc.active перед RunLogicTick (C10) |
| BTLoader live reload thread-safety | Concurrent tree modification crash | volatile needs_reload_ — тільки OnReloadCallback пише (background); PollReload() читає+скидає на main thread (IMP-2) |
| UtilityScorer не замінює MotivationCheck | Мотивація не спрацьовує у BT | UtilityScorer::SelectMotivation() пише в as.motivation ЗЗОВНІ BT; BT читає через MotivationCheck як і раніше (IMP-8) |
| NpcMemoryComponent: max 8 spatial/events | Events не записуються | AddSpatial → LRU evict oldest; AddEvent → ring overwrite index 0 (IMP-3) |
| WorldSimulation не fire'ится | Економіка не оновлюється | Tick(LOGIC_TICK_S) у logic_tick.cpp — накопичує accum_s_, спрацьовує при ≥1.0f (10 logic ticks = 1 секунда) (IMP-6) |
| TileMapRenderer dirty_ не скинутий | Стара карта залишається у кеші | MarkDirty() або SetAtlases() — скидай dirty_=true після зміни карти (IMP-4) |
🗓️ Roadmap — стан на 2026-05-16
Завершено (M0–M58 + FIX-1–10 + AI-Ext-1–20 + BT VM + IMP-1–9 + Batch 3–27)
| Milestone | Зміст | Статус |
| M0–M34 | Core gameplay: ECS, AI, Nav, Combat, Build, Save, Dialog, Quest, Editor, Chunk, GPU | ✅ |
| M6 | Monorepo: engine/+game/+tools/; 3 CMake targets; split-ready | ✅ 2026-05-02 |
| M7.1–M7.27 | Flare runtime: map parser, multi-atlas, billboard, FRINGE, tile anim, x_off | ✅ |
| SDL_GPU A1–A7 | SPIR-V toolchain, GpuRingBuffer, GPU HAL, CSM, Compute culling | ✅ 2026-05-02 |
| M_SPLIT + П.1–П.3 | ECS→engine; Raylib unlinked; MD_SDL_GPU exclusive | ✅ 2026-05-07 |
| M14–M17 | FlareAnimSystem; LuaEventBus; AudioSystem; Lua API | ✅ 2026-05-07 |
| M18–M25 | AgentState, SenseSystem, DirectorSystem, BT Extensions, FlowGraph, MapManager, ProceduralModulator, NpcConfig | ✅ 2026-05-13 |
| M26–M28 | RenderTier, AmbientProbe, GBuffer 2-RT | ✅ 2026-05-13 |
| M29–M32 | PointLight, SSAO, StripLight, SMAA | ✅ 2026-05-13 |
| M33–M35 | PowerSystem, ProjectileSystem, PowerSlotManager + Lua API | ✅ 2026-05-13 |
| M36 | SaveSystem v7: AgentState inline + FlowGraph vars tail | ✅ 2026-05-13 |
| M38–M41 | Editor panels: ViewCone, FlowGraph, Director, GpuProfiler | ✅ 2026-05-13 |
| M42 | Integration pass: ProjectileSystem+DirectorSystem у logic tick; GpuProfiler probes | ✅ 2026-05-13 |
| M43 | Google Test suite: 58 тестів, 6 suite (FNV/BB/FlowGraph/Director/Powers/NpcConfig); PowerManager::LoadFromJson+Find impl | ✅ 2026-05-13 |
| M44 | HotReload file watcher: POSIX stat, SDL_Thread, PollOnce | ✅ 2026-05-13 |
| M45 | FlowVar typed union (Float/Bool/Int/Str); typed Get/Set Lua bridge | ✅ 2026-05-13 |
| M46 | md_hints.h (MD_HOT/MD_UNLIKELY); AVX2 BulkComputeDistSq/_LOD; alignas(64) SoA | ✅ 2026-05-13 |
| FIX-1 | system()→POSIX mkdir (world_serializer.cpp) | ✅ |
| FIX-2/3 | Lua sandbox: 8MB allocator + block io/os/package/debug | ✅ |
| FIX-4 | SaveHeader v8: CRC32 + data_size integrity | ✅ |
| FIX-5/6 | shadow_cull #version 460; MAX_BONES 128→6 (4MB→192KB) | ✅ |
| FIX-7/8/9/10 | _mm_pause spinlock; MD_OPENGL43 cleanup -1449 LOC; SaveAsync char[256]; stale comment | ✅ |
| AI-Ext-1–10 | C1 MotivationGate; C2 BranchType+ShutdownSpeed; C3 Reference sub-tree; C4 LogicCharacterFlags uint64+lcf; C5 AgentTimerSlot×26; C6 GaugeType+AgentGauges; C7 fnv_combine+fnv_path; C8 EntityStateFlag; C9 bt_stage[4][32]; C10 LogicTickContext | ✅ 2026-05-13 |
| AI-Ext-11–20 | C11 SequenceStateless; C12 TimerOnlyIncrease; C13 FrameFlag (per-tick, BTSystem reset); C14 WeightedSelector (LCG rng); C15 AwarenessState×7; C16 AlertnessState×4; C17 NpcMood×6; C18 RoleRegistry+RoleCheck/Claim/Release; C19 WithdrawState×3; C20 FlowDurableTrigger (MAX=16, refcount) | ✅ 2026-05-13 · 105/105 tests |
| BT VM повний | BTNodePool (flat 32KB arena, sizeof(BT)=4616); BTSystem (phase1 frame_flags+hints, phase2 bare, phase3 tick); BTJsonLoader (strstr recursive, 30+ node types, 11 enum tables); role_registry.h (8 NpcRoles, singleton); bt_components.h (BehaviorTreeComponent+DirectorHintComponent); data/bt/systematic_search.bt.json | ✅ 2026-05-13 · 131/131 tests |
| IMP-1–9 | IMP-1 ACES tonemapping (pbr.frag+npc_instanced.frag) · IMP-2 Live BT reload (WatchFile+PollReload) · IMP-3 NpcMemoryComponent POD (SpatialMemory[8]+events[8], MemoryCheck/Forget BT) · IMP-4 Dirty TINST tile cache (skip PASS1–3) · IMP-5 Dirty faction SSBO partial upload · IMP-6 WorldSimulation 1Hz economy · IMP-7 MaterialDesc OGRE-style JSON→GpuPipeline::Desc · IMP-8 UtilityScorer Echo-style goal scoring · IMP-9 md::fs thin FS abstraction | ✅ 2026-05-13 |
| M47 | NavLodTier (Close/Frozen); PathCache::Freeze/Unfreeze/UnfreezeAll; QueryPathLod(tier) bypasses PATH_TTL_S для frozen paths | ✅ 2026-05-16 |
| M48 | Save v9: NpcMemoryComponent (260 B) inline per NpcRecord; is_v9=(_pad[1]==1); backward compat v7/v8 | ✅ 2026-05-16 |
| M49 | ReplaySnapshot: ReplayNpcState(16B)+ReplayFrame(1048B, 64 NPCs+CRC32)+ReplayBuffer(300 frames ring, ~307KB BSS); RPLY file format; CaptureFrame/WriteToFile/ReadFromFile | ✅ 2026-05-16 |
| M50 | FlowVar::Vec3=4 (val.v3[3]) + EntityRef=5 (val.entity_ref uint32); SetVarVec3/GetVarVec3/SetVarEntity/GetVarEntity; coerce_to_float | ✅ 2026-05-16 |
| M51 | Save v10: SaveHeaderV10Extra (+ws_faction_count+ws_route_count, 108B); FactionState[]+TradeRoute[] tail; WorldSimulation::Reset()+direct array restore | ✅ 2026-05-16 |
| M52 | AllianceMatrix singleton (9×9 stance table); AllianceGroup enum; IsEnemy/GetStance/SetStance (symmetric); defaults: Player↔Alien/Android/WeylandYutani hostile | ✅ 2026-05-16 |
| M53 | FrameFlagDispatcher: reads frame_flags after BT tick → NavAgent movement, WorldTransform rot_y, Combat::is_dead, lcf::SHOULD_DESPAWN entity destroy; 20 engine tests | ✅ 2026-05-16 |
| M54 | NPC archetype BT templates: BuildGuardBT/BuildAlienBT/BuildVendorBT; bt_templates.json 7 entries; BT breadth-first child-ordering invariant fixed; 23 tests | ✅ 2026-05-16 · 1134 tests |
| M55 | SenseSystemUpdate(now_ms): Visual = max cone activation (dist+half-angle vs ViewConeSet); Audio = linear falloff 15m; rising-edge → last_activated_ms; Visual → last_known_x/z; 31 tests | ✅ 2026-05-16 · 1165 tests |
| M56 | CombatDispatch(now_ms): ff::SHOULD_MELEE_ATTACK(12)+SHOULD_RANGED_SHOOT(9); target from bb["target_entity"]; range+cooldown gate; CalcDamage+RollHitZone; kill → Combat::is_dead+lcf::IS_DEAD; collect→apply 64-cap; 38 tests | ✅ 2026-05-16 · 1203 tests |
| M57 | DeferredLightingSystem: fullscreen triangle (no VBO); GBuffer RT0+RT1 → RGBA16F hdr_color; DeferredAmbientUBO std140 64B (sun_dir/sun_color/ambient_color/emissive_scale=1.4875); deferred_lighting.vert/frag SPIR-V; gate IsDeferred(); 17 tests | ✅ 2026-05-16 · 1220 tests |
| M58 | DialogCondType::QuestActive=4+QuestComplete=5; "quest_active:N"/"quest_complete:N" JSON; NpcInteractionComponent(20B, range 2.5m, cooldown 5s); dialogs.json expanded (nodes 4+5 quest-gated); 32 tests | ✅ 2026-05-16 · 1252 tests |
| Batch 3–27 | 77 BT node types, SenseType×9, SquadSignalBus, NpcDevelopment, AlienConfig, VentLock, NpcRelationship, ActionAdjustMenace, lcf::IS_SUSPENDED/IS_PLAYER, ConditionTargetDist/RoutingDist, ShaderFeature, TileCullBounds, NamedBranchRegistry, NpcSoundBus | ✅ 2026-05-16 |
🔗 SSBO / Texture Bindings
SSBO bindings (SDL_GPU set=1 / OpenGL binding=N)
| Binding | Призначення | Розмір |
| 0 | TransformSoA: vec4(x, z, y, rot_y) per slot | 16 B × 500 |
| 1 | Main cull visible indices | 4 B × 500 |
| 2 | Main draw indirect command (DrawElementsIndirectCommand) | 20 B |
| 3 | Faction IDs per slot | 4 B × 500 |
| 4 | FinalBones: 500 NPC × 6 кісток × mat4 (~192 KB, FIX-6) | 500×6×64 B |
| 5 | AnimNpcState: slot+clip_id+time_s+_pad (ring-buffered upload) | 16 B × 500 |
| 6 | Shadow visible indices | 4 B × 500 |
| 7 | Shadow draw indirect command | 20 B |
| 8 | AmbientProbe SSBO: SH irradiance coefficients (M27) | MAX=64 probes |
Texture units
| Unit | Призначення |
| 0–3 | Tile atlas 0–3 (u_atlas0..u_atlas3 у tile_map.frag) |
| 5 | CSM cascade 0 depth map (1024×1024) |
| 6 | CSM cascade 1 depth map |
| 7 | CSM cascade 2 depth map |
SDL_GPU descriptor sets (SPIRV_COMPILE path)
| set | binding | Шейдер | Зміст |
| 0 | 0–N | Compute | RO storage buffers |
| 1 | 0–N | Compute | RW storage buffers |
| 2 | 0 | Compute | UBO |
| 0 | 0–N | Graphics vertex | Storage buffers (contiguous) |
| 1 | 0 | Graphics vertex | UBO (VP matrix, sun, time) |
| 2 | 0–3 | Graphics fragment | Texture samplers (atlases) |
© Ліцензія
Proprietary Software — All Rights Reserved
Copyright (c) 2024–2026 monkey_dust project author.
Несанкціоноване копіювання, модифікація, розповсюдження або використання суворо заборонено. Повний текст: LICENSE у корені репо.
Сторонні компоненти (дозвільні ліцензії)
| Бібліотека | Ліцензія | Використання |
| SDL3 + SDL_GPU | zlib | Вікно, рендер, input |
| EnTT 3.16 | MIT | ECS |
| Recast/Detour | zlib | NavMesh |
| Dear ImGui | MIT | Editor UI, debug overlay |
| GLM 1.1.0 | MIT | Math (USE_GLM path) |
| Lua 5.4 | MIT | Scripting |
| stb_image / stb_truetype | MIT / Public Domain | Textures; MdDraw2D fonts |
| miniaudio | MIT / Public Domain | Audio |
| glad2 | MIT | GL function loader (editor) |
| cgltf | MIT | glTF model loading |
| Google Test | BSD-3 | Test suite (md_tests) |
Субмодуль third_party/flare-game/ — виключно для розробки та тестування, не розповсюджується (GPL-2.0).