@majesticfudgie/ifp-reader
v1.0.0
Published
A TypeScript library for parsing GTA:SA animation (`.ifp`) files - the ANP3 format used by both `anim/ped.ifp` (the default, always-loaded ped animation set) and every entry inside `anim/anim.img` (per-vehicle/per-scenario animation sets). Works in both *
Readme
ifp-reader
A TypeScript library for parsing GTA:SA animation (.ifp) files - the ANP3 format used by both
anim/ped.ifp (the default, always-loaded ped animation set) and every entry inside anim/anim.img
(per-vehicle/per-scenario animation sets). Works in both Node.js and the browser (uses
Uint8Array input).
Installation
npm install @majesticfudgie/ifp-readerUsage
import fs from 'fs';
import IFPReader from '@majesticfudgie/ifp-reader';
const data = new Uint8Array(fs.readFileSync('ped.ifp'));
const ifp = new IFPReader(data);
console.log(ifp.animations.length); // e.g. 294
const walk = ifp.getAnimation('WALK_player'); // case-insensitive lookup
console.log(walk?.bones.length); // one track per animated boneEach bone track is keyed by boneId, matching the nodeId values exposed by
@majesticfudgie/dff-reader's
Geometry.animData (HAnim PLG) - that's the reliable way to match an animation track to a bone in a
target skeleton, not boneName (casing/whitespace vary between files, e.g. "Pelvis" vs " Pelvis").
for (const bone of walk.bones) {
console.log(bone.boneId, bone.boneName, bone.frames.length);
for (const frame of bone.frames) {
console.log(frame.time, frame.quaternion, frame.translation);
}
}Format notes
This wasn't parsed from a written spec - the byte layout below was reverse-engineered directly against
anim/ped.ifp and cross-checked against every other .ifp file in the game (133 files, 1,851 animations,
46,432 bone tracks, 980,487 keyframes - zero parse errors, see this package's test history for the
verification method).
Header:
char[4] magic "ANP3" - the only variant seen anywhere in GTA:SA; other games/versions may
use a different container (e.g. ANPK) which this library does not handle.
uint32 fileSize informational, not required to parse
char[24] packageName null-terminated; bytes after the null are uninitialised garbage, not data
(e.g. a stray leftover "=C:=C:\3dsmax5" path from whatever export tool wrote
the file - ignore anything past the first null)
uint32 numAnimations
Per animation (numAnimations times):
char[24] name null-terminated, same trailing-garbage caveat as packageName
uint32 numBones
uint32 (unknown) total keyframe byte size across all of this animation's bones - redundant
with each bone's own numFrames below, not needed to parse
uint32 (unknown) always observed as 1
Per bone (numBones times):
char[24] boneName
uint32 keyFrameType 3 = compressed rotation only, 4 = compressed rotation + translation - the
only two values ever observed (see verification above)
uint32 numFrames
uint32 boneId matches the target DFF's HAnim node ID (Root=0, Pelvis=1, Spine=2,
R Clavicle=21, R UpperArm=22, L Thigh=41, R Thigh=51, ...)
Per frame (numFrames times):
int16 qx, qy, qz, qw rotation quaternion, each component = raw / 4096.0
int16 time raw / 60.0 = seconds from clip start
int16 tx, ty, tz translation, each component = raw / 4096.0 - ONLY present when
keyFrameType is 4, absent entirely for type 3How the scale factors were confirmed:
- Quaternion scale (
4096.0): dividing by it and taking the resulting quaternion's magnitude averaged0.99983(min0.70703, max1.00000) across every keyframe in the game (980,487 samples) - a unit quaternion is the only reason that convergence would happen at all, so this is about as confirmed as a reverse-engineered constant can get. - Translation scale: reuses the same
4096.0(RenderWare's standard compression scale) - the resulting values are small, real-world-plausible per-frame deltas (centimeter scale). - Time scale (
60.0):WALK_player's Pelvis track runs0..72raw across 37 frames -72 / 60 = 1.2s, a plausible walk-cycle duration. Unlike quaternion scale, there's no hard invariant to check this against - it's the best available evidence, not a proven constant. If animation playback ever looks consistently too fast or too slow, this is the first thing to revisit.
The two "unknown" per-animation uint32 fields aren't consumed by this parser (both are derivable from
data already read), but are documented here for completeness in case a future version needs them.
API
new IFPReader(data: Uint8Array)
Parses the whole file eagerly. Throws if the file isn't ANP3. Individual animations that fail to parse
(e.g. an unrecognised keyFrameType) are logged via console.error and skipped, along with every
animation after them in the same file (a bad read desyncs the pointer for everything that follows) -
this hasn't been observed on any real game file, but mirrors the defensive fallback
@majesticfudgie/col-reader uses for the
same reason.
ifp.packageName: string
ifp.animations: IFPAnimation[]
ifp.getAnimation(name: string): IFPAnimation | undefined
Case-insensitive lookup - the game itself resolves animation name references the same way.
Supported Games
Currently only tested against GTA: San Andreas. GTA III/Vice City animation files may use a different (older/simpler) IFP variant - not yet investigated.
