Skip to content

Encoding gaps: state_flags, hitstun, death state #41

Description

@ScavieFae

Summary

Analysis of frame-by-frame prediction errors from demo playback reveals three categories of systematic model failures traceable to missing input features. The raw Slippi replay data contains the signals needed to fix them — they were excluded during initial encoding design but should be reconsidered.

Evidence

Single-match error log (~7800 frames) shows:

Error category Frequency Severity (pos error) Root cause
Death → respawn transition 4 per match 226–316 units No death/respawn state signal
Knockback trajectory drift ~30 sequences 3–22 units No hitstun countdown
Offscreen death position drift ~200 frames total 5–7 units sustained Model doesn't know death resting position
Shield/hitlag transitions ~15 instances 0.4–1.9 units Missing hitlag/shield interaction flags

The death→respawn errors are the largest in the entire match by an order of magnitude.

Missing features

1. state_flags (5 bytes, currently unused)

Raw arrow struct: post.state_flags.{0,1,2,3,4} — 5 uint8 fields, all populated with real data.

Investigated on a real replay (13,607 frames). Key bit meanings decoded:

Byte 4 (bits 6–7): death state — clean 2-bit encoding:

  • 0b11 (192) = dead (only with action states DEAD_DOWN, DEAD_LEFT)
  • 0b10 (128) = dying/in kill trajectory (knockback fly states)
  • 0b01 (64) = respawning (ENTRY, ON_HALO_DESCENT)
  • 0b00 (0) = alive and normal

Byte 1: combat interaction

  • Bit 5 (0x20) = in hitlag — perfectly correlated with hitlag > 0
  • Bit 0 (0x01) = shielding — correlates with shield < 60
  • Bit 3 (0x08) = appears during attack animations (active hitbox?)

Byte 3: movement state

  • Bit 1 (0x02) = in knockback/hitstun — distinct from just "airborne"
  • Bit 7 (0x80) = appears across many action states (possibly "actionable" flag)

Old decision notes said: "Most of these are inferrable from action_state + character. Not worth the encoding complexity." This was wrong — byte 4's death state is NOT inferrable from action_state alone (the model currently proves this by failing at every death transition), and byte 3 bit 1 distinguishes "airborne and can act" from "airborne and in knockback" which is a critical distinction the model doesn't have.

Recommendation: Expose ~6 individual binary features extracted from specific bits, not raw byte embeddings.

2. misc_as / hitstun (currently unused — believed to be garbage)

Raw arrow struct: post.misc_as — float field. Contains hitstun frames remaining.

Old decision notes said: "Data quality issue. Produces garbage values like 5.6e-45 (bit-reinterpreted)." This is half right. Investigation shows:

  • ~42% of non-zero values are subnormal floats (e.g. 5.6e-45) — these are integers bit-reinterpreted as float32. Reinterpreting the bytes back as uint32 recovers clean values: 1, 2, 3, ... 20+.
  • ~58% of non-zero values are valid floats, 90.9% of which are integer-like.
  • Combined, we get a clean countdown timer: e.g. frames 330–349 show hitstun counting down 33→14 perfectly.

Recovery method:

raw = post.misc_as.to_numpy()  # float32
subnormal = (np.abs(raw) < 1e-10) & (raw != 0)
hitstun = np.where(subnormal,
    np.frombuffer(raw.tobytes(), dtype=np.uint32).view(np.float32),  # reinterpret
    raw)

This directly addresses knockback trajectory errors. The model currently doesn't know how long a character will be in hitstun — hitstun is a frame countdown that tells it exactly when the character regains control.

3. flags (post.flags) — confirmed absent

Field does not exist in the peppi_py arrow schema. The Slippi spec defines it but the Rust parser doesn't populate it. Nothing to recover. Can be removed from the "unused fields" tracking.

Impact on specific error patterns

Model failure Feature that fixes it
DEAD_LEFT → ON_HALO_DESCENT (226+ unit error) state_flags byte 4 death/respawn bits
50-frame position drift at death location state_flags byte 4 (model would know player is dead, position frozen)
Knockback trajectory undershoot (3–22 units) Hitstun countdown (model knows knockback duration)
SHIELD_STUN → SHIELD_REFLECT confusion state_flags byte 1 hitlag/shield bits
Landing frame off by 1–3 frames Possibly byte 3 bit 1 (knockback vs normal airborne)

Proposed encoding additions

New EncodingConfig flags, all defaulting to False:

state_flags: bool = False      # 6 binary features from state_flags bits
hitstun: bool = False          # 1 continuous feature (normalized countdown)

When state_flags=True, add per-player binary features:

  • is_dead (byte4 bits 6–7 == 0b11)
  • is_dying (byte4 bit 7)
  • is_respawning (byte4 bit 6 only)
  • in_hitlag (byte1 bit 5)
  • in_knockback (byte3 bit 1)
  • is_shielding (byte1 bit 0)

When hitstun=True, add per-player continuous feature:

  • hitstun_remaining scaled by ×0.02 (range 0–50ish → 0–1)

Total: +7 floats per player when both enabled. Current baseline is 32 floats/player.

Scope

Requires changes to:

  • build_dataset.py / parse_archive.py — extract state_flags bytes and misc_as from raw arrow struct (same pattern as item extraction)
  • parse.py — add fields to PlayerFrame dataclass
  • encoding.py — new config flags, encoding logic
  • dataset.py — pass new fields through

Does NOT require model architecture changes — these are additional input features in the continuous/binary tensor.

Priority

High — the death→respawn error is the single largest systematic failure mode in the current model, and state_flags byte 4 is a direct fix. Hitstun recovery is the second highest value for knockback prediction.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions