air

package
v0.7.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 10, 2026 License: MIT Imports: 8 Imported by: 0

Documentation

Overview

Package air defines the pure-data model for MUGEN/Ikemen GO animation (.air) files: Animation, Frame, and ClsnBox.

This is the read-path surface — the stable vocabulary a library consumer (editor, engine) works with. It carries no parsing, file I/O, or write-only (format-preservation) logic; per CLAUDE.md's read/write separation constraint, that lives elsewhere so importing this data model alone never pulls in write-only dependencies.

Collision boxes on a Frame are already resolved to the boxes active on that specific frame — the .air format's Clsn1Default/Clsn2Default fallback mechanism is a parsing concern, not part of this data model. See .vibe/decisions/001-frame-clsn-boxes-pre-resolved.md.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Serialize

func Serialize(w io.Writer, animations []Animation) error

Serialize writes animations to w as MUGEN/Ikemen GO .air text, emitting one "[Begin Action N]" block per Animation in order.

This is a first-pass write path: it does not attempt a byte-exact round-trip of any original file's formatting or comments (a separate, not-yet-implemented concern) — it only guarantees valid, readable output that Parse reads back into an equivalent []Animation. Because Frame's Clsn1/Clsn2 already hold each frame's fully resolved collision boxes (see .vibe/decisions/001-frame-clsn-boxes-pre-resolved.md), boxes are always written per frame rather than reconstructing a Clsn1Default/Clsn2Default an original file might have used. A Loopstart marker is written only when LoopStart is non-zero, since the zero value already matches .air's own default of looping to the first frame.

Types

type Animation

type Animation struct {
	Number    int     `json:"number"`
	Frames    []Frame `json:"frames"`
	LoopStart int     `json:"loopStart"`
}

Animation is a MUGEN/Ikemen "[Begin Action N]" block: an ordered sequence of Frames plus the loop point frames return to once they have all played through once.

LoopStart is the index into Frames the animation loops back to. Its zero value (0) matches .air's own default: an animation with no Loopstart marker loops back to its very first frame.

func Parse

func Parse(r io.Reader) ([]Animation, error)

Parse reads MUGEN/Ikemen GO .air animation text from r and returns the Animations it describes, in file order.

This covers "[Begin Action N]" headers, frame lines, Clsn1Default/Clsn2Default declarations, indexed Clsn[i] box lines, the Loopstart marker, and Ikemen GO's Interpolate Offset/Blend/Scale/Angle directive lines (recognized and skipped, not yet represented on the Animation/Frame model). Comment lines (';', whole-line or trailing) are ignored, as is a whole line starting with ':' — a real-world ad-hoc comment marker some .air files use instead of ';' (item 054). An empty input returns an empty, non-nil-error result rather than an error. A frame line's group or image field may be -1, the ".air" convention for "no sprite shown on this frame" (see Frame.IsBlank). A bracket line that isn't even an attempt at "[Begin Action N]" — e.g. a "[Statedef N]"/"[State N]" block some real-world .air files carry embedded alongside their animation data — is skipped, along with the content lines that follow it, rather than erroring (item 055), the same tolerance cns.Parse already applies to sections outside its own scope. Malformed input — a bracket line that looks like an attempted "[Begin Action N]" header yet fails to parse (missing or non-numeric action number), a frame line with missing or non-numeric fields, or a group/image index more negative than the -1 sentinel — still returns a descriptive error naming the offending line rather than panicking or silently producing incorrect data, as does a reader that fails outright.

type BlendMode

type BlendMode string

BlendMode is a frame's blending mode token as found in .air files (e.g. "A" for additive, "S" for subtractive). The zero value means normal blending (no special blend mode).

type ClsnBox

type ClsnBox struct {
	Left   int `json:"left"`
	Top    int `json:"top"`
	Right  int `json:"right"`
	Bottom int `json:"bottom"`
}

ClsnBox is an axis-aligned collision box, expressed in the coordinate system used by Clsn1 (attack) and Clsn2 (vulnerability) boxes in .air files: Left/Top is one corner, Right/Bottom the opposite corner.

type Document

type Document struct {
	Animations []Animation
	// contains filtered or unexported fields
}

Document is the write-path counterpart to Parse/Serialize: it exists so a .air file can be round-tripped — parsed, then serialized back out — without losing the comments, blank lines, and exact formatting that the pure-data Animation/Frame model deliberately does not carry (see .vibe/decisions/002-air-comments-stripped-not-preserved-by-parse.md).

Document.Animations is decoded the same way Parse's return value is, for convenient structured access to what was parsed — but Serialize does not read it back. As long as Document.Animations is left untouched, ParseDocument followed by Serialize reproduces the original source byte-for-byte, comments included. Mutating Animations has no effect on Serialize's output: regenerating text from an edited Animations slice while still preserving unrelated comments/ordering around the edit is a heavier per-line reconciliation this type does not attempt (see .vibe/decisions/003-air-round-trip-via-separate-document-type.md).

func ParseDocument

func ParseDocument(r io.Reader) (*Document, error)

ParseDocument reads MUGEN/Ikemen GO .air animation text from r, decoding it the same way Parse does while also retaining the exact source bytes needed for a faithful round trip through Serialize.

func (*Document) Serialize

func (d *Document) Serialize(w io.Writer) error

Serialize writes the Document's retained source back out to w verbatim, reproducing the exact text ParseDocument read — including comments, blank lines, and original line endings.

type Flip

type Flip string

Flip is a frame's mirroring axis, matching the tokens used in .air files.

const (
	// FlipNone means the frame is drawn without mirroring.
	FlipNone Flip = ""
	// FlipH mirrors the frame horizontally.
	FlipH Flip = "H"
	// FlipV mirrors the frame vertically.
	FlipV Flip = "V"
	// FlipHV mirrors the frame both horizontally and vertically.
	FlipHV Flip = "HV"
)

type Frame

type Frame struct {
	Group int       `json:"group"`
	Image int       `json:"image"`
	X     int       `json:"x"`
	Y     int       `json:"y"`
	Time  int       `json:"time"`
	Flip  Flip      `json:"flip"`
	Blend BlendMode `json:"blend"`
	Clsn1 []ClsnBox `json:"clsn1"`
	Clsn2 []ClsnBox `json:"clsn2"`
}

Frame is a single displayed image within an Animation: which sprite to show (Group, Image), where to show it (X, Y), how long to hold it (Time), how to mirror and blend it, and the collision boxes active while it is displayed.

Group and Image are normally non-negative sprite indices, but MUGEN/Ikemen GO ".air" files also use -1 on either field — most commonly -1,-1 — as a sentinel meaning "show no sprite on this frame". Such a frame is not malformed; see IsBlank.

func (Frame) IsBlank

func (f Frame) IsBlank() bool

IsBlank reports whether frame uses the ".air" "no sprite shown" sentinel: a Group or Image of -1. A blank frame intentionally has no sprite to resolve, distinct from a frame whose sprite reference is simply missing from the loaded sprite collection.

type SpriteResolver

type SpriteResolver struct {
	// contains filtered or unexported fields
}

SpriteResolver resolves a Frame's (Group, Image) reference against a loaded set of sprite groups. sff.Sprite/sff.SpriteGroup are the same version-agnostic shape regardless of whether they came from a .sff v1 or v2 file, so Resolve needs no version-specific branching.

This is the first cross-package integration point between air and sff: per CLAUDE.md, .air and .sff are interdependent formats, not independent ones — a Frame reference is meaningless without a sprite collection to resolve it against. See .vibe/decisions/008-air-sprite-resolution-lives-in-air-package.md.

func NewSpriteResolver

func NewSpriteResolver(groups []sff.SpriteGroup) *SpriteResolver

NewSpriteResolver indexes the given sprite groups by (Group, Image) so Resolve can look up any Frame's sprite without rescanning groups on every call. Passing nil or an empty slice is valid and produces a resolver for which every Resolve call fails.

func (*SpriteResolver) Resolve

func (r *SpriteResolver) Resolve(frame Frame) (sff.Sprite, error)

Resolve returns the Sprite that frame's (Group, Image) reference names. It returns a descriptive error, rather than a zero Sprite, when no sprite with a matching (Group, Image) exists in the resolver — a missing reference must fail explicitly rather than silently rendering blank.

A blank frame (frame.IsBlank(), the ".air" -1 "no sprite" sentinel) is not a missing reference: Resolve recognizes it directly and returns a zero Sprite with a nil error, without looking it up.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL