Press Jump just before landing and it is frustrating when the press disappears. Walk a tiny distance off a ledge and a short forgiveness window can still let the jump happen. These two timers give the game those choices without hiding the rules inside the input reader.

Include, initialise and call
Reserve JUMPED, COYOTE, BUFFER, GROUNDED, PRESSED as 5 contiguous private bytes in that order. Include the helper once. Call jump_windows_init at the intended reset boundary: it clears every named byte, including input fields. Set the starting values afterwards, then call jump_windows_step for each intended service.
Initialise at the session/reset boundary. Update grounded state from collision, supply the new-press bit, then service once per simulation update. On JUMPED, the caller applies the chosen vertical velocity and updates its movement state. The helper grants a jump; it does not move the player or decide whether a floor contact is real.
GROUNDED and PRESSED are caller inputs; any nonzero value is true, and PRESSED must mean a new press. Each step first decrements positive COYOTE and BUFFER timers, then reloads COYOTE from COYOTE_STEPS when grounded and BUFFER from BUFFER_STEPS on a press. If both timers are positive, it emits JUMPED = 1 and consumes both timers; otherwise JUMPED is zero. Both configured windows are 1–255 service calls. The maintained builds use four and two.
Reusable initialisation and service
; SPDX-License-Identifier: MIT
; Jump Buffering and Coyote Time. Contiguous private bytes: JUMPED, COYOTE, BUFFER, GROUNDED, PRESSED.
; init clears all fields; set inputs afterwards. No I/O, waits or IRQ ownership.
; D0-D3/A0-A1/CCR change; D4-D7/A2-A6 survive.
jump_windows_init:
clr.b JUMPED
clr.b COYOTE
clr.b BUFFER
clr.b GROUNDED
clr.b PRESSED
rts
jump_windows_step:
clr.b JUMPED
tst.b COYOTE
beq.s jump_buffer
subq.b #1,COYOTE
jump_buffer:
tst.b BUFFER
beq.s jump_ground
subq.b #1,BUFFER
jump_ground:
tst.b GROUNDED
beq.s jump_press
move.b #COYOTE_STEPS,COYOTE
jump_press:
tst.b PRESSED
beq.s jump_test
move.b #BUFFER_STEPS,BUFFER
jump_test:
tst.b COYOTE
beq.s jump_done
tst.b BUFFER
beq.s jump_done
move.b #1,JUMPED
clr.b COYOTE
clr.b BUFFER
jump_done:
rts
component_end:State, registers and ownership
Five bytes: JUMPED is a replaced pulse, COYOTE and BUFFER persist, GROUNDED and PRESSED are inputs. A timer of one expires at the start of this call unless its corresponding current input reloads it. A stationary grounded player with no buffered or new press does not jump.
D0–D3/A0–A1/CCR may change; D4–D7/A2–A6 survive. A direct BSR/JSR needs four return-address stack bytes.
The implementation binds fixed symbols and has one mutation owner. Independent users need separate state blocks and scoped/renamed labels, including routine labels; sharing the same bindings means sharing the state. Do not let an interrupt and the main loop mutate the same block without an explicit ownership policy.
The bootable caller takes over Copper/bitplane/interrupt/DMA resources and explicitly disarms all eight sprites. It does not restore Workbench. These are caller presentation choices, not dependencies of the reusable helper.
A caller you can adapt
The buildable program supplies bindings and include order. Its separate client retains state between calls. checked_step calls the public jump_windows_step entry and checks register sentinels; emit_state records diagnostics. Your game can call the public entry directly, remove the diagnostics and consume the documented outputs.
Bindings, buildable caller and include order
section code,code_c
CUSTOM = 0
BROKEN = 0
COYOTE_STEPS = 4
BUFFER_STEPS = 4
TEXT_CAP = 8
PROPERTY_COUNT = 16
STREAM_BYTES = 13
CRC_POLY = 7
include "caller.inc"
include "exercise.inc"
component_start:
include "component.inc"
include "data.inc"
include "cases.inc"
even
include "display.inc"
include "../../framework/turn-deadline/font.inc"
code_end:
STATE:
JUMPED: dc.b 0
COYOTE: dc.b 0
BUFFER: dc.b 0
GROUNDED: dc.b 0
PRESSED: dc.b 0
even
ready: dc.w 0
preview: dcb.b 8,0
OUTPUT: dcb.b 1300,0
even
signature: dc.b "198XPLAYTOOLS00",0
even
copperlist:
dc.w $08e,$2c81,$090,$2cc1,$092,$0038,$094,$00d0
dc.w $100,$1200,$102,0,$104,0,$108,0,$10a,0
dc.w $180,$012,$182,$fff
copbpl:
dc.w $0e0,0,$0e2,0
dc.w $ffff,$fffe
plane: dcb.b 10240,0A separate client retaining state between calls
preview_rules:
ifne CUSTOM
move.b #1,PRESSED
ifne BROKEN
move.b #1,PRESSED
bsr checked_step
else
bsr checked_step
endif
bsr emit_state
move.b #0,PRESSED
ifne BROKEN
move.b #1,PRESSED
bsr checked_step
else
bsr checked_step
endif
bsr emit_state
ifne BROKEN
move.b #1,PRESSED
bsr checked_step
else
bsr checked_step
endif
bsr emit_state
move.b #1,GROUNDED
ifne BROKEN
move.b #1,PRESSED
bsr checked_step
else
bsr checked_step
endif
bsr emit_state
ifne BROKEN
move.b #1,PRESSED
bsr checked_step
else
bsr checked_step
endif
bsr emit_state
else
move.b #1,PRESSED
ifne BROKEN
move.b #1,PRESSED
bsr checked_step
else
bsr checked_step
endif
bsr emit_state
move.b #0,PRESSED
ifne BROKEN
move.b #1,PRESSED
bsr checked_step
else
bsr checked_step
endif
bsr emit_state
ifne BROKEN
move.b #1,PRESSED
bsr checked_step
else
bsr checked_step
endif
bsr emit_state
move.b #1,GROUNDED
ifne BROKEN
move.b #1,PRESSED
bsr checked_step
else
bsr checked_step
endif
bsr emit_state
ifne BROKEN
move.b #1,PRESSED
bsr checked_step
else
bsr checked_step
endif
bsr emit_state
endif
rtsThe display fields are JUMPED, COYOTE, BUFFER, GROUNDED, PRESSED; remaining display fields are zero. Each is one hexadecimal byte. The normal row is 00 04 00 01 00 00 00 00. This is a completed state observation, not a recording of an actual character jumping, a camera scrolling or an asset being displayed.
Make it your own
The custom build shortens both windows from four calls to two. In the supplied trace, a press three calls before landing is accepted with the four-call buffer and has expired with the two-call buffer. The final row alone does not count earlier jump pulses; the complete packet retains every call.
Maintained custom configuration or caller
section code,code_c
CUSTOM = 1
BROKEN = 0
COYOTE_STEPS = 2
BUFFER_STEPS = 2
TEXT_CAP = 4
PROPERTY_COUNT = 16
STREAM_BYTES = 13
CRC_POLY = 49
include "caller.inc"
include "exercise.inc"
component_start:
include "component.inc"
include "data.inc"
include "cases.inc"
even
include "display.inc"
include "../../framework/turn-deadline/font.inc"
code_end:
STATE:
JUMPED: dc.b 0
COYOTE: dc.b 0
BUFFER: dc.b 0
GROUNDED: dc.b 0
PRESSED: dc.b 0
even
ready: dc.w 0
preview: dcb.b 8,0
OUTPUT: dcb.b 1300,0
even
signature: dc.b "198XPLAYTOOLS00",0
even
copperlist:
dc.w $08e,$2c81,$090,$2cc1,$092,$0038,$094,$00d0
dc.w $100,$1200,$102,0,$104,0,$108,0,$10a,0
dc.w $180,$012,$182,$fff
copbpl:
dc.w $0e0,0,$0e2,0
dc.w $ffff,$fffe
plane: dcb.b 10240,0The custom row is 00 02 00 01 00 00 00 00. These records qualify the named builds and inputs; another layout, format or input domain needs explicit bounds and fresh execution evidence.
Common mistake and repair
The wrong client supplies a held level as PRESSED on every call. The permission can be reissued on successive grounded updates even though the player never made another press. Feed a new-press edge and retain the timers; also update the caller’s grounded/movement state when it accepts a jump.
Select the deliberately wrong client
section code,code_c
CUSTOM = 0
BROKEN = 1
COYOTE_STEPS = 4
BUFFER_STEPS = 4
TEXT_CAP = 8
PROPERTY_COUNT = 16
STREAM_BYTES = 13
CRC_POLY = 7
include "caller.inc"
include "exercise.inc"
component_start:
include "component.inc"
include "data.inc"
include "cases.inc"
even
include "display.inc"
include "../../framework/turn-deadline/font.inc"
code_end:
STATE:
JUMPED: dc.b 0
COYOTE: dc.b 0
BUFFER: dc.b 0
GROUNDED: dc.b 0
PRESSED: dc.b 0
even
ready: dc.w 0
preview: dcb.b 8,0
OUTPUT: dcb.b 1300,0
even
signature: dc.b "198XPLAYTOOLS00",0
even
copperlist:
dc.w $08e,$2c81,$090,$2cc1,$092,$0038,$094,$00d0
dc.w $100,$1200,$102,0,$104,0,$108,0,$10a,0
dc.w $180,$012,$182,$fff
copbpl:
dc.w $0e0,0,$0e2,0
dc.w $ffff,$fffe
plane: dcb.b 10240,0
The BROKEN branch is visible in the client. repaired.asm restores the normal selection; its executable matches demo exactly. Spectrum tape filenames may differ while loaded CODE remains identical. The fault is a labelled experiment, not a suggested alternative.
Cost, limits and verification
The measured layout has 136 code bytes, 0 immutable data bytes and 5 private state bytes. Alignment, stack usage, diagnostic tables and display machinery are additional. The extents come from the loaded program’s symbols, not from counting source lines.
Two saturating decrements, input-dependent reloads and a pair of tests per update. There is no busy wait or keyboard scan. The same call-count settings represent different real durations under different service rates; choose a simulation clock before tuning them.
This is a logical permission service, not a complete platformer jump controller. Variable jump height, multiple air jumps, wall jumps and input consumption across menus need explicit additional policies. Freeze or advance the timers during pause deliberately. Respawn invulnerability is a different grace window and remains covered by damage/recovery.
Four native runs use A500 OCS PAL with Kickstart 1.3 and an OFS cold-boot disk; Asm198x 0.0.72 and Emu198x 0.44.2, with Build198x 0.2.9 for disk mastering. Each checks 64 boundary/varied/error cases, dirty initialisation and every state of a separate sequential client against an independent integer model. This is not exhaustive coverage of every possible byte combination. Source/tool/firmware hashes, immutable loaded code/data, native startup/completion, preserved-register sentinels, helper/data sizes, screenshot bytes/pixels and exact repair are checked. Deliberately altered observations must fail. Firmware is hashed, never distributed.