ticktime/
lib.rs

1use std::fmt;
2use std::fmt::Formatter;
3use crate::event::{TickTimeEvent, TicketTimeEventValue};
4
5pub mod event;
6mod lib_tests;
7
8const LUNAR_MONTH_DURATION: usize = 30;
9const LUNAR_YEAR_DURATION: usize = LUNAR_MONTH_DURATION * 12;
10
11/// The way the in game datetime will be handled
12#[derive(Clone, Debug)]
13pub enum TickTimeType {
14    /// The date and time is like on the planet earth (12 months, 24 hours a day, 60 minutes an hour, 60 seconds a minute)
15    EarthLike {
16        /// How much seconds represent a tick. Should be minimum 1.
17        seconds_per_tick: usize,
18        /// Which kind of calendar to compute values
19        month_type: EarthLikeMonthType,
20    },
21    /// A configurable date and time type. An hour will still be 60 minutes and a minute 60 seconds.
22    /// Note that sum of `season_duration` and `months_durations` must match to be consistent.
23    Custom {
24        /// How much seconds represent a tick. Should be minimum 1.
25        seconds_per_tick: usize,
26        /// The duration of a day
27        hours_in_a_day: usize,
28        /// A list of month durations.
29        months_durations: Vec<usize>,
30        /// A list of seasons durations.
31        seasons_durations: Vec<usize>,
32        /// duration of a single week.
33        week_duration: usize,
34    },
35}
36
37/// List of available month type for an Earth-like calendar
38#[derive(Clone, Debug)]
39pub enum EarthLikeMonthType {
40    /// A simple mode where each month is 30 days long
41    Lunar,
42    /// A mode where real month duration will be computed as long as leap years
43    Real,
44}
45
46/// Options to give to `TickTime` to enable/configure features
47#[derive(Clone, Debug)]
48pub struct TickTimeOptions {
49    /// Type of time to use when computing values to display
50    pub tick_time_type: TickTimeType,
51    /// Flag to decide whether or not the tick() function compute and returns update events
52    pub compute_events: bool,
53}
54
55#[derive(Clone, Debug, Default)]
56struct TickTimeValue {
57    /// Computed year, according to the tick_time_type
58    year: usize,
59    /// Computed season, according to the tick_time_type
60    season: usize,
61    /// Computed month, according to the tick_time_type
62    month: usize,
63    /// Computed week, according to the tick_time_type
64    week: usize,
65    /// Computed day, according to the tick_time_type
66    day: usize,
67    /// Computed hour, according to the tick_time_type
68    hour: usize,
69    /// Computed minute, according to the tick_time_type
70    minute: usize,
71    /// Computed second, according to the tick_time_type
72    second: usize,
73}
74
75/// A `TickTime` helps to keep track of the current tick in the game.
76/// Following a `TickTimeType`, it will translate the current tick to
77/// a list of computed values, representing year, season, month...
78#[derive(Clone, Debug)]
79pub struct TickTime {
80    /// Options to configure / enable / disable features from the computing step
81    options: TickTimeOptions,
82    /// Number of tick since the beginning of the game.
83    current_tick: usize,
84    /// Computed values from the tick method
85    values: TickTimeValue,
86    /// Last tick Computed values from the tick method
87    old_values: TickTimeValue,
88}
89
90impl TickTime {
91    /// Initialise a TickTime with a given tick (usefull to reload the state of a save) and
92    /// a `TickTimeType`.
93    pub fn init(current_tick: usize, options: TickTimeOptions) -> Result<Self, &'static str> {
94        if let Err(e) = verify_tick_time_type_values(&options.tick_time_type) {
95            return Err(e);
96        }
97        let mut tick_time = TickTime {
98            current_tick,
99            options,
100            values: Default::default(),
101            old_values: Default::default()
102        };
103        tick_time.apply_current_tick();
104        Ok(tick_time)
105    }
106
107    /// Add a tick to the current_tick. Will also compute values
108    pub fn tick(&mut self) -> Option<TickTimeEvent> {
109        self.current_tick += 1;
110        self.apply_current_tick();
111        if self.options.compute_events {
112            Some(self.compute_event())
113        }else{
114            None
115        }
116    }
117
118    /// Return a tuple of computed usizes for (year, season, month, day, hour, minute, second)
119    pub fn values(&self) -> (usize, usize, usize, usize, usize, usize, usize, usize) {
120        (
121            self.values.year,
122            self.values.season,
123            self.values.week,
124            self.values.month,
125            self.values.day,
126            self.values.hour,
127            self.values.minute,
128            self.values.second,
129        )
130    }
131
132    fn compute_event(&self) -> TickTimeEvent {
133        let mut event = TickTimeEvent::default();
134        let mut update_level = 0;
135
136        if self.old_values.year != self.values.year {
137            update_level += 1;
138            event.year_update = Some(TicketTimeEventValue{ old_value: self.old_values.year, new_value: self.values.year });
139        }
140
141        if update_level > 0 || self.old_values.season != self.values.season {
142            event.season_update = Some(TicketTimeEventValue{ old_value: self.old_values.season, new_value: self.values.season });
143        }
144
145        if update_level > 0 || self.old_values.week != self.values.week {
146            event.week_update = Some(TicketTimeEventValue{ old_value: self.old_values.week, new_value: self.values.week });
147        }
148
149        if update_level > 0 || self.old_values.month != self.values.month {
150            update_level += 1;
151            event.month_update = Some(TicketTimeEventValue{ old_value: self.old_values.month, new_value: self.values.month });
152        }
153
154        if update_level > 0 || self.old_values.day != self.values.day {
155            update_level += 1;
156            event.day_update = Some(TicketTimeEventValue{ old_value: self.old_values.day, new_value: self.values.day });
157        }
158
159        if update_level > 0 || self.old_values.hour != self.values.hour {
160            update_level += 1;
161            event.hour_update = Some(TicketTimeEventValue{ old_value: self.old_values.hour, new_value: self.values.hour });
162        }
163
164        if update_level > 0 || self.old_values.minute != self.values.minute {
165            update_level += 1;
166            event.minute_update = Some(TicketTimeEventValue{ old_value: self.old_values.minute, new_value: self.values.minute });
167        }
168
169        if update_level > 0 || self.old_values.second != self.values.second {
170            event.second_update = Some(TicketTimeEventValue{ old_value: self.old_values.second, new_value: self.values.second });
171        }
172
173        event
174    }
175
176    /// Total tick count
177    pub fn current_tick(&self) -> usize {
178        self.current_tick
179    }
180
181    /// Return the read only computed year
182    pub fn year(&self) -> usize {
183        self.values.year
184    }
185
186    /// Return the read only computed month
187    pub fn month(&self) -> usize {
188        self.values.month
189    }
190
191    /// Return the read only computed season
192    pub fn season(&self) -> usize {
193        self.values.season
194    }
195
196    /// Return the read only computed week
197    pub fn week(&self) -> usize {
198        self.values.week
199    }
200
201    /// Return the read only computed day
202    pub fn day(&self) -> usize {
203        self.values.day
204    }
205
206    /// Return the read only computed hour
207    pub fn hour(&self) -> usize {
208        self.values.hour
209    }
210
211    /// Return the read only computed minute
212    pub fn minute(&self) -> usize {
213        self.values.minute
214    }
215
216    /// Return the read only computed second
217    pub fn second(&self) -> usize {
218        self.values.second
219    }
220
221    fn apply_current_tick(&mut self) {
222        if self.options.compute_events {
223            self.old_values = self.values.clone();
224        }
225        match self.options.tick_time_type {
226            TickTimeType::EarthLike { .. } => { self.compute_earthlike_time(); }
227            TickTimeType::Custom { .. } => { self.compute_custom_date_time_values() }
228        }
229    }
230
231    fn compute_earthlike_time(&mut self) {
232        if let TickTimeType::EarthLike {
233            seconds_per_tick,
234            month_type,
235        } = &self.options.tick_time_type
236        {
237            let total_seconds = self.current_tick * seconds_per_tick;
238            self.values.second = total_seconds % 60;
239            self.values.minute = (total_seconds / 60) % 60;
240            self.values.hour = (total_seconds / 3600) % 24;
241            let total_days = total_seconds / 86400;
242            let (day, week, month, season, year) = match month_type {
243                EarthLikeMonthType::Lunar => compute_lunar_calendar_value(total_days),
244                EarthLikeMonthType::Real => compute_real_calendar_value(total_days)
245            };
246            self.values.day = day;
247            self.values.month = month;
248            self.values.week = week;
249            self.values.season = season;
250            self.values.year = year;
251        }
252    }
253
254    fn compute_custom_date_time_values(&mut self) {
255        if let TickTimeType::Custom {
256            seconds_per_tick, hours_in_a_day, months_durations, seasons_durations, week_duration
257        } = &self.options.tick_time_type
258        {
259            let total_seconds = self.current_tick * seconds_per_tick;
260            self.values.second = total_seconds % 60;
261            self.values.minute = (total_seconds / 60) % 60;
262            self.values.hour = (total_seconds / 3600) % hours_in_a_day;
263            let total_days = total_seconds / 3600 / hours_in_a_day;
264            let year_duration: usize = months_durations.iter().sum();
265            let (day, week, month, season, year) = {
266                let (day, current_year) = (total_days % year_duration, total_days / year_duration);
267
268                let (month, day_of_month) = find_correct_index_and_day_in_section(
269                    day,
270                    months_durations.len(),
271                    months_durations,
272                );
273
274                let (season, _) = find_correct_index_and_day_in_section(
275                    day,
276                    seasons_durations.len(),
277                    seasons_durations,
278                );
279
280                (day_of_month, day / week_duration, month, season % 4, current_year)
281            };
282            self.values.day = day;
283            self.values.week = week;
284            self.values.month = month;
285            self.values.season = season;
286            self.values.year = year;
287        }
288    }
289}
290
291impl fmt::Display for TickTime {
292    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
293        write!(f, "Tick time: [ Current tick: {}, Year: {}, Season: {}, Week: {} Month: {}, Day: {}, Hour: {}, Minute: {}, Second: {}]",
294               self.current_tick, self.year(), self.season(), self.week(), self.month(), self.day(), self.hour(), self.minute(), self.second())
295    }
296}
297
298fn compute_real_calendar_value(total_days: usize) -> (usize, usize, usize, usize, usize) {
299    let (day, current_year, is_leap_year) =
300        normalize_total_day_to_year_information(total_days);
301
302    let (month, day_of_month) = find_correct_index_and_day_in_section(
303        day,
304        12,
305        &get_month_duration(is_leap_year),
306    );
307
308    let (season, _) = find_correct_index_and_day_in_section(
309        day,
310        4,
311        &get_season_duration(is_leap_year),
312    );
313
314    (day_of_month, day / 7, month, season % 4, current_year)
315}
316
317fn compute_lunar_calendar_value(total_days: usize) -> (usize, usize, usize, usize, usize) {
318    (
319        total_days % LUNAR_YEAR_DURATION % LUNAR_MONTH_DURATION,
320        total_days % LUNAR_YEAR_DURATION / 7,
321        total_days % LUNAR_YEAR_DURATION / LUNAR_MONTH_DURATION,
322        (total_days % LUNAR_YEAR_DURATION) / (LUNAR_YEAR_DURATION / 4),
323        total_days / LUNAR_YEAR_DURATION,
324    )
325}
326
327fn get_month_duration(is_leap_year: bool) -> Vec<usize> {
328    vec![31, if is_leap_year { 29 } else { 28 }, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
329}
330
331fn get_season_duration(is_leap_year: bool) -> Vec<usize> {
332    vec![if is_leap_year { 81 } else { 80 }, 92, 92, 91]
333}
334
335fn verify_tick_time_type_values(tick_time_type: &TickTimeType) -> Result<(), &'static str> {
336    match tick_time_type {
337        TickTimeType::EarthLike {
338            seconds_per_tick, ..
339        } => {
340            if *seconds_per_tick == 0 {
341                return Err("The minimum value for EarthLike::seconds_per_tick is 1");
342            }
343        }
344        TickTimeType::Custom {
345            seconds_per_tick, hours_in_a_day: _, months_durations, seasons_durations, ..
346        } => {
347            if *seconds_per_tick == 0 {
348                return Err("The minimum value for Custom::seconds_per_tick is 1");
349            }
350            if months_durations.iter().sum::<usize>() != seasons_durations.iter().sum::<usize>() {
351                return Err("The sum of values of Custom::months_durations and Custom::season_duration should be the same to keep consistent");
352            }
353        }
354    }
355    Ok(())
356}
357
358fn normalize_total_day_to_year_information(total_days: usize) -> (usize, usize, bool) {
359    let base_4_year_days = total_days % 1461;
360    let base_4_year_start = (total_days / 1461) * 4;
361    match base_4_year_days {
362        0..=365 => (base_4_year_days, base_4_year_start, true),
363        366..=730 => (base_4_year_days - 366, base_4_year_start + 1, false),
364        731..=1095 => (base_4_year_days - 731, base_4_year_start + 2, false),
365        _ => (base_4_year_days - 1095, base_4_year_start + 3, false),
366    }
367}
368
369fn find_correct_index_and_day_in_section(
370    day: usize,
371    max: usize,
372    array: &Vec<usize>,
373) -> (usize, usize) {
374    let (mut day_counter, mut stop, mut index) = (day, false, 0);
375    while !stop && index < max {
376        let next_month_duration = array[index];
377        if day_counter < next_month_duration {
378            stop = true;
379        } else {
380            day_counter -= next_month_duration;
381            index += 1;
382        }
383    }
384    (index, day_counter)
385}