steel_registry/
timeline.rs1use rustc_hash::FxHashMap;
2use simdnbt::ToNbtTag;
3use simdnbt::owned::NbtTag;
4use steel_utils::Identifier;
5
6use crate::RegistryTags;
7use crate::world_clock::WorldClockRef;
8
9#[derive(Debug, Clone)]
10pub enum KeyframeValue {
11 Float(f32),
12 Int(i32),
13 Bool(bool),
14 String(&'static str),
15}
16
17#[derive(Debug, Clone)]
18pub enum Ease {
19 Named(&'static str),
20 CubicBezier([f32; 4]),
21}
22
23#[derive(Debug)]
24pub struct Keyframe {
25 pub ticks: i64,
26 pub value: KeyframeValue,
27}
28
29#[derive(Debug)]
30pub struct Track {
31 pub name: &'static str,
32 pub ease: Option<Ease>,
33 pub modifier: Option<&'static str>,
34 pub keyframes: &'static [Keyframe],
35}
36
37#[derive(Debug)]
40pub struct TimeMarker {
41 pub key: Identifier,
42 pub ticks: i32,
43 pub show_in_commands: Option<bool>,
44}
45
46impl TimeMarker {
47 #[must_use]
49 pub fn resolve_time_to_move_to(&self, total_ticks: i64, period_ticks: Option<i32>) -> i64 {
50 let Some(period_ticks) = period_ticks else {
51 return i64::from(self.ticks);
52 };
53 let period_ticks = i64::from(period_ticks);
54 let duration = i64::from(self.ticks) - total_ticks % period_ticks;
55 total_ticks.wrapping_add(if duration > 0 {
56 duration
57 } else {
58 period_ticks + duration
59 })
60 }
61}
62
63#[derive(Debug)]
65pub struct Timeline {
66 pub key: Identifier,
67 pub clock: WorldClockRef,
68 pub period_ticks: Option<i32>,
69 pub tracks: &'static [Track],
70 pub time_markers: &'static [TimeMarker],
71}
72
73impl Timeline {
74 #[must_use]
76 pub fn current_ticks(&self, total_ticks: i64) -> i64 {
77 self.period_ticks
78 .map_or(total_ticks, |period| total_ticks % i64::from(period))
79 }
80
81 #[must_use]
83 #[expect(
84 clippy::cast_possible_truncation,
85 reason = "vanilla narrows the completed long period count to a signed int"
86 )]
87 pub fn period_count(&self, total_ticks: i64) -> i32 {
88 self.period_ticks
89 .map_or(0, |period| (total_ticks / i64::from(period)) as i32)
90 }
91}
92
93impl ToNbtTag for &Timeline {
94 fn to_nbt_tag(self) -> NbtTag {
95 use simdnbt::owned::{NbtCompound, NbtList, NbtTag};
96 let mut compound = NbtCompound::new();
97
98 compound.insert("clock", self.clock.key.to_string().as_str());
99 if let Some(pt) = self.period_ticks {
100 compound.insert("period_ticks", pt);
101 }
102
103 let mut tracks_compound = NbtCompound::new();
104 for track in self.tracks {
105 let mut tc = NbtCompound::new();
106 if let Some(ease) = &track.ease {
107 match ease {
108 Ease::Named(s) => tc.insert("ease", *s),
109 Ease::CubicBezier(coeffs) => {
110 let mut ec = NbtCompound::new();
111 ec.insert(
112 "cubic_bezier",
113 NbtTag::List(NbtList::Float(coeffs.to_vec())),
114 );
115 tc.insert("ease", NbtTag::Compound(ec));
116 }
117 }
118 }
119 if let Some(modifier) = track.modifier {
120 tc.insert("modifier", modifier);
121 }
122 let kf_compounds: Vec<NbtCompound> = track
123 .keyframes
124 .iter()
125 .map(|kf| {
126 let mut kc = NbtCompound::new();
127 kc.insert("ticks", kf.ticks); match &kf.value {
129 KeyframeValue::Float(f) => kc.insert("value", *f),
130 KeyframeValue::Int(i) => kc.insert("value", *i),
131 KeyframeValue::Bool(b) => kc.insert("value", i8::from(*b)),
132 KeyframeValue::String(s) => kc.insert("value", *s),
133 }
134 kc
135 })
136 .collect();
137 tc.insert("keyframes", NbtTag::List(NbtList::Compound(kf_compounds)));
138 tracks_compound.insert(track.name, NbtTag::Compound(tc));
139 }
140 compound.insert("tracks", NbtTag::Compound(tracks_compound));
141
142 if !self.time_markers.is_empty() {
143 let mut mc = NbtCompound::new();
144 for marker in self.time_markers {
145 let key = marker.key.to_string();
146 match marker.show_in_commands {
147 None => mc.insert(key.as_str(), marker.ticks),
148 Some(show) => {
149 let mut mcc = NbtCompound::new();
150 mcc.insert("ticks", marker.ticks);
151 mcc.insert("show_in_commands", i8::from(show));
152 mc.insert(key.as_str(), NbtTag::Compound(mcc));
153 }
154 }
155 }
156 compound.insert("time_markers", NbtTag::Compound(mc));
157 }
158
159 NbtTag::Compound(compound)
160 }
161}
162
163pub type TimelineRef = &'static Timeline;
164
165pub struct TimelineRegistry {
166 timelines_by_id: Vec<TimelineRef>,
167 timelines_by_key: FxHashMap<Identifier, usize>,
168 tags: RegistryTags,
169 allows_registering: bool,
170}
171
172impl TimelineRegistry {
173 #[must_use]
174 pub fn new() -> Self {
175 Self {
176 timelines_by_id: Vec::new(),
177 timelines_by_key: FxHashMap::default(),
178 tags: RegistryTags::default(),
179 allows_registering: true,
180 }
181 }
182}
183
184crate::impl_standard_methods!(
185 TimelineRegistry,
186 TimelineRef,
187 timelines_by_id,
188 timelines_by_key,
189 allows_registering
190);
191
192crate::impl_registry!(
193 TimelineRegistry,
194 Timeline,
195 timelines_by_id,
196 timelines_by_key,
197 timelines
198);
199crate::impl_tagged_registry!(TimelineRegistry, timelines_by_key, "timeline");
200
201#[cfg(test)]
202mod tests {
203 use crate::vanilla_timelines::{DAY, EARLY_GAME};
204
205 #[test]
206 fn repeating_timeline_reports_current_tick_and_completed_periods() {
207 assert_eq!(DAY.current_ticks(25_000), 1_000);
208 assert_eq!(DAY.period_count(25_000), 1);
209 }
210
211 #[test]
212 fn non_repeating_timeline_uses_absolute_ticks_and_zero_periods() {
213 assert_eq!(EARLY_GAME.current_ticks(25_000), 25_000);
214 assert_eq!(EARLY_GAME.period_count(25_000), 0);
215 }
216}