Explosion, smoke and flame recipes
Recipe browser · Structured export · Native checks
The executable's complete recipe-data interval 7014–783E is now accounted
for: 21 referenced entry points, 119 distinct 15-byte emitter records,
48 distinct six-byte sound records, and record terminators. Entry points
share suffixes, so summing their individual record counts would double-count
assets. This interval is not the whole effects system: a separate single-sprite
trail path generates records procedurally, and rendering remains procedural.
The browser now includes a composite preview with orbit, elevation and distance controls, alongside emitter timing, phase thumbnails, motion parameters, sound layers and building bindings. The composite uses the native per-emitter projection and drawing order described below. The table thumbnails remain individual phase samples, not complete emitters.
Recipe format and original constructor
6820–689E consumes a stream pointed to by SI, spawning particles relative to
DX/BX/CX (the two horizontal coordinates and height). The normal entry 680C
loads those coordinates from an object; 6815 applies visibility/admission
checking. 67BF/67EA are map-cell entry paths. The constructor restores its
parent BP and leaves SI pointing at the terminating FF byte.
Each emitter occupies 15 bytes:
| Offset | Size | Meaning / destination |
|---|---|---|
| +0 | word | Start delay; runtime deadline BF8 + delay at object +2C |
| +2,+3 | signed bytes | Horizontal offsets from the supplied origin |
| +4 | signed word | Height offset |
| +6 | word | Initial radius/extent parameter at object +22 |
| +8 | byte | Initial angle high byte; low byte starts at zero, object +24 |
| +9 | word | Sampling word at object +26; low byte is sample count |
| +11 | signed byte | Radius rate at object +2A |
| +12 | signed byte | Height rate at object +2B |
| +13 | signed byte | Angle rate at object +29 |
| +14 | byte | Lifetime/bright flags; bit 40h ends the emitter section |
The constructor clears bit 40h before storing the runtime flags at +7. Remaining low bits specify lifetime in 64-tick units; bit 80h selects the bright phase branch. Expiry occurs only when remaining lifetime is negative: the exact zero point still draws phase 0. Likewise the initial maximum phase may last only the initial tick; do not assign an unconditional 64 ticks to every frame. Future start deadlines suppress both drawing and movement.
Emitter objects come from list 6FA0, whose native pool has 25 records of 46
bytes. The largest recipe has 24 emitters. The native allocator can recycle
objects when a pool is exhausted; overlapping effects and pool pressure are
not simulated in the timeline or constructor comparisons.
Multiple sprites and movement
689F draws each emitter through repeated projected sprite calls. Its sampling
word supplies a count and sine-table stride:
count = sampling_word & 255
stride = ((sampling_word >> 6) | 3) + 1
initial sine byte index = angle_word >> 5
The renderer combines sine/cosine samples with camera-transformed radius vectors, clips and orders projected points, then calls the existing depth-based sprite selector. This is more than a single billboard increasing in size. The composite preview now reproduces this per-emitter projection and traversal with integer arithmetic, checked against original execution. Camera setup, whole-scene ordering and world visibility remain inspection substitutions, as detailed below.
6CD8–6D00 updates an active emitter with the signed low byte of the frame tick
increment:
- Radius word += signed radius rate × signed tick increment, wrapping at 16 bits.
- Angle word += signed angle rate × signed tick increment, wrapping at 16 bits.
- Height advances by signed height rate × tick increment / 8. Object +6 retains the fractional byte, so repeated negative or fractional changes must not be replaced by separately rounded floating-point additions.
The comparison uses whole object records, including the fractional byte, with 1, 16 and 63-tick steps for every distinct emitter. These checks do not establish arbitrary large-frame scheduling behaviour.
Attached sound layers
After the last emitter come six-byte sound records, terminated by a byte FF:
| Offset | Meaning |
|---|---|
| +0 | Duration in 16-tick units |
| +1 | FM patch ID |
| +2..3 | Pitch parameter |
| +4 | Behaviour flags |
| +5 | Level high byte; low byte is initialised to FF |
They allocate into list 6F90, at the recipe's supplied origin. Their deadlines
are initialised during the same spawn call; emitter start delays do not delay
individual sound layers. The constructor uses clock BF8, while sound processing
at 362E checks BF0. Their relationship during pauses/transitions is not
established by these isolated tests. Live admission, Doppler, panning and voice
competition also remain separate.
Patch references link to the existing reference-tone auditions. Those tones are not newly rendered recipe audio and use different audition inputs; the exact recipe pitch, level and duration are preserved in JSON and the table. Combined recipe auditions are now available separately through the browser playback controls.
Caller associations and shared entries
Names in the browser are descriptive associations, not recovered symbols. They preserve pointers and caller addresses for further investigation.
7014: player destruction path through6F4F;7050enters its suffix, omitting the first four emitters while sharing the sound tail.70C3/70F0and716C/7199are additional full/suffix pairs used by collision responses. Shared data does not mean the callers have identical gameplay roles.75D4: the Diffuser's building-marking path (CE74–CE81). Its 24 emitters extend through tick 9926; tick 9927 is after the last lifetime. At the inspection rate of approximately 500 Hz, the sequence lasts nearly 20 seconds. Early bright emitters give way to numerous ordinary smoke emitters; the last starts at tick 9350 with a 576-tick lifetime.75A3: Diffuser unsuccessful-hit fallback;77C5: ground-vehicle route effect selected at9028.- The remaining entries are associated with object hit/removal/damage paths or the map-effect directory. Their precise visible identity in every mission is not inferred merely from their position in the executable.
Building-to-effect mapping
67BF advances map state by 20h, then reads byte +5 of the eight-byte city
type record and indexes the seven-word directory at 783F. The export maps
all 391 types across banks 30–32 to a recipe and links their model views.
Seven native tests verify directory selection and the state increment with
visibility admitted; this is not a test of all destruction eligibility rules.
| Effect index | Recipe |
|---|---|
| 0 | 7199 |
| 1 | 7749 |
| 2 | 7518 |
| 3 | 747E |
| 4 | 73C6 |
| 5 | 721C |
| 6 | 77FF |
For Delphi, type 11 streetlights select effect 5, type 76 skylight roofs select 1, and type 80 factory chimneys select 2. Underground types 111/112, the doors previously identified on the Wrecker route, select effect 6. This links those objects to their effect recipes; it does not establish that ordinary player collisions can destroy every one of them.
Separate damage-trail emission
79C2–79E4 can call 676E while updating objects. It reads the high byte of the
damage accumulator at +64/+65, skips object flags 18h, and gates emission with
bits from C50. Values below 80h produce no trail through this branch. Values
80h–9Fh use several cadence masks and the minimum lifetime; A0h upward uses
mask 40h and a clamped severity value 0–63. These masks are not labelled as
emission rates in hertz without tracing their clock producer.
676E emits one sprite into the distinct 6FA8 pool, at saved object position
+38/+3A/+3C with random horizontal offsets −16..15 and vertical offsets
−128..124 in steps of four. Flags are 88h + (severity >> 3), giving lifetimes
512..960 ticks. At the low end, the bright flag never reaches the bright phase
range: its starting phase is only 8. Higher severity can expose the fire phases.
These are stationary records after spawning, unlike the moving/expanding
recipe emitters. Their placement behind a moving object creates a trail.
There are 192 native emission checks using three supplied random words and all
64 severity values. Random-number generation and visibility are substituted;
allocation, offset arithmetic, flags and timestamp storage execute natively.
The impact path 6730/6742 also uses the single-sprite constructor 6795 and
adds sound. Its input-dependent impact classification remains to be expanded
into separately named recipes.
Verification and reproduction
- 42 constructor cases: all 21 entries at ordinary and wrapping clock/world coordinates, using the original pool initialiser and allocator. Checks cover every decoded field, sound record, parent BP restoration and stream boundary.
- 357 movement cases: three frame increments for each of 119 unique emitters.
- 152 aging cases: every stored flag value, waiting/initial/phase-boundary/ last-live/expired states, including clock wrap.
- 192 trail cases and 7 map-directory cases, with substitutions stated above.
- Coverage assertion accounts for all 2091 bytes in
7014–783E. - Browser checks compare its phase decisions with the native results, visit all 21 entries, load their sprites and exercise playback/reset.
PYTHONPATH=/tmp/darker-python python3 tools/verify_effect_projection.py
PYTHONPATH=/tmp/darker-python python3 tools/extract_effect_recipes.py
NODE_PATH=/tmp/riotfire-browser/node_modules node tools/verify_effect_recipes.js
Original DOS files are untouched. The exported recipes are evidence for the faithful reconstruction; the timeline is an inspection tool, not the start of an engine implementation.
Composite preview and native projection comparison
The browser's projection translation is checked
against 1091 native executions of 689F: all 119 emitter definitions at
initial, middle and final live ages under three supplied camera matrices, plus
20 cases exercising near-plane rejection, small sample counts and far depth.
The captured calls
retain the synthetic inputs and the exact ordered (screen X, screen Y, depth)
arguments observed at 6AA6. Browser verification compares every result,
including order and duplicate calls, rather than comparing just a bounding box.
The implementation preserves:
- Signed radius multiplication and high-word arithmetic before the additional two-bit shift used to build the projected radius coefficients.
- Separate 24-bit camera-space coordinates with eight fractional bits. Sine and cosine contributions are truncated independently before addition, including their carries and signed wraparound.
- Rejection below native integer depth 32, signed division truncating toward zero, and screen offsets (160,84) for the inspection viewport.
- The original two-direction stack traversal around the ring. It begins at the farthest accepted sample and walks the two arcs toward the nearest. Ties keep the first selected extremum. In some cases it draws the nearest sample twice; this redundant call is retained, not simplified away.
- Depth thresholds 1752, 2454, 3510, 4914 and 8190, selecting the extracted 16/12/8/6/4-pixel sprites, then the phase-specific single-pixel colour.
- The even-X destination alignment and corresponding source alignment variant. Canvas clipping handles the inspection viewport edges without scaling sprites.
The composite uses these original-order calls to place the existing opaque, masked PNGs. It does not substitute alpha-faded particles or interpolated sizes. Changing distance therefore visibly changes size in the same discrete steps as the native selector. CSS enlarges the finished 320×168 canvas with nearest-neighbour sampling; it does not change the internal projection resolution.
Inspection scene limits
The per-emitter comparison starts with a supplied camera-space centre and camera coefficients. The browser's orbit/elevation controls construct a synthetic camera and quantise those inputs; this does not verify the full original world-to-view setup. The inspection camera aims at a fixed height of 100 raw height units, uses horizontal scale 2 and height scale 1/4, and exposes depth independently. These are explicit scene setup choices, not recovered player camera behaviour.
Between emitters, the browser draws by centre depth (ties by descending record address). The game's complete render-list construction uses a different spatial metric and also interleaves world objects; that is not reproduced here. Within each emitter, the captured native ordering is retained exactly. City occlusion, visibility admission and pool recycling are absent.
Motion is evaluated from ideal elapsed ticks rather than running the original frame scheduler. The previously verified rates and wrapping radius/angle words are retained; first-update timing and delayed-start frame overshoot remain outside the comparison. Sound layers now have synchronized combined auditions, with nonspatial voice assignment and a 500 Hz clock assumption. The separate single-sprite damage trails are not emitted by this recipe-only preview.
Browser checks exercise all 21 recipes, compare all native projection fixtures and existing phase fixtures, test playback/reset and the camera controls, and retain composite screenshots beside the JSON. No original DOS files are changed.