twgame_core/
database.rs

1use twsnap::time::Duration;
2
3#[derive(Clone, Debug, PartialEq, Eq)]
4pub struct FinishTee {
5    // TODO: no allocation
6    pub name: String,
7    pub time: Duration,
8}
9
10impl FinishTee {
11    pub fn new_from_str(name: &str, time: Duration) -> Self {
12        // TODO: stack string somehow
13        // let mut n = [0u8; 16];
14        // name.bytes().zip(n.iter_mut()).for_each(|(b, ptr)| *ptr = b);
15        Self {
16            name: name.to_owned(),
17            time,
18        }
19    }
20}
21
22#[derive(Clone, Debug, PartialEq, Eq)]
23pub struct FinishTeam {
24    pub team: i32,
25    pub names: Vec<String>,
26    pub time: Duration,
27}
28
29#[derive(Clone, Debug)]
30pub struct SaveTeam {
31    buf: String,
32}
33
34impl SaveTeam {
35    pub fn compat_eq(&self, expected_compat: &Self) -> bool {
36        let mut got = self.buf.split('\n');
37        let mut expected = expected_compat.buf.split('\n');
38
39        let (g, e) = match (got.next(), expected.next()) {
40            (Some(g), Some(e)) => (g, e),
41            (None, None) => return true,
42            _ => return false,
43        };
44        SaveTeam::compare_variables("header", 0, g, e, &TEAM_DESC);
45        let (g_tees, g_switchers) = SaveTeam::parse_head(g);
46        let (e_tees, e_switchers) = SaveTeam::parse_head(e);
47        if g_tees != e_tees || g_switchers != e_switchers {
48            return false;
49        }
50        for _ in 0..g_tees {
51            let g = got.next().unwrap_or("");
52            let e = expected.next().unwrap_or("");
53            if !SaveTeam::are_tees_same_compat(g, e) {
54                return false;
55            }
56        }
57        for _ in 0..g_switchers {
58            let g = got.next().unwrap_or("");
59            let e = expected.next().unwrap_or("");
60            if !SaveTeam::are_switchers_same_compat(g, e) {
61                return false;
62            }
63        }
64        true
65    }
66}
67
68impl SaveTeam {
69    pub fn buf(&self) -> &str {
70        &self.buf
71    }
72}
73
74#[derive(Clone, Debug)]
75pub enum DatabaseWrite {
76    FinishTee(FinishTee),
77    FinishTeam(FinishTeam),
78    // TODO: send out and receive TeamId
79    Save(i32, SaveTeam),
80    Load(i32),
81}
82
83#[derive(Clone, Debug, PartialEq, Eq)]
84pub enum Finishes {
85    FinishTee(FinishTee),
86    // team_id and tees
87    FinishTeam(FinishTeam),
88}
89
90const TEAM_DESC: [&str; 5] = [
91    "m_TeamState",
92    "m_MembersCount",
93    "m_HighestSwitchNumber",
94    "m_TeamLocked",
95    "m_Practice",
96];
97
98const TEE_DESC: [&str; 115] = [
99    "m_aName",
100    "m_Alive",
101    "m_Paused",
102    "m_NeededFaketuning",
103    "m_TeeFinished",
104    "m_IsSolo",
105    "m_aWeapons[0].m_AmmoRegenStart",
106    "m_aWeapons[0].m_Ammo",
107    "m_aWeapons[0].m_Ammocost",
108    "m_aWeapons[0].m_Got",
109    "m_aWeapons[1].m_AmmoRegenStart",
110    "m_aWeapons[1].m_Ammo",
111    "m_aWeapons[1].m_Ammocost",
112    "m_aWeapons[1].m_Got",
113    "m_aWeapons[2].m_AmmoRegenStart",
114    "m_aWeapons[2].m_Ammo",
115    "m_aWeapons[2].m_Ammocost",
116    "m_aWeapons[2].m_Got",
117    "m_aWeapons[3].m_AmmoRegenStart",
118    "m_aWeapons[3].m_Ammo",
119    "m_aWeapons[3].m_Ammocost",
120    "m_aWeapons[3].m_Got",
121    "m_aWeapons[4].m_AmmoRegenStart",
122    "m_aWeapons[4].m_Ammo",
123    "m_aWeapons[4].m_Ammocost",
124    "m_aWeapons[4].m_Got",
125    "m_aWeapons[5].m_AmmoRegenStart",
126    "m_aWeapons[5].m_Ammo",
127    "m_aWeapons[5].m_Ammocost",
128    "m_aWeapons[5].m_Got",
129    "m_LastWeapon",
130    "m_QueuedWeapon",
131    // tee states
132    "m_EndlessJump",
133    "m_Jetpack",
134    "m_NinjaJetpack",
135    "m_FreezeTime",
136    "m_FreezeStart",
137    "m_DeepFrozen",
138    "m_EndlessHook",
139    "m_DDRaceState",
140    "m_HitDisabledFlags",
141    "m_CollisionEnabled",
142    "m_TuneZone",
143    "m_TuneZoneOld",
144    "m_HookHitEnabled",
145    "m_Time",
146    "(int)m_Pos.x",
147    "(int)m_Pos.y",
148    "(int)m_PrevPos.x",
149    "(int)m_PrevPos.y",
150    "m_TeleCheckpoint",
151    "m_LastPenalty",
152    "(int)m_CorePos.x",
153    "(int)m_CorePos.y",
154    "m_Vel.x",
155    "m_Vel.y",
156    "m_ActiveWeapon",
157    "m_Jumped",
158    "m_JumpedTotal",
159    "m_Jumps",
160    "(int)m_HookPos.x",
161    "(int)m_HookPos.y",
162    "m_HookDir.x",
163    "m_HookDir.y",
164    "(int)m_HookTeleBase.x",
165    "(int)m_HookTeleBase.y",
166    "m_HookTick",
167    "m_HookState",
168    "m_TimeCpBroadcastEndTime",
169    "m_LastTimeCp",
170    "m_LastTimeCpBroadcasted",
171    "m_aCurrentTimeCp[0]",
172    "m_aCurrentTimeCp[1]",
173    "m_aCurrentTimeCp[2]",
174    "m_aCurrentTimeCp[3]",
175    "m_aCurrentTimeCp[4]",
176    "m_aCurrentTimeCp[5]",
177    "m_aCurrentTimeCp[6]",
178    "m_aCurrentTimeCp[7]",
179    "m_aCurrentTimeCp[8]",
180    "m_aCurrentTimeCp[9]",
181    "m_aCurrentTimeCp[10]",
182    "m_aCurrentTimeCp[11]",
183    "m_aCurrentTimeCp[12]",
184    "m_aCurrentTimeCp[13]",
185    "m_aCurrentTimeCp[14]",
186    "m_aCurrentTimeCp[15]",
187    "m_aCurrentTimeCp[16]",
188    "m_aCurrentTimeCp[17]",
189    "m_aCurrentTimeCp[18]",
190    "m_aCurrentTimeCp[19]",
191    "m_aCurrentTimeCp[20]",
192    "m_aCurrentTimeCp[21]",
193    "m_aCurrentTimeCp[22]",
194    "m_aCurrentTimeCp[23]",
195    "m_aCurrentTimeCp[24]",
196    "m_NotEligibleForFinish",
197    "m_HasTelegunGun",
198    "m_HasTelegunLaser",
199    "m_HasTelegunGrenade",
200    "m_aGameUuid",
201    "HookedPlayer",
202    "m_NewHook",
203    "m_InputDirection",
204    "m_InputJump",
205    "m_InputFire",
206    "m_InputHook",
207    "m_ReloadTimer",
208    "m_TeeStarted",
209    "m_LiveFrozen",
210    "m_Ninja.m_ActivationDir.x",
211    "m_Ninja.m_ActivationDir.y",
212    "m_Ninja.m_ActivationTick",
213    "m_Ninja.m_CurrentMoveTime",
214    "m_Ninja.m_OldVelAmount",
215];
216
217const SWITCHER_DESC: [&str; 3] = [
218    "m_pSwitchers[i].m_Status",
219    "m_pSwitchers[i].m_EndTime",
220    "m_pSwitchers[i].m_Type",
221];
222
223impl SaveTeam {
224    pub fn from_buf(buf: &[u8]) -> Self {
225        Self {
226            buf: String::from_utf8_lossy(buf).to_string(),
227        }
228    }
229
230    pub fn from_string(buf: String) -> Self {
231        Self { buf }
232    }
233
234    fn print_variables(part: &str, id: u32, values: &str, variables: &[&str]) {
235        let mut vals = values.split('\t');
236        for (i, var) in variables.iter().enumerate() {
237            let v = vals.next();
238            println!("{part}[{id}] {var} ({i}): {v:?}");
239        }
240    }
241
242    fn compare_format_variables(
243        part: &str,
244        id: u32,
245        got: &str,
246        expected: &str,
247        variables: &[&str],
248    ) -> Vec<String> {
249        let mut strings = Vec::new();
250        let mut got = got.split('\t');
251        let mut expected = expected.split('\t');
252        for (i, var) in variables.iter().enumerate() {
253            let g = got.next();
254            let e = expected.next();
255            if g != e {
256                strings.push(format!(
257                    "{part}[{id}] {var} ({i}): (got) {g:?} != {e:?} (expected)"
258                ));
259            }
260        }
261        strings
262    }
263
264    fn compare_variables(part: &str, id: u32, got: &str, expected: &str, variables: &[&str]) {
265        for msg in Self::compare_format_variables(part, id, got, expected, variables) {
266            println!("{msg}");
267        }
268    }
269
270    fn are_tees_same_compat(got: &str, expected: &str) -> bool {
271        // whether this is relevant is decided later by HookState and HookedPlayer
272        let mut hook_tick_correct = true;
273        let mut got = got.split('\t');
274        let expected = expected.split('\t');
275        for (el, e) in expected.enumerate() {
276            let g = got.next();
277            if matches!(
278                el,
279                3 // matches m_NeededFakeTuning
280                    | 7 | 11 | 15 | 19 | 23 | 27 // ammo
281                    | 68 // m_TimeCpBroadcastEndTime
282            ) {
283                continue;
284            }
285            if el == 67 && !hook_tick_correct && e == "4" {
286                // m_HookState == HOOK_FLYING
287                return false;
288            }
289            if el == 101 && !hook_tick_correct && e != "-1" {
290                // HookedPlayer != -1
291                return false;
292            }
293            if g.map(|g| g != e).unwrap_or(true) {
294                // m_HookTick
295                if el == 66 {
296                    // store that hook tick is incorrect whether that is relevant depends on whether
297                    // the hook is currently flying or attached to a Tee
298                    hook_tick_correct = false;
299                    continue;
300                }
301                return false;
302            }
303        }
304        true
305    }
306
307    fn are_switchers_same_compat(got: &str, expected: &str) -> bool {
308        let mut got = got.split('\t');
309        let expected = expected.split('\t');
310        for e in expected {
311            let g = got.next();
312            if g.map(|g| g != e).unwrap_or(true) {
313                return false;
314            }
315        }
316        true
317    }
318
319    fn parse_head(head: &str) -> (u32, u32) {
320        let mut head = head.split('\t');
321        let _ = head.next();
322        let num_tees = head
323            .next()
324            .map(|el| el.parse::<u32>().unwrap_or(0))
325            .unwrap_or(0);
326        let num_switchers = head
327            .next()
328            .map(|el| el.parse::<u32>().unwrap_or(0))
329            .unwrap_or(0);
330        (num_tees, num_switchers)
331    }
332
333    pub fn format_diff(&self, expected: &Self) -> Vec<String> {
334        let mut diffs = Vec::new();
335        let mut got = self.buf.split('\n');
336        let mut expected = expected.buf.split('\n');
337
338        let (g, e) = match (got.next(), expected.next()) {
339            (Some(g), Some(e)) => (g, e),
340            (Some(g), None) => {
341                diffs.push(format!("expected no header got {g}"));
342                return diffs;
343            }
344            (None, Some(e)) => {
345                diffs.push(format!("expected header {e}, got nothing"));
346                return diffs;
347            }
348            (None, None) => {
349                return diffs;
350            }
351        };
352        diffs.extend(SaveTeam::compare_format_variables(
353            "header", 0, g, e, &TEAM_DESC,
354        ));
355        let (mut g_tees, mut g_switchers) = SaveTeam::parse_head(g);
356        let (mut e_tees, mut e_switchers) = SaveTeam::parse_head(e);
357        let mut tee = 0;
358        while g_tees > 0 || e_tees > 0 {
359            let g = if g_tees > 0 {
360                let g = got.next().unwrap_or("");
361                g_tees -= 1;
362                g
363            } else {
364                ""
365            };
366            let e = if e_tees > 0 {
367                let e = expected.next().unwrap_or("");
368                e_tees -= 1;
369                e
370            } else {
371                ""
372            };
373            diffs.extend(SaveTeam::compare_format_variables(
374                "tee", tee, g, e, &TEE_DESC,
375            ));
376            tee += 1;
377        }
378        let mut switchers = 0;
379        while g_switchers > 0 || e_switchers > 0 {
380            let g = if g_switchers > 0 {
381                let g = got.next().unwrap_or("");
382                g_switchers -= 1;
383                g
384            } else {
385                ""
386            };
387            let e = if e_switchers > 0 {
388                let e = expected.next().unwrap_or("");
389                e_switchers -= 1;
390                e
391            } else {
392                ""
393            };
394            diffs.extend(SaveTeam::compare_format_variables(
395                "switcher",
396                switchers,
397                g,
398                e,
399                &SWITCHER_DESC,
400            ));
401            switchers += 1;
402        }
403        if let Some(remaining) = expected.next() {
404            diffs.push(format!(
405                "unexpected remaining line(s) in expected: {}",
406                remaining
407            ));
408        }
409        if let Some(remaining) = got.next() {
410            diffs.push(format!(
411                "unexpected remaining line(s) in got: {}",
412                remaining
413            ));
414        }
415        diffs
416    }
417
418    pub fn print_diff(&self, expected: &Self) {
419        for diff in self.format_diff(expected) {
420            println!("{diff}");
421        }
422    }
423
424    pub fn pretty_print(&self) {
425        let mut parts = self.buf.split('\n');
426        let Some(header) = parts.next() else {
427            return;
428        };
429        SaveTeam::print_variables("header", 0, header, &TEAM_DESC);
430        let (num_tees, num_switchers) = SaveTeam::parse_head(header);
431        for tee_id in 0..num_tees {
432            SaveTeam::print_variables("tee", tee_id, parts.next().unwrap_or(""), &TEE_DESC);
433        }
434        for switch_id in 0..num_switchers {
435            SaveTeam::print_variables(
436                "switcher",
437                switch_id,
438                parts.next().unwrap_or(""),
439                &SWITCHER_DESC,
440            );
441        }
442    }
443}
444
445#[derive(Clone, Debug)]
446pub enum DatabaseResult {
447    LoadSuccess(i32, SaveTeam),
448    LoadFailure(i32),
449    SaveSuccess(i32),
450    SaveFailure(i32),
451}
452
453impl DatabaseResult {
454    pub fn team(&self) -> i32 {
455        match self {
456            DatabaseResult::LoadSuccess(team, _) => *team,
457            DatabaseResult::LoadFailure(team) => *team,
458            DatabaseResult::SaveSuccess(team) => *team,
459            DatabaseResult::SaveFailure(team) => *team,
460        }
461    }
462}
463
464impl DatabaseWrite {
465    pub fn finish_tee(name: &str, time: Duration) -> Self {
466        Self::FinishTee(FinishTee::new_from_str(name, time))
467    }
468}