Skip to main content

banished_game/behaviour/
animation.rs

1//! Animation as the engine plays it: sampling a pose, the clock of each playing clip, and the
2//! events (such as sounds) a clip fires.
3//!
4//! Original: `Animation::GetPoseAtTime`, `Quaternion::Lerp`, `AnimationState::Update`,
5//! `Animation::ApplyEvents`.
6//!
7//! Original: Runtime `0x1800fd530`, Runtime `0x18001dde0`, Runtime `0x1801003b0`, Runtime
8//! `0x1800fd230`.
9
10use crate::scheduler::{seconds, ticks};
11use banished_formats::skeleton::Transform;
12use banished_formats::types::AnimationClip;
13
14/// Single, unique animation playback. Tree expiry starts from an empty animation state.
15/// Blended stacks and AnimationManager's shared instances require their separate lifecycle port.
16#[derive(Debug, Clone, PartialEq)]
17pub struct Playback {
18    /// The clip.
19    pub clip: usize,
20    /// The time played so far.
21    pub elapsed: i64,
22    /// UpdateNodes publishes the pose at the OLD clock, then Update advances elapsed.
23    pub sample_time: i64,
24    /// The time step of the last sample.
25    pub sample_delta: i64,
26    /// The playback speed.
27    pub speed: f32,
28}
29
30impl Playback {
31    /// A clip that starts playing at `start`, at speed 1. Tree removal starts at time zero.
32    ///
33    /// Original: `FUN_14012a5c0`; `AnimationState::PlayAnimation`, Runtime `0x1801007e0`.
34    pub fn new(clip: usize, start: i64) -> Self {
35        Self {
36            clip,
37            elapsed: start,
38            sample_time: start,
39            sample_delta: 0,
40            speed: 1.0,
41        }
42    }
43
44    /// Whether the clip is still playing. The end comes from the clip's stored duration (with its
45    /// rounded value), not from frames and rate.
46    ///
47    /// Original: `MeshInstance::IsAnimating`, Runtime `0x180102060`.
48    pub fn is_animating(&self, clip: &AnimationClip) -> bool {
49        clip.looping || self.elapsed < clip.duration_ticks
50    }
51
52    /// Port of AnimationState::Update's unique-state clock and ApplyEvents interval. Returns
53    /// indices of callbacks in resource order; effect dispatch remains the caller's responsibility.
54    pub fn update(&mut self, clip: &AnimationClip, dt: i64) -> Vec<usize> {
55        self.sample_time = self.elapsed;
56        self.sample_delta = ticks(seconds(dt) * self.speed);
57        let delta = if self.speed == 1.0 {
58            dt
59        } else {
60            self.sample_delta
61        };
62        let next = self.elapsed + delta;
63        let events = clip
64            .events
65            .iter()
66            .enumerate()
67            .filter_map(|(index, event)| {
68                let time = ticks(
69                    (event.frame - clip.start_frame) as f32
70                        / (clip.end_frame - clip.start_frame) as f32
71                        * clip.frames.len() as f32
72                        * clip.sample_interval,
73                );
74                let crossed = self.elapsed <= time && time < next;
75                let wrapped = next >= clip.duration_ticks
76                    && self.elapsed - clip.duration_ticks <= time
77                    && time < next - clip.duration_ticks;
78                (crossed || wrapped).then_some(index)
79            })
80            .collect();
81        self.elapsed = next;
82        if !clip.looping {
83            if self.elapsed >= clip.duration_ticks {
84                self.elapsed = clip.duration_ticks;
85            }
86        } else if clip.duration_ticks > 0 && self.elapsed >= clip.duration_ticks {
87            // Original repeatedly subtracts the full duration, then adds the source loop point.
88            self.elapsed %= clip.duration_ticks;
89            let loop_seconds = if clip.loop_start_frame == 0 {
90                0.0
91            } else {
92                (clip.loop_start_frame - clip.start_frame) as f32
93                    / (clip.end_frame - clip.start_frame) as f32
94                    * clip.frames.len() as f32
95                    * clip.sample_interval
96            };
97            self.elapsed += ticks(loop_seconds);
98        }
99        events
100    }
101
102    /// The pose of the clip at the current time.
103    pub fn pose(&self, clip: &AnimationClip) -> Vec<Transform> {
104        sample_pose(clip, self.sample_time, self.sample_delta)
105    }
106}
107
108/// Port of Math::Quaternion::Lerp 0x18001dde0, including its dot <= 0 sign choice and y/x/z/w
109/// normalization sum. Input/output use the format reader's conjugated quaternion convention.
110fn quaternion_lerp(a: [f32; 4], b: [f32; 4], fraction: f32) -> [f32; 4] {
111    // Perform the arithmetic in the native convention before conjugating its result. Flipping
112    // signs before summation changes signed-zero results even when the rotations are equivalent.
113    let native = |mut q: [f32; 4]| {
114        for value in &mut q[..3] {
115            *value = -*value;
116        }
117        q
118    };
119    let a = native(a);
120    let b = native(b);
121    let old = 1.0 - fraction;
122    let dot = a[0] * b[0] + a[1] * b[1] + a[2] * b[2] + a[3] * b[3];
123    let new = if dot <= 0.0 { -fraction } else { fraction };
124    let q = std::array::from_fn::<_, 4, _>(|i| a[i] * old + b[i] * new);
125    let length = (q[1] * q[1] + q[0] * q[0] + q[2] * q[2] + q[3] * q[3]).sqrt();
126    let inverse = 1.0 / length;
127    native(q.map(|v| v * inverse))
128}
129
130/// The pose of a clip at a time: the two samples around it, interpolated (normalised quaternion
131/// lerp for rotations, linear for translations), with the original's loop arithmetic. The root's
132/// rotation is reset and its translation becomes a movement delta, as in the original.
133///
134/// Original: `Animation::GetPoseAtTime`, Runtime `0x1800fd530`.
135pub fn sample_pose(clip: &AnimationClip, time: i64, delta: i64) -> Vec<Transform> {
136    let count = clip.frames.len();
137    if count == 0 {
138        return Vec::new();
139    }
140    let position = seconds(time) * clip.sample_rate;
141    let raw = position as i32;
142    let loop_start = if clip.loop_start_frame == 0 {
143        0
144    } else {
145        ((clip.loop_start_frame - clip.start_frame) as f32
146            / (clip.end_frame - clip.start_frame) as f32
147            * count as f32) as i32
148    };
149    // Deviation: malformed negative times and empty loop spans must not index outside resources.
150    let span = (count as i32 - loop_start).max(1);
151    let first = if raw < count as i32 {
152        raw.max(0)
153    } else if clip.looping {
154        loop_start + raw % span
155    } else {
156        count as i32 - 1
157    } as usize;
158    let second = if first + 1 < count {
159        first + 1
160    } else if clip.looping {
161        (first as i32 + 1) as usize % span as usize + loop_start as usize
162    } else {
163        count - 1
164    };
165    let fraction = position - raw as f32;
166    let mut pose = if first == second || fraction == 0.0 {
167        clip.frames[first].clone()
168    } else {
169        clip.frames[first]
170            .iter()
171            .zip(&clip.frames[second])
172            .map(|(a, b)| Transform {
173                rotation: quaternion_lerp(a.rotation, b.rotation, fraction),
174                translation: std::array::from_fn(|i| {
175                    a.translation[i] + (b.translation[i] - a.translation[i]) * fraction
176                }),
177            })
178            .collect()
179    };
180    if let Some(root) = pose.first_mut() {
181        root.rotation = Transform::IDENTITY.rotation;
182        let scale = seconds(delta) * clip.sample_rate;
183        root.translation = root.translation.map(|v| v * scale);
184    }
185    pose
186}
187
188// +0x20; +0x28
189/// One layer of an [`AnimationStack`] (`AnimationState` entry, 0x38 bytes): its clip and clock, and
190/// while it fades out under a newer layer, the blend time and what is left of it.
191#[derive(Debug, Clone, PartialEq)]
192pub struct Layer {
193    /// The clip and its clock.
194    pub playback: Playback,
195    /// The blend time.
196    pub blend: i64,
197    /// The blend time left.
198    pub left: i64,
199    // +0x30
200    /// The layer has been updated once.
201    pub started: bool,
202    // +0x0c
203    /// The shared instance it follows (an instanced clip), see [`shared`].
204    pub instance: Option<i32>,
205}
206
207/// `Graphics::AnimationState` with its blend stack, newest layer first. Each play pushes a
208/// layer; the one it covers fades out over the group's blend time, and the pose mixes them.
209#[derive(Debug, Clone, PartialEq, Default)]
210pub struct AnimationStack {
211    /// The layers, newest first.
212    pub layers: Vec<Layer>,
213    /// The pose `UpdateNodes` published at the last update.
214    pose: Vec<Transform>,
215}
216
217/// `AnimationGroup` `+0x48`: the blend time from `old` to `new` (`[new][old]`, the group's
218/// clips square), else the default (`+0x30`).
219fn blend_seconds(group: &banished_formats::types::AnimationGroup, old: usize, new: usize) -> f32 {
220    let n = group.clips.len();
221    group
222        .blend_matrix
223        .get(n * new + old)
224        .copied()
225        .unwrap_or(group.default_blend)
226}
227
228impl AnimationStack {
229    /// One layer playing `clip` from `start`.
230    pub fn new(clip: usize, start: i64) -> Self {
231        Self {
232            layers: vec![Layer {
233                playback: Playback::new(clip, start),
234                blend: 0,
235                left: 0,
236                started: false,
237                instance: None,
238            }],
239            pose: Vec::new(),
240        }
241    }
242
243    /// The newest layer's clip.
244    pub fn clip(&self) -> Option<usize> {
245        self.layers.first().map(|l| l.playback.clip)
246    }
247
248    /// `AnimationState::PlayAnimation`. A looping clip already on top keeps its time (speed back to
249    /// 1). Otherwise the top layer starts fading over the blend time from its clip to `clip` and
250    /// the new layer goes on top; a top layer never updated is replaced instead, the one below
251    /// taking the blend.
252    ///
253    /// Original: Runtime `0x1801007e0`.
254    pub fn play(
255        &mut self,
256        group: &banished_formats::types::AnimationGroup,
257        clip: usize,
258        start: i64,
259    ) {
260        if let Some(top) = self.layers.first_mut() {
261            if top.playback.clip == clip && group.clips.get(clip).is_some_and(|c| c.looping) {
262                top.playback.speed = 1.0;
263                return;
264            }
265        }
266        // `MeshInstance::PlayAnimation` asks for a shared instance (`GetInstanceId`) when the
267        // clip is instanced.
268        let instance = group
269            .clips
270            .get(clip)
271            .filter(|c| c.instanced && c.instance_count > 0)
272            .map(|_| shared::instance_id(group, clip));
273        let layer = Layer {
274            playback: Playback::new(clip, start),
275            blend: 0,
276            left: 0,
277            started: false,
278            instance,
279        };
280        if self.layers.is_empty() {
281            self.layers.push(layer);
282            return;
283        }
284        if !self.layers[0].started && self.layers.len() != 1 {
285            let t = ticks(blend_seconds(group, self.layers[1].playback.clip, clip));
286            self.layers[1].blend = t;
287            self.layers[1].left = t;
288            self.layers[0] = layer;
289            return;
290        }
291        let t = ticks(blend_seconds(group, self.layers[0].playback.clip, clip));
292        self.layers[0].blend = t;
293        self.layers[0].left = t;
294        self.layers.insert(0, layer);
295    }
296
297    /// `AnimationState::Update` with `UpdateNodes`: the pose at the old clocks, the oldest layer's
298    /// first and each newer one mixed over it by the older layer's share left (`Quaternion::Lerp`
299    /// and a linear translation); then every clock advances (only the newest fires its events), and
300    /// the fading layers count down, a layer leaving once less than a tick is left. Returns the
301    /// newest layer's events.
302    ///
303    /// Original: Runtime `0x1801003b0`, Runtime `0x1800ff4b0`.
304    pub fn update(
305        &mut self,
306        group: &banished_formats::types::AnimationGroup,
307        dt: i64,
308    ) -> Vec<usize> {
309        let Some(top) = self.layers.first_mut() else {
310            return Vec::new();
311        };
312        top.started = true;
313        let weights: Vec<f32> = self
314            .layers
315            .iter()
316            .map(|l| {
317                if l.blend > 0 {
318                    seconds(l.left) / seconds(l.blend)
319                } else {
320                    0.0
321                }
322            })
323            .collect();
324        let mut events = Vec::new();
325        for (i, layer) in self.layers.iter_mut().enumerate() {
326            if let Some(clip) = group.clips.get(layer.playback.clip) {
327                let fired = layer.playback.update(clip, dt);
328                if i == 0 {
329                    events = fired;
330                }
331            }
332        }
333        // `UpdateNodes`: an instanced layer takes its shared instance's pose
334        // (`AnimationManager::GetPose`), the others their own clocks'.
335        let poses: Vec<Option<Vec<Transform>>> = self
336            .layers
337            .iter()
338            .map(|l| match l.instance {
339                Some(id) => shared::pose(id),
340                None => group.clips.get(l.playback.clip).map(|c| l.playback.pose(c)),
341            })
342            .collect();
343        let mut pose: Option<Vec<Transform>> = None;
344        for i in (0..self.layers.len()).rev() {
345            let Some(new) = &poses[i] else { continue };
346            pose = Some(match pose {
347                None => new.clone(),
348                Some(old) => {
349                    let w = weights.get(i + 1).copied().unwrap_or(0.0);
350                    new.iter()
351                        .zip(&old)
352                        .map(|(n, o)| Transform {
353                            rotation: quaternion_lerp(n.rotation, o.rotation, w),
354                            translation: std::array::from_fn(|k| {
355                                n.translation[k] + (o.translation[k] - n.translation[k]) * w
356                            }),
357                        })
358                        .collect()
359                }
360            });
361        }
362        self.pose = pose.unwrap_or_default();
363        let step = ticks(seconds(dt));
364        let mut i = 0;
365        while i < self.layers.len() {
366            let l = &mut self.layers[i];
367            if l.blend > 0 {
368                l.left -= step;
369                if l.left < 1 {
370                    if let Some(id) = l.instance {
371                        shared::free(id);
372                    }
373                    self.layers.remove(i);
374                    continue;
375                }
376            }
377            i += 1;
378        }
379        events
380    }
381
382    /// The pose of the last update, or the newest layer's at its clock before any update.
383    pub fn pose(&self, group: &banished_formats::types::AnimationGroup) -> Option<Vec<Transform>> {
384        if !self.pose.is_empty() {
385            return Some(self.pose.clone());
386        }
387        let top = self.layers.first()?;
388        group
389            .clips
390            .get(top.playback.clip)
391            .map(|c| top.playback.pose(c))
392    }
393}
394
395// +0x2c; +0x28
396/// `Graphics::AnimationManager` (Runtime): the shared instances of instanced clips. Each clip a
397/// character plays with its flag set is followed through one of shared clocks (3, 4 or 8 for the
398/// citizens' and animals'), so the characters on one walk or work in step.
399///
400/// - `GetInstanceId`: the first request for a group's clip makes its instances, the `k`-th starting
401///   `k × duration / count` into the clip (and updated twice at no time); each request counts one
402///   more user and gets the instance whose clock is earliest.
403/// - `Update`: every instance advances, before the characters; its pose's root is the identity
404///   turned and its movement.
405/// - `FreeInstanceId`: the last user gone, the clip's instances go.
406///
407/// The original keeps one manager for the process; the port one per thread, reset with each town
408/// ([`shared::reset`]).
409///
410/// Original: Runtime `0x1800fe260`, Runtime `0x1800fead0`, Runtime `0x1800fe200`.
411pub mod shared {
412    use super::Playback;
413    use banished_formats::skeleton::Transform;
414    use banished_formats::types::{AnimationClip, AnimationGroup};
415    use std::cell::RefCell;
416
417    struct Entry {
418        group: usize,
419        clip: usize,
420        users: i32,
421        data: AnimationClip,
422        instances: Vec<(Playback, Vec<Transform>)>,
423    }
424
425    thread_local! {
426        static ENTRIES: RefCell<Vec<Option<Entry>>> = const { RefCell::new(Vec::new()) };
427    }
428
429    /// A new town: no shared instances.
430    pub fn reset() {
431        ENTRIES.with(|e| e.borrow_mut().clear());
432    }
433
434    /// One town's shared instances, kept aside while another town on the same thread runs (a
435    /// lockstep test runs several towns in one process; on separate computers each has its own).
436    #[derive(Default)]
437    pub struct Table(Vec<Option<Entry>>);
438
439    /// Puts `table` in place of this thread's instances, and the thread's in `table`.
440    pub fn swap(table: &mut Table) {
441        ENTRIES.with(|e| std::mem::swap(&mut *e.borrow_mut(), &mut table.0));
442    }
443
444    /// `GetInstanceId`: `entry << 8 | instance`.
445    pub fn instance_id(group: &AnimationGroup, clip: usize) -> i32 {
446        let key = group as *const AnimationGroup as usize;
447        ENTRIES.with(|e| {
448            let mut entries = e.borrow_mut();
449            let found = entries
450                .iter()
451                .position(|x| x.as_ref().is_some_and(|x| x.group == key && x.clip == clip));
452            let index = match found {
453                Some(i) => i,
454                None => {
455                    let data = group.clips[clip].clone();
456                    let count = data.instance_count.max(1) as usize;
457                    let step = super::seconds(data.duration_ticks) * (1.0 / count as f32);
458                    let mut at = 0.0f32;
459                    let mut instances = Vec::with_capacity(count);
460                    for _ in 0..count {
461                        let mut p = Playback::new(clip, super::ticks(at));
462                        for _ in 0..2 {
463                            p.update(&data, 0);
464                        }
465                        let pose = p.pose(&data);
466                        instances.push((p, pose));
467                        at += step;
468                    }
469                    let entry = Entry {
470                        group: key,
471                        clip,
472                        users: 0,
473                        data,
474                        instances,
475                    };
476                    match entries.iter().position(Option::is_none) {
477                        Some(free) => {
478                            entries[free] = Some(entry);
479                            free
480                        }
481                        None => {
482                            entries.push(Some(entry));
483                            entries.len() - 1
484                        }
485                    }
486                }
487            };
488            let entry = entries[index].as_mut().unwrap();
489            entry.users += 1;
490            let mut best = (f32::MAX, 0usize);
491            for (k, (p, _)) in entry.instances.iter().enumerate() {
492                let t = super::seconds(p.elapsed);
493                if t < best.0 {
494                    best = (t, k);
495                }
496            }
497            ((index as i32) << 8) | best.1 as i32
498        })
499    }
500
501    /// `FreeInstanceId`.
502    pub fn free(id: i32) {
503        ENTRIES.with(|e| {
504            let mut entries = e.borrow_mut();
505            let i = (id >> 8 & 0xff) as usize;
506            if let Some(Some(entry)) = entries.get_mut(i) {
507                entry.users -= 1;
508                if entry.users == 0 {
509                    entries[i] = None;
510                }
511            }
512        });
513    }
514
515    /// `Update`: every instance by `dt`.
516    pub fn update(dt: i64) {
517        ENTRIES.with(|e| {
518            for entry in e.borrow_mut().iter_mut().flatten() {
519                for (p, pose) in &mut entry.instances {
520                    p.update(&entry.data, dt);
521                    *pose = p.pose(&entry.data);
522                }
523            }
524        });
525    }
526
527    /// `GetPose`.
528    pub fn pose(id: i32) -> Option<Vec<Transform>> {
529        ENTRIES.with(|e| {
530            let entries = e.borrow();
531            let entry = entries.get((id >> 8 & 0xff) as usize)?.as_ref()?;
532            entry
533                .instances
534                .get((id & 0xff) as usize)
535                .map(|(_, pose)| pose.clone())
536        })
537    }
538}