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).

C++17, без C++20
Linux · Intel HD 520 (AVX2)
1280×720 · 60 FPS
SDL3 + SDL_GPU (Vulkan)
1252 GTests · Proprietary
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–M5Platform abstraction: input.h, audio.h, window.h; SSBO→glad; miniaudio2026-05-02
M6Monorepo: engine/ + game/ + tools/; split-ready; MdCamera unified2026-05-02
M7.1–M7.27Flare parser, TileMetaRegistry, billboard, multi-atlas, animation, FRINGE, x_off2026-04-30
M11–M12math_types.h; MdDraw2D; standalone editor SDL3; Raylib removed from engine2026-05-02
SDL_GPU A1SPIR-V toolchain: compile_shaders.sh; 21 шейдерів Vulkan 1.12026-05-02
SDL_GPU A2–A7GpuRingBuffer, GPU HAL, GpuTexture/StaticBuffer/ComputePipeline/RenderPass/DrawIndexedIndirect2026-05-02
М_SPLITECS компоненти + combat типи → engine/include/monkey_dust/2026-05-06
П.1–П.3Raylib повністю видалено з SDL3 build path; MD_SDL_GPU авто з USE_SDL32026-05-07
FIX-8MD_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_dustbuild/game/monkey_dust
Flare viewerninja -C build md_flare_demobuild/tools/md_flare_demo
Standalone editorninja -C build monkey_dust_editorbuild/tools/monkey_dust_editor
Flare конвертерninja -C build md_flare_convertbuild/tools/md_flare_convert
Test suiteninja -C build md_testsbuild/tests/md_tests
SPIR-V шейдериbash scripts/compile_shaders.shshaders/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

TargetDefinesПримітка
monkey_dust_engineUSE_SDL3 USE_GLM MD_LOG_NO_RAYLIB MD_SDL_GPUPUBLIC → propagate до всіх consumers
monkey_dust (game)inherits engine PUBLIC+ DEBUG у Debug build
md_flare_demoMD_SDL_GPU USE_SDL3 USE_GLMSDL_GPU Vulkan рендер
monkey_dust_editorMONKEY_DUST_STANDALONE_EDITOR USE_SDL3 -UMD_SDL_GPUOpenGL контекст; SDL_GPU device не ініціалізується
md_testsinherits engine PUBLICGoogle 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 МУСИТЬ зберігати цю конвенцію.
ПРИЧИНА: MdLoadTexturePixelArtstbi_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 — фільтрація (НЕ СПРОЩУВАТИ)

ParameterValueНавіщо
MIN_FILTERGL_LINEAR_MIPMAP_LINEARБез mipmap GL_NEAREST = прозорий піксель на zoom-out
MAG_FILTERGL_NEARESTCrisp pixel-art на zoom-in
WRAPGL_CLAMP_TO_EDGEБез seam bleeding
flip_vtruestbi flip — частина UV конвенції
gen_mipmaptrueПотрібен для 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

ТипУмоваГеометріяПриклад
Flatoffset_y ≤ h/2XZ-ромб на y=0 (y_bot=y_top=0)Трава, вода, скелі, будівлі
Billboardoffset_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Зміст
0vec2per-vertexa_corner — quad corner [0,1]×[0,1]
1vec20a_tile_pos — grid (col, row)
2vec48a_uv_rect — (u0, v0, u1, v1)
3float24a_y_bot — world Y основи billboard (≤0); 0 = flat
4float28a_y_top — world Y верхівки billboard (>0); 0 = flat
5float32a_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Призначення
GpuPipelineSDL_CreateGPUGraphicsPipeline (SPIR-V)Графічний пайплайн + VAO layout
GpuVertexBufferSDL_CreateGPUBuffer + TransferBufferPer-frame ring-buffered upload
GpuCommandBufferSDL_AcquireGPUCommandBufferFrame command recording
GpuStaticBufferSDL_CreateGPUBuffer (static)Vertex/index дані
GpuTextureSDL_CreateGPUTextureTextures + samplers
GpuComputePipelineSDL_CreateGPUComputePipelineCompute shaders (cull, skin)
GpuComputePassSDL_BeginGPUComputePassDispatch + bindings
GpuDepthTextureSDL_CreateGPUTexture (depth)CSM cascades, depth buffer
GpuRenderPassSDL_BeginGPURenderPassScoped render pass
GpuDrawIndexedIndirectSDL_DrawGPUIndexedPrimitivesIndirectGPU-driven draw
GpuRingBufferN=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.fragGraphicsGPU skeletal animation (MAX_BONES=6)
skinning.compComputeBone transform (local_size=64, procedural)
cull.compComputeMain frustum + distance cull
shadow_cull.compComputeShadow frustum cull (#version 460, FIX-5)
shadow_csm.vert/fragGraphics3-cascade CSM depth pass
deferred_point.vert/fragGraphicsPoint light icosphere volumes (M29)
deferred_strip.vert/fragGraphicsStrip (capsule) lights AABB (M31)
ssao.compComputeSSAO half-res R8_UNORM 16-tap (M30)
smaa_edge/blend/final.vert/fragGraphics3-pass SMAA post-process (M32)
tile_map.vert/fragGraphicsIsometric tile map 3D (stride=36)
tile_map_2d.vert/fragGraphics2D SDL_GPU tile map (4-atlas)
particle.vert/fragGraphicsPoint 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)

СистемаГеометріяШейдер
PointLightSystemIcosphere 80 трикутниківdeferred_point.vert/frag (additive)
StripLightSystemAABB volume (capsule SDF)deferred_strip.vert/frag (additive)
SSAOSystemHalf-res compute dispatchssao.comp (R8_UNORM, 16-tap rotated disk)
SMAASystem3× fullscreen trianglesmaa_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ФайлОпис
actPickWanderTargetai_goal.hВибрати випадкову ціль у радіусі
actMoveToTargetai_goal.hPathCache-first QueryPath → nav waypoints
actChaseTargetai_goal.hChase ворога з SpatialGrid lookup
actAttackTargetai_goal.hАтака в радіусі → CombatSystem event
actFleeai_goal.hВтеча в протилежному напрямку
actPatrolai_goal.h4-waypoint patrol loop (static table, без malloc)
actSetMotivationIdle/Attack/Flee/Searchai_goal.hЗаписує MotivationType в AgentState (C1)
actLuaScriptlua_bt_bridge.hВиконати Lua функцію як BT leaf
actSenseCheckbt_extensions.hПеревірка SenseActivation (M21)
actFlagCheck/Setbt_extensions.hLogicCharacterFlags uint64 bit-index (C4)
actTimerStart/Checkbt_extensions.hAgentTimerSlot[26] (C5)

BT Node Types (behavior_tree.h)

ТипКласПримітка
Selector, SequenceCompositeСтандартні
Action, ConditionLeafФункція-покажчик
Inverter, Repeat, WaitDecoratorСтандартні
TimerStart, TimerCheckDecoratorAgentTimerSlot індекс у data
FlagCheck, FlagSetLeaflcf bit-index (0–39) у data — НЕ bitmask (C4)
SenseCheckLeafViewConeSet activation_level (M19)
BranchDecoratorBranchType + ShutdownSpeed у data (C2)
MotivationCheckLeafas->motivation == (MotivationType)data (C1)
SetMotivationLeafas->motivation = (MotivationType)data; Success (C1)
ReferenceDecoratorptr у nd._padding → sub-tree tick; null = Failure (C3)
GaugeCheckLeafas->gauges.get(gauge) >= threshold (C6)
GaugeSetLeafas->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 (базові структури)

PatternPatternmonkey_dust реалізаціяФайл
C1 MotivationGatePriority selector with motivation gateMotivationType (14 values) + MotivationCheck/SetMotivation BT nodes; BuildMotivationGateBT()agent_state.h, behavior_tree.h
C2 BranchTypeDecoratorBranch + named interrupt categoriesBranchType enum (17 values) + ShutdownSpeed; addBranch(cond, btype, speed)behavior_tree.h
C3 Reference nodeReferencedBehavior (sub-tree delegation)BTNodeType::Reference + addReference(BehaviorTree*); ptr у nd._padding[2]; null → Failurebehavior_tree.h
C4 LogicCharacterFlagsLOGIC_CHARACTER_FLAGS 40-bit bitmaskLogicCharacterFlags{uint64_t} + namespace lcf, 40 named bits; FlagCheck/FlagSet — bit-index uint8 (НЕ bitmask)agent_state.h
C5 TimerSlotsLOGIC_CHARACTER_TIMER_TYPE (26 named)AgentTimerSlot enum; MAX_AGENT_TIMERS=26; timers[] uint64 (ms)agent_state.h
C6 Gauge systemRetreat/StunDamage gaugesGaugeType + AgentGauges{val[2]} + GaugeCheck/GaugeSetagent_state.h, behavior_tree.h
C7 Hierarchical IDsShortGuid::combine() SHA1-basedfnv_combine(parent, child) + fnv_path(a,b,c) — pure FNV, без SHA1fnv.h
C8 EntityStateEntityStateFlag 23-flag bitmaskEntityStateFlag enum (23 flags) + entity_state uint32 + esf_test()agent_state.h
C9 Per-stage BTPer-stage BT override in NPC configNpcConfig::bt_stage[4][32] — per-DirectorStage override; empty = fall back to bt_day/bt_nightnpc_config.h
C10 LogicTickContextTriggerProcessingContext::begin_cycle()LogicTickContext{dt, frame_index, active}; active guard re-entrant RunLogicTicklogic_tick.h

C11–C20 (розширені AI примітиви)

PatternPatternmonkey_dust реалізаціяФайл
C11 SequenceStatelessStateless sequence — restarts from child 0 on re-entryBTNodeType::SequenceStateless; на Running скидає currentChild=0 (на відміну від Sequence)behavior_tree.h/.cpp
C12 TimerOnlyIncreaseTIMER_ONLY_INCREASE flag у DecoratorTimeraddTimerStartOnlyIncrease(slot, dur); flags bit0=1; не вкорочує активний таймерbehavior_tree.h/.cpp
C13 FrameFlagFrame-scoped signal flagsframe_flags uint64 в AgentState; FrameFlagCheck/Set; BTSystem скидає на початку кожного tickagent_state.h, bt_system.h
C14 WeightedSelectorWeighted random child selector4 ваги packed у data uint32; LCG RNG = entity ^ frame_index; max 4 дочірніхbehavior_tree.h/.cpp
C15 AwarenessStateAwareness state enumAwarenessState (Dead…Aware, 7 рівнів) + поле в AgentState + AwarenessCheckagent_state.h, behavior_tree.h
C16 AlertnessStateALERTNESS_STATE enum (Relaxed→Combat)AlertnessState (4 рівні) + поле + AlertnessCheckagent_state.h, behavior_tree.h
C17 NpcMoodNPC_MOOD (Neutral/Curious/Panicked...)NpcMood (6 значень) + поле mood в AgentState + MoodCheckagent_state.h, behavior_tree.h
C18 RoleSystemRoleClaim/RoleCheck для multi-NPCNpcRole enum (8 ролей) + RoleRegistry singleton + RoleCheck/RoleClaim/RoleRelease BT nodesrole_registry.h, behavior_tree.h
C19 WithdrawStateWITHDRAW_STATE у NPC_RETREAT.XMLWithdrawState (3 стани) + withdraw_state поле + WithdrawCheck/SetWithdrawagent_state.h, behavior_tree.h
C20 FlowDurableTriggerTriggerInfo refcounted у FlowGraphFlowDurableTrigger{node_id, duration, ref_count}; MAX=16; AcquireDurable/AddRef/Release; Tick() decayflow_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++ метод
SelectorLinearaddSelector()
SequenceLinearaddSequence()
SequenceStatelessaddSequenceStateless() (C11)
WeightedSelectorweights[4]addWeightedSelector(w) (C14)
DecoratorBranchbranch_type, condition, shutdown_speedaddBranch()
ConditionNodeconditionaddCondition()
ActionNodeactionaddAction()
ReferencedBehaviortree (deferred)addReference(nullptr)
TimerStarttimer_slot, duration_ms, only_increaseaddTimerStart / addTimerStartOnlyIncrease (C12)
TimerChecktimer_slotaddTimerCheck()
MotivationCheck / SetMotivationmotivationaddMotivationCheck/Set() (C1)
FlagCheck / FlagSetflag, check_set|setaddFlagCheck/Set() (C4)
FrameFlagCheck / FrameFlagSetflag, check_set|setaddFrameFlagCheck/Set() (C13)
AwarenessCheck / AlertnessCheck / MoodCheckstate / moodaddAwarenessCheck/AlertnessCheck/MoodCheck() (C15–17)
RoleCheck / RoleClaim / RoleReleaserole, mode, query_idaddRoleCheck/Claim/Release() (C18)
WithdrawCheck / SetWithdrawstateaddWithdrawCheck/SetWithdraw() (C19)
GaugeCheck / GaugeSetgauge, threshold|valueaddGaugeCheck/Set() (C6)
SenseChecksense, thresholdaddSenseCheck()
Inverter / Repeat / Waitcount / duration_msaddInverter/Repeat/Wait()

🔬 9 Multi-Engine Improvements

Аналіз механік OGRE v1 · OGRE-Next · Echo · Banshee 3D · Cocos2d-x
9 патернів адаптовано з 6 engine'ів після порівняльного аналізу (2026-05-13). Всі реалізовані без нових зовнішніх залежностей.
#ДжерелоФічаФайли
IMP-1Filmic tonemappingACES 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-2Live BT reloadLive 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-3EchoNpcMemoryComponent POD: SpatialMemory[8] (LRU evict, timestamp+confidence) + events[8] (FNV IDs, ring overwrite); MemoryCheck/MemoryForget BT nodesengine/include/monkey_dust/components/npc_memory.h, engine/src/ai/behavior_tree.cpp, engine/src/ai/bt_json_loader.cpp
IMP-4OGRE-NextDirty TINST: TileMapRenderer кешує vbuf/ibuf між кадрами; PASS1–3 пропускаються коли !dirty_ && !anim_changedengine/include/monkey_dust/flare/tile_map_renderer.h, engine/src/flare/tile_map_renderer.cpp
IMP-5OGRE-NextDirty faction SSBO: TransformSoA::MarkFactionDirty(slot) відслідковує faction_dirty_min_/max_; завантажує тільки брудний діапазон слотівengine/include/monkey_dust/world/transform_soa.h, engine/src/world/transform_soa.cpp
IMP-6OGRE v1WorldSimulation 1 Hz: FactionState[8] + TradeRoute[32]; economy Tick(delta_s) накопичує з 10 TPS logic tick; торгівля впливає на gold/prosperity/populationengine/include/monkey_dust/world/world_simulation.h, engine/src/world/world_simulation.cpp, game/src/logic_tick.cpp
IMP-7OGRE v1MaterialDesc: OGRE-style JSON → GpuPipeline::Desc auto-builder; strstr parser (read_str/read_bool helpers); MatBlend/MatCull/GpuTopology mappingengine/include/monkey_dust/render/material_desc.h, engine/src/render/material_desc.cpp
IMP-8EchoUtilityScorer: Echo-style goal utility over MotivationType; FillCandidates (AwarenessState+AlertnessState+Gauges weights) + Bias(current, 10) (інерція) + BestFrom; без mallocengine/include/monkey_dust/ai/utility_scorer.h, engine/src/ai/utility_scorer.cpp
IMP-9Cocos2d-xmd::fs: тонкий FS шар (fs_read_all/fs_read_alloc/fs_free/fs_exists); BTJsonLoader + BTLoader мігровані з raw fopen; єдина точка platform IOengine/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_posmd_get_pos(entity) → x,zWorldTransform позиція
md_set_targetmd_set_target(entity, x, z)NavAgent ціль
md_get_healthmd_get_health(entity) → hpHealth component
md_get_factionmd_get_faction(entity) → idFaction ID
md_on_eventmd_on_event(name, fn)LuaEventBus::Register
md_fire_eventmd_fire_event(name)LuaEventBus::Fire
md_quest_activemd_quest_active(id) → boolQuest state check
md_quest_completemd_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.

Вкладки редактора

ВкладкаФункція
ItemsCRUD для data/items/items.json: назва, вага, вартість, категорія
FactionsCRUD + матриця відносин для 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+ZUndo (до 256 операцій)
Ctrl+YRedo

Viewport Map — керування мишею

ДіяЕфект
LMB (mode: Tiles)Малювати tile (brush 1×1 / 3×3 / 5×5)
Shift+LMBFlood fill (з урахуванням активного шару)
LMB (mode: Spawns)Додати spawn-маркер у вибраній позиції
RMBPan viewport
ScrollZoom in / out
LMB on minimapPan viewport до вибраної позиції

Palette — вкладки

ВкладкаЗміст
TilesTileMetaRegistry thumbnails; вибір tile → ЛКМ на viewport малює; brush size 1/3/5; Erase toggle
SpawnsPlace/move/delete enemy spawn-маркерів; відображаються як S-маркери на viewport
PropsHero 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 пауза активна за замовчуванням при відкритті.

Клавіатурні скорочення

КлавішаДія
F3Toggle editor on/off
WGizmo: Translate (переміщення)
EGizmo: Rotate (обертання)
RGizmo: Scale (масштаб)
GToggle World ↔ Local gizmo space
FFocus camera on selected entity (cam_dist=15)
Ctrl+ZUndo
Ctrl+YRedo
Ctrl+DDuplicate selected entity (+1.0 x offset)
Ctrl+ASelect all entities (max 64)
DelDelete selected entities
Ctrl+PCommand Palette (fuzzy search всіх команд)

Gizmo — snapping

РежимSnap (Ctrl hold)
Translate1.0 world unit
Rotate15°
Scale0.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 EntityPopup: Transform / NPC Bandit / NPC Trader / NPC Holy / Building (spawn at cam_target)
Translate / Rotate / ScaleВибір gizmo режиму (W/E/R)
World/LocalToggle gizmo space (G) — підсвічено якщо World
Physics PauseПауза logic tick — оранжевий коли активна
FPS counterВідображає поточний fps у toolbar

Меню Bar

МенюПунктДія
FileNew SceneDeselectAll + Registry::clear() + TransformSoA::Init()
FileImport/Export Scene (.json)…SceneSerializer JSON повна сцена
FileSave/Load Game (F5/F9)SaveSystem::SaveAsync / Load
EditDuplicate (Ctrl+D)Копія entity + offset +1x
EditDelete (Del)Видалити вибрані + Free TransformSoA slot
EditSelect All (Ctrl+A)До 64 entities
ViewToggle panelsHierarchy/Inspector/Assets/Console/Graphics/Camera/Animation/ViewCone/FlowGraph/Director/GPU Profiler/Node Graph/Sequencer
ViewReset LayoutВмикає panels [0..5], вимикає [6..13] (всього 14 панелей)
SceneReload JSON DataHot-reload factions.json + buildings.json + dialogs.json + quests.json
SceneRebuild NavMeshEnqueueRebuild у cam_target позиції (async)
SceneSpawn NPC (Bandit/Trader)Spawn у cam_target
DebugDebug Overlay (F1)DebugSystem::overlay_on toggle
DebugSpatialGrid (F2)DebugSystem::grid_on toggle
DebugNavMesh WireframeDebugSystem::navmesh_on toggle
DebugPhysics PausedEditorCore::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ПанельСтан за замовч.Опис
0HierarchyOnEntity list з фільтром; клік = select; Ctrl+клік = add to selection
1InspectorOnКомпоненти вибраного: Transform (DragFloat x/y/z/rotY), Health (progress bar + slider), Combat, AIAgent, NavAgent, Inventory, Building
2AssetsOnAssetBrowser: дерево data/; drag-to-place планується
3ConsoleOnMD_LOG output + FPS/dt stat + Lua REPL. Кнопка Lua перемикає на Lua mode: ImGuiColorTextEdit з Lua syntax highlighting (read-only), показує лише рядки з тегом [Lua], auto-refresh при нових лог-подіях.
4GraphicsOnFog (near/far), sun direction, ambient color, render tier
5CameraOnOrbit/Flythrough toggle, cam_speed, cam_fovy, cam_dist; Reset view
6AnimationOffAnimationSoA: перелік кліпів вибраного entity; SetClip/Advance preview
7Paint (stub)OffЗарезервовано
8ViewCone (M38)OffSenseComponent activation bars (0..1) + ViewConeSet table (Close/Focused/Normal/Peripheral)
9FlowGraph (M39)Offimnodes візуальний граф: вузли кольоровані по типу (Event/Action/Condition/Sequence/Custom), drag-to-connect links. Нижче — typed FlowVar таблиця + MAX_PENDING ring; "Fire Trigger" кнопка.
10Director (M40)OffMenace gauge bar [0..1], stage кольором (CALM=зелений, CRISIS=червоний), profile params + manual override
11GPU Profiler (M41)OffPass budget bars: Upload / Skinning / ShadowCSM / Scene у ms; budget line 16ms. Внизу — flame graph (imgui-flame-graph): кожен pass = прямокутник з часткою від frame time, tooltip з ms.
12Node GraphOffBlueprint-style material/logic граф (imgui-node-editor). Вузли: TexSample / Multiply / Add / Lerp / ConstFloat / ConstColor / MatOutput. ПКМ на canvas = "Add Node" popup; drag між пінами = link; Delete = видалення вузла/лінка. Налаштування зберігаються у data/node_graph.json.
13SequencerOffImSequencer 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_WindowSDL_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
RReset view (origin + default scale)
EscВийти

WorldPlacer (Debug build, F4)

In-game об'єктний редактор у DEBUG build. Активується F4. Коли активний — Tab не перемикає тип будівлі, а тип маркера.

MarkerTypeIdx
TREE0
ROCK1
CAMP2
SPAWN_BANDIT3
SPAWN_TRADER4
КлавішаДія
F4Toggle WorldPlacer on/off
Tab (коли active)Cycle MarkerType (0→4→0)
LMBPlace marker на y=0 plane (ray vs plane intersection)
RMBDelete найближчий маркер
F6Export 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ТестівЩо тестує
FNV6FNV-1a хеш коректність і колізії; fnv_combine/fnv_path (C7)
AgentBlackboard17Set/Get/Evict BB[24]; LogicCharacterFlags (C4); MotivationType (C1); GaugeSystem (C6); TimerSlots×26 (C5); EntityStateFlag (C8)
FlowGraph24Nodes, vars, FlowVar typed union (M45), triggers, ring buffer, FlowDurableTrigger (C20)
DirectorSystemTest11Menace fill/decay, stage transitions, profile params
PowerSlotManagerTest11Slot assign, cooldowns, use, Lua API
NpcConfigTest6JSON parse, $inherit, field override, bt_stage (C9)
HotReloadTest7POSIX stat, change detection, PollOnce
BTAIPatternTest20C11–C19: SequenceStateless, TimerOnlyIncrease, FrameFlag, WeightedSelector, AwarenessCheck, AlertnessCheck, MoodCheck, RoleSystem, WithdrawState
FlowDurableTriggerTest3C20: AcquireDurable, AddRef, Release decay
BTJsonLoaderTest13LoadFromString/File; ReadName; MotivationCheck; TimerStart only_increase; AwarenessCheck; WeightedSelector; RoleCheck; unknown action fallback; SystematicSearch JSON
BTSystemTest5frame_flags cleared; disabled tree skipped; enabled tree executes; multiple entities all ticked
DirectorHintTest3Stale hint expires after MAX_PENDING_TICKS; active hint not cleared early; no-pending untouched
BTNodePoolTest5AllocTree; 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/)

КомпонентРозмірПризначення
WorldTransform12Bx, z, y позиція
Health8Bcurrent / max
AIAgent~32BBT state, target entity
NavAgent~256Bpath verts[64×3], len, target
FactionComponent4Bfaction_id uint32
AgentState (M18+C1–C8)~440Btimers[26]×uint64 + lcflags×uint64 + entity_state + gauges + motivation + blackboard[24]
SenseComponent (M19)~32BViewConeSet ref + SenseActivation[2]
ProjectileComponent (M34)~32Bvel + dmg + max_range + owner

⚔️ 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-1system("mkdir -p …") — shell injectionPOSIX stat+mkdir ітерацією по '/'
FIX-2Lua необмежена пам'ятьlua_newstate(lua_alloc, …) з 8 MB hard cap
FIX-3Lua io/os/require/exec доступніnil io/os/package/debug + dofile/loadfile/load після openlibs
FIX-4Save files без integrity checkSaveHeader v8: CRC32 (poly 0xEDB88320) + data_size
FIX-5shadow_cull.comp #version 430#version 460 core
FIX-6MAX_BONES=128, stride помилковийMAX_BONES=6 (root+body+4 limbs); FinalBones 4MB→192KB
FIX-7PathCache spinlock без pause_mm_pause() у while-loop на x86_64
FIX-81449 рядків мертвого GL43 кодуremove_gl43_guards.py: 32 файли очищено
FIX-9SaveAsync std::string heap allocchar 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 CRCLoad fail "CRC mismatch"crc32=0 під час обчислення, потім patch
MAX_BONES=6Неправильна анімація або сміттяboneBase = instanceIdx * 6u (не 128)
Lua sandboxScript escape через os.executeio/os nil-ed після luaL_openlibs
MINIAUDIO_IMPLLinker: multiple definitionТільки в audio_system.cpp
Registry мутаціяSegfault під час view.eachCollect → apply
MD_OPENGL43_ENABLEDКомпілятор видасть undef macroНе визначати, видалено FIX-8
БОРГ-1Будівлі після Load "floating"BuildSystem::OccupyGrid() у Deserialize
БОРГ-6TransformSoA slot drift після Load✅ Виправлено — AssignSlot() при десеріалізації (БОРГ-6 / Save v5+)
БОРГ-9ChunkLoad тиха втрата при radius≥5MAX_STAGING=8 (знати обмеження)
FlagCheck bitmask vs bit-indexFlagCheck завжди FailureaddFlagCheck(lcf::SHOULD_ATTACK) — передавай bit-index uint8, НЕ bitmask (C4)
MotivationType не споживаєтьсяAttack branch спрацьовує кожен тікПісля MotivationCheck → SetMotivation(Idle), щоб "спожити" (C1)
Reference null ptrBT sub-tree завжди FailureaddReference(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-safetyConcurrent tree modification crashvolatile needs_reload_ — тільки OnReloadCallback пише (background); PollReload() читає+скидає на main thread (IMP-2)
UtilityScorer не замінює MotivationCheckМотивація не спрацьовує у BTUtilityScorer::SelectMotivation() пише в as.motivation ЗЗОВНІ BT; BT читає через MotivationCheck як і раніше (IMP-8)
NpcMemoryComponent: max 8 spatial/eventsEvents не записуються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–M34Core gameplay: ECS, AI, Nav, Combat, Build, Save, Dialog, Quest, Editor, Chunk, GPU
M6Monorepo: engine/+game/+tools/; 3 CMake targets; split-ready✅ 2026-05-02
M7.1–M7.27Flare runtime: map parser, multi-atlas, billboard, FRINGE, tile anim, x_off
SDL_GPU A1–A7SPIR-V toolchain, GpuRingBuffer, GPU HAL, CSM, Compute culling✅ 2026-05-02
M_SPLIT + П.1–П.3ECS→engine; Raylib unlinked; MD_SDL_GPU exclusive✅ 2026-05-07
M14–M17FlareAnimSystem; LuaEventBus; AudioSystem; Lua API✅ 2026-05-07
M18–M25AgentState, SenseSystem, DirectorSystem, BT Extensions, FlowGraph, MapManager, ProceduralModulator, NpcConfig✅ 2026-05-13
M26–M28RenderTier, AmbientProbe, GBuffer 2-RT✅ 2026-05-13
M29–M32PointLight, SSAO, StripLight, SMAA✅ 2026-05-13
M33–M35PowerSystem, ProjectileSystem, PowerSlotManager + Lua API✅ 2026-05-13
M36SaveSystem v7: AgentState inline + FlowGraph vars tail✅ 2026-05-13
M38–M41Editor panels: ViewCone, FlowGraph, Director, GpuProfiler✅ 2026-05-13
M42Integration pass: ProjectileSystem+DirectorSystem у logic tick; GpuProfiler probes✅ 2026-05-13
M43Google Test suite: 58 тестів, 6 suite (FNV/BB/FlowGraph/Director/Powers/NpcConfig); PowerManager::LoadFromJson+Find impl✅ 2026-05-13
M44HotReload file watcher: POSIX stat, SDL_Thread, PollOnce✅ 2026-05-13
M45FlowVar typed union (Float/Bool/Int/Str); typed Get/Set Lua bridge✅ 2026-05-13
M46md_hints.h (MD_HOT/MD_UNLIKELY); AVX2 BulkComputeDistSq/_LOD; alignas(64) SoA✅ 2026-05-13
FIX-1system()→POSIX mkdir (world_serializer.cpp)
FIX-2/3Lua sandbox: 8MB allocator + block io/os/package/debug
FIX-4SaveHeader v8: CRC32 + data_size integrity
FIX-5/6shadow_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–10C1 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–20C11 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–9IMP-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
M47NavLodTier (Close/Frozen); PathCache::Freeze/Unfreeze/UnfreezeAll; QueryPathLod(tier) bypasses PATH_TTL_S для frozen paths✅ 2026-05-16
M48Save v9: NpcMemoryComponent (260 B) inline per NpcRecord; is_v9=(_pad[1]==1); backward compat v7/v8✅ 2026-05-16
M49ReplaySnapshot: ReplayNpcState(16B)+ReplayFrame(1048B, 64 NPCs+CRC32)+ReplayBuffer(300 frames ring, ~307KB BSS); RPLY file format; CaptureFrame/WriteToFile/ReadFromFile✅ 2026-05-16
M50FlowVar::Vec3=4 (val.v3[3]) + EntityRef=5 (val.entity_ref uint32); SetVarVec3/GetVarVec3/SetVarEntity/GetVarEntity; coerce_to_float✅ 2026-05-16
M51Save v10: SaveHeaderV10Extra (+ws_faction_count+ws_route_count, 108B); FactionState[]+TradeRoute[] tail; WorldSimulation::Reset()+direct array restore✅ 2026-05-16
M52AllianceMatrix singleton (9×9 stance table); AllianceGroup enum; IsEnemy/GetStance/SetStance (symmetric); defaults: Player↔Alien/Android/WeylandYutani hostile✅ 2026-05-16
M53FrameFlagDispatcher: 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
M54NPC 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
M55SenseSystemUpdate(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
M56CombatDispatch(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
M57DeferredLightingSystem: 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
M58DialogCondType::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–2777 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ПризначенняРозмір
0TransformSoA: vec4(x, z, y, rot_y) per slot16 B × 500
1Main cull visible indices4 B × 500
2Main draw indirect command (DrawElementsIndirectCommand)20 B
3Faction IDs per slot4 B × 500
4FinalBones: 500 NPC × 6 кісток × mat4 (~192 KB, FIX-6)500×6×64 B
5AnimNpcState: slot+clip_id+time_s+_pad (ring-buffered upload)16 B × 500
6Shadow visible indices4 B × 500
7Shadow draw indirect command20 B
8AmbientProbe SSBO: SH irradiance coefficients (M27)MAX=64 probes

Texture units

UnitПризначення
0–3Tile atlas 0–3 (u_atlas0..u_atlas3 у tile_map.frag)
5CSM cascade 0 depth map (1024×1024)
6CSM cascade 1 depth map
7CSM cascade 2 depth map

SDL_GPU descriptor sets (SPIRV_COMPILE path)

setbindingШейдерЗміст
00–NComputeRO storage buffers
10–NComputeRW storage buffers
20ComputeUBO
00–NGraphics vertexStorage buffers (contiguous)
10Graphics vertexUBO (VP matrix, sun, time)
20–3Graphics fragmentTexture samplers (atlases)

© Ліцензія

Proprietary Software — All Rights Reserved

Copyright (c) 2024–2026 monkey_dust project author.

Несанкціоноване копіювання, модифікація, розповсюдження або використання суворо заборонено. Повний текст: LICENSE у корені репо.

Сторонні компоненти (дозвільні ліцензії)

БібліотекаЛіцензіяВикористання
SDL3 + SDL_GPUzlibВікно, рендер, input
EnTT 3.16MITECS
Recast/DetourzlibNavMesh
Dear ImGuiMITEditor UI, debug overlay
GLM 1.1.0MITMath (USE_GLM path)
Lua 5.4MITScripting
stb_image / stb_truetypeMIT / Public DomainTextures; MdDraw2D fonts
miniaudioMIT / Public DomainAudio
glad2MITGL function loader (editor)
cgltfMITglTF model loading
Google TestBSD-3Test suite (md_tests)

Субмодуль third_party/flare-game/ — виключно для розробки та тестування, не розповсюджується (GPL-2.0).