# AGENTS.md - EBLAN ENGINE 3D ## Project Identity **EBLAN ENGINE 3D** is a lightweight, modular, pure x86 assembly (MASM32) 2.5D retro raycasting game engine with software rendering to a 320x240 framebuffer stretched to a 1280x960 Windows GDI window. - **Language**: Pure x86 Assembly (MASM32 / MSVC `ml.exe`). - **Dependencies**: Windows API only (`kernel32.lib`, `user32.lib`, `gdi32.lib`, `winmm.lib`). No external DLLs, DirectX, OpenGL, or C runtimes. - **Rendering**: Software DIBits raycaster (Wolfenstein 3D style DDA grid stepping, texture mapping, distance shading, billboard sprites, 2D UI canvas with 8x8 bitmap font). - **Coordinate Space**: Player/entity positions are raw world units (64 per cell). Trigonometry and sprite scale use 16.16; DDA distances retain 8 fractional bits (24.8) until projection. Trigonometry uses 512-entry lookup tables. --- ## Directory Structure ``` EBLAN_ENGINE/ │ ├── main.asm # Entry point, Win32 window, message loop, 60 FPS frame ticker ├── build.bat # 1-click build script (supports MASM32 and MSVC BuildTools) ├── engine.exe # Compiled game executable │ ├── include/ │ ├── engine_defs.inc # Screen resolution, colors, key codes, constants, structs │ ├── engine_data.inc # Framebuffer, depthbuffer, player state, map array, textures │ ├── engine_math.inc # FPU Sin/Cos table generator, RNG, fixed-point math │ ├── engine_map.inc # Grid map accessors, wall collision queries, door interaction │ ├── engine_textures.inc # Procedural textures + external texturepack.bin loader │ ├── engine_input.inc # Keyboard polling (WASD, arrows, actions) │ ├── engine_physics.inc # Player movement vector, strafe, wall-sliding collision │ ├── engine_raycaster.inc # 320-ray DDA raycasting, fog, textured sorted sprites │ ├── engine_render2d.inc # Pixel, Rect, Outline, 8x8 Font (CP1251), Minimap radar │ └── font8x8.inc # 8x8 font bitmap glyphs (256 chars, CP1251) │ ├── tools/ │ └── make_texturepack.py # Builds assets/textures/texturepack.bin (from KAZIK pack or procedural) ├── assets/ # map.dat (level), textures/texturepack.bin (optional texture override) └── docs/ └── ENGINE_ARCHITECTURE.md # Detailed deep-dive into engine math and rendering pipeline ``` --- ## Build System Run `build.bat` from command line or IDE: ```cmd build.bat ``` - **Assembler**: `ml.exe /c /coff /Cp /nologo /I. /Iinclude main.asm` - **Linker**: `link.exe /NOLOGO /SUBSYSTEM:WINDOWS /OUT:engine.exe main.obj kernel32.lib winmm.lib user32.lib gdi32.lib` --- ## Core Engine Architecture ### 1. Frame Lifecycle Each tick (~60 times per second), `main.asm` executes: 1. `PollInput`: queries keyboard state (`keyForward`, `keyBackward`, `keyStrafeLeft`, `keyStrafeRight`, `keyTurnLeft`, `keyTurnRight`, `keyAction`). 2. `UpdatePlayer`: applies camera turning, calculates forward/strafe movement vectors, runs wall-sliding collision against `gameMap`, checks door interaction. 3. `Render3DScene`: - Clears ceiling (`COLOR_CEILING`) and floor (`COLOR_FLOOR`). - Casts 320 rays across FOV (~67.5°). - Traverses grid using DDA until a solid cell is hit. - Computes perpendicular wall distance (fish-eye corrected). - Samples 128x128 texture stripe, applies side shading (Y-walls darkened for 3D depth) and distance fog. - Populates `depthBuffer[screenX]`. - Renders billboard sprites from `spriteList` with depth testing. 4. `Engine_RenderHUD`: draws 2D overlay (title, FPS counter, minimap radar, controls). 5. `StretchDIBits`: copies `screenBuffer` (320x240 BGRA) to window DC (1280x960). --- ## How to Extend This Engine (Guidelines for AI Agents) ### A. How to Add a New Wall Texture 1. Open `include/engine_defs.inc` and define a new cell ID: ```masm CELL_MYWALL equ 8 ``` 2. Open `include/engine_textures.inc` and generate pixels into `textureBuffer + (8 * TEX_SIZE_BYTES)`. Each pixel is a 32-bit DWORD in `00RRGGBBh` format. 3. Place `CELL_MYWALL` into `gameMap` in `include/engine_map.inc`. ### B. How to Add a New Sprite / Entity (e.g. Enemy, Coin, Prop) Sprite positions use **raw world units** (1 cell = 64 units), the same scale as `playerX`/`playerY`. In your initialization code (e.g. `Engine_Init`): ```masm mov eax, spriteCount cmp eax, MAX_SPRITES jge @F mov ebx, eax imul ebx, sizeof SpriteStruct add ebx, OFFSET spriteList mov [ebx].SpriteStruct.x, (15 * 64 + 32) ; X in raw world units (cell 15.5) mov [ebx].SpriteStruct.y, (10 * 64 + 32) ; Y in raw world units (cell 10.5) mov [ebx].SpriteStruct.textureId, 11 ; texture slot (11 = barrel) mov [ebx].SpriteStruct.scale, 65536 ; 1.0 scale (16.16 fixed) inc spriteCount @@: ``` Sprites are rendered as textured billboards: back-to-front sorted by distance, per-column depth-tested against `depthBuffer`, floor-anchored, with distance fog. Texel value `00FF00FFh` (magenta, `COLOR_CHROMA_KEY`) is transparent. `textureId` 0 samples slot 0 (empty/black) — use it as a debug marker. `scale` multiplies the projected size (65536 = 1.0, 131072 = 2.0). ### C. How to Draw Custom 2D HUD (Health Bar, Text, Dialogs) Inside `Engine_RenderHUD` in `main.asm`: ```masm ; Draw Health Bar background (Red) invoke DrawRect, 10, 40, 100, 10, 00000088h ; Draw Health Bar fill (Green) invoke DrawRect, 10, 40, playerHealth, 10, 0000FF00h ; Draw Outline invoke DrawRectOutline, 10, 40, 100, 10, COLOR_WHITE ; Print String invoke DrawString8x8, 10, 55, OFFSET myMessageStr, COLOR_YELLOW ; Print Number invoke DrawNumber8x8, 120, 42, playerHealth, COLOR_WHITE ``` ### D. Math Rules - All angles are integers in range `0..511` (`ANGLE_512`). Always mask with `and eax, ANGLE_MASK` (511). - Trigonometry: `[tableCos + eax*4]` and `[tableSin + eax*4]` return 16.16 signed fixed point (65536 = 1.0). - Player and sprite positions (`playerX`, `playerY`, `SpriteStruct.x/y`) are **raw world units**: 1 cell = 64 units. Convert to cell index with `sar eax, 6`. - Multiplying a raw distance by cos/sin: ```masm mov eax, playerAngle and eax, ANGLE_MASK mov ebx, [tableCos + eax*4] imul ebx, distance sar ebx, 16 ; result in ebx (raw units) ``` ### E. Binary Level Format (`assets/map.dat`) & EblanStudio The engine supports both procedural default generation and binary level loading. When `assets/map.dat` exists, `LoadMapBinary` in `include/engine_map.inc` automatically loads it: - **Header (`MapHeader`, 38 bytes, packed)**: - `magic` (uint32): `0x4E4C4245` ("EBLN") - `version` (uint16): `4` (readers also accept v1 fixed sprite coordinates and v2/v3 raw-coordinate maps, including early v2 maps with transitions) - `mapWidth` (uint16): `32` - `mapHeight` (uint16): `32` - `playerStartX` (uint16): cell X coordinate (e.g. 2) - `playerStartY` (uint16): cell Y coordinate (e.g. 2) - `playerStartAngle` (uint16): 0..511 angle - `spriteCount` (uint16): count of sprite entries - `ceilingColor` (uint32): 00RRGGBBh - `floorColor` (uint32): 00RRGGBBh - `transitionCount` (uint16): 0..16 (v1 ignores this formerly reserved field) - `cutsceneCount` (uint16): 0..128 commands (offset 28, v4 only) - `cutsceneStringCount` (uint16): 0..32 strings (offset 30, v4 only) - `cutsceneFlags` (uint16): bit 0 = autoplay on room entry (offset 32, v4 only) - `reserved[4]` (bytes): reserved padding - **Grid Data**: `32 * 32 = 1024` bytes (row-major order). Cell value = texture slot (1 brick, 2 wood, 3 stone, 4 metal, 5 door, 7 pillar, 8 glass, 9 marble). - **Sprite Array**: `spriteCount * 24` bytes (`SpriteStruct`: `x`, `y` in raw world units, `textureId`, `scale` 16.16, `dist`, `flags`). - **Transition Array**: `transitionCount * 42` bytes (`LevelTransition` in both `engine_defs.inc` and `editor/src/level_format.h`). Target names are ASCII basenames, 5..31 bytes including `.dat`, with a terminating NUL inside `targetMap[32]`. Put linked maps in the same assets directory. Triggers must be unique and occupy empty, open-door or portal cells; every portal cell needs a transition. - Files are validated completely before replacing the active room. Counts above capacity, truncated/extra payload, invalid coordinates/scales, blocked spawns and invalid transition names are rejected. Black ceiling/floor colors are valid. A failed runtime transition keeps the current room; a failed startup load creates the procedural map. - **Cutscene Array (v4)**: after transitions, `cutsceneCount * 16` bytes; followed by `cutsceneStringCount * 96` bytes of NUL-terminated CP1251 strings. No serialized pointers. Earlier versions ignore the new header fields and load an empty track. - New saves use v4 without rewriting existing map files until explicitly saved. Sprite positions must remain inside the map; scales accepted by loaders are 0..16.0 (0 keeps the legacy 1.0 behavior). Human users can design levels visually using `run_editor.bat` (`editor/editor.exe`, written in C++17 + ImGui + DirectX 11), with 1-click Playtest (F5). AI agents can directly read or write `assets/map.dat` following this specification. F5 copies neighboring maps and resources into a dedicated temporary directory, writes the current unsaved map there under its original filename, validates all reachable destinations, and launches `engine.exe "assets\selected-map.dat"`. It never replaces the source `assets/map.dat` or changes the editor's current file path. Starting the engine without an argument still loads `assets/map.dat`. ### F. Texture Pack (`assets/textures/texturepack.bin`) At startup the engine always generates procedural textures first, then `LoadTexturePack` tries to override them from `assets/textures/texturepack.bin`: - **Header (8 bytes)**: `magic` (uint32) = `0x58544245` ("EBTX"), `version` (uint16) = 1, `slotCount` (uint16) ≤ 16. - **Pixels**: `slotCount * 65536` bytes — slot 0 first, each slot is 128x128 BGRA32. Rebuild the pack from the KAZIK 48-slot atlas (`casino3d-hd.bin`) or fully procedurally: ```cmd python tools\make_texturepack.py ``` If the pack is missing or invalid, the engine silently keeps procedural textures. The loader stages and validates the complete pack before replacing pixels. Slots 10..15 are sprite slots; `CELL_PORTAL` uses a separate `portalTextureBuffer`, so importing slot 10 never changes portals. First import creates missing directories and seeds untouched slots with procedural defaults. Import/reset writes are atomic; an invalid existing pack is preserved and reported to the user. ### G. Regression Checks Run `python tools\test_regressions.py` on Windows with the build toolchains installed. The scenarios compile the real editor logic and ASM procedures, exercise file I/O, playtest isolation, transitions, keyboard edges, renderer geometry, cutscene validation and editor undo in TEMP, and verify that project assets remain unchanged. C++ preview and ASM camera/angle/fade/subtitle states are compared frame by frame. Use `--keep` to retain generated framebuffers and fixtures. These checks do not replace a manual UI playtest. ### H. Cutscenes (v4) `include/engine_cutscene.inc` owns validation, playback and overlays. `CutsceneEvent` is packed: `uint16 tickTimestamp`, `uint8 opCode`, `uint8 param8`, `int32 paramA`, `int32 paramB`, `int32 dataId`. Time uses 60 Hz ticks, timestamps 0..65535, individual durations 0..3600 ticks. Commands are sorted by timestamp; a nonempty track ends with a single final END. | Opcode | paramA | paramB | dataId / param8 | |---|---|---|---| | 0 END | ignored | ignored | Returns control, retains camera pose | | 1 WAIT | duration | ignored | Delays dispatch; existing tweens continue | | 2 MOVE_CAM_TO | raw world X | raw world Y | dataId = duration (0 snaps) | | 3 PAN_CAM_TO | target angle 0..511 | duration | Shortest arc | | 4 SHOW_SUBTITLE | string index | duration (0 persistent) | CP1251, up to 95 characters | | 5 HIDE_SUBTITLE | ignored | ignored | Clears subtitle | | 6 PLAY_SOUND | string index, or -1 to stop | ignored | param8 bit 0 = loop | | 7 FADE_SCREEN | darkness 0..16 | duration | 0 clear, 16 black | | 8 CHANGE_SUBLEVEL | transition index | ignored | Uses existing transition destination/spawn/fade | MOVE and PAN interpolate linearly using signed integer math. Camera endpoints must be walkable; movement segments ignore collisions. Each tick advances existing tweens, expires text and waits, then dispatches due commands in stored order. Same-timestamp commands can start independent movement, pan and fade together. WAIT delays overdue commands without shifting their timestamps. A replacement tween starts from the current pose. Autoplay begins after a room transition finishes. C starts/replays the current map's track; Tab skips it. Movement/actions are blocked during playback. End/skip/level changes stop cinematic sound, clear subtitles and fade, and return control at the current camera pose. Letterbox is 2.35:1, subtitles use up to 3 centered rows of 38 glyphs over a half-brightness panel. WAV names are ASCII basenames of up to 63 characters in `assets/sounds`; playback uses one asynchronous PlaySound channel. F6 opens EblanStudio's timeline. Author commands with buttons, drag diamonds to change timestamps, select overlapping events through the command list, and place MOVE points by clicking the map. The inspector edits parameters and strings. Autoplay, scene data and strings participate in save/load and undo/redo. Preview shows the actual game perspective in the embedded viewport with the camera path on the map; F5 runs the full game, validates referenced WAV files and stages them with linked maps. Editor preview stops at a level-change command. F5/F6 are handled before ImGui keyboard-capture checks. `examples/cutscene_demo/run_demo.bat` runs an isolated two-room example with camera waypoints, Russian subtitles, WAV, fades and a scripted transition. Regenerate it with `python tools/make_cutscene_demo.py`. ### I. Embedded Game View The x64 editor uses a windowless x86 `engine.exe --preview:Local\EblanPreview-...` process to execute the actual `Render3DScene`, cinematic overlay and fade procedures. No duplicate raycaster. `editor/src/preview_protocol.h` and `include/engine_preview.inc` define a versioned 1,357,792-byte shared-memory mailbox: 128-byte header, 1024 cells, 32 sprites, a 96-byte CP1251 subtitle, 16 texture slots and a 320x240 DIB frame. Header state is 0 idle, 1 engine owns payload, 2 editor owns result, 3 shutdown. Publish with InterlockedExchange/xchg; never modify payload while state is 1. All fields are fixed-width; do not put pointers or size_t into the wire layout. The renderer validates camera and sprite bounds, uploads textures only when their revision changes, and exits if its parent process ends. The editor has process-exit/timeout checks and a retry button, converts DIB padding to opaque alpha, uploads BGRA to DX11 and displays with point sampling at 4:3. Map/inspector changes appear without saving. Timeline playback/scrubbing drives the viewport camera, subtitles and fade. Free-camera inspection affects no authored data. The viewport shows the scene and cinematic overlays; normal game HUD, door interactions and room switching are tested in F5. `tools/test_preview_ipc.py` exercises the actual renderer without a visible window. The full regression suite also checks the x86-to-x64-to-DX11 path on a WARP device and compares the live viewport's pixels with a direct ASM rendering. Build engine.exe before running these tests. ### J. Game Studio IDE, Entity Archetypes & Combat Subsystems EblanStudio has evolved from a pure level editor into a **Full Game Engine Studio / IDE for retro x86 Assembly (MASM32) games**: 1. **Entity Archetypes & AI (`include/engine_entity.inc`)**: - `0: Prop`: static decoration / obstacles. - `1: Guard AI`: patrolling enemy guard that detects the player within an aggro radius, chases, and deals melee damage. - `2: Medkit`: collectible healing item (+25 HP up to 100). - `3: Ammo Box`: collectible ammunition pickup (+20 rounds up to 100). - `4: Key`: key card for unlocking locked doors. - `5: Switch`: interactive trigger/lever toggled by proximity action ([E]). - Packed into `SpriteStruct.flags` without altering binary map compatibility (`sizeof(SpriteEntry) == 24`). 2. **Player Combat & HUD (`include/engine_combat.inc`)**: - Weapon attack via `Space`, `Ctrl`, or Left Mouse Button (`VK_LBUTTON`). - Center crosshair and hitscan raycasting against active enemy guards. - Animated retro pistol with muzzle flash, recoil, and cooldowns. - Damage screen flash (red overlay), death state screen, and instant level restart with `[R]`. - Comprehensive combat HUD: Health bar, Ammo counter, Score counter, and Key indicator. 3. **In-Editor MASM32 IDE (`F7` / "Код / IDE")**: - Integrated in-engine code editor (`editor/src/editor_code.inl`) accessible via `F7` or the main menu. - MASM syntax highlighting (instructions, registers, directives, comments, strings, numbers). - Project file navigator (`engine_entity.inc`, `engine_combat.inc`, `main.asm`, etc.). - Script templates (Custom Guard AI, Mega-Medkit, Secret Door Trigger, Custom HUD). - Asynchronous 1-click compiler runner (`build.bat` / `ml.exe`) with interactive output console and clickable error lines. 4. **Project Settings & Standalone Game Exporter (`editor/src/project_manager.h`)**: - Project configuration file (`project.eblanproj`) defining title, author, version, window title, and startup map. - "Экспорт автономной игры (.exe)" builds a standalone release folder containing the renamed game executable, packaged assets (maps, textures, sounds), launcher batch file, and readme.