18 KiB
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:
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:
PollInput: queries keyboard state (keyForward,keyBackward,keyStrafeLeft,keyStrafeRight,keyTurnLeft,keyTurnRight,keyAction).UpdatePlayer: applies camera turning, calculates forward/strafe movement vectors, runs wall-sliding collision againstgameMap, checks door interaction.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
spriteListwith depth testing.
- Clears ceiling (
Engine_RenderHUD: draws 2D overlay (title, FPS counter, minimap radar, controls).StretchDIBits: copiesscreenBuffer(320x240 BGRA) to window DC (1280x960).
How to Extend This Engine (Guidelines for AI Agents)
A. How to Add a New Wall Texture
- Open
include/engine_defs.incand define a new cell ID:CELL_MYWALL equ 8 - Open
include/engine_textures.incand generate pixels intotextureBuffer + (8 * TEX_SIZE_BYTES). Each pixel is a 32-bit DWORD in00RRGGBBhformat. - Place
CELL_MYWALLintogameMapininclude/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):
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:
; 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 withand 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 withsar eax, 6. - Multiplying a raw distance by cos/sin:
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):32mapHeight(uint16):32playerStartX(uint16): cell X coordinate (e.g. 2)playerStartY(uint16): cell Y coordinate (e.g. 2)playerStartAngle(uint16): 0..511 anglespriteCount(uint16): count of sprite entriesceilingColor(uint32): 00RRGGBBhfloorColor(uint32): 00RRGGBBhtransitionCount(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 = 1024bytes (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 * 24bytes (SpriteStruct:x,yin raw world units,textureId,scale16.16,dist,flags). - Transition Array:
transitionCount * 42bytes (LevelTransitionin bothengine_defs.incandeditor/src/level_format.h). Target names are ASCII basenames, 5..31 bytes including.dat, with a terminating NUL insidetargetMap[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 * 16bytes; followed bycutsceneStringCount * 96bytes 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 * 65536bytes — slot 0 first, each slot is 128x128 BGRA32. Rebuild the pack from the KAZIK 48-slot atlas (casino3d-hd.bin) or fully procedurally:
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:
-
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.flagswithout altering binary map compatibility (sizeof(SpriteEntry) == 24).
-
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.
- Weapon attack via
-
In-Editor MASM32 IDE (
F7/ "Код / IDE"):- Integrated in-engine code editor (
editor/src/editor_code.inl) accessible viaF7or 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.
- Integrated in-engine code editor (
-
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.
- Project configuration file (