use twsnap::time::Duration;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct FinishTee {
pub name: String,
pub time: Duration,
}
impl FinishTee {
pub fn new_from_str(name: &str, time: Duration) -> Self {
Self {
name: name.to_owned(),
time,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct FinishTeam {
pub team: i32,
pub names: Vec<String>,
pub time: Duration,
}
#[derive(Clone, Debug)]
pub struct SaveTeam {
buf: String,
}
impl SaveTeam {
pub fn compat_eq(&self, expected_compat: &Self) -> bool {
let mut got = self.buf.split('\n');
let mut expected = expected_compat.buf.split('\n');
let (g, e) = match (got.next(), expected.next()) {
(Some(g), Some(e)) => (g, e),
(None, None) => return true,
_ => return false,
};
SaveTeam::compare_variables("header", 0, g, e, &TEAM_DESC);
let (g_tees, g_switchers) = SaveTeam::parse_head(g);
let (e_tees, e_switchers) = SaveTeam::parse_head(e);
if g_tees != e_tees || g_switchers != e_switchers {
return false;
}
for _ in 0..g_tees {
let g = got.next().unwrap_or("");
let e = expected.next().unwrap_or("");
if !SaveTeam::are_tees_same_compat(g, e) {
return false;
}
}
for _ in 0..g_switchers {
let g = got.next().unwrap_or("");
let e = expected.next().unwrap_or("");
if !SaveTeam::are_switchers_same_compat(g, e) {
return false;
}
}
true
}
}
impl SaveTeam {
pub fn buf(&self) -> &str {
&self.buf
}
}
#[derive(Clone, Debug)]
pub enum DatabaseWrite {
FinishTee(FinishTee),
FinishTeam(FinishTeam),
Save(i32, SaveTeam),
Load(i32),
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Finishes {
FinishTee(FinishTee),
FinishTeam(FinishTeam),
}
const TEAM_DESC: [&str; 5] = [
"m_TeamState",
"m_MembersCount",
"m_HighestSwitchNumber",
"m_TeamLocked",
"m_Practice",
];
const TEE_DESC: [&str; 115] = [
"m_aName",
"m_Alive",
"m_Paused",
"m_NeededFaketuning",
"m_TeeFinished",
"m_IsSolo",
"m_aWeapons[0].m_AmmoRegenStart",
"m_aWeapons[0].m_Ammo",
"m_aWeapons[0].m_Ammocost",
"m_aWeapons[0].m_Got",
"m_aWeapons[1].m_AmmoRegenStart",
"m_aWeapons[1].m_Ammo",
"m_aWeapons[1].m_Ammocost",
"m_aWeapons[1].m_Got",
"m_aWeapons[2].m_AmmoRegenStart",
"m_aWeapons[2].m_Ammo",
"m_aWeapons[2].m_Ammocost",
"m_aWeapons[2].m_Got",
"m_aWeapons[3].m_AmmoRegenStart",
"m_aWeapons[3].m_Ammo",
"m_aWeapons[3].m_Ammocost",
"m_aWeapons[3].m_Got",
"m_aWeapons[4].m_AmmoRegenStart",
"m_aWeapons[4].m_Ammo",
"m_aWeapons[4].m_Ammocost",
"m_aWeapons[4].m_Got",
"m_aWeapons[5].m_AmmoRegenStart",
"m_aWeapons[5].m_Ammo",
"m_aWeapons[5].m_Ammocost",
"m_aWeapons[5].m_Got",
"m_LastWeapon",
"m_QueuedWeapon",
"m_EndlessJump",
"m_Jetpack",
"m_NinjaJetpack",
"m_FreezeTime",
"m_FreezeStart",
"m_DeepFrozen",
"m_EndlessHook",
"m_DDRaceState",
"m_HitDisabledFlags",
"m_CollisionEnabled",
"m_TuneZone",
"m_TuneZoneOld",
"m_HookHitEnabled",
"m_Time",
"(int)m_Pos.x",
"(int)m_Pos.y",
"(int)m_PrevPos.x",
"(int)m_PrevPos.y",
"m_TeleCheckpoint",
"m_LastPenalty",
"(int)m_CorePos.x",
"(int)m_CorePos.y",
"m_Vel.x",
"m_Vel.y",
"m_ActiveWeapon",
"m_Jumped",
"m_JumpedTotal",
"m_Jumps",
"(int)m_HookPos.x",
"(int)m_HookPos.y",
"m_HookDir.x",
"m_HookDir.y",
"(int)m_HookTeleBase.x",
"(int)m_HookTeleBase.y",
"m_HookTick",
"m_HookState",
"m_TimeCpBroadcastEndTime",
"m_LastTimeCp",
"m_LastTimeCpBroadcasted",
"m_aCurrentTimeCp[0]",
"m_aCurrentTimeCp[1]",
"m_aCurrentTimeCp[2]",
"m_aCurrentTimeCp[3]",
"m_aCurrentTimeCp[4]",
"m_aCurrentTimeCp[5]",
"m_aCurrentTimeCp[6]",
"m_aCurrentTimeCp[7]",
"m_aCurrentTimeCp[8]",
"m_aCurrentTimeCp[9]",
"m_aCurrentTimeCp[10]",
"m_aCurrentTimeCp[11]",
"m_aCurrentTimeCp[12]",
"m_aCurrentTimeCp[13]",
"m_aCurrentTimeCp[14]",
"m_aCurrentTimeCp[15]",
"m_aCurrentTimeCp[16]",
"m_aCurrentTimeCp[17]",
"m_aCurrentTimeCp[18]",
"m_aCurrentTimeCp[19]",
"m_aCurrentTimeCp[20]",
"m_aCurrentTimeCp[21]",
"m_aCurrentTimeCp[22]",
"m_aCurrentTimeCp[23]",
"m_aCurrentTimeCp[24]",
"m_NotEligibleForFinish",
"m_HasTelegunGun",
"m_HasTelegunLaser",
"m_HasTelegunGrenade",
"m_aGameUuid",
"HookedPlayer",
"m_NewHook",
"m_InputDirection",
"m_InputJump",
"m_InputFire",
"m_InputHook",
"m_ReloadTimer",
"m_TeeStarted",
"m_LiveFrozen",
"m_Ninja.m_ActivationDir.x",
"m_Ninja.m_ActivationDir.y",
"m_Ninja.m_ActivationTick",
"m_Ninja.m_CurrentMoveTime",
"m_Ninja.m_OldVelAmount",
];
const SWITCHER_DESC: [&str; 3] = [
"m_pSwitchers[i].m_Status",
"m_pSwitchers[i].m_EndTime",
"m_pSwitchers[i].m_Type",
];
impl SaveTeam {
pub fn from_buf(buf: &[u8]) -> Self {
Self {
buf: String::from_utf8_lossy(buf).to_string(),
}
}
pub fn from_string(buf: String) -> Self {
Self { buf }
}
fn print_variables(part: &str, id: u32, values: &str, variables: &[&str]) {
let mut vals = values.split('\t');
for (i, var) in variables.iter().enumerate() {
let v = vals.next();
println!("{part}[{id}] {var} ({i}): {v:?}");
}
}
fn compare_format_variables(
part: &str,
id: u32,
got: &str,
expected: &str,
variables: &[&str],
) -> Vec<String> {
let mut strings = Vec::new();
let mut got = got.split('\t');
let mut expected = expected.split('\t');
for (i, var) in variables.iter().enumerate() {
let g = got.next();
let e = expected.next();
if g != e {
strings.push(format!(
"{part}[{id}] {var} ({i}): (got) {g:?} != {e:?} (expected)"
));
}
}
strings
}
fn compare_variables(part: &str, id: u32, got: &str, expected: &str, variables: &[&str]) {
for msg in Self::compare_format_variables(part, id, got, expected, variables) {
println!("{msg}");
}
}
fn are_tees_same_compat(got: &str, expected: &str) -> bool {
let mut hook_tick_correct = true;
let mut got = got.split('\t');
let expected = expected.split('\t');
for (el, e) in expected.enumerate() {
let g = got.next();
if matches!(
el,
3 | 7 | 11 | 15 | 19 | 23 | 27 | 68 ) {
continue;
}
if el == 67 && !hook_tick_correct && e == "4" {
return false;
}
if el == 101 && !hook_tick_correct && e != "-1" {
return false;
}
if g.map(|g| g != e).unwrap_or(true) {
if el == 66 {
hook_tick_correct = false;
continue;
}
return false;
}
}
true
}
fn are_switchers_same_compat(got: &str, expected: &str) -> bool {
let mut got = got.split('\t');
let expected = expected.split('\t');
for e in expected {
let g = got.next();
if g.map(|g| g != e).unwrap_or(true) {
return false;
}
}
true
}
fn parse_head(head: &str) -> (u32, u32) {
let mut head = head.split('\t');
let _ = head.next();
let num_tees = head
.next()
.map(|el| el.parse::<u32>().unwrap_or(0))
.unwrap_or(0);
let num_switchers = head
.next()
.map(|el| el.parse::<u32>().unwrap_or(0))
.unwrap_or(0);
(num_tees, num_switchers)
}
pub fn format_diff(&self, expected: &Self) -> Vec<String> {
let mut diffs = Vec::new();
let mut got = self.buf.split('\n');
let mut expected = expected.buf.split('\n');
let (g, e) = match (got.next(), expected.next()) {
(Some(g), Some(e)) => (g, e),
(Some(g), None) => {
diffs.push(format!("expected no header got {g}"));
return diffs;
}
(None, Some(e)) => {
diffs.push(format!("expected header {e}, got nothing"));
return diffs;
}
(None, None) => {
return diffs;
}
};
diffs.extend(SaveTeam::compare_format_variables(
"header", 0, g, e, &TEAM_DESC,
));
let (mut g_tees, mut g_switchers) = SaveTeam::parse_head(g);
let (mut e_tees, mut e_switchers) = SaveTeam::parse_head(e);
let mut tee = 0;
while g_tees > 0 || e_tees > 0 {
let g = if g_tees > 0 {
let g = got.next().unwrap_or("");
g_tees -= 1;
g
} else {
""
};
let e = if e_tees > 0 {
let e = expected.next().unwrap_or("");
e_tees -= 1;
e
} else {
""
};
diffs.extend(SaveTeam::compare_format_variables(
"tee", tee, g, e, &TEE_DESC,
));
tee += 1;
}
let mut switchers = 0;
while g_switchers > 0 || e_switchers > 0 {
let g = if g_switchers > 0 {
let g = got.next().unwrap_or("");
g_switchers -= 1;
g
} else {
""
};
let e = if e_switchers > 0 {
let e = expected.next().unwrap_or("");
e_switchers -= 1;
e
} else {
""
};
diffs.extend(SaveTeam::compare_format_variables(
"switcher",
switchers,
g,
e,
&SWITCHER_DESC,
));
switchers += 1;
}
if let Some(remaining) = expected.next() {
diffs.push(format!(
"unexpected remaining line(s) in expected: {remaining}"
));
}
if let Some(remaining) = got.next() {
diffs.push(format!("unexpected remaining line(s) in got: {remaining}"));
}
diffs
}
pub fn print_diff(&self, expected: &Self) {
for diff in self.format_diff(expected) {
println!("{diff}");
}
}
pub fn pretty_print(&self) {
let mut parts = self.buf.split('\n');
let Some(header) = parts.next() else {
return;
};
SaveTeam::print_variables("header", 0, header, &TEAM_DESC);
let (num_tees, num_switchers) = SaveTeam::parse_head(header);
for tee_id in 0..num_tees {
SaveTeam::print_variables("tee", tee_id, parts.next().unwrap_or(""), &TEE_DESC);
}
for switch_id in 0..num_switchers {
SaveTeam::print_variables(
"switcher",
switch_id,
parts.next().unwrap_or(""),
&SWITCHER_DESC,
);
}
}
}
#[derive(Clone, Debug)]
pub enum DatabaseResult {
LoadSuccess(i32, SaveTeam),
LoadFailure(i32),
SaveSuccess(i32),
SaveFailure(i32),
}
impl DatabaseResult {
pub fn team(&self) -> i32 {
match self {
DatabaseResult::LoadSuccess(team, _) => *team,
DatabaseResult::LoadFailure(team) => *team,
DatabaseResult::SaveSuccess(team) => *team,
DatabaseResult::SaveFailure(team) => *team,
}
}
}
impl DatabaseWrite {
pub fn finish_tee(name: &str, time: Duration) -> Self {
Self::FinishTee(FinishTee::new_from_str(name, time))
}
}