Skip to content

A Counted Text-entry Buffer on Spectrum

Entering a player name needs more than appending characters.

Entering a player name needs more than appending characters. Full buffers must refuse another byte, Backspace at the start must stay at zero, and the caller must know how many characters are real.

Spectrum shows the working diagnostic row 00 00 42 01 00 00 00 00.
A native completed caller shows its state as hexadecimal bytes. The field order below explains the row; the full packet retains every call.

Include, initialise and call

Reserve LENGTH, STATUS, KEY, MODE, BUF0, BUF1, BUF2, BUF3, BUF4, BUF5, BUF6, BUF7 as 12 contiguous private bytes in that order. Include the helper once. Call text_entry_buffer_init at the intended reset boundary: it clears every named byte, including input fields. Set the starting values afterwards, then call text_entry_buffer_step for each intended service.

Initialise once, translate the machine’s input into a logical key/action, then call step. Draw exactly LENGTH bytes. This is a counted buffer, not a promise of a zero terminator when full. The helper does not scan a keyboard, wait for input or implement key repeat.

The buffer reserves BUF0–BUF7. LENGTH is the number of live bytes. MODE 0 appends KEY only when it is ASCII 32–126 and room remains; MODE 1 removes the last byte and clears that slot; MODE 2 clears all eight slots and length, even if the old length is corrupt. STATUS is 1 for a change/clear, 0 for full append or empty Backspace, 2 for a rejected character, and 3 for an invalid mode/length. Apart from clear, LENGTH above TEXT_CAP rejects without touching the buffer. The maintained capacities are eight and four.

Reusable initialisation and service

component.inc
; SPDX-License-Identifier: MIT
; A Counted Text-entry Buffer. Contiguous private bytes: LENGTH, STATUS, KEY, MODE, BUF0, BUF1, BUF2, BUF3, BUF4, BUF5, BUF6, BUF7.
; init clears all fields; set inputs afterwards. No I/O, waits or IRQ ownership.
; AF/BC/DE/HL change; IX/IY survive.
text_entry_buffer_init:
    xor a
    ld (LENGTH),a
    ld (STATUS),a
    ld (KEY),a
    ld (MODE),a
    ld (BUF0),a
    ld (BUF1),a
    ld (BUF2),a
    ld (BUF3),a
    ld (BUF4),a
    ld (BUF5),a
    ld (BUF6),a
    ld (BUF7),a
    ret
text_entry_buffer_step:
    xor a
    ld (STATUS),a
    ld a,(MODE)
    cp 2
    jr z,text_clear
    ld a,(LENGTH)
    cp TEXT_CAP+1
    jr nc,text_bad
    ld a,(MODE)
    or a
    jr z,text_append
    cp 1
    jr nz,text_bad
    ld a,(LENGTH)
    or a
    ret z
    dec a
    ld (LENGTH),a
    ld e,a
    ld d,0
    ld hl,BUF0
    add hl,de
    ld (hl),0
    jr text_changed
text_append:
    ld a,(KEY)
    cp 32
    jr c,text_bad_key
    cp 127
    jr nc,text_bad_key
    ld b,a
    ld a,(LENGTH)
    cp TEXT_CAP
    ret nc
    ld e,a
    ld d,0
    ld hl,BUF0
    add hl,de
    ld (hl),b
    ld a,(LENGTH)
    inc a
    ld (LENGTH),a
text_changed:
    ld a,1
    ld (STATUS),a
    ret
text_bad_key:
    ld a,2
    ld (STATUS),a
    ret
text_bad:
    ld a,3
    ld (STATUS),a
    ret
text_clear:
    ld hl,BUF0
    ld b,8
    xor a
text_clear_loop:
    ld (hl),a
    inc hl
    djnz text_clear_loop
    ld (LENGTH),a
    jr text_changed
component_end:

State, registers and ownership

Four control/input/output bytes followed by eight reserved character bytes. KEY and MODE survive. Appending preserves inactive slots; Backspace clears the removed slot, and clear empties all eight. The custom smaller capacity retains eight-byte storage and changes accepted length, not the allocation.

AF/BC/DE/HL may change; IX/IY survive. A direct CALL needs two 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 standalone tape program enters at $8000, disables interrupts and owns bitmap/attribute RAM. It does not restore BASIC. Those choices belong to the test program; the helper itself never takes over the display.

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 text_entry_buffer_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

demo.asm
    org $8000
CUSTOM equ 0
BROKEN equ 0
COYOTE_STEPS equ 4
BUFFER_STEPS equ 4
TEXT_CAP equ 8
PROPERTY_COUNT equ 16
STREAM_BYTES equ 13
CRC_POLY equ 7
    include "caller.inc"
    include "exercise.inc"
component_start:
    include "component.inc"
    include "data.inc"
    include "cases.inc"
    include "../../physics/rectangle-overlap/display.inc"
code_end:
STATE:
LENGTH: defb 0
STATUS: defb 0
KEY: defb 0
MODE: defb 0
BUF0: defb 0
BUF1: defb 0
BUF2: defb 0
BUF3: defb 0
BUF4: defb 0
BUF5: defb 0
BUF6: defb 0
BUF7: defb 0
input_ptr: defw 0
output_ptr: defw 0
cases_left: defb 0
ready: defb 0
preview: defs 8,0
OUTPUT: defs 1300,0
    end start

A separate client retaining state between calls

exercise.inc, excerpt
preview_rules:
    if CUSTOM
    ld a,0
    ld (MODE),a
    ld a,65
    ld (KEY),a
    if BROKEN
    ld a,(MODE)
    cp 1
    jr nz,bad_text_regular_0_custom
    ld a,(LENGTH)
    or a
    jr nz,bad_text_regular_0_custom
    dec a
    ld (LENGTH),a
    ld a,1
    ld (STATUS),a
    jr bad_text_done_0_custom
bad_text_regular_0_custom:
    call checked_step
bad_text_done_0_custom:
    else
    call checked_step
    endif
    call emit_state
    ld a,66
    ld (KEY),a
    if BROKEN
    ld a,(MODE)
    cp 1
    jr nz,bad_text_regular_1_custom
    ld a,(LENGTH)
    or a
    jr nz,bad_text_regular_1_custom
    dec a
    ld (LENGTH),a
    ld a,1
    ld (STATUS),a
    jr bad_text_done_1_custom
bad_text_regular_1_custom:
    call checked_step
bad_text_done_1_custom:
    else
    call checked_step
    endif
    call emit_state
    ld a,1
    ld (MODE),a
    if BROKEN
    ld a,(MODE)
    cp 1
    jr nz,bad_text_regular_2_custom
    ld a,(LENGTH)
    or a
    jr nz,bad_text_regular_2_custom
    dec a
    ld (LENGTH),a
    ld a,1
    ld (STATUS),a
    jr bad_text_done_2_custom
bad_text_regular_2_custom:
    call checked_step
bad_text_done_2_custom:
    else
    call checked_step
    endif
    call emit_state
    if BROKEN
    ld a,(MODE)
    cp 1
    jr nz,bad_text_regular_3_custom
    ld a,(LENGTH)
    or a
    jr nz,bad_text_regular_3_custom
    dec a
    ld (LENGTH),a
    ld a,1
    ld (STATUS),a
    jr bad_text_done_3_custom
bad_text_regular_3_custom:
    call checked_step
bad_text_done_3_custom:
    else
    call checked_step
    endif
    call emit_state
    if BROKEN
    ld a,(MODE)
    cp 1
    jr nz,bad_text_regular_4_custom
    ld a,(LENGTH)
    or a
    jr nz,bad_text_regular_4_custom
    dec a
    ld (LENGTH),a
    ld a,1
    ld (STATUS),a
    jr bad_text_done_4_custom
bad_text_regular_4_custom:
    call checked_step
bad_text_done_4_custom:
    else
    call checked_step
    endif
    call emit_state
    ld a,0
    ld (MODE),a
    ld a,67
    ld (KEY),a
    if BROKEN
    ld a,(MODE)
    cp 1
    jr nz,bad_text_regular_5_custom
    ld a,(LENGTH)
    or a
    jr nz,bad_text_regular_5_custom
    dec a
    ld (LENGTH),a
    ld a,1
    ld (STATUS),a
    jr bad_text_done_5_custom
bad_text_regular_5_custom:
    call checked_step
bad_text_done_5_custom:
    else
    call checked_step
    endif
    call emit_state
    else
    ld a,0
    ld (MODE),a
    ld a,65
    ld (KEY),a
    if BROKEN
    ld a,(MODE)
    cp 1
    jr nz,bad_text_regular_0
    ld a,(LENGTH)
    or a
    jr nz,bad_text_regular_0
    dec a
    ld (LENGTH),a
    ld a,1
    ld (STATUS),a
    jr bad_text_done_0
bad_text_regular_0:
    call checked_step
bad_text_done_0:
    else
    call checked_step
    endif
    call emit_state
    ld a,66
    ld (KEY),a
    if BROKEN
    ld a,(MODE)
    cp 1
    jr nz,bad_text_regular_1
    ld a,(LENGTH)
    or a
    jr nz,bad_text_regular_1
    dec a
    ld (LENGTH),a
    ld a,1
    ld (STATUS),a
    jr bad_text_done_1
bad_text_regular_1:
    call checked_step
bad_text_done_1:
    else
    call checked_step
    endif
    call emit_state
    ld a,1
    ld (MODE),a
    if BROKEN
    ld a,(MODE)
    cp 1
    jr nz,bad_text_regular_2
    ld a,(LENGTH)
    or a
    jr nz,bad_text_regular_2
    dec a
    ld (LENGTH),a
    ld a,1
    ld (STATUS),a
    jr bad_text_done_2
bad_text_regular_2:
    call checked_step
bad_text_done_2:
    else
    call checked_step
    endif
    call emit_state
    if BROKEN
    ld a,(MODE)
    cp 1
    jr nz,bad_text_regular_3
    ld a,(LENGTH)
    or a
    jr nz,bad_text_regular_3
    dec a
    ld (LENGTH),a
    ld a,1
    ld (STATUS),a
    jr bad_text_done_3
bad_text_regular_3:
    call checked_step
bad_text_done_3:
    else
    call checked_step
    endif
    call emit_state
    if BROKEN
    ld a,(MODE)
    cp 1
    jr nz,bad_text_regular_4
    ld a,(LENGTH)
    or a
    jr nz,bad_text_regular_4
    dec a
    ld (LENGTH),a
    ld a,1
    ld (STATUS),a
    jr bad_text_done_4
bad_text_regular_4:
    call checked_step
bad_text_done_4:
    else
    call checked_step
    endif
    call emit_state
    endif
    ret

The display fields are LENGTH, STATUS, KEY, MODE, BUF0, BUF1, BUF2, BUF3; later state bytes are retained in the full packet. Each is one hexadecimal byte. The normal row is 00 00 42 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 lowers TEXT_CAP from eight to four and its client appends C after clearing the two-character example with Backspace. It finishes with the one-character name C. Boundary cases also exercise full/invalid lengths under the smaller capacity.

Maintained custom configuration or caller

custom.asm
    org $8000
CUSTOM equ 1
BROKEN equ 0
COYOTE_STEPS equ 2
BUFFER_STEPS equ 2
TEXT_CAP equ 4
PROPERTY_COUNT equ 16
STREAM_BYTES equ 13
CRC_POLY equ 49
    include "caller.inc"
    include "exercise.inc"
component_start:
    include "component.inc"
    include "data.inc"
    include "cases.inc"
    include "../../physics/rectangle-overlap/display.inc"
code_end:
STATE:
LENGTH: defb 0
STATUS: defb 0
KEY: defb 0
MODE: defb 0
BUF0: defb 0
BUF1: defb 0
BUF2: defb 0
BUF3: defb 0
BUF4: defb 0
BUF5: defb 0
BUF6: defb 0
BUF7: defb 0
input_ptr: defw 0
output_ptr: defw 0
cases_left: defb 0
ready: defb 0
preview: defs 8,0
OUTPUT: defs 1300,0
    end start

The custom row is 01 01 43 00 43 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 decrements an empty length on Backspace. Zero wraps to 255, making the supposed name length exceed the entire buffer. Keep zero at zero; subsequent operations on a corrupt length must reject or explicitly clear it.

Select the deliberately wrong client

wrong.asm
    org $8000
CUSTOM equ 0
BROKEN equ 1
COYOTE_STEPS equ 4
BUFFER_STEPS equ 4
TEXT_CAP equ 8
PROPERTY_COUNT equ 16
STREAM_BYTES equ 13
CRC_POLY equ 7
    include "caller.inc"
    include "exercise.inc"
component_start:
    include "component.inc"
    include "data.inc"
    include "cases.inc"
    include "../../physics/rectangle-overlap/display.inc"
code_end:
STATE:
LENGTH: defb 0
STATUS: defb 0
KEY: defb 0
MODE: defb 0
BUF0: defb 0
BUF1: defb 0
BUF2: defb 0
BUF3: defb 0
BUF4: defb 0
BUF5: defb 0
BUF6: defb 0
BUF7: defb 0
input_ptr: defw 0
output_ptr: defw 0
cases_left: defb 0
ready: defb 0
preview: defs 8,0
OUTPUT: defs 1300,0
    end start
Spectrum shows the deliberate fault row FF 01 42 01 00 00 00 00.
The wrong client completes with FF 01 42 01 00 00 00 00. Its code and the explanation above identify the cause; the capture observes the result.

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 152 code bytes, 0 immutable data bytes and 12 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.

Append and Backspace touch at most one slot. Clear touches the eight reserved slots. Bounds and key checks are part of the implementation; removing them creates a different trust contract and can corrupt neighbouring state.

Cursor movement, insertion in the middle, key mapping, accented characters and Unicode require additional rules and often more storage. ASCII here is an interchange choice; C64 screen codes and NES tile numbers need a presentation mapping. Do not scan past the counted span looking for a terminator.

Four native runs use Spectrum 48K with stock ROM tape loading and USR entry; Asm198x 0.0.72 and Emu198x 0.44.2. 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.

Related technique · Maths for Games · The Craft.

Source and native evidence.