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 ¶
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 ¶
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 ¶
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.
type Flip ¶
type Flip string
Flip is a frame's mirroring axis, matching the tokens used in .air files.
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.
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.