1use std::fmt;
2
3use crate::lockstep::checksum;
4use crate::{Board, Call, Display, Entry, Error, Range, Sort, SteamApi, Upload, User};
5
6const DAY: u64 = 86_400;
7
8#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
9pub struct Date {
10 year: u64,
11 month: u8,
12 day: u8,
13}
14
15#[derive(Clone, Copy, Debug, PartialEq, Eq)]
16pub enum Clock {
17 Server,
18 System,
19}
20
21#[derive(Clone, Copy, Debug, PartialEq, Eq)]
22pub struct Today {
23 pub date: Date,
24 pub clock: Clock,
25}
26
27#[derive(Clone, Copy, Debug, PartialEq, Eq)]
28pub enum Repeat {
29 FirstOnly,
30 Best,
31}
32
33#[derive(Clone, Debug, PartialEq, Eq)]
34pub struct Daily {
35 pub salt: String,
36 pub prefix: String,
37 pub repeat: Repeat,
38 pub sort: Sort,
39 pub display: Display,
40}
41
42impl Date {
43 pub fn new(year: u64, month: u8, day: u8) -> Option<Self> {
44 if !(1..=12).contains(&month) || day == 0 || day > days_in(year, month) {
45 return None;
46 }
47 Some(Self { year, month, day })
48 }
49
50 pub fn from_unix(seconds: u64) -> Self {
51 let z = seconds / DAY + 719_468;
52 let era = z / 146_097;
53 let doe = z - era * 146_097;
54 let yoe = (doe - doe / 1_460 + doe / 36_524 - doe / 146_096) / 365;
55 let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
56 let mp = (5 * doy + 2) / 153;
57 let day = doy - (153 * mp + 2) / 5 + 1;
58 let month = if mp < 10 { mp + 3 } else { mp - 9 };
59 let year = yoe + era * 400 + u64::from(month <= 2);
60 Self {
61 year,
62 month: month as u8,
63 day: day as u8,
64 }
65 }
66
67 pub fn year(self) -> u64 {
68 self.year
69 }
70
71 pub fn month(self) -> u8 {
72 self.month
73 }
74
75 pub fn day(self) -> u8 {
76 self.day
77 }
78}
79
80impl fmt::Display for Date {
81 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
82 write!(f, "{:04}-{:02}-{:02}", self.year, self.month, self.day)
83 }
84}
85
86fn days_in(year: u64, month: u8) -> u8 {
87 match month {
88 2 if year.is_multiple_of(4) && (!year.is_multiple_of(100) || year.is_multiple_of(400)) => {
89 29
90 }
91 2 => 28,
92 4 | 6 | 9 | 11 => 30,
93 _ => 31,
94 }
95}
96
97pub fn daily_seed(salt: &str, date: Date) -> u64 {
98 let mut bytes = Vec::with_capacity(salt.len() + 11);
99 bytes.extend_from_slice(salt.as_bytes());
100 bytes.push(0);
101 bytes.extend_from_slice(date.to_string().as_bytes());
102 checksum(&bytes)
103}
104
105impl Today {
106 pub fn pick(server: Option<u64>, system: u64) -> Self {
107 match server {
108 Some(seconds) => Self {
109 date: Date::from_unix(seconds),
110 clock: Clock::Server,
111 },
112 None => Self {
113 date: Date::from_unix(system),
114 clock: Clock::System,
115 },
116 }
117 }
118}
119
120impl Daily {
121 pub fn new(salt: &str, prefix: &str, repeat: Repeat, sort: Sort, display: Display) -> Self {
122 Self {
123 salt: salt.to_string(),
124 prefix: prefix.to_string(),
125 repeat,
126 sort,
127 display,
128 }
129 }
130
131 pub fn today(&self, steam: &impl SteamApi, system: u64) -> Today {
132 Today::pick(steam.server_time().ok().flatten(), system)
133 }
134
135 pub fn seed(&self, date: Date) -> u64 {
136 daily_seed(&self.salt, date)
137 }
138
139 pub fn board_name(&self, date: Date) -> String {
140 format!("{}-{date}", self.prefix)
141 }
142
143 pub fn open(&self, steam: &mut impl SteamApi, date: Date) -> Result<Call, Error> {
144 steam.find_or_create_board(&self.board_name(date), self.sort, self.display)
145 }
146
147 pub fn marker(&self) -> i32 {
148 match self.sort {
149 Sort::Descending => 0,
150 Sort::Ascending => i32::MAX,
151 }
152 }
153
154 pub fn start(&self, steam: &mut impl SteamApi, board: Board) -> Result<Call, Error> {
155 steam.upload_score(board, self.marker(), &[], Upload::KeepBest)
156 }
157
158 pub fn check(&self, steam: &mut impl SteamApi, board: Board) -> Result<Call, Error> {
159 steam.download_scores(
160 board,
161 Range::AroundUser {
162 before: 0,
163 after: 0,
164 },
165 0,
166 )
167 }
168
169 pub fn used(entries: &[Entry], me: User) -> bool {
170 entries.iter().any(|entry| entry.user == me)
171 }
172
173 pub fn upload(&self, date: Date, last_counted: Option<Date>) -> Option<Upload> {
174 match self.repeat {
175 Repeat::FirstOnly if last_counted == Some(date) => None,
176 Repeat::FirstOnly | Repeat::Best => Some(Upload::KeepBest),
177 }
178 }
179
180 pub fn submit(
181 &self,
182 steam: &mut impl SteamApi,
183 board: Board,
184 date: Date,
185 last_counted: Option<Date>,
186 score: i32,
187 details: &[i32],
188 ) -> Result<Option<Call>, Error> {
189 match self.upload(date, last_counted) {
190 Some(upload) => steam.upload_score(board, score, details, upload).map(Some),
191 None => Ok(None),
192 }
193 }
194}