From d892f9beda681f0a5ec42b943baf374040093c43 Mon Sep 17 00:00:00 2001 From: Christian Semmler Date: Fri, 30 Jan 2026 18:03:07 -0800 Subject: [PATCH] Add Kaitai Struct definition for animation files (.ani) (#1734) Add animation.ksy documenting the binary format for LEGO Island's animation files, including keyframe data, actor references, and optional camera animation. Includes sample file pns065rd.ani. --- docs/README.md | 10 +- docs/animation.ksy | 310 ++++++++++++++++++++++++++++++++++++++ docs/samples/pns065rd.ani | Bin 0 -> 4788 bytes 3 files changed, 319 insertions(+), 1 deletion(-) create mode 100644 docs/animation.ksy create mode 100644 docs/samples/pns065rd.ani diff --git a/docs/README.md b/docs/README.md index 0ea419d7..1c309a3e 100644 --- a/docs/README.md +++ b/docs/README.md @@ -18,6 +18,7 @@ Kaitai Struct allows you to define binary formats in a YAML-based `.ksy` file, w | [`savegame.ksy`](/docs/savegame.ksy) | `.GS` | Main game save data (game state, progress, customizations) | | [`players.ksy`](/docs/players.ksy) | `.gsi` | Player profile save data (usernames) | | [`history.ksy`](/docs/history.ksy) | `.gsi` | Score history and high scores | +| [`animation.ksy`](/docs/animation.ksy) | `.ani` | Animation data (keyframes, actor references, camera animation) | ## Using the Tools @@ -38,6 +39,9 @@ ksv samples/Players.gsi players.ksy # View a History.gsi file ksv samples/History.gsi history.ksy + +# View an animation file +ksv samples/pns065rd.ani animation.ksy ``` ### Kaitai Struct Dump (ksdump) @@ -53,11 +57,15 @@ ksdump --format json samples/Players.gsi players.ksy # Dump History.gsi to YAML ksdump --format yaml samples/History.gsi history.ksy + +# Dump an animation file to JSON +ksdump --format json samples/pns065rd.ani animation.ksy ``` ## Sample Files -The [`samples/`](/docs/samples/) directory contains example save files for testing: +The [`samples/`](/docs/samples/) directory contains example files for testing: - `G0.GS`, `G1.GS`, `G2.GS` - Sample main game save files (slots 0, 1, 2) - `Players.gsi` - Sample player profile data - `History.gsi` - Sample score history data +- `pns065rd.ani` - Sample animation file diff --git a/docs/animation.ksy b/docs/animation.ksy new file mode 100644 index 00000000..6f582904 --- /dev/null +++ b/docs/animation.ksy @@ -0,0 +1,310 @@ +meta: + id: animation + title: Animation File + application: LEGO Island + file-extension: ani + license: CC0-1.0 + endian: le +doc: | + Animation file format for LEGO Island (1997). Contains skeletal animation + data including actor references, keyframes for translation/rotation/scale, + morph visibility keys, and optional camera animation data. + + Animation files are embedded within SI (Interleaf) files and + parsed by LegoAnimPresenter. The format consists of a header with bounding + information, an actor list, animation duration, optional camera animation, + and a hierarchical tree of animation nodes. + +seq: + - id: magic + type: s4 + doc: | + Magic number identifying the file format. Must be 0x11 (17 decimal). + - id: bounding_radius + type: f4 + doc: | + Radius of the bounding sphere encompassing the entire animation. + Used for visibility culling and collision detection. + - id: center_x + type: f4 + doc: X coordinate of the bounding sphere center point. + - id: center_y + type: f4 + doc: Y coordinate of the bounding sphere center point. + - id: center_z + type: f4 + doc: Z coordinate of the bounding sphere center point. + - id: has_camera_anim + type: s4 + doc: | + Flag indicating whether camera animation data follows the actor list. + If non-zero, a camera_anim structure is present after the duration field. + - id: unused + type: s4 + doc: | + Unused field. Read by the parser but not used for anything. + - id: num_actors + type: u4 + doc: Number of actor entries in the actor list. + - id: actors + type: actor_entry + repeat: expr + repeat-expr: num_actors + doc: | + List of actors referenced by this animation. Each entry contains + the actor name and type, which determines how the actor ROI is + managed during animation playback. + - id: duration + type: s4 + doc: Total duration of the animation in milliseconds. + - id: camera_anim + type: camera_anim + if: has_camera_anim != 0 + doc: | + Camera animation data including position, target, and rotation keys. + Only present if has_camera_anim is non-zero. + - id: root_node + type: tree_node + doc: | + Root node of the animation tree. The tree structure mirrors the + skeletal hierarchy of the animated model, with each node containing + keyframe data for its corresponding bone/part. + +types: + actor_entry: + doc: | + An actor reference in the animation. The name identifies which ROI + (Realtime Object Instance) to animate, and the type determines + how the actor is managed by the character manager. + seq: + - id: name_length + type: u4 + doc: Length of the actor name in bytes. + - id: name + type: str + size: name_length + encoding: ASCII + if: name_length > 0 + doc: Actor name used to look up the ROI in the scene. + - id: actor_type + type: u4 + enum: actor_type + if: name_length > 0 + doc: | + Determines how the actor ROI is created and managed. + See actor_type enum for possible values. + + camera_anim: + doc: | + Camera animation data (LegoAnimScene). Contains keyframes for camera + position, look-at target position, and roll rotation around the + view axis. + seq: + - id: num_translation_keys + type: u2 + doc: Number of camera position keyframes. + - id: translation_keys + type: translation_key + repeat: expr + repeat-expr: num_translation_keys + doc: Camera position keyframes. + - id: num_target_keys + type: u2 + doc: Number of look-at target position keyframes. + - id: target_keys + type: translation_key + repeat: expr + repeat-expr: num_target_keys + doc: Look-at target position keyframes. + - id: num_rotation_keys + type: u2 + doc: Number of camera roll rotation keyframes. + - id: rotation_keys + type: rotation_z_key + repeat: expr + repeat-expr: num_rotation_keys + doc: Camera roll rotation keyframes (rotation around view axis). + + tree_node: + doc: | + A node in the animation tree hierarchy. Each node contains animation + data for one part of the model and references to child nodes. + seq: + - id: data + type: node_data + doc: Animation keyframe data for this node. + - id: num_children + type: u4 + doc: Number of child nodes. + - id: children + type: tree_node + repeat: expr + repeat-expr: num_children + doc: Child nodes in the animation hierarchy. + + node_data: + doc: | + Animation data for a single node (LegoAnimNodeData). Contains the + node name and arrays of keyframes for translation, rotation, scale, + and morph (visibility) animations. + seq: + - id: name_length + type: u4 + doc: Length of the node name in bytes. + - id: name + type: str + size: name_length + encoding: ASCII + if: name_length > 0 + doc: | + Node name used to match this animation data to a ROI in the scene. + Names starting with '*' indicate special handling (actor name + substitution). Names starting with '-' are ignored. + - id: num_translation_keys + type: u2 + doc: Number of translation keyframes. + - id: translation_keys + type: translation_key + repeat: expr + repeat-expr: num_translation_keys + doc: Translation (position) keyframes. + - id: num_rotation_keys + type: u2 + doc: Number of rotation keyframes. + - id: rotation_keys + type: rotation_key + repeat: expr + repeat-expr: num_rotation_keys + doc: Rotation keyframes (quaternion format). + - id: num_scale_keys + type: u2 + doc: Number of scale keyframes. + - id: scale_keys + type: scale_key + repeat: expr + repeat-expr: num_scale_keys + doc: Scale keyframes. + - id: num_morph_keys + type: u2 + doc: Number of morph (visibility) keyframes. + - id: morph_keys + type: morph_key + repeat: expr + repeat-expr: num_morph_keys + doc: Morph keyframes controlling visibility. + + anim_key: + doc: | + Base animation key containing time and flags. The time and flags + are packed into a single 32-bit value: bits 0-23 contain the time + in milliseconds, and bits 24-31 contain flags. + seq: + - id: time_and_flags + type: s4 + doc: | + Packed time and flags value. + - Bits 0-23: Time in milliseconds (mask with 0xFFFFFF) + - Bits 24-31: Flags (shift right by 24) + instances: + time: + value: time_and_flags & 0xFFFFFF + doc: Keyframe time in milliseconds. + flags: + value: (time_and_flags >> 24) & 0xFF + doc: | + Keyframe flags: + - 0x01 (active): Key has meaningful data + - 0x02 (negate_rotation): Negate quaternion for interpolation + - 0x04 (skip_interpolation): Use this key's value without blending + + translation_key: + doc: | + Translation keyframe containing position offset (LegoTranslationKey). + The translation is applied relative to the parent node's transform. + seq: + - id: key + type: anim_key + doc: Base key with time and flags. + - id: x + type: f4 + doc: X component of translation. + - id: y + type: f4 + doc: Y component of translation. + - id: z + type: f4 + doc: Z component of translation. + + rotation_key: + doc: | + Rotation keyframe containing a quaternion (LegoRotationKey). + The quaternion is stored as (angle, x, y, z) where angle is the + scalar/w component and (x, y, z) is the vector part. + seq: + - id: key + type: anim_key + doc: Base key with time and flags. + - id: angle + type: f4 + doc: | + Quaternion scalar component (w). A value of 1.0 with x=y=z=0 + represents no rotation (identity quaternion). + - id: x + type: f4 + doc: Quaternion x component. + - id: y + type: f4 + doc: Quaternion y component. + - id: z + type: f4 + doc: Quaternion z component. + + scale_key: + doc: | + Scale keyframe containing scale factors (LegoScaleKey). + Scale is applied relative to the local origin of the node. + seq: + - id: key + type: anim_key + doc: Base key with time and flags. + - id: x + type: f4 + doc: X scale factor (1.0 = no scaling). + - id: y + type: f4 + doc: Y scale factor (1.0 = no scaling). + - id: z + type: f4 + doc: Z scale factor (1.0 = no scaling). + + morph_key: + doc: | + Morph/visibility keyframe (LegoMorphKey). Controls whether the + node's ROI is visible at a given time. + seq: + - id: key + type: anim_key + doc: Base key with time and flags. + - id: visible + type: u1 + doc: Visibility flag. Non-zero means visible. + + rotation_z_key: + doc: | + Z-axis rotation keyframe (LegoRotationZKey). Used for camera roll + animation where only rotation around the view axis is needed. + seq: + - id: key + type: anim_key + doc: Base key with time and flags. + - id: z + type: f4 + doc: Rotation angle around the Z axis in radians. + +enums: + actor_type: + 2: managed_lego_actor + 3: managed_invisible_roi_trimmed + 4: managed_invisible_roi + 5: scene_roi_1 + 6: scene_roi_2 diff --git a/docs/samples/pns065rd.ani b/docs/samples/pns065rd.ani new file mode 100644 index 0000000000000000000000000000000000000000..7a9128831844dbd0d0923d6a6e3060d452169584 GIT binary patch literal 4788 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 V664Brc$oS*