From 3449f91dacf24d3b34634c80b517b8c80322927f Mon Sep 17 00:00:00 2001 From: lily_and_doll Date: Thu, 13 Nov 2025 19:18:56 -0500 Subject: [PATCH 01/52] add White Vanilla support --- common/src/lib.rs | 5 +- payload/src/lib.rs | 6 + splitter/src/main.rs | 648 +++++++++++++++++++++++++++++++++++-------- 3 files changed, 545 insertions(+), 114 deletions(-) diff --git a/common/src/lib.rs b/common/src/lib.rs index d461236..42b795b 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -3,7 +3,7 @@ use std::io::{self, Read}; use bytemuck::{Pod, Zeroable}; #[derive(Debug, Default, Clone, Copy, Zeroable, Pod)] -#[repr(C)] +#[repr(C, packed(2))] pub struct FrameData { pub score_p1: i32, pub score_p2: i32, @@ -11,6 +11,9 @@ pub struct FrameData { pub game_loop: u8, pub checkpoint: u8, pub difficulty: i8, + pub realm: u8, + pub checkpoint_sub: u8, + pub timer_wave: u32, } impl FrameData { diff --git a/payload/src/lib.rs b/payload/src/lib.rs index e6a80b0..2bb1e86 100644 --- a/payload/src/lib.rs +++ b/payload/src/lib.rs @@ -100,6 +100,9 @@ fn get_frame_data() -> FrameData { let stage_p1 = read_var(c"stage_one").unwrap().value as u8; let stage_p2 = read_var(c"stage_two").unwrap().value as u8; let stage = stage_p1.max(stage_p2); + let realm = read_var(c"realm").unwrap().value as u8; + let checkpoint_sub = read_var(c"checkpoint_sub").unwrap().value as u8; + let timer_wave = read_var(c"timer_wave").unwrap().value as u32; FrameData { score_p1: score_p1 as i32, score_p2: score_p2 as i32, @@ -107,6 +110,9 @@ fn get_frame_data() -> FrameData { game_loop, checkpoint, difficulty, + realm, + checkpoint_sub, + timer_wave, } } diff --git a/splitter/src/main.rs b/splitter/src/main.rs index ee05796..c15cb85 100644 --- a/splitter/src/main.rs +++ b/splitter/src/main.rs @@ -15,7 +15,7 @@ use eframe::{ App, Frame, NativeOptions, egui::{ Align, CentralPanel, Color32, ComboBox, Context, IconData, Id, Key, Layout, Sides, ThemePreference, - ViewportBuilder, ViewportId, + TopBottomPanel, ViewportBuilder, ViewportId, }, }; use log::{error, warn}; @@ -37,7 +37,7 @@ fn main() { let options = NativeOptions { viewport: ViewportBuilder::default() - .with_inner_size([300., 300.]) + .with_inner_size([300., 290.]) .with_icon(IconData::default()) .with_title("ZeroSplitter"), ..Default::default() @@ -90,23 +90,26 @@ struct ZeroSplitter { waiting_for_category: bool, waiting_for_rename: bool, waiting_for_confirm: bool, - dialog_rx: Receiver, - dialog_tx: Sender, + dialog_rx: Receiver>, + dialog_tx: Sender>, comparison: Category, + active: bool, } impl ZeroSplitter { fn new(data_source: Receiver) -> Self { let (tx, rx) = mpsc::channel(); let mut default_categories = Vec::new(); - default_categories.push(Category::new("Type-C".to_string())); - default_categories.push(Category::new("Type-B".to_string())); + default_categories.push(Category::new("Type-C GO".to_string(), Gamemode::GreenOrange)); + default_categories.push(Category::new("Type-B GO".to_string(), Gamemode::GreenOrange)); + default_categories.push(Category::new("Type-C WV".to_string(), Gamemode::WhiteVanilla)); + default_categories.push(Category::new("Type-B WV".to_string(), Gamemode::WhiteVanilla)); Self { categories: default_categories, data_source, last_frame: Default::default(), current_category: 0, - current_run: Default::default(), + current_run: Run::new(Gamemode::GreenOrange), current_split: None, current_split_score_offset: 0, dialog_rx: rx, @@ -114,7 +117,8 @@ impl ZeroSplitter { waiting_for_category: false, waiting_for_rename: false, waiting_for_confirm: false, - comparison: Category::new("".to_string()), + comparison: Category::new("".to_string(), Gamemode::GreenOrange), + active: false, } } @@ -134,14 +138,33 @@ impl ZeroSplitter { match File::open(&data_path) { Ok(file) => { - let data: Vec = serde_json::from_reader(file).expect("Loading data"); - if data.is_empty() { - Self::new(data_source) + let try_new_cat = serde_json::from_reader::<_, Vec>(&file); + if let Ok(data) = try_new_cat { + if data.is_empty() { + Self::new(data_source) + } else { + Self { + current_category: 0, + categories: data, + ..Self::new(data_source) + } + } } else { - Self { - current_category: 0, - categories: data, - ..Self::new(data_source) + // An attempt at old-save migration. Probably doesn't work. + let try_old_cat = serde_json::from_reader::<_, Vec>(&file); + if let Ok(data) = try_old_cat { + Self { + current_category: 0, + categories: data.iter().map(|c| c.to_new(Gamemode::GreenOrange)).collect(), + ..Self::new(data_source) + } + } else { + panic!( + "Data failed to parse as Category or OldCategory at {:?}: {} and {}", + &data_path, + try_new_cat.unwrap_err(), + try_old_cat.unwrap_err() + ) } } } @@ -169,55 +192,107 @@ impl ZeroSplitter { } fn update_frame(&mut self, frame: FrameData) { - // WV not yet implemented, idk if it'll crash or not + // Difficulty is ZR-speak for gamemode if frame.difficulty == 0 { - // Reset if we just left the menu or returned to 1-1 - if frame.stage != self.last_frame.stage && (self.last_frame.is_menu() || frame.is_first_stage()) { - self.reset(); - } + self.update_greenorange(frame); + } else if frame.difficulty == -1 { + self.update_whitevanilla(frame); + } else if frame.difficulty == 1 { + // Black Onion placeholder + } + self.last_frame = frame; + } - if !frame.is_menu() { - let frame_split = (frame.stage - 1 - frame.game_loop) as usize; + fn update_greenorange(&mut self, frame: FrameData) { + // Skip update if current category isn't Green Orange + if self.categories[self.current_category].mode != Gamemode::GreenOrange { + return; + } - if frame_split >= 8 { - // TLB or credits - return; - } + // Reset if we just left the menu or returned to 1-1 + if frame.stage != self.last_frame.stage && (self.last_frame.is_menu() || frame.is_first_stage()) { + self.reset(); + } - // Split if necessary - if frame.stage != self.last_frame.stage { - self.current_split = Some(frame_split); - self.current_split_score_offset = self.last_frame.total_score(); - self.save_splits(); - } + if !frame.is_menu() { + let frame_split = (frame.stage - 1 - frame.game_loop) as usize; - // If our score got reset by a continue, fix the score offset. - if self.current_split_score_offset > frame.total_score() { - self.current_split_score_offset = 0; - } + if frame_split >= 8 { + // TLB or credits + return; + } - // Update run and split scores - self.current_run.score = frame.total_score(); - let split_score = frame.total_score() - self.current_split_score_offset; - self.current_run.splits[frame_split] = split_score; - } else { - // End the run if we're back on the menu - self.end_run(); + // Split if necessary + if frame.stage != self.last_frame.stage { + self.current_split = Some(frame_split); + self.current_split_score_offset = self.last_frame.total_score(); + self.save_splits(); + } + + // If our score got reset by a continue, fix the score offset. + if self.current_split_score_offset > frame.total_score() { + self.current_split_score_offset = 0; } + + // Update run and split scores + self.current_run.score = frame.total_score(); + let split_score = frame.total_score() - self.current_split_score_offset; + self.current_run.splits[frame_split] = split_score; + } else { + // End the run if we're back on the menu + self.end_run(); } + } - self.last_frame = frame; + fn update_whitevanilla(&mut self, frame: FrameData) { + // Skip update if current category isn't White Vanilla + if self.categories[self.current_category].mode != Gamemode::WhiteVanilla { + return; + } + + // Reset if we returned to 1-1 + if frame.total_score() == 0 && self.last_frame.total_score() > 0 || self.last_frame.is_menu() { + // Only support starting at stage 1 for now + if frame.is_first_stage() { + self.reset(); + self.current_split = Some(0); + return + } // TODO: detect start of each other stage and start splits there properly + } + + if !frame.is_menu() && self.active { + // Split if necessary; score requirement prevents spurious splits after a reset + if frame.timer_wave == 0 && self.last_frame.timer_wave != 0 && frame.total_score() > 0 { + self.current_split = self.current_split.or(Some(0)).map(|s| s + 1); + self.current_split_score_offset = self.last_frame.total_score(); + self.save_splits(); + } + + // TODO: reimplement continue support + + // Update run and split scores + self.current_run.score = frame.total_score(); + let split_score = frame.total_score() - self.current_split_score_offset; + self.current_run.splits[self.current_split.unwrap_or(0)] = split_score; + } else { + // End the run if we're back on the menu + self.end_run(); + } } fn reset(&mut self) { self.end_run(); - self.current_run = Default::default(); + self.current_run = Run::new(self.categories[self.current_category].mode); self.comparison = self.categories[self.current_category].clone(); + self.current_split = Some(0); + self.active = true; + self.current_split_score_offset = 0; } fn end_run(&mut self) { self.save_splits(); self.current_split = None; + self.active = false; } } @@ -245,94 +320,158 @@ impl App for ZeroSplitter { } }); + // Detect gamemode change persist between frames + let prev_mode_id = Id::new("prev_mode"); + let cur_mode = self.categories[self.current_category].mode; + if let Some(prev_mode) = ctx.data(|data| data.get_temp::(prev_mode_id)) { + if prev_mode != cur_mode { + self.reset(); + } + } + ctx.data_mut(|data| data.insert_temp(prev_mode_id, cur_mode)); + let cur_category = &self.categories[self.current_category]; - for (i, split) in self.current_run.splits.iter().enumerate() { - let stage_n = (i & 3) + 1; - let loop_n = (i >> 2) + 1; - let best = cur_category.best_splits[i]; - let stored_best = self.comparison.personal_best.splits[i]; - let pb = self.comparison.personal_best.splits[i]; - - Sides::new().show( - ui, - |left| { - left.label(format!("{}-{}", loop_n, stage_n)); - - if best > 0 { - left.colored_label(GREEN, best.to_string()); - } - }, - |right| { - if *split != 0 || self.current_split == Some(i) { - let split_color = if self.current_split == Some(i) { - Color32::WHITE - } else { - DARK_ORANGE - }; - - right.colored_label(split_color, split.to_string()); - - if self.current_split != Some(i) { - // past split, we should show a diff - let diff = *split - pb; - let diff_color = if *split > stored_best { - LIGHT_ORANGE - } else if diff >= 0 { - Color32::WHITE + match cur_mode { + Gamemode::GreenOrange => { + for (i, split) in self.current_run.splits.iter().enumerate() { + let stage_n = (i & 3) + 1; + let loop_n = (i >> 2) + 1; + let best = cur_category.best_splits[i]; + let stored_best = self.comparison.personal_best.splits[i]; + let pb = self.comparison.personal_best.splits[i]; + + Sides::new().show( + ui, + |left| { + left.label(format!("{}-{}", loop_n, stage_n)); + + if best > 0 { + left.colored_label(GREEN, best.to_string()); + } + }, + |right| { + if *split != 0 || self.current_split == Some(i) { + let split_color = if self.current_split == Some(i) { + Color32::WHITE + } else { + DARK_ORANGE + }; + + right.colored_label(split_color, split.to_string()); + + if self.current_split != Some(i) { + // past split, we should show a diff + let diff = *split - pb; + let diff_color = if *split > stored_best { + LIGHT_ORANGE + } else if diff >= 0 { + Color32::WHITE + } else { + DARK_GREEN + }; + + if diff > 0 { + right.colored_label(diff_color, format!("+{}", diff)); + } else { + right.colored_label(diff_color, diff.to_string()); + } + } } else { - DARK_GREEN - }; - - if diff > 0 { - right.colored_label(diff_color, format!("+{}", diff)); + right.colored_label(DARK_GREEN, "--"); + } + }, + ); + } + } + Gamemode::WhiteVanilla => { + for (i, &split) in self.current_run.splits.iter().enumerate() { + let best_this_checkpoint = cur_category.best_splits[i]; + let stored_best = self.comparison.personal_best.splits[i]; + let pb = self.comparison.personal_best.splits[i]; + + Sides::new().show( + ui, + |left| { + left.label(vanilla_split_names(i)); + + if best_this_checkpoint > 0 { + left.colored_label(GREEN, best_this_checkpoint.to_string()); + } + }, + |right| { + if split != 0 || self.current_split == Some(i) { + let split_color = if self.current_split == Some(i) { + Color32::WHITE + } else { + DARK_ORANGE + }; + + right.colored_label(split_color, split.to_string()); + + if self.current_split != Some(i) { + // past split, we should show a diff + let diff = split - pb; + let diff_color = if split > stored_best { + LIGHT_ORANGE + } else if diff >= 0 { + Color32::WHITE + } else { + DARK_GREEN + }; + + if diff > 0 { + right.colored_label(diff_color, format!("+{}", diff)); + } else { + right.colored_label(diff_color, diff.to_string()); + } + } } else { - right.colored_label(diff_color, diff.to_string()); + right.colored_label(DARK_GREEN, "--"); } - } - } else { - right.colored_label(DARK_GREEN, "--"); - } - }, - ); + }, + ); + } + } + Gamemode::BlackOnion => { + todo!() + } } ui.label(format!("Personal Best: {}", cur_category.personal_best.score)); - ui.label(format!( - "Sum of Best: {}", - cur_category.best_splits.into_iter().sum::() - )) + ui.label(format!("Sum of Best: {}", cur_category.best_splits.iter().sum::())) }); }); if self.waiting_for_category { if let Ok(new_category) = self.dialog_rx.try_recv() { - if !new_category.is_empty() { - self.categories.push(Category::new(new_category)); + if let Some(data) = new_category { + self.categories.push(Category::new(data.textbox, data.mode)); self.current_category = self.categories.len() - 1; self.save_splits(); } self.waiting_for_category = false; } else { - entry_dialog(ctx, self.dialog_tx.clone(), "Enter new category name"); + category_maker_dialog(ctx, self.dialog_tx.clone(), "Enter new category name", true); } } if self.waiting_for_rename { - if let Ok(new_name) = self.dialog_rx.try_recv() { - if !new_name.is_empty() { - self.categories[self.current_category].name = new_name; + if let Ok(new_category) = self.dialog_rx.try_recv() { + if let Some(data) = new_category { + self.categories[self.current_category].name = data.textbox; + self.categories[self.current_category].mode = data.mode; self.save_splits(); } self.waiting_for_rename = false; } else { - entry_dialog(ctx, self.dialog_tx.clone(), "Enter new name for category"); + category_maker_dialog(ctx, self.dialog_tx.clone(), "Enter new name for category", false); } } if self.waiting_for_confirm { - if let Ok(confirmation) = self.dialog_rx.try_recv() { - if confirmation == "Deleted" { + if let Ok(Some(confirmation)) = self.dialog_rx.try_recv() { + if confirmation.textbox == "Deleted" { self.categories.remove(self.current_category); self.current_category = self.current_category.saturating_sub(1); } @@ -390,7 +529,85 @@ fn entry_dialog(ctx: &Context, tx: Sender, msg: &'static str) { }); } -fn confirm_dialog(ctx: &Context, tx: Sender, msg: String) { +/// Category entry dialoge menu with a gamemode dropdown. +fn category_maker_dialog(ctx: &Context, tx: Sender>, msg: &'static str, mode_select: bool) { + let vp_builder = ViewportBuilder::default() + .with_title("ZeroSplitter") + .with_active(true) + .with_resizable(false) + .with_minimize_button(false) + .with_maximize_button(false) + .with_inner_size([200., 100.]); + + ctx.show_viewport_deferred(ViewportId::from_hash_of("entry dialog"), vp_builder, move |ctx, _| { + if ctx.input(|input| input.viewport().close_requested()) { + let _ = tx.send(None); + request_repaint(); + return; + } + + let text_id = Id::new("edit text"); + let mode_id = Id::new("gamemode"); + let mut edit_str = ctx.data_mut(|data| data.get_temp_mut_or_insert_with(text_id, || String::new()).clone()); + let mut mode = ctx.data_mut(|data| *data.get_temp_mut_or(mode_id, Gamemode::GreenOrange)); + + CentralPanel::default().show(ctx, |ui| { + ui.vertical_centered_justified(|ui| { + ui.label(msg); + ui.text_edit_singleline(&mut edit_str); + if ui.button("Confirm").clicked() { + let _ = tx.send(Some(EntryDialogData { + textbox: edit_str.clone(), + mode: mode, + })); + request_repaint(); + ctx.send_viewport_cmd(eframe::egui::ViewportCommand::Close); + } + }); + }); + + if mode_select { + TopBottomPanel::bottom("mode_select").show(ctx, |ui| { + ui.vertical_centered_justified(|ui| { + ComboBox::from_label("Mode") + .selected_text(format!("{:?}", mode)) + .show_ui(ui, |ui| { + ui.selectable_value(&mut mode, Gamemode::GreenOrange, "Green Orange"); + ui.selectable_value(&mut mode, Gamemode::WhiteVanilla, "White Vanilla"); + ui.selectable_value(&mut mode, Gamemode::BlackOnion, "Black Onion"); + }); + }) + }); + } + + ctx.data_mut(|data| { + data.insert_temp(text_id, edit_str); + data.insert_temp(mode_id, mode); + }); + }); +} +#[derive(PartialEq, Eq, Clone, Copy, Debug, Serialize, Deserialize)] +pub enum Gamemode { + GreenOrange, + WhiteVanilla, + BlackOnion, +} + +impl Gamemode { + fn splits(&self) -> usize { + match self { + Gamemode::GreenOrange => 8, + Gamemode::WhiteVanilla => 26, + Gamemode::BlackOnion => todo!(), + } + } +} +pub struct EntryDialogData { + pub textbox: String, + pub mode: Gamemode, +} + +fn confirm_dialog(ctx: &Context, tx: Sender>, msg: String) { let vp_builder = ViewportBuilder::default() .with_title("ZeroSplitter") .with_active(true) @@ -401,7 +618,7 @@ fn confirm_dialog(ctx: &Context, tx: Sender, msg: String) { ctx.show_viewport_deferred(ViewportId::from_hash_of("confirm dialog"), vp_builder, move |ctx, _| { if ctx.input(|input| input.viewport().close_requested()) { - let _ = tx.send("".to_string()); + let _ = tx.send(None); request_repaint(); return; } @@ -411,10 +628,13 @@ fn confirm_dialog(ctx: &Context, tx: Sender, msg: String) { ui.label(msg.clone()); ui.columns_const(|[left, right]| { if left.button("Delete").clicked() { - let _ = tx.send("Deleted".to_string()); + let _ = tx.send(Some(EntryDialogData { + textbox: "Deleted".to_string(), + mode: Gamemode::GreenOrange, + })); request_repaint(); } else if right.button("Cancel").clicked() { - let _ = tx.send("".to_string()); + let _ = tx.send(None); request_repaint(); } }); @@ -429,31 +649,233 @@ fn request_repaint() { } } +/// Translation of function of the same name in ZR. +/// Checkpoints in WV have the same stage/checkpoint as in GO, +/// e.g. the cloudoo segment in stage 1 is technically stage 3 checkpoint 2 +/// just like it is in GO. Also, some segments have multiple checkpoints. +/// This function gets the segment number in WV from the GO data. +/// "Realm" seems to indicate when the ship is changed, like in TLB, the dream, or bonus stages +/// +/// Someone much smarter than me could try to call this function directly from the +/// game instead of using this. +fn vanilla_get_simstage(stage: u8, checkpoint: u8, checkpoint_sub: u8, realm: u8) -> Result<(u32, u32), ()> { + Ok(match (stage, checkpoint) { + // GO stage/check => WV stage/check + (1, _) if realm == 3 => (1, 4), + (1, 0 | 1) => (1, 0), + (1, 2) => (2, 4), + (1, 3) => (1, 2), + (1, 4 | 5 | 6) => (1, 3), + + (2, _) if realm == 3 => (2, 6), + (2, 0 | 1) => (2, 0), + (2, 2) => (3, 1), + (2, 3) => (2, 1), + (2, 4) => (2, 3), + (2, 5) => (4, 1), + (2, 6 | 7 | 8) => (2, 5), + + (3, 0) => (3, 0), + (3, 1) => (1, 1), + (3, 2) => (4, 4), + (3, 3) => (3, 2), + (3, 4) if checkpoint_sub < 1 => (2, 2), + (3, 4) => (3, 3), + + (3, 6) => (4, 3), + (3, 7) => (3, 4), + (3, 8 | 9) => (3, 5), + + (4, _) if realm == 3 => (3, 6), + (4, 3) => (3, 6), + (4, 5 | 6) => (4, 0), + (4, 7 | 8) => (4, 2), + (4, 9 | 10) => (4, 5), + _ => return Err(()), + }) +} + +/// Translate sim stage and checkpoint to split count +/// e.g. snake is split 7 (0 indexed) +fn vanilla_get_split_count(simstage: u32, simcheckpoint: u32) -> usize { + [ + (1, 1), + (1, 2), + (1, 3), + (1, 4), + (1, 5), + (2, 1), + (2, 2), + (2, 3), + (2, 4), + (2, 5), + (2, 6), + (2, 7), + (3, 1), + (3, 2), + (3, 3), + (3, 4), + (3, 5), + (3, 6), + (3, 7), + (4, 1), + (4, 2), + (4, 3), + (4, 4), + (4, 5), + (4, 6), + (4, 7), + ] + .iter() + .position(|&x| x == (simstage, simcheckpoint + 1)) + .unwrap() +} + +fn vanilla_stage_from_split(split: usize) -> (u32, u32) { + [ + (1, 1), + (1, 2), + (1, 3), + (1, 4), + (1, 5), + (2, 1), + (2, 2), + (2, 3), + (2, 4), + (2, 5), + (2, 6), + (2, 7), + (3, 1), + (3, 2), + (3, 3), + (3, 4), + (3, 5), + (3, 6), + (3, 7), + (4, 1), + (4, 2), + (4, 3), + (4, 4), + (4, 5), + (4, 6), + (4, 7), + ][split] +} + +fn vanilla_split_names(split: usize) -> &'static str { + [ + "1-1", "1-2", "1-3", "1-4", "Bonus 1", "2-1", "2-2", "2-3", "2-4", "2-5", "2-6", "Bonus 2", "3-1", "3-2", + "3-3", "3-4", "3-5", "3-6", "Bonus 3", "4-1", "4-2", "4-3", "4-4", "4-5", "4-6", "Stage EX", + ][split] +} + +fn vanilla_descriptive_split_names(split: usize) -> &'static str { + [ + "Stage 1 Start", + "Cloudoos", + "Arc Adder", + "Catastrophy", + "Bonus 1", + "Stage 2 Start", + "Box Blockade", + "Snake", + "Artypo", + "Skull Taxis", + "2nd Apocalypse", + "Bonus 2", + "Stage 3 Start", + "Crab Landing", + "Plane", + "Crab", + "Tank", + "Grapefruit", + "Bonus 3", + "Stage 4 Start", + "Left Tunnel", + "Maze", + "Trains", + "Knight Ships", + "Orb Spewer", + "Stage EX", + ][split] +} + #[derive(Debug, Default, Serialize, Deserialize, Clone, Copy)] -struct Run { +struct OldRun { splits: [i32; 8], score: i32, } +#[derive(Debug, Serialize, Deserialize, Clone)] +struct Run { + splits: Vec, + score: i32, + mode: Gamemode +} + +impl Run { + fn new(mode: Gamemode) -> Self { + Run { + splits: vec![0; mode.splits()], + score: 0, + mode + } + } +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +struct OldCategory { + personal_best: OldRun, + best_splits: [i32; 8], + name: String, +} + +impl OldRun { + fn to_new(self) -> Run { + Run { + splits: self.splits.to_vec(), + score: self.score, + mode: Gamemode::GreenOrange + } + } +} + +impl OldCategory { + fn to_new(&self, mode: Gamemode) -> Category { + Category { + personal_best: self.personal_best.to_new(), + best_splits: self.best_splits.to_vec(), + name: self.name.clone(), + mode, + } + } +} + #[derive(Debug, Serialize, Deserialize, Clone)] struct Category { personal_best: Run, - best_splits: [i32; 8], + best_splits: Vec, name: String, + mode: Gamemode, } impl Category { - fn new(name: String) -> Self { + fn new(name: String, mode: Gamemode) -> Self { Category { - personal_best: Default::default(), - best_splits: Default::default(), + personal_best: Run::new(mode), + best_splits: vec![0; mode.splits()], name, + mode, } } fn update_from_run(&mut self, run: &Run) { + if run.mode != self.mode { + return + } + if run.score > self.personal_best.score { - self.personal_best = *run; + self.personal_best = run.clone(); } for (best, new) in self.best_splits.iter_mut().zip(run.splits.iter()) { From 8da983f1b97a649c430061f765e539599773553a Mon Sep 17 00:00:00 2001 From: lily_and_doll Date: Thu, 13 Nov 2025 20:45:31 -0500 Subject: [PATCH 02/52] Fix restart detection --- splitter/src/main.rs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/splitter/src/main.rs b/splitter/src/main.rs index c15cb85..c890320 100644 --- a/splitter/src/main.rs +++ b/splitter/src/main.rs @@ -252,12 +252,10 @@ impl ZeroSplitter { // Reset if we returned to 1-1 if frame.total_score() == 0 && self.last_frame.total_score() > 0 || self.last_frame.is_menu() { - // Only support starting at stage 1 for now - if frame.is_first_stage() { - self.reset(); - self.current_split = Some(0); - return - } // TODO: detect start of each other stage and start splits there properly + self.reset(); + self.current_split = Some(0); + return + // TODO: detect start of each other stage and start splits there properly } if !frame.is_menu() && self.active { From e61388b43b2d48041477e60fb7dd2cbca74fe7ae Mon Sep 17 00:00:00 2001 From: lily_and_doll <103815266+lily-and-doll@users.noreply.github.com> Date: Thu, 13 Nov 2025 21:01:29 -0500 Subject: [PATCH 03/52] add White Vanilla support --- common/src/lib.rs | 5 +- payload/src/lib.rs | 6 + splitter/src/main.rs | 648 +++++++++++++++++++++++++++++++++++-------- 3 files changed, 545 insertions(+), 114 deletions(-) diff --git a/common/src/lib.rs b/common/src/lib.rs index d461236..42b795b 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -3,7 +3,7 @@ use std::io::{self, Read}; use bytemuck::{Pod, Zeroable}; #[derive(Debug, Default, Clone, Copy, Zeroable, Pod)] -#[repr(C)] +#[repr(C, packed(2))] pub struct FrameData { pub score_p1: i32, pub score_p2: i32, @@ -11,6 +11,9 @@ pub struct FrameData { pub game_loop: u8, pub checkpoint: u8, pub difficulty: i8, + pub realm: u8, + pub checkpoint_sub: u8, + pub timer_wave: u32, } impl FrameData { diff --git a/payload/src/lib.rs b/payload/src/lib.rs index e6a80b0..2bb1e86 100644 --- a/payload/src/lib.rs +++ b/payload/src/lib.rs @@ -100,6 +100,9 @@ fn get_frame_data() -> FrameData { let stage_p1 = read_var(c"stage_one").unwrap().value as u8; let stage_p2 = read_var(c"stage_two").unwrap().value as u8; let stage = stage_p1.max(stage_p2); + let realm = read_var(c"realm").unwrap().value as u8; + let checkpoint_sub = read_var(c"checkpoint_sub").unwrap().value as u8; + let timer_wave = read_var(c"timer_wave").unwrap().value as u32; FrameData { score_p1: score_p1 as i32, score_p2: score_p2 as i32, @@ -107,6 +110,9 @@ fn get_frame_data() -> FrameData { game_loop, checkpoint, difficulty, + realm, + checkpoint_sub, + timer_wave, } } diff --git a/splitter/src/main.rs b/splitter/src/main.rs index ee05796..c15cb85 100644 --- a/splitter/src/main.rs +++ b/splitter/src/main.rs @@ -15,7 +15,7 @@ use eframe::{ App, Frame, NativeOptions, egui::{ Align, CentralPanel, Color32, ComboBox, Context, IconData, Id, Key, Layout, Sides, ThemePreference, - ViewportBuilder, ViewportId, + TopBottomPanel, ViewportBuilder, ViewportId, }, }; use log::{error, warn}; @@ -37,7 +37,7 @@ fn main() { let options = NativeOptions { viewport: ViewportBuilder::default() - .with_inner_size([300., 300.]) + .with_inner_size([300., 290.]) .with_icon(IconData::default()) .with_title("ZeroSplitter"), ..Default::default() @@ -90,23 +90,26 @@ struct ZeroSplitter { waiting_for_category: bool, waiting_for_rename: bool, waiting_for_confirm: bool, - dialog_rx: Receiver, - dialog_tx: Sender, + dialog_rx: Receiver>, + dialog_tx: Sender>, comparison: Category, + active: bool, } impl ZeroSplitter { fn new(data_source: Receiver) -> Self { let (tx, rx) = mpsc::channel(); let mut default_categories = Vec::new(); - default_categories.push(Category::new("Type-C".to_string())); - default_categories.push(Category::new("Type-B".to_string())); + default_categories.push(Category::new("Type-C GO".to_string(), Gamemode::GreenOrange)); + default_categories.push(Category::new("Type-B GO".to_string(), Gamemode::GreenOrange)); + default_categories.push(Category::new("Type-C WV".to_string(), Gamemode::WhiteVanilla)); + default_categories.push(Category::new("Type-B WV".to_string(), Gamemode::WhiteVanilla)); Self { categories: default_categories, data_source, last_frame: Default::default(), current_category: 0, - current_run: Default::default(), + current_run: Run::new(Gamemode::GreenOrange), current_split: None, current_split_score_offset: 0, dialog_rx: rx, @@ -114,7 +117,8 @@ impl ZeroSplitter { waiting_for_category: false, waiting_for_rename: false, waiting_for_confirm: false, - comparison: Category::new("".to_string()), + comparison: Category::new("".to_string(), Gamemode::GreenOrange), + active: false, } } @@ -134,14 +138,33 @@ impl ZeroSplitter { match File::open(&data_path) { Ok(file) => { - let data: Vec = serde_json::from_reader(file).expect("Loading data"); - if data.is_empty() { - Self::new(data_source) + let try_new_cat = serde_json::from_reader::<_, Vec>(&file); + if let Ok(data) = try_new_cat { + if data.is_empty() { + Self::new(data_source) + } else { + Self { + current_category: 0, + categories: data, + ..Self::new(data_source) + } + } } else { - Self { - current_category: 0, - categories: data, - ..Self::new(data_source) + // An attempt at old-save migration. Probably doesn't work. + let try_old_cat = serde_json::from_reader::<_, Vec>(&file); + if let Ok(data) = try_old_cat { + Self { + current_category: 0, + categories: data.iter().map(|c| c.to_new(Gamemode::GreenOrange)).collect(), + ..Self::new(data_source) + } + } else { + panic!( + "Data failed to parse as Category or OldCategory at {:?}: {} and {}", + &data_path, + try_new_cat.unwrap_err(), + try_old_cat.unwrap_err() + ) } } } @@ -169,55 +192,107 @@ impl ZeroSplitter { } fn update_frame(&mut self, frame: FrameData) { - // WV not yet implemented, idk if it'll crash or not + // Difficulty is ZR-speak for gamemode if frame.difficulty == 0 { - // Reset if we just left the menu or returned to 1-1 - if frame.stage != self.last_frame.stage && (self.last_frame.is_menu() || frame.is_first_stage()) { - self.reset(); - } + self.update_greenorange(frame); + } else if frame.difficulty == -1 { + self.update_whitevanilla(frame); + } else if frame.difficulty == 1 { + // Black Onion placeholder + } + self.last_frame = frame; + } - if !frame.is_menu() { - let frame_split = (frame.stage - 1 - frame.game_loop) as usize; + fn update_greenorange(&mut self, frame: FrameData) { + // Skip update if current category isn't Green Orange + if self.categories[self.current_category].mode != Gamemode::GreenOrange { + return; + } - if frame_split >= 8 { - // TLB or credits - return; - } + // Reset if we just left the menu or returned to 1-1 + if frame.stage != self.last_frame.stage && (self.last_frame.is_menu() || frame.is_first_stage()) { + self.reset(); + } - // Split if necessary - if frame.stage != self.last_frame.stage { - self.current_split = Some(frame_split); - self.current_split_score_offset = self.last_frame.total_score(); - self.save_splits(); - } + if !frame.is_menu() { + let frame_split = (frame.stage - 1 - frame.game_loop) as usize; - // If our score got reset by a continue, fix the score offset. - if self.current_split_score_offset > frame.total_score() { - self.current_split_score_offset = 0; - } + if frame_split >= 8 { + // TLB or credits + return; + } - // Update run and split scores - self.current_run.score = frame.total_score(); - let split_score = frame.total_score() - self.current_split_score_offset; - self.current_run.splits[frame_split] = split_score; - } else { - // End the run if we're back on the menu - self.end_run(); + // Split if necessary + if frame.stage != self.last_frame.stage { + self.current_split = Some(frame_split); + self.current_split_score_offset = self.last_frame.total_score(); + self.save_splits(); + } + + // If our score got reset by a continue, fix the score offset. + if self.current_split_score_offset > frame.total_score() { + self.current_split_score_offset = 0; } + + // Update run and split scores + self.current_run.score = frame.total_score(); + let split_score = frame.total_score() - self.current_split_score_offset; + self.current_run.splits[frame_split] = split_score; + } else { + // End the run if we're back on the menu + self.end_run(); } + } - self.last_frame = frame; + fn update_whitevanilla(&mut self, frame: FrameData) { + // Skip update if current category isn't White Vanilla + if self.categories[self.current_category].mode != Gamemode::WhiteVanilla { + return; + } + + // Reset if we returned to 1-1 + if frame.total_score() == 0 && self.last_frame.total_score() > 0 || self.last_frame.is_menu() { + // Only support starting at stage 1 for now + if frame.is_first_stage() { + self.reset(); + self.current_split = Some(0); + return + } // TODO: detect start of each other stage and start splits there properly + } + + if !frame.is_menu() && self.active { + // Split if necessary; score requirement prevents spurious splits after a reset + if frame.timer_wave == 0 && self.last_frame.timer_wave != 0 && frame.total_score() > 0 { + self.current_split = self.current_split.or(Some(0)).map(|s| s + 1); + self.current_split_score_offset = self.last_frame.total_score(); + self.save_splits(); + } + + // TODO: reimplement continue support + + // Update run and split scores + self.current_run.score = frame.total_score(); + let split_score = frame.total_score() - self.current_split_score_offset; + self.current_run.splits[self.current_split.unwrap_or(0)] = split_score; + } else { + // End the run if we're back on the menu + self.end_run(); + } } fn reset(&mut self) { self.end_run(); - self.current_run = Default::default(); + self.current_run = Run::new(self.categories[self.current_category].mode); self.comparison = self.categories[self.current_category].clone(); + self.current_split = Some(0); + self.active = true; + self.current_split_score_offset = 0; } fn end_run(&mut self) { self.save_splits(); self.current_split = None; + self.active = false; } } @@ -245,94 +320,158 @@ impl App for ZeroSplitter { } }); + // Detect gamemode change persist between frames + let prev_mode_id = Id::new("prev_mode"); + let cur_mode = self.categories[self.current_category].mode; + if let Some(prev_mode) = ctx.data(|data| data.get_temp::(prev_mode_id)) { + if prev_mode != cur_mode { + self.reset(); + } + } + ctx.data_mut(|data| data.insert_temp(prev_mode_id, cur_mode)); + let cur_category = &self.categories[self.current_category]; - for (i, split) in self.current_run.splits.iter().enumerate() { - let stage_n = (i & 3) + 1; - let loop_n = (i >> 2) + 1; - let best = cur_category.best_splits[i]; - let stored_best = self.comparison.personal_best.splits[i]; - let pb = self.comparison.personal_best.splits[i]; - - Sides::new().show( - ui, - |left| { - left.label(format!("{}-{}", loop_n, stage_n)); - - if best > 0 { - left.colored_label(GREEN, best.to_string()); - } - }, - |right| { - if *split != 0 || self.current_split == Some(i) { - let split_color = if self.current_split == Some(i) { - Color32::WHITE - } else { - DARK_ORANGE - }; - - right.colored_label(split_color, split.to_string()); - - if self.current_split != Some(i) { - // past split, we should show a diff - let diff = *split - pb; - let diff_color = if *split > stored_best { - LIGHT_ORANGE - } else if diff >= 0 { - Color32::WHITE + match cur_mode { + Gamemode::GreenOrange => { + for (i, split) in self.current_run.splits.iter().enumerate() { + let stage_n = (i & 3) + 1; + let loop_n = (i >> 2) + 1; + let best = cur_category.best_splits[i]; + let stored_best = self.comparison.personal_best.splits[i]; + let pb = self.comparison.personal_best.splits[i]; + + Sides::new().show( + ui, + |left| { + left.label(format!("{}-{}", loop_n, stage_n)); + + if best > 0 { + left.colored_label(GREEN, best.to_string()); + } + }, + |right| { + if *split != 0 || self.current_split == Some(i) { + let split_color = if self.current_split == Some(i) { + Color32::WHITE + } else { + DARK_ORANGE + }; + + right.colored_label(split_color, split.to_string()); + + if self.current_split != Some(i) { + // past split, we should show a diff + let diff = *split - pb; + let diff_color = if *split > stored_best { + LIGHT_ORANGE + } else if diff >= 0 { + Color32::WHITE + } else { + DARK_GREEN + }; + + if diff > 0 { + right.colored_label(diff_color, format!("+{}", diff)); + } else { + right.colored_label(diff_color, diff.to_string()); + } + } } else { - DARK_GREEN - }; - - if diff > 0 { - right.colored_label(diff_color, format!("+{}", diff)); + right.colored_label(DARK_GREEN, "--"); + } + }, + ); + } + } + Gamemode::WhiteVanilla => { + for (i, &split) in self.current_run.splits.iter().enumerate() { + let best_this_checkpoint = cur_category.best_splits[i]; + let stored_best = self.comparison.personal_best.splits[i]; + let pb = self.comparison.personal_best.splits[i]; + + Sides::new().show( + ui, + |left| { + left.label(vanilla_split_names(i)); + + if best_this_checkpoint > 0 { + left.colored_label(GREEN, best_this_checkpoint.to_string()); + } + }, + |right| { + if split != 0 || self.current_split == Some(i) { + let split_color = if self.current_split == Some(i) { + Color32::WHITE + } else { + DARK_ORANGE + }; + + right.colored_label(split_color, split.to_string()); + + if self.current_split != Some(i) { + // past split, we should show a diff + let diff = split - pb; + let diff_color = if split > stored_best { + LIGHT_ORANGE + } else if diff >= 0 { + Color32::WHITE + } else { + DARK_GREEN + }; + + if diff > 0 { + right.colored_label(diff_color, format!("+{}", diff)); + } else { + right.colored_label(diff_color, diff.to_string()); + } + } } else { - right.colored_label(diff_color, diff.to_string()); + right.colored_label(DARK_GREEN, "--"); } - } - } else { - right.colored_label(DARK_GREEN, "--"); - } - }, - ); + }, + ); + } + } + Gamemode::BlackOnion => { + todo!() + } } ui.label(format!("Personal Best: {}", cur_category.personal_best.score)); - ui.label(format!( - "Sum of Best: {}", - cur_category.best_splits.into_iter().sum::() - )) + ui.label(format!("Sum of Best: {}", cur_category.best_splits.iter().sum::())) }); }); if self.waiting_for_category { if let Ok(new_category) = self.dialog_rx.try_recv() { - if !new_category.is_empty() { - self.categories.push(Category::new(new_category)); + if let Some(data) = new_category { + self.categories.push(Category::new(data.textbox, data.mode)); self.current_category = self.categories.len() - 1; self.save_splits(); } self.waiting_for_category = false; } else { - entry_dialog(ctx, self.dialog_tx.clone(), "Enter new category name"); + category_maker_dialog(ctx, self.dialog_tx.clone(), "Enter new category name", true); } } if self.waiting_for_rename { - if let Ok(new_name) = self.dialog_rx.try_recv() { - if !new_name.is_empty() { - self.categories[self.current_category].name = new_name; + if let Ok(new_category) = self.dialog_rx.try_recv() { + if let Some(data) = new_category { + self.categories[self.current_category].name = data.textbox; + self.categories[self.current_category].mode = data.mode; self.save_splits(); } self.waiting_for_rename = false; } else { - entry_dialog(ctx, self.dialog_tx.clone(), "Enter new name for category"); + category_maker_dialog(ctx, self.dialog_tx.clone(), "Enter new name for category", false); } } if self.waiting_for_confirm { - if let Ok(confirmation) = self.dialog_rx.try_recv() { - if confirmation == "Deleted" { + if let Ok(Some(confirmation)) = self.dialog_rx.try_recv() { + if confirmation.textbox == "Deleted" { self.categories.remove(self.current_category); self.current_category = self.current_category.saturating_sub(1); } @@ -390,7 +529,85 @@ fn entry_dialog(ctx: &Context, tx: Sender, msg: &'static str) { }); } -fn confirm_dialog(ctx: &Context, tx: Sender, msg: String) { +/// Category entry dialoge menu with a gamemode dropdown. +fn category_maker_dialog(ctx: &Context, tx: Sender>, msg: &'static str, mode_select: bool) { + let vp_builder = ViewportBuilder::default() + .with_title("ZeroSplitter") + .with_active(true) + .with_resizable(false) + .with_minimize_button(false) + .with_maximize_button(false) + .with_inner_size([200., 100.]); + + ctx.show_viewport_deferred(ViewportId::from_hash_of("entry dialog"), vp_builder, move |ctx, _| { + if ctx.input(|input| input.viewport().close_requested()) { + let _ = tx.send(None); + request_repaint(); + return; + } + + let text_id = Id::new("edit text"); + let mode_id = Id::new("gamemode"); + let mut edit_str = ctx.data_mut(|data| data.get_temp_mut_or_insert_with(text_id, || String::new()).clone()); + let mut mode = ctx.data_mut(|data| *data.get_temp_mut_or(mode_id, Gamemode::GreenOrange)); + + CentralPanel::default().show(ctx, |ui| { + ui.vertical_centered_justified(|ui| { + ui.label(msg); + ui.text_edit_singleline(&mut edit_str); + if ui.button("Confirm").clicked() { + let _ = tx.send(Some(EntryDialogData { + textbox: edit_str.clone(), + mode: mode, + })); + request_repaint(); + ctx.send_viewport_cmd(eframe::egui::ViewportCommand::Close); + } + }); + }); + + if mode_select { + TopBottomPanel::bottom("mode_select").show(ctx, |ui| { + ui.vertical_centered_justified(|ui| { + ComboBox::from_label("Mode") + .selected_text(format!("{:?}", mode)) + .show_ui(ui, |ui| { + ui.selectable_value(&mut mode, Gamemode::GreenOrange, "Green Orange"); + ui.selectable_value(&mut mode, Gamemode::WhiteVanilla, "White Vanilla"); + ui.selectable_value(&mut mode, Gamemode::BlackOnion, "Black Onion"); + }); + }) + }); + } + + ctx.data_mut(|data| { + data.insert_temp(text_id, edit_str); + data.insert_temp(mode_id, mode); + }); + }); +} +#[derive(PartialEq, Eq, Clone, Copy, Debug, Serialize, Deserialize)] +pub enum Gamemode { + GreenOrange, + WhiteVanilla, + BlackOnion, +} + +impl Gamemode { + fn splits(&self) -> usize { + match self { + Gamemode::GreenOrange => 8, + Gamemode::WhiteVanilla => 26, + Gamemode::BlackOnion => todo!(), + } + } +} +pub struct EntryDialogData { + pub textbox: String, + pub mode: Gamemode, +} + +fn confirm_dialog(ctx: &Context, tx: Sender>, msg: String) { let vp_builder = ViewportBuilder::default() .with_title("ZeroSplitter") .with_active(true) @@ -401,7 +618,7 @@ fn confirm_dialog(ctx: &Context, tx: Sender, msg: String) { ctx.show_viewport_deferred(ViewportId::from_hash_of("confirm dialog"), vp_builder, move |ctx, _| { if ctx.input(|input| input.viewport().close_requested()) { - let _ = tx.send("".to_string()); + let _ = tx.send(None); request_repaint(); return; } @@ -411,10 +628,13 @@ fn confirm_dialog(ctx: &Context, tx: Sender, msg: String) { ui.label(msg.clone()); ui.columns_const(|[left, right]| { if left.button("Delete").clicked() { - let _ = tx.send("Deleted".to_string()); + let _ = tx.send(Some(EntryDialogData { + textbox: "Deleted".to_string(), + mode: Gamemode::GreenOrange, + })); request_repaint(); } else if right.button("Cancel").clicked() { - let _ = tx.send("".to_string()); + let _ = tx.send(None); request_repaint(); } }); @@ -429,31 +649,233 @@ fn request_repaint() { } } +/// Translation of function of the same name in ZR. +/// Checkpoints in WV have the same stage/checkpoint as in GO, +/// e.g. the cloudoo segment in stage 1 is technically stage 3 checkpoint 2 +/// just like it is in GO. Also, some segments have multiple checkpoints. +/// This function gets the segment number in WV from the GO data. +/// "Realm" seems to indicate when the ship is changed, like in TLB, the dream, or bonus stages +/// +/// Someone much smarter than me could try to call this function directly from the +/// game instead of using this. +fn vanilla_get_simstage(stage: u8, checkpoint: u8, checkpoint_sub: u8, realm: u8) -> Result<(u32, u32), ()> { + Ok(match (stage, checkpoint) { + // GO stage/check => WV stage/check + (1, _) if realm == 3 => (1, 4), + (1, 0 | 1) => (1, 0), + (1, 2) => (2, 4), + (1, 3) => (1, 2), + (1, 4 | 5 | 6) => (1, 3), + + (2, _) if realm == 3 => (2, 6), + (2, 0 | 1) => (2, 0), + (2, 2) => (3, 1), + (2, 3) => (2, 1), + (2, 4) => (2, 3), + (2, 5) => (4, 1), + (2, 6 | 7 | 8) => (2, 5), + + (3, 0) => (3, 0), + (3, 1) => (1, 1), + (3, 2) => (4, 4), + (3, 3) => (3, 2), + (3, 4) if checkpoint_sub < 1 => (2, 2), + (3, 4) => (3, 3), + + (3, 6) => (4, 3), + (3, 7) => (3, 4), + (3, 8 | 9) => (3, 5), + + (4, _) if realm == 3 => (3, 6), + (4, 3) => (3, 6), + (4, 5 | 6) => (4, 0), + (4, 7 | 8) => (4, 2), + (4, 9 | 10) => (4, 5), + _ => return Err(()), + }) +} + +/// Translate sim stage and checkpoint to split count +/// e.g. snake is split 7 (0 indexed) +fn vanilla_get_split_count(simstage: u32, simcheckpoint: u32) -> usize { + [ + (1, 1), + (1, 2), + (1, 3), + (1, 4), + (1, 5), + (2, 1), + (2, 2), + (2, 3), + (2, 4), + (2, 5), + (2, 6), + (2, 7), + (3, 1), + (3, 2), + (3, 3), + (3, 4), + (3, 5), + (3, 6), + (3, 7), + (4, 1), + (4, 2), + (4, 3), + (4, 4), + (4, 5), + (4, 6), + (4, 7), + ] + .iter() + .position(|&x| x == (simstage, simcheckpoint + 1)) + .unwrap() +} + +fn vanilla_stage_from_split(split: usize) -> (u32, u32) { + [ + (1, 1), + (1, 2), + (1, 3), + (1, 4), + (1, 5), + (2, 1), + (2, 2), + (2, 3), + (2, 4), + (2, 5), + (2, 6), + (2, 7), + (3, 1), + (3, 2), + (3, 3), + (3, 4), + (3, 5), + (3, 6), + (3, 7), + (4, 1), + (4, 2), + (4, 3), + (4, 4), + (4, 5), + (4, 6), + (4, 7), + ][split] +} + +fn vanilla_split_names(split: usize) -> &'static str { + [ + "1-1", "1-2", "1-3", "1-4", "Bonus 1", "2-1", "2-2", "2-3", "2-4", "2-5", "2-6", "Bonus 2", "3-1", "3-2", + "3-3", "3-4", "3-5", "3-6", "Bonus 3", "4-1", "4-2", "4-3", "4-4", "4-5", "4-6", "Stage EX", + ][split] +} + +fn vanilla_descriptive_split_names(split: usize) -> &'static str { + [ + "Stage 1 Start", + "Cloudoos", + "Arc Adder", + "Catastrophy", + "Bonus 1", + "Stage 2 Start", + "Box Blockade", + "Snake", + "Artypo", + "Skull Taxis", + "2nd Apocalypse", + "Bonus 2", + "Stage 3 Start", + "Crab Landing", + "Plane", + "Crab", + "Tank", + "Grapefruit", + "Bonus 3", + "Stage 4 Start", + "Left Tunnel", + "Maze", + "Trains", + "Knight Ships", + "Orb Spewer", + "Stage EX", + ][split] +} + #[derive(Debug, Default, Serialize, Deserialize, Clone, Copy)] -struct Run { +struct OldRun { splits: [i32; 8], score: i32, } +#[derive(Debug, Serialize, Deserialize, Clone)] +struct Run { + splits: Vec, + score: i32, + mode: Gamemode +} + +impl Run { + fn new(mode: Gamemode) -> Self { + Run { + splits: vec![0; mode.splits()], + score: 0, + mode + } + } +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +struct OldCategory { + personal_best: OldRun, + best_splits: [i32; 8], + name: String, +} + +impl OldRun { + fn to_new(self) -> Run { + Run { + splits: self.splits.to_vec(), + score: self.score, + mode: Gamemode::GreenOrange + } + } +} + +impl OldCategory { + fn to_new(&self, mode: Gamemode) -> Category { + Category { + personal_best: self.personal_best.to_new(), + best_splits: self.best_splits.to_vec(), + name: self.name.clone(), + mode, + } + } +} + #[derive(Debug, Serialize, Deserialize, Clone)] struct Category { personal_best: Run, - best_splits: [i32; 8], + best_splits: Vec, name: String, + mode: Gamemode, } impl Category { - fn new(name: String) -> Self { + fn new(name: String, mode: Gamemode) -> Self { Category { - personal_best: Default::default(), - best_splits: Default::default(), + personal_best: Run::new(mode), + best_splits: vec![0; mode.splits()], name, + mode, } } fn update_from_run(&mut self, run: &Run) { + if run.mode != self.mode { + return + } + if run.score > self.personal_best.score { - self.personal_best = *run; + self.personal_best = run.clone(); } for (best, new) in self.best_splits.iter_mut().zip(run.splits.iter()) { From 615248f4ece547c1a7fd6657f242bf3b1184628a Mon Sep 17 00:00:00 2001 From: lily_and_doll <103815266+lily-and-doll@users.noreply.github.com> Date: Thu, 13 Nov 2025 21:01:29 -0500 Subject: [PATCH 04/52] Fix restart detection --- splitter/src/main.rs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/splitter/src/main.rs b/splitter/src/main.rs index c15cb85..c890320 100644 --- a/splitter/src/main.rs +++ b/splitter/src/main.rs @@ -252,12 +252,10 @@ impl ZeroSplitter { // Reset if we returned to 1-1 if frame.total_score() == 0 && self.last_frame.total_score() > 0 || self.last_frame.is_menu() { - // Only support starting at stage 1 for now - if frame.is_first_stage() { - self.reset(); - self.current_split = Some(0); - return - } // TODO: detect start of each other stage and start splits there properly + self.reset(); + self.current_split = Some(0); + return + // TODO: detect start of each other stage and start splits there properly } if !frame.is_menu() && self.active { From 693dd634c8de3c6af69eaaeb182288a50a67f00f Mon Sep 17 00:00:00 2001 From: lily_and_doll <103815266+lily-and-doll@users.noreply.github.com> Date: Thu, 13 Nov 2025 21:49:46 -0500 Subject: [PATCH 05/52] Allow starting from other stages in WV --- splitter/src/main.rs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/splitter/src/main.rs b/splitter/src/main.rs index c890320..309ae68 100644 --- a/splitter/src/main.rs +++ b/splitter/src/main.rs @@ -245,17 +245,23 @@ impl ZeroSplitter { } fn update_whitevanilla(&mut self, frame: FrameData) { - // Skip update if current category isn't White Vanilla - if self.categories[self.current_category].mode != Gamemode::WhiteVanilla { + // Skip update if current category isn't White Vanilla or if on menu + if self.categories[self.current_category].mode != Gamemode::WhiteVanilla || frame.is_menu() { return; } // Reset if we returned to 1-1 if frame.total_score() == 0 && self.last_frame.total_score() > 0 || self.last_frame.is_menu() { self.reset(); - self.current_split = Some(0); + self.current_split = match frame.stage { + 1 => Some(0), + 2 => Some(5), + 3 => Some(12), + 4 => Some(19), + _ => panic!("Stage out of bounds! {}", frame.stage) + }; return - // TODO: detect start of each other stage and start splits there properly + } if !frame.is_menu() && self.active { From 948296c40a3bcfa089fb637c7ed40c94299e4f33 Mon Sep 17 00:00:00 2001 From: lily_and_doll <103815266+lily-and-doll@users.noreply.github.com> Date: Fri, 14 Nov 2025 00:15:23 -0500 Subject: [PATCH 06/52] Add icon --- Cargo.lock | 103 +++++++++++++++++++++++++++++++++------ splitter/Cargo.toml | 3 ++ splitter/assets/icon.ico | Bin 0 -> 5694 bytes splitter/build.rs | 13 +++++ 4 files changed, 105 insertions(+), 14 deletions(-) create mode 100644 splitter/assets/icon.ico create mode 100644 splitter/build.rs diff --git a/Cargo.lock b/Cargo.lock index ef9c507..f2931fd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -45,7 +45,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f47983a1084940ba9a39c077a8c63e55c619388be5476ac04c804cfbd1e63459" dependencies = [ "accesskit", - "hashbrown", + "hashbrown 0.15.2", "immutable-chunkmap", ] @@ -57,7 +57,7 @@ checksum = "7329821f3bd1101e03a7d2e03bd339e3ac0dc64c70b4c9f9ae1949e3ba8dece1" dependencies = [ "accesskit", "accesskit_consumer", - "hashbrown", + "hashbrown 0.15.2", "objc2 0.5.2", "objc2-app-kit 0.2.2", "objc2-foundation 0.2.2", @@ -89,7 +89,7 @@ checksum = "24fcd5d23d70670992b823e735e859374d694a3d12bfd8dd32bd3bd8bedb5d81" dependencies = [ "accesskit", "accesskit_consumer", - "hashbrown", + "hashbrown 0.15.2", "paste", "static_assertions", "windows 0.58.0", @@ -1315,7 +1315,7 @@ checksum = "dcf29e94d6d243368b7a56caa16bc213e4f9f8ed38c4d9557069527b5d5281ca" dependencies = [ "bitflags 2.9.0", "gpu-descriptor-types", - "hashbrown", + "hashbrown 0.15.2", ] [[package]] @@ -1336,6 +1336,12 @@ dependencies = [ "foldhash", ] +[[package]] +name = "hashbrown" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" + [[package]] name = "heck" version = "0.5.0" @@ -1544,12 +1550,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.8.0" +version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3954d50fe15b02142bf25d3b8bdadb634ec3948f103d04ffe3031bc8fe9d7058" +checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.16.0", ] [[package]] @@ -2561,18 +2567,28 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.219" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.219" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", @@ -2602,6 +2618,15 @@ dependencies = [ "syn", ] +[[package]] +name = "serde_spanned" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e24345aa0fe688594e73770a5f6d1b216508b4f93484c0026d521acd30134392" +dependencies = [ + "serde_core", +] + [[package]] name = "sha1" version = "0.10.6" @@ -2882,12 +2907,36 @@ dependencies = [ "zerovec", ] +[[package]] +name = "toml" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0dc8b1fb61449e27716ec0e1bdf0f6b8f3e8f6b05391e8497b8b6d7804ea6d8" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned", + "toml_datetime 0.7.3", + "toml_parser", + "toml_writer", + "winnow", +] + [[package]] name = "toml_datetime" version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" +[[package]] +name = "toml_datetime" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2cdb639ebbc97961c51720f858597f7f24c4fc295327923af55b74c3c724533" +dependencies = [ + "serde_core", +] + [[package]] name = "toml_edit" version = "0.22.24" @@ -2895,10 +2944,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474" dependencies = [ "indexmap", - "toml_datetime", + "toml_datetime 0.6.8", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e" +dependencies = [ "winnow", ] +[[package]] +name = "toml_writer" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df8b2b54733674ad286d16267dcfc7a71ed5c776e4ac7aa3c3e2561f7c637bf2" + [[package]] name = "tracing" version = "0.1.41" @@ -3821,13 +3885,23 @@ dependencies = [ [[package]] name = "winnow" -version = "0.7.4" +version = "0.7.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e97b544156e9bebe1a0ffbc03484fc1ffe3100cbce3ffb17eac35f7cdd7ab36" +checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" dependencies = [ "memchr", ] +[[package]] +name = "winresource" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1ef04dd590e94ff7431a8eda99d5ca659e688d60e930bd0a330062acea4608f" +dependencies = [ + "toml", + "version_check", +] + [[package]] name = "wit-bindgen-rt" version = "0.39.0" @@ -4118,6 +4192,7 @@ dependencies = [ "serde", "serde_json", "windows 0.60.0", + "winresource", ] [[package]] diff --git a/splitter/Cargo.toml b/splitter/Cargo.toml index a106acd..2871a0e 100644 --- a/splitter/Cargo.toml +++ b/splitter/Cargo.toml @@ -22,3 +22,6 @@ features = [ "Win32_System_Memory", "Win32_Security" ] + +[build-dependencies] +winresource = "0.1.27" diff --git a/splitter/assets/icon.ico b/splitter/assets/icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..04e9ae7f8f51944761e3643ab32d6c4c1a533c78 GIT binary patch literal 5694 zcmeHLJ8l#~5bb4x*>yq&5wZ}8GbBd@2>Swvm=kaS5|BW<0XYddC*TaWAn+-WR`Z*Yz z8jl`?aCjKP^A{o9fBYpJy!;;Cegb+E!|T%+VvJMNTmNkbZaeTlbfD{deB`1}jI^m` zj&#Y{ObUKK4r)?KvYX#`7_6b;dt^6X$z20-KS~&9E?24k%lO`zX+AVWhH1*QoLg#v z)G=F=t0Kr#V=OfRZE*s2X#!e^x0M2I6bZELrr#pQ{H8ospsDqw8vyGTX=AzmP zY9P6_SMVu>QD@isrAIsUwZW5oR5ztBp{V<3!KZ}llqzOD*dGo5tkbl`o`D$*AYu>rr!T*Wzp42wU8$B#Z^EW z{&Hzi8tDckosTIL$B;@N;wv4PX|pq*yYIRYq^du#c;Rz0$=GMU%0*jw#@bA&SCphTOkeKq%8sXMg2&Y z=$57C8{MMX!MuMvJ=udoDt@A;1f~9%E|4+5pce^TzorvX@vSm7$R-=e{LYWfaTVX) zV&!h-^P=F(vP6yb%LqFU`HD|(iJIgSIKeLyjQC~Znf&q#P||s`P3p^ClB)uR9W3tW1%{+iD%`MwOTsr3z_a0!a=(cs#B3bwGl zft0=y>q(r;b&1vbD<3c=o}6+md^$NyUz4hf*Akz-Iver%2Es9Rs&Cdcy#dWax1by5 z$BVEY!hXEO;y-@kSNjwH0t|O!+|u?jp3-(PZfFDHmh1&gdyy8OV>}*53IRB{j z*XPZ4|4qO6eWDh<;u^hMZ1ak&9zE?ptcO?I^|SuRdRWsAwDAZQ2NO?lTpTBI G9ov6v|0+WO literal 0 HcmV?d00001 diff --git a/splitter/build.rs b/splitter/build.rs new file mode 100644 index 0000000..c4b65d5 --- /dev/null +++ b/splitter/build.rs @@ -0,0 +1,13 @@ +use std::{env, io}; + +use winresource::WindowsResource; + +fn main() -> io::Result<()> { + if env::var_os("CARGO_CFG_WINDOWS").is_some() { + WindowsResource::new() + // This path can be absolute, or relative to your crate root. + .set_icon("assets/icon.ico") + .compile()?; + } + Ok(()) +} \ No newline at end of file From 34943c0c9ee71a149ffa3db49f4804e1d3cd9d17 Mon Sep 17 00:00:00 2001 From: lily_and_doll <103815266+lily-and-doll@users.noreply.github.com> Date: Fri, 14 Nov 2025 03:09:07 -0500 Subject: [PATCH 07/52] add relative/absolute score display Use the checkbox to switch between showing relative scores, e.g. score within split, and absolute scores, e.g. score up to that split --- splitter/src/main.rs | 217 ++++++++++++++++++++++--------------------- 1 file changed, 109 insertions(+), 108 deletions(-) diff --git a/splitter/src/main.rs b/splitter/src/main.rs index 309ae68..93d5c16 100644 --- a/splitter/src/main.rs +++ b/splitter/src/main.rs @@ -25,10 +25,22 @@ mod hook; mod system; mod ui; +#[allow(unused)] const DARK_GREEN: Color32 = Color32::from_rgb(0, 0x4f, 0x4d); +#[allow(unused)] const GREEN: Color32 = Color32::from_rgb(0, 0x94, 0x79); +#[allow(unused)] const LIGHT_ORANGE: Color32 = Color32::from_rgb(0xff, 0xc0, 0x73); +#[allow(unused)] const DARK_ORANGE: Color32 = Color32::from_rgb(0xff, 0x80, 0); +#[allow(unused)] +const DARKER_ORANGE: Color32 = Color32::from_rgb(0xdd, 0x59, 0x28); +#[allow(unused)] +const ORANGEST: Color32 = Color32::from_rgb(0xad, 0x2f, 0x17); +#[allow(unused)] +const DARKER_GREEN: Color32 = Color32::from_rgb(0x00, 0x32, 0x32); +#[allow(unused)] +const GREENEST: Color32 = Color32::from_rgb(0x00, 0x1d, 0x23); static EGUI_CTX: OnceLock = OnceLock::new(); @@ -94,6 +106,7 @@ struct ZeroSplitter { dialog_tx: Sender>, comparison: Category, active: bool, + relative_score: bool, } impl ZeroSplitter { @@ -119,6 +132,7 @@ impl ZeroSplitter { waiting_for_confirm: false, comparison: Category::new("".to_string(), Gamemode::GreenOrange), active: false, + relative_score: true, } } @@ -258,10 +272,9 @@ impl ZeroSplitter { 2 => Some(5), 3 => Some(12), 4 => Some(19), - _ => panic!("Stage out of bounds! {}", frame.stage) + _ => panic!("Stage out of bounds! {}", frame.stage), }; - return - + return; } if !frame.is_menu() && self.active { @@ -309,6 +322,7 @@ impl App for ZeroSplitter { CentralPanel::default().show(ctx, |ui| { ui.with_layout(Layout::top_down_justified(Align::Min), |ui| { ui.horizontal(|ui| { + ui.checkbox(&mut self.relative_score, "").on_hover_text("Use relative (per-split) scores"); ui.label("Category: "); ComboBox::from_label("").show_index(ui, &mut self.current_category, self.categories.len(), |i| { &self.categories[i].name @@ -336,110 +350,97 @@ impl App for ZeroSplitter { let cur_category = &self.categories[self.current_category]; - match cur_mode { - Gamemode::GreenOrange => { - for (i, split) in self.current_run.splits.iter().enumerate() { - let stage_n = (i & 3) + 1; - let loop_n = (i >> 2) + 1; - let best = cur_category.best_splits[i]; - let stored_best = self.comparison.personal_best.splits[i]; - let pb = self.comparison.personal_best.splits[i]; - - Sides::new().show( - ui, - |left| { - left.label(format!("{}-{}", loop_n, stage_n)); - - if best > 0 { - left.colored_label(GREEN, best.to_string()); - } - }, - |right| { - if *split != 0 || self.current_split == Some(i) { - let split_color = if self.current_split == Some(i) { - Color32::WHITE - } else { - DARK_ORANGE - }; - - right.colored_label(split_color, split.to_string()); - - if self.current_split != Some(i) { - // past split, we should show a diff - let diff = *split - pb; - let diff_color = if *split > stored_best { - LIGHT_ORANGE - } else if diff >= 0 { - Color32::WHITE - } else { - DARK_GREEN - }; - - if diff > 0 { - right.colored_label(diff_color, format!("+{}", diff)); - } else { - right.colored_label(diff_color, diff.to_string()); - } - } - } else { - right.colored_label(DARK_GREEN, "--"); - } - }, - ); - } + for (i, split) in self.current_run.splits.iter().enumerate().map(|(i, &s)| { + if self.relative_score { + (i, s) + } else { + (i, self.current_run + .splits + .iter() + .enumerate() + .take_while(|&(idx, _)| idx <= i) + .fold(0, |acc, (_, &s)| acc + s)) } - Gamemode::WhiteVanilla => { - for (i, &split) in self.current_run.splits.iter().enumerate() { - let best_this_checkpoint = cur_category.best_splits[i]; - let stored_best = self.comparison.personal_best.splits[i]; - let pb = self.comparison.personal_best.splits[i]; - - Sides::new().show( - ui, - |left| { - left.label(vanilla_split_names(i)); - - if best_this_checkpoint > 0 { - left.colored_label(GREEN, best_this_checkpoint.to_string()); - } - }, - |right| { - if split != 0 || self.current_split == Some(i) { - let split_color = if self.current_split == Some(i) { - Color32::WHITE - } else { - DARK_ORANGE - }; - - right.colored_label(split_color, split.to_string()); - - if self.current_split != Some(i) { - // past split, we should show a diff - let diff = split - pb; - let diff_color = if split > stored_best { - LIGHT_ORANGE - } else if diff >= 0 { - Color32::WHITE - } else { - DARK_GREEN - }; - - if diff > 0 { - right.colored_label(diff_color, format!("+{}", diff)); - } else { - right.colored_label(diff_color, diff.to_string()); - } - } + }) { + let stage_n = (i & 3) + 1; + let loop_n = (i >> 2) + 1; + let gold_split = if self.relative_score { + cur_category.best_splits[i] + } else { + cur_category + .best_splits + .iter() + .enumerate() + .take_while(|&(idx, _)| idx <= i) + .fold(0, |acc, (_, &s)| acc + s) + }; + let pb_split = if self.relative_score { + self.comparison.personal_best.splits[i] + } else { + self.comparison.personal_best.splits + .iter() + .enumerate() + .take_while(|&(idx, _)| idx <= i) + .fold(0, |acc, (_, &s)| acc + s) + }; + + Sides::new().show( + ui, + |left| { + match cur_category.mode { + Gamemode::GreenOrange => left.label(format!("{}-{}", loop_n, stage_n)), + Gamemode::WhiteVanilla => left.label(vanilla_split_names(i)), + Gamemode::BlackOnion => todo!(), + }; + + if gold_split > 0 { + left.colored_label(GREEN, gold_split.to_string()); + } + }, + |right| { + if i <= self.current_split.unwrap_or(0) { + let split_color = if self.current_split == Some(i) { + Color32::WHITE + } else if split >= gold_split{ + DARKER_ORANGE + } else { + DARK_ORANGE + }; + + if self.relative_score { + right.colored_label(split_color, split.to_string()); + } else { + let split_absolute = self.current_run + .splits + .iter() + .enumerate() + .take_while(|&(idx, _)| idx <= i) + .fold(0, |acc, (_, &s)| acc + s); + right.colored_label(split_color, split_absolute.to_string()); + } + + if i < self.current_split.unwrap_or(0) { + // past split, we should show a diff + let diff = split - pb_split; + let diff_color = if diff > 0 { + LIGHT_ORANGE + } else if diff == 0 { + Color32::WHITE + } else { + DARK_GREEN + }; + + if diff > 0 { + right.colored_label(diff_color, format!("+{}", diff)); } else { - right.colored_label(DARK_GREEN, "--"); + right.colored_label(diff_color, diff.to_string()); } - }, - ); - } - } - Gamemode::BlackOnion => { - todo!() - } + } + } else { + right.colored_label(DARK_GREEN, "--"); + } + }, + ); } ui.label(format!("Personal Best: {}", cur_category.personal_best.score)); @@ -814,7 +815,7 @@ struct OldRun { struct Run { splits: Vec, score: i32, - mode: Gamemode + mode: Gamemode, } impl Run { @@ -822,7 +823,7 @@ impl Run { Run { splits: vec![0; mode.splits()], score: 0, - mode + mode, } } } @@ -839,7 +840,7 @@ impl OldRun { Run { splits: self.splits.to_vec(), score: self.score, - mode: Gamemode::GreenOrange + mode: Gamemode::GreenOrange, } } } @@ -875,7 +876,7 @@ impl Category { fn update_from_run(&mut self, run: &Run) { if run.mode != self.mode { - return + return; } if run.score > self.personal_best.score { From 8e983694073e266b4751bb7101f2288f773ec691 Mon Sep 17 00:00:00 2001 From: lily_and_doll <103815266+lily-and-doll@users.noreply.github.com> Date: Fri, 14 Nov 2025 04:00:58 -0500 Subject: [PATCH 08/52] replace checkbox with toggle button --- splitter/src/main.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/splitter/src/main.rs b/splitter/src/main.rs index 93d5c16..ac4f03b 100644 --- a/splitter/src/main.rs +++ b/splitter/src/main.rs @@ -14,8 +14,7 @@ use common::FrameData; use eframe::{ App, Frame, NativeOptions, egui::{ - Align, CentralPanel, Color32, ComboBox, Context, IconData, Id, Key, Layout, Sides, ThemePreference, - TopBottomPanel, ViewportBuilder, ViewportId, + Align, Button, CentralPanel, Color32, ComboBox, Context, IconData, Id, Key, Layout, Sides, ThemePreference, TopBottomPanel, ViewportBuilder, ViewportId }, }; use log::{error, warn}; @@ -320,9 +319,13 @@ impl App for ZeroSplitter { } CentralPanel::default().show(ctx, |ui| { + ui.visuals_mut().selection.bg_fill = DARK_ORANGE; + ui.visuals_mut().selection.stroke.color = GREENEST; ui.with_layout(Layout::top_down_justified(Align::Min), |ui| { ui.horizontal(|ui| { - ui.checkbox(&mut self.relative_score, "").on_hover_text("Use relative (per-split) scores"); + ui.toggle_value(&mut self.relative_score, "RELATIVE") + }); + ui.horizontal(|ui| { ui.label("Category: "); ComboBox::from_label("").show_index(ui, &mut self.current_category, self.categories.len(), |i| { &self.categories[i].name From cef473f02239027d613b575f2e984ac9d7bddeae Mon Sep 17 00:00:00 2001 From: lily_and_doll <103815266+lily-and-doll@users.noreply.github.com> Date: Fri, 14 Nov 2025 04:22:44 -0500 Subject: [PATCH 09/52] Clean up score display --- splitter/src/main.rs | 28 +++++++++++----------------- 1 file changed, 11 insertions(+), 17 deletions(-) diff --git a/splitter/src/main.rs b/splitter/src/main.rs index ac4f03b..f96cc7d 100644 --- a/splitter/src/main.rs +++ b/splitter/src/main.rs @@ -354,9 +354,11 @@ impl App for ZeroSplitter { let cur_category = &self.categories[self.current_category]; for (i, split) in self.current_run.splits.iter().enumerate().map(|(i, &s)| { + // Switch current split score between modes if self.relative_score { (i, s) } else { + // Get total score up to the current split (i, self.current_run .splits .iter() @@ -365,8 +367,11 @@ impl App for ZeroSplitter { .fold(0, |acc, (_, &s)| acc + s)) } }) { + // translate split number to stage/loop for GO let stage_n = (i & 3) + 1; let loop_n = (i >> 2) + 1; + // Get relative/absolute gold split + // Gold split = high score of this split in any run let gold_split = if self.relative_score { cur_category.best_splits[i] } else { @@ -377,6 +382,8 @@ impl App for ZeroSplitter { .take_while(|&(idx, _)| idx <= i) .fold(0, |acc, (_, &s)| acc + s) }; + // Get relative/absolute split in the PB + // PB split = score of this split in the PB run let pb_split = if self.relative_score { self.comparison.personal_best.splits[i] } else { @@ -401,7 +408,9 @@ impl App for ZeroSplitter { } }, |right| { + // Only write splits up to the current split if i <= self.current_split.unwrap_or(0) { + // Set color of split (rightmost number) let split_color = if self.current_split == Some(i) { Color32::WHITE } else if split >= gold_split{ @@ -410,17 +419,7 @@ impl App for ZeroSplitter { DARK_ORANGE }; - if self.relative_score { - right.colored_label(split_color, split.to_string()); - } else { - let split_absolute = self.current_run - .splits - .iter() - .enumerate() - .take_while(|&(idx, _)| idx <= i) - .fold(0, |acc, (_, &s)| acc + s); - right.colored_label(split_color, split_absolute.to_string()); - } + right.colored_label(split_color, split.to_string()); if i < self.current_split.unwrap_or(0) { // past split, we should show a diff @@ -432,12 +431,7 @@ impl App for ZeroSplitter { } else { DARK_GREEN }; - - if diff > 0 { - right.colored_label(diff_color, format!("+{}", diff)); - } else { - right.colored_label(diff_color, diff.to_string()); - } + right.colored_label(diff_color, format!("{diff:+}")); } } else { right.colored_label(DARK_GREEN, "--"); From ee0ab7cf9c22b5617d222ee51dfa3b0d143f87a5 Mon Sep 17 00:00:00 2001 From: lily_and_doll <103815266+lily-and-doll@users.noreply.github.com> Date: Fri, 14 Nov 2025 19:40:28 -0500 Subject: [PATCH 10/52] update README --- README.md | 34 +++++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 0eee087..73aef2b 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,34 @@ # ZeroSplitter -Automatic split-tracker for ZeroRanger. Very alpha. Does not yet support White Vanilla. +Automatic split-tracker for ZeroRanger. Very beta. +Supports Green Orange and White Vanilla. + +[DOWNLOAD](https://github.com/lily-and-doll/ZeroSplitter/releases/tag/0.2.0) + +# How to use +Extract the zip and run `zerosplitter.exe`. The program will detect Zeroranger and start reading data automatically. +You can start playing and your scores will automatically show up in ZeroSplitter. If you restart your run and your score +doesn't show up as the correct split, just go back to the main menu and launch from there. + +Continues should be tracked properly in Green Orange but not White Vanilla. + +Your data is stored in the `zs_data.json` next to the .exe. + +A "category" is a set of splits and personal bests to run against. ZeroSplitter will try to detect which mode +you are playing and not overwrite scores from one mode with another - but don't push your luck: have the right +category selected before you take off. + +Press the plus button to add a new category - don't click Black Onion for the mode (it won't work at all). + +Currently the only way to delete categories is by manually deleting them in the `zs_data.json`, but you can rename them. + +The "relative" button switches the display between showing your score per split or your running total up to each split. +Turn on relative mode to see how much better or worse you did each split versus your PB run. Turn off relative mode +to see how far ahead or behind you are versus your PB run. + +The relative button only changes the display, not how the data is saved: toggle it as much as you like, even mid-run. + +If you want to move the program to another folder, just copy `zerosplitter.exe`, `payload.dll`, and `zs_data.json`. + +# How to build from source +Just run `cargo run --release` in the top level of the repository, next to this `README.md`. `build.sh` will zip `zerosplitter.exe` +and `payload.dll` for you, but you don't need to do this. \ No newline at end of file From b9afa0e1cf6fce5767c20cec6dba3a799ab7566f Mon Sep 17 00:00:00 2001 From: lillies <103815266+lily-and-doll@users.noreply.github.com> Date: Fri, 14 Nov 2025 20:42:00 -0500 Subject: [PATCH 11/52] Add screenshots to README.md --- README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 73aef2b..3e4f451 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,10 @@ Supports Green Orange and White Vanilla. [DOWNLOAD](https://github.com/lily-and-doll/ZeroSplitter/releases/tag/0.2.0) +zerosplitter_hjuYG7gioU +zerosplitter_1v8gZHNMn1 + + # How to use Extract the zip and run `zerosplitter.exe`. The program will detect Zeroranger and start reading data automatically. You can start playing and your scores will automatically show up in ZeroSplitter. If you restart your run and your score @@ -31,4 +35,4 @@ If you want to move the program to another folder, just copy `zerosplitter.exe`, # How to build from source Just run `cargo run --release` in the top level of the repository, next to this `README.md`. `build.sh` will zip `zerosplitter.exe` -and `payload.dll` for you, but you don't need to do this. \ No newline at end of file +and `payload.dll` for you, but you don't need to do this. From c6021820329f1f6584536e64455123c5f1d4596f Mon Sep 17 00:00:00 2001 From: lily_and_doll <103815266+lily-and-doll@users.noreply.github.com> Date: Fri, 14 Nov 2025 21:15:22 -0500 Subject: [PATCH 12/52] Add "show best scores" toggle --- splitter/src/main.rs | 39 ++++++++++++++++++++++++++++----------- 1 file changed, 28 insertions(+), 11 deletions(-) diff --git a/splitter/src/main.rs b/splitter/src/main.rs index f96cc7d..308f3d9 100644 --- a/splitter/src/main.rs +++ b/splitter/src/main.rs @@ -14,7 +14,8 @@ use common::FrameData; use eframe::{ App, Frame, NativeOptions, egui::{ - Align, Button, CentralPanel, Color32, ComboBox, Context, IconData, Id, Key, Layout, Sides, ThemePreference, TopBottomPanel, ViewportBuilder, ViewportId + Align, CentralPanel, Color32, ComboBox, Context, IconData, Id, Key, Layout, Sides, ThemePreference, + TopBottomPanel, ViewportBuilder, ViewportId, }, }; use log::{error, warn}; @@ -106,6 +107,7 @@ struct ZeroSplitter { comparison: Category, active: bool, relative_score: bool, + show_gold_split: bool, } impl ZeroSplitter { @@ -132,6 +134,7 @@ impl ZeroSplitter { comparison: Category::new("".to_string(), Gamemode::GreenOrange), active: false, relative_score: true, + show_gold_split: true, } } @@ -324,6 +327,9 @@ impl App for ZeroSplitter { ui.with_layout(Layout::top_down_justified(Align::Min), |ui| { ui.horizontal(|ui| { ui.toggle_value(&mut self.relative_score, "RELATIVE") + .on_hover_text("Display relative score per split or running total of score"); + ui.toggle_value(&mut self.show_gold_split, "BEST SPLITS") + .on_hover_text("Show your PB's splits or your best splits on the left"); }); ui.horizontal(|ui| { ui.label("Category: "); @@ -359,12 +365,15 @@ impl App for ZeroSplitter { (i, s) } else { // Get total score up to the current split - (i, self.current_run - .splits - .iter() - .enumerate() - .take_while(|&(idx, _)| idx <= i) - .fold(0, |acc, (_, &s)| acc + s)) + ( + i, + self.current_run + .splits + .iter() + .enumerate() + .take_while(|&(idx, _)| idx <= i) + .fold(0, |acc, (_, &s)| acc + s), + ) } }) { // translate split number to stage/loop for GO @@ -387,7 +396,9 @@ impl App for ZeroSplitter { let pb_split = if self.relative_score { self.comparison.personal_best.splits[i] } else { - self.comparison.personal_best.splits + self.comparison + .personal_best + .splits .iter() .enumerate() .take_while(|&(idx, _)| idx <= i) @@ -403,8 +414,14 @@ impl App for ZeroSplitter { Gamemode::BlackOnion => todo!(), }; - if gold_split > 0 { - left.colored_label(GREEN, gold_split.to_string()); + if self.show_gold_split { + if gold_split > 0 { + left.colored_label(GREEN, gold_split.to_string()); + } + } else { + if pb_split > 0 { + left.colored_label(GREEN, pb_split.to_string()); + } } }, |right| { @@ -413,7 +430,7 @@ impl App for ZeroSplitter { // Set color of split (rightmost number) let split_color = if self.current_split == Some(i) { Color32::WHITE - } else if split >= gold_split{ + } else if split >= gold_split { DARKER_ORANGE } else { DARK_ORANGE From 565a8431e9619258973cba88dd5b84ba4044f4b9 Mon Sep 17 00:00:00 2001 From: lily_and_doll <103815266+lily-and-doll@users.noreply.github.com> Date: Sat, 15 Nov 2025 22:42:25 -0500 Subject: [PATCH 13/52] implement a fix for time bonus spillover If you run out of time in a stage (or no matter what in Maze), your time bonus might be applied to the next split. I've added a 20 frame delay to splitting to catch that, although it is not very elegant. --- splitter/src/main.rs | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/splitter/src/main.rs b/splitter/src/main.rs index 308f3d9..df49e3b 100644 --- a/splitter/src/main.rs +++ b/splitter/src/main.rs @@ -42,6 +42,8 @@ const DARKER_GREEN: Color32 = Color32::from_rgb(0x00, 0x32, 0x32); #[allow(unused)] const GREENEST: Color32 = Color32::from_rgb(0x00, 0x1d, 0x23); +const SPLIT_DELAY_FRAMES: u32 = 20; + static EGUI_CTX: OnceLock = OnceLock::new(); fn main() { @@ -108,6 +110,7 @@ struct ZeroSplitter { active: bool, relative_score: bool, show_gold_split: bool, + split_delay: Option } impl ZeroSplitter { @@ -135,6 +138,7 @@ impl ZeroSplitter { active: false, relative_score: true, show_gold_split: true, + split_delay: None, } } @@ -282,9 +286,18 @@ impl ZeroSplitter { if !frame.is_menu() && self.active { // Split if necessary; score requirement prevents spurious splits after a reset if frame.timer_wave == 0 && self.last_frame.timer_wave != 0 && frame.total_score() > 0 { - self.current_split = self.current_split.or(Some(0)).map(|s| s + 1); + self.split_delay = Some(SPLIT_DELAY_FRAMES); + } + + if let Some(split_delay) = self.split_delay { + if split_delay > 1 { + self.split_delay = Some(split_delay - 1) + } else { + self.current_split = self.current_split.or(Some(0)).map(|s| s + 1); self.current_split_score_offset = self.last_frame.total_score(); self.save_splits(); + self.split_delay = None + } } // TODO: reimplement continue support From c167402565cf0add1a32a61fa62f601a7d37adc8 Mon Sep 17 00:00:00 2001 From: lily_and_doll <103815266+lily-and-doll@users.noreply.github.com> Date: Sat, 15 Nov 2025 22:42:25 -0500 Subject: [PATCH 14/52] implement a fix for time bonus spillover If you run out of time in a stage (or no matter what in Maze), your time bonus might be applied to the next split. I've added a 20 frame delay to splitting to catch that, although it is not very elegant. --- splitter/src/main.rs | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/splitter/src/main.rs b/splitter/src/main.rs index 308f3d9..5261e28 100644 --- a/splitter/src/main.rs +++ b/splitter/src/main.rs @@ -42,6 +42,8 @@ const DARKER_GREEN: Color32 = Color32::from_rgb(0x00, 0x32, 0x32); #[allow(unused)] const GREENEST: Color32 = Color32::from_rgb(0x00, 0x1d, 0x23); +const SPLIT_DELAY_FRAMES: u32 = 20; + static EGUI_CTX: OnceLock = OnceLock::new(); fn main() { @@ -108,6 +110,7 @@ struct ZeroSplitter { active: bool, relative_score: bool, show_gold_split: bool, + split_delay: Option } impl ZeroSplitter { @@ -135,6 +138,7 @@ impl ZeroSplitter { active: false, relative_score: true, show_gold_split: true, + split_delay: None, } } @@ -282,9 +286,18 @@ impl ZeroSplitter { if !frame.is_menu() && self.active { // Split if necessary; score requirement prevents spurious splits after a reset if frame.timer_wave == 0 && self.last_frame.timer_wave != 0 && frame.total_score() > 0 { - self.current_split = self.current_split.or(Some(0)).map(|s| s + 1); + self.split_delay = Some(SPLIT_DELAY_FRAMES); + } + + if let Some(split_delay) = self.split_delay { + if split_delay > 1 { + self.split_delay = Some(split_delay - 1) + } else { + self.current_split = self.current_split.or(Some(0)).map(|s| s + 1); self.current_split_score_offset = self.last_frame.total_score(); self.save_splits(); + self.split_delay = None + } } // TODO: reimplement continue support @@ -293,7 +306,7 @@ impl ZeroSplitter { self.current_run.score = frame.total_score(); let split_score = frame.total_score() - self.current_split_score_offset; self.current_run.splits[self.current_split.unwrap_or(0)] = split_score; - } else { + } else if frame.is_menu(){ // End the run if we're back on the menu self.end_run(); } @@ -428,7 +441,7 @@ impl App for ZeroSplitter { // Only write splits up to the current split if i <= self.current_split.unwrap_or(0) { // Set color of split (rightmost number) - let split_color = if self.current_split == Some(i) { + let split_color = if self.current_split == Some(i) && self.split_delay.is_none() { Color32::WHITE } else if split >= gold_split { DARKER_ORANGE From 3ba7d63ef2c08c554b35613672f7d3ecb3c5d1b5 Mon Sep 17 00:00:00 2001 From: lillies <103815266+lily-and-doll@users.noreply.github.com> Date: Sat, 15 Nov 2025 23:46:28 -0500 Subject: [PATCH 15/52] Update README.md Point the download link at the releases page instead of a specific release --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3e4f451..ea104af 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Automatic split-tracker for ZeroRanger. Very beta. Supports Green Orange and White Vanilla. -[DOWNLOAD](https://github.com/lily-and-doll/ZeroSplitter/releases/tag/0.2.0) +[DOWNLOAD](https://github.com/lily-and-doll/ZeroSplitter/releases) zerosplitter_hjuYG7gioU zerosplitter_1v8gZHNMn1 From ffb850b76732ec5e1aab328b84cd6c216184723e Mon Sep 17 00:00:00 2001 From: lily_and_doll <103815266+lily-and-doll@users.noreply.github.com> Date: Tue, 18 Nov 2025 20:18:45 -0500 Subject: [PATCH 16/52] Add automatic window resizing --- splitter/src/main.rs | 42 +++++++++++++++++++++++++----------------- 1 file changed, 25 insertions(+), 17 deletions(-) diff --git a/splitter/src/main.rs b/splitter/src/main.rs index 5261e28..71260ed 100644 --- a/splitter/src/main.rs +++ b/splitter/src/main.rs @@ -54,6 +54,7 @@ fn main() { .with_inner_size([300., 290.]) .with_icon(IconData::default()) .with_title("ZeroSplitter"), + ..Default::default() }; @@ -110,7 +111,7 @@ struct ZeroSplitter { active: bool, relative_score: bool, show_gold_split: bool, - split_delay: Option + split_delay: Option, } impl ZeroSplitter { @@ -294,9 +295,9 @@ impl ZeroSplitter { self.split_delay = Some(split_delay - 1) } else { self.current_split = self.current_split.or(Some(0)).map(|s| s + 1); - self.current_split_score_offset = self.last_frame.total_score(); - self.save_splits(); - self.split_delay = None + self.current_split_score_offset = self.last_frame.total_score(); + self.save_splits(); + self.split_delay = None } } @@ -306,7 +307,7 @@ impl ZeroSplitter { self.current_run.score = frame.total_score(); let split_score = frame.total_score() - self.current_split_score_offset; self.current_run.splits[self.current_split.unwrap_or(0)] = split_score; - } else if frame.is_menu(){ + } else if frame.is_menu() { // End the run if we're back on the menu self.end_run(); } @@ -334,6 +335,25 @@ impl App for ZeroSplitter { self.update_frame(data); } + // Detect gamemode change persist between frames + let prev_mode_id = Id::new("prev_mode"); + let cur_mode = self.categories[self.current_category].mode; + if let Some(prev_mode) = ctx.data(|data| data.get_temp::(prev_mode_id)) { + if prev_mode != cur_mode { + let min_size = match self.categories[self.current_category].mode { + Gamemode::GreenOrange => eframe::egui::Vec2 { x: 300.0, y: 300.0 }, + Gamemode::WhiteVanilla => eframe::egui::Vec2 { x: 300.0, y: 650.0 }, + Gamemode::BlackOnion => todo!(), + }; + ctx.send_viewport_cmd(eframe::egui::ViewportCommand::MinInnerSize(min_size)); + ctx.send_viewport_cmd(eframe::egui::ViewportCommand::InnerSize(min_size)); + self.reset(); + } + } + ctx.data_mut(|data| data.insert_temp(prev_mode_id, cur_mode)); + + let cur_category = &self.categories[self.current_category]; + CentralPanel::default().show(ctx, |ui| { ui.visuals_mut().selection.bg_fill = DARK_ORANGE; ui.visuals_mut().selection.stroke.color = GREENEST; @@ -360,18 +380,6 @@ impl App for ZeroSplitter { } }); - // Detect gamemode change persist between frames - let prev_mode_id = Id::new("prev_mode"); - let cur_mode = self.categories[self.current_category].mode; - if let Some(prev_mode) = ctx.data(|data| data.get_temp::(prev_mode_id)) { - if prev_mode != cur_mode { - self.reset(); - } - } - ctx.data_mut(|data| data.insert_temp(prev_mode_id, cur_mode)); - - let cur_category = &self.categories[self.current_category]; - for (i, split) in self.current_run.splits.iter().enumerate().map(|(i, &s)| { // Switch current split score between modes if self.relative_score { From def06f673ec383bd9cea3dcdb234ce5d7eb5ac6c Mon Sep 17 00:00:00 2001 From: lily_and_doll <103815266+lily-and-doll@users.noreply.github.com> Date: Tue, 18 Nov 2025 21:30:08 -0500 Subject: [PATCH 17/52] Split UI code out of main.rs --- splitter/src/app.rs | 208 ++++++++++++++++++++++++++ splitter/src/main.rs | 342 +------------------------------------------ splitter/src/ui.rs | 139 ++++++++++++++++++ 3 files changed, 351 insertions(+), 338 deletions(-) create mode 100644 splitter/src/app.rs diff --git a/splitter/src/app.rs b/splitter/src/app.rs new file mode 100644 index 0000000..0b24e45 --- /dev/null +++ b/splitter/src/app.rs @@ -0,0 +1,208 @@ +use eframe::{App, Frame, egui::{Align, CentralPanel, Color32, ComboBox, Context, Id, Layout, Sides}}; + +use crate::{Category, DARK_GREEN, DARK_ORANGE, DARKER_ORANGE, GREEN, GREENEST, Gamemode, LIGHT_ORANGE, ZeroSplitter, ui::{category_maker_dialog, confirm_dialog}, vanilla_split_names}; + +impl App for ZeroSplitter { + fn update(&mut self, ctx: &Context, _frame: &mut Frame) { + while let Ok(data) = self.data_source.try_recv() { + self.update_frame(data); + } + + // Detect gamemode change persist between frames + let prev_mode_id = Id::new("prev_mode"); + let cur_mode = self.categories[self.current_category].mode; + if let Some(prev_mode) = ctx.data(|data| data.get_temp::(prev_mode_id)) { + if prev_mode != cur_mode { + let min_size = match self.categories[self.current_category].mode { + Gamemode::GreenOrange => eframe::egui::Vec2 { x: 300.0, y: 300.0 }, + Gamemode::WhiteVanilla => eframe::egui::Vec2 { x: 300.0, y: 650.0 }, + Gamemode::BlackOnion => todo!(), + }; + ctx.send_viewport_cmd(eframe::egui::ViewportCommand::MinInnerSize(min_size)); + ctx.send_viewport_cmd(eframe::egui::ViewportCommand::InnerSize(min_size)); + self.reset(); + } + } + ctx.data_mut(|data| data.insert_temp(prev_mode_id, cur_mode)); + + let cur_category = &self.categories[self.current_category]; + + CentralPanel::default().show(ctx, |ui| { + ui.visuals_mut().selection.bg_fill = DARK_ORANGE; + ui.visuals_mut().selection.stroke.color = GREENEST; + ui.with_layout(Layout::top_down_justified(Align::Min), |ui| { + ui.horizontal(|ui| { + ui.toggle_value(&mut self.relative_score, "RELATIVE") + .on_hover_text("Display relative score per split or running total of score"); + ui.toggle_value(&mut self.show_gold_split, "BEST SPLITS") + .on_hover_text("Show your PB's splits or your best splits on the left"); + }); + ui.horizontal(|ui| { + ui.label("Category: "); + ComboBox::from_label("").show_index(ui, &mut self.current_category, self.categories.len(), |i| { + &self.categories[i].name + }); + if ui.small_button("+").clicked() { + self.waiting_for_category = true; + } + /*if ui.button("Delete").clicked() { + self.waiting_for_confirm = true; + }*/ + if ui.button("Rename").clicked() { + self.waiting_for_rename = true; + } + }); + + for (i, split) in self.current_run.splits.iter().enumerate().map(|(i, &s)| { + // Switch current split score between modes + if self.relative_score { + (i, s) + } else { + // Get total score up to the current split + ( + i, + self.current_run + .splits + .iter() + .enumerate() + .take_while(|&(idx, _)| idx <= i) + .fold(0, |acc, (_, &s)| acc + s), + ) + } + }) { + // translate split number to stage/loop for GO + let stage_n = (i & 3) + 1; + let loop_n = (i >> 2) + 1; + // Get relative/absolute gold split + // Gold split = high score of this split in any run + let gold_split = if self.relative_score { + cur_category.best_splits[i] + } else { + cur_category + .best_splits + .iter() + .enumerate() + .take_while(|&(idx, _)| idx <= i) + .fold(0, |acc, (_, &s)| acc + s) + }; + // Get relative/absolute split in the PB + // PB split = score of this split in the PB run + let pb_split = if self.relative_score { + self.comparison.personal_best.splits[i] + } else { + self.comparison + .personal_best + .splits + .iter() + .enumerate() + .take_while(|&(idx, _)| idx <= i) + .fold(0, |acc, (_, &s)| acc + s) + }; + + Sides::new().show( + ui, + |left| { + match cur_category.mode { + Gamemode::GreenOrange => left.label(format!("{}-{}", loop_n, stage_n)), + Gamemode::WhiteVanilla => left.label(vanilla_split_names(i)), + Gamemode::BlackOnion => todo!(), + }; + + if self.show_gold_split { + if gold_split > 0 { + left.colored_label(GREEN, gold_split.to_string()); + } + } else { + if pb_split > 0 { + left.colored_label(GREEN, pb_split.to_string()); + } + } + }, + |right| { + // Only write splits up to the current split + if i <= self.current_split.unwrap_or(0) { + // Set color of split (rightmost number) + let split_color = if self.current_split == Some(i) && self.split_delay.is_none() { + Color32::WHITE + } else if split >= gold_split { + DARKER_ORANGE + } else { + DARK_ORANGE + }; + + right.colored_label(split_color, split.to_string()); + + if i < self.current_split.unwrap_or(0) { + // past split, we should show a diff + let diff = split - pb_split; + let diff_color = if diff > 0 { + LIGHT_ORANGE + } else if diff == 0 { + Color32::WHITE + } else { + DARK_GREEN + }; + right.colored_label(diff_color, format!("{diff:+}")); + } + } else { + right.colored_label(DARK_GREEN, "--"); + } + }, + ); + } + + ui.label(format!("Personal Best: {}", cur_category.personal_best.score)); + ui.label(format!("Sum of Best: {}", cur_category.best_splits.iter().sum::())) + }); + }); + + if self.waiting_for_category { + if let Ok(new_category) = self.dialog_rx.try_recv() { + if let Some(data) = new_category { + self.categories.push(Category::new(data.textbox, data.mode)); + self.current_category = self.categories.len() - 1; + self.save_splits(); + } + self.waiting_for_category = false; + } else { + category_maker_dialog(ctx, self.dialog_tx.clone(), "Enter new category name", true); + } + } + + if self.waiting_for_rename { + if let Ok(new_category) = self.dialog_rx.try_recv() { + if let Some(data) = new_category { + self.categories[self.current_category].name = data.textbox; + self.categories[self.current_category].mode = data.mode; + self.save_splits(); + } + self.waiting_for_rename = false; + } else { + category_maker_dialog(ctx, self.dialog_tx.clone(), "Enter new name for category", false); + } + } + + if self.waiting_for_confirm { + if let Ok(Some(confirmation)) = self.dialog_rx.try_recv() { + if confirmation.textbox == "Deleted" { + self.categories.remove(self.current_category); + self.current_category = self.current_category.saturating_sub(1); + } + self.waiting_for_confirm = false; + } else { + confirm_dialog( + ctx, + self.dialog_tx.clone(), + format!( + "Are you sure you want to delete category {}?", + self.categories[self.current_category].name + ), + ); + } + } + } + + fn on_exit(&mut self, _gl: Option<&eframe::glow::Context>) { + self.save_splits(); + } +} \ No newline at end of file diff --git a/splitter/src/main.rs b/splitter/src/main.rs index 71260ed..7b3b58c 100644 --- a/splitter/src/main.rs +++ b/splitter/src/main.rs @@ -12,10 +12,10 @@ use std::{ use common::FrameData; use eframe::{ - App, Frame, NativeOptions, + NativeOptions, egui::{ - Align, CentralPanel, Color32, ComboBox, Context, IconData, Id, Key, Layout, Sides, ThemePreference, - TopBottomPanel, ViewportBuilder, ViewportId, + Color32, Context, IconData, ThemePreference, + ViewportBuilder, }, }; use log::{error, warn}; @@ -24,6 +24,7 @@ use serde::{Deserialize, Serialize}; mod hook; mod system; mod ui; +mod app; #[allow(unused)] const DARK_GREEN: Color32 = Color32::from_rgb(0, 0x4f, 0x4d); @@ -329,303 +330,9 @@ impl ZeroSplitter { } } -impl App for ZeroSplitter { - fn update(&mut self, ctx: &Context, _frame: &mut Frame) { - while let Ok(data) = self.data_source.try_recv() { - self.update_frame(data); - } - - // Detect gamemode change persist between frames - let prev_mode_id = Id::new("prev_mode"); - let cur_mode = self.categories[self.current_category].mode; - if let Some(prev_mode) = ctx.data(|data| data.get_temp::(prev_mode_id)) { - if prev_mode != cur_mode { - let min_size = match self.categories[self.current_category].mode { - Gamemode::GreenOrange => eframe::egui::Vec2 { x: 300.0, y: 300.0 }, - Gamemode::WhiteVanilla => eframe::egui::Vec2 { x: 300.0, y: 650.0 }, - Gamemode::BlackOnion => todo!(), - }; - ctx.send_viewport_cmd(eframe::egui::ViewportCommand::MinInnerSize(min_size)); - ctx.send_viewport_cmd(eframe::egui::ViewportCommand::InnerSize(min_size)); - self.reset(); - } - } - ctx.data_mut(|data| data.insert_temp(prev_mode_id, cur_mode)); - - let cur_category = &self.categories[self.current_category]; - - CentralPanel::default().show(ctx, |ui| { - ui.visuals_mut().selection.bg_fill = DARK_ORANGE; - ui.visuals_mut().selection.stroke.color = GREENEST; - ui.with_layout(Layout::top_down_justified(Align::Min), |ui| { - ui.horizontal(|ui| { - ui.toggle_value(&mut self.relative_score, "RELATIVE") - .on_hover_text("Display relative score per split or running total of score"); - ui.toggle_value(&mut self.show_gold_split, "BEST SPLITS") - .on_hover_text("Show your PB's splits or your best splits on the left"); - }); - ui.horizontal(|ui| { - ui.label("Category: "); - ComboBox::from_label("").show_index(ui, &mut self.current_category, self.categories.len(), |i| { - &self.categories[i].name - }); - if ui.small_button("+").clicked() { - self.waiting_for_category = true; - } - /*if ui.button("Delete").clicked() { - self.waiting_for_confirm = true; - }*/ - if ui.button("Rename").clicked() { - self.waiting_for_rename = true; - } - }); - - for (i, split) in self.current_run.splits.iter().enumerate().map(|(i, &s)| { - // Switch current split score between modes - if self.relative_score { - (i, s) - } else { - // Get total score up to the current split - ( - i, - self.current_run - .splits - .iter() - .enumerate() - .take_while(|&(idx, _)| idx <= i) - .fold(0, |acc, (_, &s)| acc + s), - ) - } - }) { - // translate split number to stage/loop for GO - let stage_n = (i & 3) + 1; - let loop_n = (i >> 2) + 1; - // Get relative/absolute gold split - // Gold split = high score of this split in any run - let gold_split = if self.relative_score { - cur_category.best_splits[i] - } else { - cur_category - .best_splits - .iter() - .enumerate() - .take_while(|&(idx, _)| idx <= i) - .fold(0, |acc, (_, &s)| acc + s) - }; - // Get relative/absolute split in the PB - // PB split = score of this split in the PB run - let pb_split = if self.relative_score { - self.comparison.personal_best.splits[i] - } else { - self.comparison - .personal_best - .splits - .iter() - .enumerate() - .take_while(|&(idx, _)| idx <= i) - .fold(0, |acc, (_, &s)| acc + s) - }; - - Sides::new().show( - ui, - |left| { - match cur_category.mode { - Gamemode::GreenOrange => left.label(format!("{}-{}", loop_n, stage_n)), - Gamemode::WhiteVanilla => left.label(vanilla_split_names(i)), - Gamemode::BlackOnion => todo!(), - }; - - if self.show_gold_split { - if gold_split > 0 { - left.colored_label(GREEN, gold_split.to_string()); - } - } else { - if pb_split > 0 { - left.colored_label(GREEN, pb_split.to_string()); - } - } - }, - |right| { - // Only write splits up to the current split - if i <= self.current_split.unwrap_or(0) { - // Set color of split (rightmost number) - let split_color = if self.current_split == Some(i) && self.split_delay.is_none() { - Color32::WHITE - } else if split >= gold_split { - DARKER_ORANGE - } else { - DARK_ORANGE - }; - - right.colored_label(split_color, split.to_string()); - - if i < self.current_split.unwrap_or(0) { - // past split, we should show a diff - let diff = split - pb_split; - let diff_color = if diff > 0 { - LIGHT_ORANGE - } else if diff == 0 { - Color32::WHITE - } else { - DARK_GREEN - }; - right.colored_label(diff_color, format!("{diff:+}")); - } - } else { - right.colored_label(DARK_GREEN, "--"); - } - }, - ); - } - - ui.label(format!("Personal Best: {}", cur_category.personal_best.score)); - ui.label(format!("Sum of Best: {}", cur_category.best_splits.iter().sum::())) - }); - }); - - if self.waiting_for_category { - if let Ok(new_category) = self.dialog_rx.try_recv() { - if let Some(data) = new_category { - self.categories.push(Category::new(data.textbox, data.mode)); - self.current_category = self.categories.len() - 1; - self.save_splits(); - } - self.waiting_for_category = false; - } else { - category_maker_dialog(ctx, self.dialog_tx.clone(), "Enter new category name", true); - } - } - - if self.waiting_for_rename { - if let Ok(new_category) = self.dialog_rx.try_recv() { - if let Some(data) = new_category { - self.categories[self.current_category].name = data.textbox; - self.categories[self.current_category].mode = data.mode; - self.save_splits(); - } - self.waiting_for_rename = false; - } else { - category_maker_dialog(ctx, self.dialog_tx.clone(), "Enter new name for category", false); - } - } - - if self.waiting_for_confirm { - if let Ok(Some(confirmation)) = self.dialog_rx.try_recv() { - if confirmation.textbox == "Deleted" { - self.categories.remove(self.current_category); - self.current_category = self.current_category.saturating_sub(1); - } - self.waiting_for_confirm = false; - } else { - confirm_dialog( - ctx, - self.dialog_tx.clone(), - format!( - "Are you sure you want to delete category {}?", - self.categories[self.current_category].name - ), - ); - } - } - } - - fn on_exit(&mut self, _gl: Option<&eframe::glow::Context>) { - self.save_splits(); - } -} - -fn entry_dialog(ctx: &Context, tx: Sender, msg: &'static str) { - let vp_builder = ViewportBuilder::default() - .with_title("ZeroSplitter") - .with_active(true) - .with_resizable(false) - .with_minimize_button(false) - .with_maximize_button(false) - .with_inner_size([200., 100.]); - - ctx.show_viewport_deferred(ViewportId::from_hash_of("entry dialog"), vp_builder, move |ctx, _| { - if ctx.input(|input| input.viewport().close_requested()) { - let _ = tx.send("".to_string()); - request_repaint(); - return; - } - let text_id = Id::new("edit text"); - let mut edit_str = ctx.data_mut(|data| data.get_temp_mut_or_insert_with(text_id, || String::new()).clone()); - CentralPanel::default().show(ctx, |ui| { - ui.vertical_centered_justified(|ui| { - ui.label(msg); - if ui.text_edit_singleline(&mut edit_str).lost_focus() && ui.input(|i| i.key_pressed(Key::Enter)) { - let _ = tx.send(edit_str.clone()); - request_repaint(); - } - }); - }); - ctx.data_mut(|data| { - data.insert_temp(text_id, edit_str); - }); - }); -} - -/// Category entry dialoge menu with a gamemode dropdown. -fn category_maker_dialog(ctx: &Context, tx: Sender>, msg: &'static str, mode_select: bool) { - let vp_builder = ViewportBuilder::default() - .with_title("ZeroSplitter") - .with_active(true) - .with_resizable(false) - .with_minimize_button(false) - .with_maximize_button(false) - .with_inner_size([200., 100.]); - - ctx.show_viewport_deferred(ViewportId::from_hash_of("entry dialog"), vp_builder, move |ctx, _| { - if ctx.input(|input| input.viewport().close_requested()) { - let _ = tx.send(None); - request_repaint(); - return; - } - - let text_id = Id::new("edit text"); - let mode_id = Id::new("gamemode"); - let mut edit_str = ctx.data_mut(|data| data.get_temp_mut_or_insert_with(text_id, || String::new()).clone()); - let mut mode = ctx.data_mut(|data| *data.get_temp_mut_or(mode_id, Gamemode::GreenOrange)); - - CentralPanel::default().show(ctx, |ui| { - ui.vertical_centered_justified(|ui| { - ui.label(msg); - ui.text_edit_singleline(&mut edit_str); - if ui.button("Confirm").clicked() { - let _ = tx.send(Some(EntryDialogData { - textbox: edit_str.clone(), - mode: mode, - })); - request_repaint(); - ctx.send_viewport_cmd(eframe::egui::ViewportCommand::Close); - } - }); - }); - - if mode_select { - TopBottomPanel::bottom("mode_select").show(ctx, |ui| { - ui.vertical_centered_justified(|ui| { - ComboBox::from_label("Mode") - .selected_text(format!("{:?}", mode)) - .show_ui(ui, |ui| { - ui.selectable_value(&mut mode, Gamemode::GreenOrange, "Green Orange"); - ui.selectable_value(&mut mode, Gamemode::WhiteVanilla, "White Vanilla"); - ui.selectable_value(&mut mode, Gamemode::BlackOnion, "Black Onion"); - }); - }) - }); - } - - ctx.data_mut(|data| { - data.insert_temp(text_id, edit_str); - data.insert_temp(mode_id, mode); - }); - }); -} #[derive(PartialEq, Eq, Clone, Copy, Debug, Serialize, Deserialize)] pub enum Gamemode { GreenOrange, @@ -647,47 +354,6 @@ pub struct EntryDialogData { pub mode: Gamemode, } -fn confirm_dialog(ctx: &Context, tx: Sender>, msg: String) { - let vp_builder = ViewportBuilder::default() - .with_title("ZeroSplitter") - .with_active(true) - .with_resizable(false) - .with_minimize_button(false) - .with_maximize_button(false) - .with_inner_size([200., 100.]); - - ctx.show_viewport_deferred(ViewportId::from_hash_of("confirm dialog"), vp_builder, move |ctx, _| { - if ctx.input(|input| input.viewport().close_requested()) { - let _ = tx.send(None); - request_repaint(); - return; - } - - CentralPanel::default().show(ctx, |ui| { - ui.vertical_centered_justified(|ui| { - ui.label(msg.clone()); - ui.columns_const(|[left, right]| { - if left.button("Delete").clicked() { - let _ = tx.send(Some(EntryDialogData { - textbox: "Deleted".to_string(), - mode: Gamemode::GreenOrange, - })); - request_repaint(); - } else if right.button("Cancel").clicked() { - let _ = tx.send(None); - request_repaint(); - } - }); - }); - }); - }); -} - -fn request_repaint() { - if let Some(ctx) = EGUI_CTX.get() { - ctx.request_repaint_after(Duration::from_millis(100)); - } -} /// Translation of function of the same name in ZR. /// Checkpoints in WV have the same stage/checkpoint as in GO, diff --git a/splitter/src/ui.rs b/splitter/src/ui.rs index 8b13789..ad71a11 100644 --- a/splitter/src/ui.rs +++ b/splitter/src/ui.rs @@ -1 +1,140 @@ +use std::{sync::mpsc::Sender, time::Duration}; +use eframe::egui::{CentralPanel, ComboBox, Context, Id, Key, TopBottomPanel, ViewportBuilder, ViewportId}; + +use crate::{EGUI_CTX, EntryDialogData, Gamemode}; + +pub fn entry_dialog(ctx: &Context, tx: Sender, msg: &'static str) { + let vp_builder = ViewportBuilder::default() + .with_title("ZeroSplitter") + .with_active(true) + .with_resizable(false) + .with_minimize_button(false) + .with_maximize_button(false) + .with_inner_size([200., 100.]); + + ctx.show_viewport_deferred(ViewportId::from_hash_of("entry dialog"), vp_builder, move |ctx, _| { + if ctx.input(|input| input.viewport().close_requested()) { + let _ = tx.send("".to_string()); + request_repaint(); + return; + } + + let text_id = Id::new("edit text"); + let mut edit_str = ctx.data_mut(|data| data.get_temp_mut_or_insert_with(text_id, || String::new()).clone()); + + CentralPanel::default().show(ctx, |ui| { + ui.vertical_centered_justified(|ui| { + ui.label(msg); + if ui.text_edit_singleline(&mut edit_str).lost_focus() && ui.input(|i| i.key_pressed(Key::Enter)) { + let _ = tx.send(edit_str.clone()); + request_repaint(); + } + }); + }); + + ctx.data_mut(|data| { + data.insert_temp(text_id, edit_str); + }); + }); +} + +/// Category entry dialoge menu with a gamemode dropdown. +pub fn category_maker_dialog(ctx: &Context, tx: Sender>, msg: &'static str, mode_select: bool) { + let vp_builder = ViewportBuilder::default() + .with_title("ZeroSplitter") + .with_active(true) + .with_resizable(false) + .with_minimize_button(false) + .with_maximize_button(false) + .with_inner_size([200., 100.]); + + ctx.show_viewport_deferred(ViewportId::from_hash_of("entry dialog"), vp_builder, move |ctx, _| { + if ctx.input(|input| input.viewport().close_requested()) { + let _ = tx.send(None); + request_repaint(); + return; + } + + let text_id = Id::new("edit text"); + let mode_id = Id::new("gamemode"); + let mut edit_str = ctx.data_mut(|data| data.get_temp_mut_or_insert_with(text_id, || String::new()).clone()); + let mut mode = ctx.data_mut(|data| *data.get_temp_mut_or(mode_id, Gamemode::GreenOrange)); + + CentralPanel::default().show(ctx, |ui| { + ui.vertical_centered_justified(|ui| { + ui.label(msg); + ui.text_edit_singleline(&mut edit_str); + if ui.button("Confirm").clicked() { + let _ = tx.send(Some(EntryDialogData { + textbox: edit_str.clone(), + mode: mode, + })); + request_repaint(); + ctx.send_viewport_cmd(eframe::egui::ViewportCommand::Close); + } + }); + }); + + if mode_select { + TopBottomPanel::bottom("mode_select").show(ctx, |ui| { + ui.vertical_centered_justified(|ui| { + ComboBox::from_label("Mode") + .selected_text(format!("{:?}", mode)) + .show_ui(ui, |ui| { + ui.selectable_value(&mut mode, Gamemode::GreenOrange, "Green Orange"); + ui.selectable_value(&mut mode, Gamemode::WhiteVanilla, "White Vanilla"); + ui.selectable_value(&mut mode, Gamemode::BlackOnion, "Black Onion"); + }); + }) + }); + } + + ctx.data_mut(|data| { + data.insert_temp(text_id, edit_str); + data.insert_temp(mode_id, mode); + }); + }); +} + +pub fn confirm_dialog(ctx: &Context, tx: Sender>, msg: String) { + let vp_builder = ViewportBuilder::default() + .with_title("ZeroSplitter") + .with_active(true) + .with_resizable(false) + .with_minimize_button(false) + .with_maximize_button(false) + .with_inner_size([200., 100.]); + + ctx.show_viewport_deferred(ViewportId::from_hash_of("confirm dialog"), vp_builder, move |ctx, _| { + if ctx.input(|input| input.viewport().close_requested()) { + let _ = tx.send(None); + request_repaint(); + return; + } + + CentralPanel::default().show(ctx, |ui| { + ui.vertical_centered_justified(|ui| { + ui.label(msg.clone()); + ui.columns_const(|[left, right]| { + if left.button("Delete").clicked() { + let _ = tx.send(Some(EntryDialogData { + textbox: "Deleted".to_string(), + mode: Gamemode::GreenOrange, + })); + request_repaint(); + } else if right.button("Cancel").clicked() { + let _ = tx.send(None); + request_repaint(); + } + }); + }); + }); + }); +} + +fn request_repaint() { + if let Some(ctx) = EGUI_CTX.get() { + ctx.request_repaint_after(Duration::from_millis(100)); + } +} \ No newline at end of file From 30cebc3ba3b1967aa9f26792484b7c7adbe70336 Mon Sep 17 00:00:00 2001 From: lily_and_doll <103815266+lily-and-doll@users.noreply.github.com> Date: Tue, 18 Nov 2025 21:30:08 -0500 Subject: [PATCH 18/52] Split UI code out of main.rs --- splitter/src/app.rs | 215 +++++++++++++++++++++++++++ splitter/src/main.rs | 342 +------------------------------------------ splitter/src/ui.rs | 139 ++++++++++++++++++ 3 files changed, 358 insertions(+), 338 deletions(-) create mode 100644 splitter/src/app.rs diff --git a/splitter/src/app.rs b/splitter/src/app.rs new file mode 100644 index 0000000..d547df2 --- /dev/null +++ b/splitter/src/app.rs @@ -0,0 +1,215 @@ +use eframe::{ + App, Frame, + egui::{Align, CentralPanel, Color32, ComboBox, Context, Id, Layout, Sides}, +}; + +use crate::{ + Category, DARK_GREEN, DARK_ORANGE, DARKER_ORANGE, GREEN, GREENEST, Gamemode, LIGHT_ORANGE, ZeroSplitter, + ui::{category_maker_dialog, confirm_dialog}, + vanilla_split_names, +}; + +impl App for ZeroSplitter { + fn update(&mut self, ctx: &Context, _frame: &mut Frame) { + while let Ok(data) = self.data_source.try_recv() { + self.update_frame(data); + } + + // Detect gamemode change persist between frames + let prev_mode_id = Id::new("prev_mode"); + let cur_mode = self.categories[self.current_category].mode; + if let Some(prev_mode) = ctx.data(|data| data.get_temp::(prev_mode_id)) { + if prev_mode != cur_mode { + let min_size = match self.categories[self.current_category].mode { + Gamemode::GreenOrange => eframe::egui::Vec2 { x: 300.0, y: 300.0 }, + Gamemode::WhiteVanilla => eframe::egui::Vec2 { x: 300.0, y: 650.0 }, + Gamemode::BlackOnion => todo!(), + }; + ctx.send_viewport_cmd(eframe::egui::ViewportCommand::MinInnerSize(min_size)); + ctx.send_viewport_cmd(eframe::egui::ViewportCommand::InnerSize(min_size)); + self.reset(); + } + } + ctx.data_mut(|data| data.insert_temp(prev_mode_id, cur_mode)); + + let cur_category = &self.categories[self.current_category]; + + CentralPanel::default().show(ctx, |ui| { + ui.visuals_mut().selection.bg_fill = DARK_ORANGE; + ui.visuals_mut().selection.stroke.color = GREENEST; + ui.with_layout(Layout::top_down_justified(Align::Min), |ui| { + ui.horizontal(|ui| { + ui.toggle_value(&mut self.relative_score, "RELATIVE") + .on_hover_text("Display relative score per split or running total of score"); + ui.toggle_value(&mut self.show_gold_split, "BEST SPLITS") + .on_hover_text("Show your PB's splits or your best splits on the left"); + }); + ui.horizontal(|ui| { + ui.label("Category: "); + ComboBox::from_label("").show_index(ui, &mut self.current_category, self.categories.len(), |i| { + &self.categories[i].name + }); + if ui.small_button("+").clicked() { + self.waiting_for_category = true; + } + /*if ui.button("Delete").clicked() { + self.waiting_for_confirm = true; + }*/ + if ui.button("Rename").clicked() { + self.waiting_for_rename = true; + } + }); + + for (i, split) in self.current_run.splits.iter().enumerate().map(|(i, &s)| { + // Switch current split score between modes + if self.relative_score { + (i, s) + } else { + // Get total score up to the current split + ( + i, + self.current_run + .splits + .iter() + .enumerate() + .take_while(|&(idx, _)| idx <= i) + .fold(0, |acc, (_, &s)| acc + s), + ) + } + }) { + // translate split number to stage/loop for GO + let stage_n = (i & 3) + 1; + let loop_n = (i >> 2) + 1; + // Get relative/absolute gold split + // Gold split = high score of this split in any run + let gold_split = if self.relative_score { + cur_category.best_splits[i] + } else { + cur_category + .best_splits + .iter() + .enumerate() + .take_while(|&(idx, _)| idx <= i) + .fold(0, |acc, (_, &s)| acc + s) + }; + // Get relative/absolute split in the PB + // PB split = score of this split in the PB run + let pb_split = if self.relative_score { + self.comparison.personal_best.splits[i] + } else { + self.comparison + .personal_best + .splits + .iter() + .enumerate() + .take_while(|&(idx, _)| idx <= i) + .fold(0, |acc, (_, &s)| acc + s) + }; + + Sides::new().show( + ui, + |left| { + match cur_category.mode { + Gamemode::GreenOrange => left.label(format!("{}-{}", loop_n, stage_n)), + Gamemode::WhiteVanilla => left.label(vanilla_split_names(i)), + Gamemode::BlackOnion => todo!(), + }; + + if self.show_gold_split { + if gold_split > 0 { + left.colored_label(GREEN, gold_split.to_string()); + } + } else { + if pb_split > 0 { + left.colored_label(GREEN, pb_split.to_string()); + } + } + }, + |right| { + // Only write splits up to the current split + if i <= self.current_split.unwrap_or(0) { + // Set color of split (rightmost number) + let split_color = if self.current_split == Some(i) && self.split_delay.is_none() { + Color32::WHITE + } else if split >= gold_split { + DARKER_ORANGE + } else { + DARK_ORANGE + }; + + right.colored_label(split_color, split.to_string()); + + if i < self.current_split.unwrap_or(0) { + // past split, we should show a diff + let diff = split - pb_split; + let diff_color = if diff > 0 { + LIGHT_ORANGE + } else if diff == 0 { + Color32::WHITE + } else { + DARK_GREEN + }; + right.colored_label(diff_color, format!("{diff:+}")); + } + } else { + right.colored_label(DARK_GREEN, "--"); + } + }, + ); + } + + ui.label(format!("Personal Best: {}", cur_category.personal_best.score)); + ui.label(format!("Sum of Best: {}", cur_category.best_splits.iter().sum::())) + }); + }); + + if self.waiting_for_category { + if let Ok(new_category) = self.dialog_rx.try_recv() { + if let Some(data) = new_category { + self.categories.push(Category::new(data.textbox, data.mode)); + self.current_category = self.categories.len() - 1; + self.save_splits(); + } + self.waiting_for_category = false; + } else { + category_maker_dialog(ctx, self.dialog_tx.clone(), "Enter new category name", true); + } + } + + if self.waiting_for_rename { + if let Ok(new_category) = self.dialog_rx.try_recv() { + if let Some(data) = new_category { + self.categories[self.current_category].name = data.textbox; + self.categories[self.current_category].mode = data.mode; + self.save_splits(); + } + self.waiting_for_rename = false; + } else { + category_maker_dialog(ctx, self.dialog_tx.clone(), "Enter new name for category", false); + } + } + + if self.waiting_for_confirm { + if let Ok(Some(confirmation)) = self.dialog_rx.try_recv() { + if confirmation.textbox == "Deleted" { + self.categories.remove(self.current_category); + self.current_category = self.current_category.saturating_sub(1); + } + self.waiting_for_confirm = false; + } else { + confirm_dialog( + ctx, + self.dialog_tx.clone(), + format!( + "Are you sure you want to delete category {}?", + self.categories[self.current_category].name + ), + ); + } + } + } + + fn on_exit(&mut self, _gl: Option<&eframe::glow::Context>) { + self.save_splits(); + } +} diff --git a/splitter/src/main.rs b/splitter/src/main.rs index 71260ed..7b3b58c 100644 --- a/splitter/src/main.rs +++ b/splitter/src/main.rs @@ -12,10 +12,10 @@ use std::{ use common::FrameData; use eframe::{ - App, Frame, NativeOptions, + NativeOptions, egui::{ - Align, CentralPanel, Color32, ComboBox, Context, IconData, Id, Key, Layout, Sides, ThemePreference, - TopBottomPanel, ViewportBuilder, ViewportId, + Color32, Context, IconData, ThemePreference, + ViewportBuilder, }, }; use log::{error, warn}; @@ -24,6 +24,7 @@ use serde::{Deserialize, Serialize}; mod hook; mod system; mod ui; +mod app; #[allow(unused)] const DARK_GREEN: Color32 = Color32::from_rgb(0, 0x4f, 0x4d); @@ -329,303 +330,9 @@ impl ZeroSplitter { } } -impl App for ZeroSplitter { - fn update(&mut self, ctx: &Context, _frame: &mut Frame) { - while let Ok(data) = self.data_source.try_recv() { - self.update_frame(data); - } - - // Detect gamemode change persist between frames - let prev_mode_id = Id::new("prev_mode"); - let cur_mode = self.categories[self.current_category].mode; - if let Some(prev_mode) = ctx.data(|data| data.get_temp::(prev_mode_id)) { - if prev_mode != cur_mode { - let min_size = match self.categories[self.current_category].mode { - Gamemode::GreenOrange => eframe::egui::Vec2 { x: 300.0, y: 300.0 }, - Gamemode::WhiteVanilla => eframe::egui::Vec2 { x: 300.0, y: 650.0 }, - Gamemode::BlackOnion => todo!(), - }; - ctx.send_viewport_cmd(eframe::egui::ViewportCommand::MinInnerSize(min_size)); - ctx.send_viewport_cmd(eframe::egui::ViewportCommand::InnerSize(min_size)); - self.reset(); - } - } - ctx.data_mut(|data| data.insert_temp(prev_mode_id, cur_mode)); - - let cur_category = &self.categories[self.current_category]; - - CentralPanel::default().show(ctx, |ui| { - ui.visuals_mut().selection.bg_fill = DARK_ORANGE; - ui.visuals_mut().selection.stroke.color = GREENEST; - ui.with_layout(Layout::top_down_justified(Align::Min), |ui| { - ui.horizontal(|ui| { - ui.toggle_value(&mut self.relative_score, "RELATIVE") - .on_hover_text("Display relative score per split or running total of score"); - ui.toggle_value(&mut self.show_gold_split, "BEST SPLITS") - .on_hover_text("Show your PB's splits or your best splits on the left"); - }); - ui.horizontal(|ui| { - ui.label("Category: "); - ComboBox::from_label("").show_index(ui, &mut self.current_category, self.categories.len(), |i| { - &self.categories[i].name - }); - if ui.small_button("+").clicked() { - self.waiting_for_category = true; - } - /*if ui.button("Delete").clicked() { - self.waiting_for_confirm = true; - }*/ - if ui.button("Rename").clicked() { - self.waiting_for_rename = true; - } - }); - - for (i, split) in self.current_run.splits.iter().enumerate().map(|(i, &s)| { - // Switch current split score between modes - if self.relative_score { - (i, s) - } else { - // Get total score up to the current split - ( - i, - self.current_run - .splits - .iter() - .enumerate() - .take_while(|&(idx, _)| idx <= i) - .fold(0, |acc, (_, &s)| acc + s), - ) - } - }) { - // translate split number to stage/loop for GO - let stage_n = (i & 3) + 1; - let loop_n = (i >> 2) + 1; - // Get relative/absolute gold split - // Gold split = high score of this split in any run - let gold_split = if self.relative_score { - cur_category.best_splits[i] - } else { - cur_category - .best_splits - .iter() - .enumerate() - .take_while(|&(idx, _)| idx <= i) - .fold(0, |acc, (_, &s)| acc + s) - }; - // Get relative/absolute split in the PB - // PB split = score of this split in the PB run - let pb_split = if self.relative_score { - self.comparison.personal_best.splits[i] - } else { - self.comparison - .personal_best - .splits - .iter() - .enumerate() - .take_while(|&(idx, _)| idx <= i) - .fold(0, |acc, (_, &s)| acc + s) - }; - - Sides::new().show( - ui, - |left| { - match cur_category.mode { - Gamemode::GreenOrange => left.label(format!("{}-{}", loop_n, stage_n)), - Gamemode::WhiteVanilla => left.label(vanilla_split_names(i)), - Gamemode::BlackOnion => todo!(), - }; - - if self.show_gold_split { - if gold_split > 0 { - left.colored_label(GREEN, gold_split.to_string()); - } - } else { - if pb_split > 0 { - left.colored_label(GREEN, pb_split.to_string()); - } - } - }, - |right| { - // Only write splits up to the current split - if i <= self.current_split.unwrap_or(0) { - // Set color of split (rightmost number) - let split_color = if self.current_split == Some(i) && self.split_delay.is_none() { - Color32::WHITE - } else if split >= gold_split { - DARKER_ORANGE - } else { - DARK_ORANGE - }; - - right.colored_label(split_color, split.to_string()); - - if i < self.current_split.unwrap_or(0) { - // past split, we should show a diff - let diff = split - pb_split; - let diff_color = if diff > 0 { - LIGHT_ORANGE - } else if diff == 0 { - Color32::WHITE - } else { - DARK_GREEN - }; - right.colored_label(diff_color, format!("{diff:+}")); - } - } else { - right.colored_label(DARK_GREEN, "--"); - } - }, - ); - } - - ui.label(format!("Personal Best: {}", cur_category.personal_best.score)); - ui.label(format!("Sum of Best: {}", cur_category.best_splits.iter().sum::())) - }); - }); - - if self.waiting_for_category { - if let Ok(new_category) = self.dialog_rx.try_recv() { - if let Some(data) = new_category { - self.categories.push(Category::new(data.textbox, data.mode)); - self.current_category = self.categories.len() - 1; - self.save_splits(); - } - self.waiting_for_category = false; - } else { - category_maker_dialog(ctx, self.dialog_tx.clone(), "Enter new category name", true); - } - } - - if self.waiting_for_rename { - if let Ok(new_category) = self.dialog_rx.try_recv() { - if let Some(data) = new_category { - self.categories[self.current_category].name = data.textbox; - self.categories[self.current_category].mode = data.mode; - self.save_splits(); - } - self.waiting_for_rename = false; - } else { - category_maker_dialog(ctx, self.dialog_tx.clone(), "Enter new name for category", false); - } - } - - if self.waiting_for_confirm { - if let Ok(Some(confirmation)) = self.dialog_rx.try_recv() { - if confirmation.textbox == "Deleted" { - self.categories.remove(self.current_category); - self.current_category = self.current_category.saturating_sub(1); - } - self.waiting_for_confirm = false; - } else { - confirm_dialog( - ctx, - self.dialog_tx.clone(), - format!( - "Are you sure you want to delete category {}?", - self.categories[self.current_category].name - ), - ); - } - } - } - - fn on_exit(&mut self, _gl: Option<&eframe::glow::Context>) { - self.save_splits(); - } -} - -fn entry_dialog(ctx: &Context, tx: Sender, msg: &'static str) { - let vp_builder = ViewportBuilder::default() - .with_title("ZeroSplitter") - .with_active(true) - .with_resizable(false) - .with_minimize_button(false) - .with_maximize_button(false) - .with_inner_size([200., 100.]); - - ctx.show_viewport_deferred(ViewportId::from_hash_of("entry dialog"), vp_builder, move |ctx, _| { - if ctx.input(|input| input.viewport().close_requested()) { - let _ = tx.send("".to_string()); - request_repaint(); - return; - } - let text_id = Id::new("edit text"); - let mut edit_str = ctx.data_mut(|data| data.get_temp_mut_or_insert_with(text_id, || String::new()).clone()); - CentralPanel::default().show(ctx, |ui| { - ui.vertical_centered_justified(|ui| { - ui.label(msg); - if ui.text_edit_singleline(&mut edit_str).lost_focus() && ui.input(|i| i.key_pressed(Key::Enter)) { - let _ = tx.send(edit_str.clone()); - request_repaint(); - } - }); - }); - ctx.data_mut(|data| { - data.insert_temp(text_id, edit_str); - }); - }); -} - -/// Category entry dialoge menu with a gamemode dropdown. -fn category_maker_dialog(ctx: &Context, tx: Sender>, msg: &'static str, mode_select: bool) { - let vp_builder = ViewportBuilder::default() - .with_title("ZeroSplitter") - .with_active(true) - .with_resizable(false) - .with_minimize_button(false) - .with_maximize_button(false) - .with_inner_size([200., 100.]); - - ctx.show_viewport_deferred(ViewportId::from_hash_of("entry dialog"), vp_builder, move |ctx, _| { - if ctx.input(|input| input.viewport().close_requested()) { - let _ = tx.send(None); - request_repaint(); - return; - } - - let text_id = Id::new("edit text"); - let mode_id = Id::new("gamemode"); - let mut edit_str = ctx.data_mut(|data| data.get_temp_mut_or_insert_with(text_id, || String::new()).clone()); - let mut mode = ctx.data_mut(|data| *data.get_temp_mut_or(mode_id, Gamemode::GreenOrange)); - - CentralPanel::default().show(ctx, |ui| { - ui.vertical_centered_justified(|ui| { - ui.label(msg); - ui.text_edit_singleline(&mut edit_str); - if ui.button("Confirm").clicked() { - let _ = tx.send(Some(EntryDialogData { - textbox: edit_str.clone(), - mode: mode, - })); - request_repaint(); - ctx.send_viewport_cmd(eframe::egui::ViewportCommand::Close); - } - }); - }); - - if mode_select { - TopBottomPanel::bottom("mode_select").show(ctx, |ui| { - ui.vertical_centered_justified(|ui| { - ComboBox::from_label("Mode") - .selected_text(format!("{:?}", mode)) - .show_ui(ui, |ui| { - ui.selectable_value(&mut mode, Gamemode::GreenOrange, "Green Orange"); - ui.selectable_value(&mut mode, Gamemode::WhiteVanilla, "White Vanilla"); - ui.selectable_value(&mut mode, Gamemode::BlackOnion, "Black Onion"); - }); - }) - }); - } - - ctx.data_mut(|data| { - data.insert_temp(text_id, edit_str); - data.insert_temp(mode_id, mode); - }); - }); -} #[derive(PartialEq, Eq, Clone, Copy, Debug, Serialize, Deserialize)] pub enum Gamemode { GreenOrange, @@ -647,47 +354,6 @@ pub struct EntryDialogData { pub mode: Gamemode, } -fn confirm_dialog(ctx: &Context, tx: Sender>, msg: String) { - let vp_builder = ViewportBuilder::default() - .with_title("ZeroSplitter") - .with_active(true) - .with_resizable(false) - .with_minimize_button(false) - .with_maximize_button(false) - .with_inner_size([200., 100.]); - - ctx.show_viewport_deferred(ViewportId::from_hash_of("confirm dialog"), vp_builder, move |ctx, _| { - if ctx.input(|input| input.viewport().close_requested()) { - let _ = tx.send(None); - request_repaint(); - return; - } - - CentralPanel::default().show(ctx, |ui| { - ui.vertical_centered_justified(|ui| { - ui.label(msg.clone()); - ui.columns_const(|[left, right]| { - if left.button("Delete").clicked() { - let _ = tx.send(Some(EntryDialogData { - textbox: "Deleted".to_string(), - mode: Gamemode::GreenOrange, - })); - request_repaint(); - } else if right.button("Cancel").clicked() { - let _ = tx.send(None); - request_repaint(); - } - }); - }); - }); - }); -} - -fn request_repaint() { - if let Some(ctx) = EGUI_CTX.get() { - ctx.request_repaint_after(Duration::from_millis(100)); - } -} /// Translation of function of the same name in ZR. /// Checkpoints in WV have the same stage/checkpoint as in GO, diff --git a/splitter/src/ui.rs b/splitter/src/ui.rs index 8b13789..7283c76 100644 --- a/splitter/src/ui.rs +++ b/splitter/src/ui.rs @@ -1 +1,140 @@ +use std::{sync::mpsc::Sender, time::Duration}; +use eframe::egui::{CentralPanel, ComboBox, Context, Id, Key, TopBottomPanel, ViewportBuilder, ViewportId}; + +use crate::{EGUI_CTX, EntryDialogData, Gamemode}; + +pub fn entry_dialog(ctx: &Context, tx: Sender, msg: &'static str) { + let vp_builder = ViewportBuilder::default() + .with_title("ZeroSplitter") + .with_active(true) + .with_resizable(false) + .with_minimize_button(false) + .with_maximize_button(false) + .with_inner_size([200., 100.]); + + ctx.show_viewport_deferred(ViewportId::from_hash_of("entry dialog"), vp_builder, move |ctx, _| { + if ctx.input(|input| input.viewport().close_requested()) { + let _ = tx.send("".to_string()); + request_repaint(); + return; + } + + let text_id = Id::new("edit text"); + let mut edit_str = ctx.data_mut(|data| data.get_temp_mut_or_insert_with(text_id, || String::new()).clone()); + + CentralPanel::default().show(ctx, |ui| { + ui.vertical_centered_justified(|ui| { + ui.label(msg); + if ui.text_edit_singleline(&mut edit_str).lost_focus() && ui.input(|i| i.key_pressed(Key::Enter)) { + let _ = tx.send(edit_str.clone()); + request_repaint(); + } + }); + }); + + ctx.data_mut(|data| { + data.insert_temp(text_id, edit_str); + }); + }); +} + +/// Category entry dialoge menu with a gamemode dropdown. +pub fn category_maker_dialog(ctx: &Context, tx: Sender>, msg: &'static str, mode_select: bool) { + let vp_builder = ViewportBuilder::default() + .with_title("ZeroSplitter") + .with_active(true) + .with_resizable(false) + .with_minimize_button(false) + .with_maximize_button(false) + .with_inner_size([200., 100.]); + + ctx.show_viewport_deferred(ViewportId::from_hash_of("entry dialog"), vp_builder, move |ctx, _| { + if ctx.input(|input| input.viewport().close_requested()) { + let _ = tx.send(None); + request_repaint(); + return; + } + + let text_id = Id::new("edit text"); + let mode_id = Id::new("gamemode"); + let mut edit_str = ctx.data_mut(|data| data.get_temp_mut_or_insert_with(text_id, || String::new()).clone()); + let mut mode = ctx.data_mut(|data| *data.get_temp_mut_or(mode_id, Gamemode::GreenOrange)); + + CentralPanel::default().show(ctx, |ui| { + ui.vertical_centered_justified(|ui| { + ui.label(msg); + ui.text_edit_singleline(&mut edit_str); + if ui.button("Confirm").clicked() { + let _ = tx.send(Some(EntryDialogData { + textbox: edit_str.clone(), + mode: mode, + })); + request_repaint(); + ctx.send_viewport_cmd(eframe::egui::ViewportCommand::Close); + } + }); + }); + + if mode_select { + TopBottomPanel::bottom("mode_select").show(ctx, |ui| { + ui.vertical_centered_justified(|ui| { + ComboBox::from_label("Mode") + .selected_text(format!("{:?}", mode)) + .show_ui(ui, |ui| { + ui.selectable_value(&mut mode, Gamemode::GreenOrange, "Green Orange"); + ui.selectable_value(&mut mode, Gamemode::WhiteVanilla, "White Vanilla"); + ui.selectable_value(&mut mode, Gamemode::BlackOnion, "Black Onion"); + }); + }) + }); + } + + ctx.data_mut(|data| { + data.insert_temp(text_id, edit_str); + data.insert_temp(mode_id, mode); + }); + }); +} + +pub fn confirm_dialog(ctx: &Context, tx: Sender>, msg: String) { + let vp_builder = ViewportBuilder::default() + .with_title("ZeroSplitter") + .with_active(true) + .with_resizable(false) + .with_minimize_button(false) + .with_maximize_button(false) + .with_inner_size([200., 100.]); + + ctx.show_viewport_deferred(ViewportId::from_hash_of("confirm dialog"), vp_builder, move |ctx, _| { + if ctx.input(|input| input.viewport().close_requested()) { + let _ = tx.send(None); + request_repaint(); + return; + } + + CentralPanel::default().show(ctx, |ui| { + ui.vertical_centered_justified(|ui| { + ui.label(msg.clone()); + ui.columns_const(|[left, right]| { + if left.button("Delete").clicked() { + let _ = tx.send(Some(EntryDialogData { + textbox: "Deleted".to_string(), + mode: Gamemode::GreenOrange, + })); + request_repaint(); + } else if right.button("Cancel").clicked() { + let _ = tx.send(None); + request_repaint(); + } + }); + }); + }); + }); +} + +fn request_repaint() { + if let Some(ctx) = EGUI_CTX.get() { + ctx.request_repaint_after(Duration::from_millis(100)); + } +} From 23412c5000571b980c0e212233ba7a2c868ccf5a Mon Sep 17 00:00:00 2001 From: lily_and_doll <103815266+lily-and-doll@users.noreply.github.com> Date: Tue, 18 Nov 2025 23:06:12 -0500 Subject: [PATCH 19/52] Overhaul visuals and add NAMES toggle --- splitter/src/app.rs | 19 +++++---- splitter/src/main.rs | 25 ++++------- splitter/src/theme.rs | 98 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 117 insertions(+), 25 deletions(-) create mode 100644 splitter/src/theme.rs diff --git a/splitter/src/app.rs b/splitter/src/app.rs index d547df2..dd28e65 100644 --- a/splitter/src/app.rs +++ b/splitter/src/app.rs @@ -1,12 +1,10 @@ use eframe::{ App, Frame, - egui::{Align, CentralPanel, Color32, ComboBox, Context, Id, Layout, Sides}, + egui::{Align, CentralPanel, Color32, ComboBox, Context, Id, Layout, Sides, Theme}, }; use crate::{ - Category, DARK_GREEN, DARK_ORANGE, DARKER_ORANGE, GREEN, GREENEST, Gamemode, LIGHT_ORANGE, ZeroSplitter, - ui::{category_maker_dialog, confirm_dialog}, - vanilla_split_names, + Category, Gamemode, ZeroSplitter, theme::{DARK_GREEN, DARK_ORANGE, DARKER_ORANGE, GREEN, GREENEST, LIGHT_ORANGE}, ui::{category_maker_dialog, confirm_dialog}, vanilla_descriptive_split_names, vanilla_split_names }; impl App for ZeroSplitter { @@ -35,14 +33,14 @@ impl App for ZeroSplitter { let cur_category = &self.categories[self.current_category]; CentralPanel::default().show(ctx, |ui| { - ui.visuals_mut().selection.bg_fill = DARK_ORANGE; - ui.visuals_mut().selection.stroke.color = GREENEST; ui.with_layout(Layout::top_down_justified(Align::Min), |ui| { ui.horizontal(|ui| { ui.toggle_value(&mut self.relative_score, "RELATIVE") .on_hover_text("Display relative score per split or running total of score"); ui.toggle_value(&mut self.show_gold_split, "BEST SPLITS") .on_hover_text("Show your PB's splits or your best splits on the left"); + ui.toggle_value(&mut self.names, "NAMES") + .on_hover_text("Toggle descriptive or number names for WV splits"); }); ui.horizontal(|ui| { ui.label("Category: "); @@ -111,7 +109,14 @@ impl App for ZeroSplitter { |left| { match cur_category.mode { Gamemode::GreenOrange => left.label(format!("{}-{}", loop_n, stage_n)), - Gamemode::WhiteVanilla => left.label(vanilla_split_names(i)), + Gamemode::WhiteVanilla => { + if self.names { + left.label(vanilla_descriptive_split_names(i)) + } else { + left.label(vanilla_split_names(i)) + } + + }, Gamemode::BlackOnion => todo!(), }; diff --git a/splitter/src/main.rs b/splitter/src/main.rs index 7b3b58c..bd19e8f 100644 --- a/splitter/src/main.rs +++ b/splitter/src/main.rs @@ -14,34 +14,20 @@ use common::FrameData; use eframe::{ NativeOptions, egui::{ - Color32, Context, IconData, ThemePreference, + Context, IconData, ThemePreference, ViewportBuilder, }, }; use log::{error, warn}; use serde::{Deserialize, Serialize}; +use crate::theme::zeroranger_visuals; + mod hook; mod system; mod ui; mod app; - -#[allow(unused)] -const DARK_GREEN: Color32 = Color32::from_rgb(0, 0x4f, 0x4d); -#[allow(unused)] -const GREEN: Color32 = Color32::from_rgb(0, 0x94, 0x79); -#[allow(unused)] -const LIGHT_ORANGE: Color32 = Color32::from_rgb(0xff, 0xc0, 0x73); -#[allow(unused)] -const DARK_ORANGE: Color32 = Color32::from_rgb(0xff, 0x80, 0); -#[allow(unused)] -const DARKER_ORANGE: Color32 = Color32::from_rgb(0xdd, 0x59, 0x28); -#[allow(unused)] -const ORANGEST: Color32 = Color32::from_rgb(0xad, 0x2f, 0x17); -#[allow(unused)] -const DARKER_GREEN: Color32 = Color32::from_rgb(0x00, 0x32, 0x32); -#[allow(unused)] -const GREENEST: Color32 = Color32::from_rgb(0x00, 0x1d, 0x23); +mod theme; const SPLIT_DELAY_FRAMES: u32 = 20; @@ -69,6 +55,7 @@ fn main() { Box::new(|c| { let _ = EGUI_CTX.set(c.egui_ctx.clone()); c.egui_ctx.set_theme(ThemePreference::Dark); + c.egui_ctx.set_visuals(zeroranger_visuals()); Ok(Box::new(ZeroSplitter::load(rx))) }), ) @@ -113,6 +100,7 @@ struct ZeroSplitter { relative_score: bool, show_gold_split: bool, split_delay: Option, + names: bool } impl ZeroSplitter { @@ -141,6 +129,7 @@ impl ZeroSplitter { relative_score: true, show_gold_split: true, split_delay: None, + names: false } } diff --git a/splitter/src/theme.rs b/splitter/src/theme.rs new file mode 100644 index 0000000..324d667 --- /dev/null +++ b/splitter/src/theme.rs @@ -0,0 +1,98 @@ +use eframe::egui::{ + self, Color32, CornerRadius, Stroke, + style::{self, WidgetVisuals}, +}; + +#[allow(unused)] +pub const DARK_GREEN: Color32 = Color32::from_rgb(0, 0x4f, 0x4d); +#[allow(unused)] +pub const GREEN: Color32 = Color32::from_rgb(0, 0x94, 0x79); +#[allow(unused)] +pub const LIGHT_ORANGE: Color32 = Color32::from_rgb(0xff, 0xc0, 0x73); +#[allow(unused)] +pub const DARK_ORANGE: Color32 = Color32::from_rgb(0xff, 0x80, 0); +#[allow(unused)] +pub const DARKER_ORANGE: Color32 = Color32::from_rgb(0xdd, 0x59, 0x28); +#[allow(unused)] +pub const ORANGEST: Color32 = Color32::from_rgb(0xad, 0x2f, 0x17); +#[allow(unused)] +pub const DARKER_GREEN: Color32 = Color32::from_rgb(0x00, 0x32, 0x32); +#[allow(unused)] +pub const GREENEST: Color32 = Color32::from_rgb(0x00, 0x1d, 0x23); +#[allow(unused)] +pub const WHITE: Color32 = Color32::WHITE; +#[allow(unused)] +pub const BLACK: Color32 = Color32::BLACK; + +pub fn zeroranger_visuals() -> egui::Visuals { + egui::Visuals { + window_fill: BLACK, + extreme_bg_color: BLACK, + panel_fill: BLACK, + widgets: egui::style::Widgets { + inactive: egui::style::WidgetVisuals { + bg_fill: GREENEST, + weak_bg_fill: GREENEST, + bg_stroke: Stroke { ..Default::default() }, + corner_radius: CornerRadius::ZERO, + fg_stroke: Stroke { + width: 1.5, + color: DARK_ORANGE, + }, + expansion: 0.0, + }, + active: egui::style::WidgetVisuals { + bg_fill: GREENEST, + weak_bg_fill: GREENEST, + bg_stroke: Stroke { ..Default::default() }, + corner_radius: CornerRadius::ZERO, + fg_stroke: Stroke { + width: 1.0, + color: DARK_ORANGE, + }, + expansion: 0.0, + }, + hovered: WidgetVisuals { + bg_fill: LIGHT_ORANGE, + weak_bg_fill: LIGHT_ORANGE, + bg_stroke: Stroke { ..Default::default() }, + corner_radius: CornerRadius::ZERO, + fg_stroke: Stroke { + width: 1.0, + color: DARK_GREEN, + }, + expansion: 0.0, + }, + noninteractive: WidgetVisuals { + bg_fill: LIGHT_ORANGE, + weak_bg_fill: LIGHT_ORANGE, + bg_stroke: Stroke { ..Default::default() }, + corner_radius: CornerRadius::ZERO, + fg_stroke: Stroke { + width: 1.0, + color: DARKER_ORANGE, + }, + expansion: 0.0, + }, + open: WidgetVisuals { + bg_fill: LIGHT_ORANGE, + weak_bg_fill: LIGHT_ORANGE, + bg_stroke: Stroke { ..Default::default() }, + corner_radius: CornerRadius::ZERO, + fg_stroke: Stroke { + width: 1.0, + color: DARKER_ORANGE, + }, + expansion: 0.0, + }, + }, + selection: style::Selection { + bg_fill: DARK_ORANGE, + stroke: Stroke { + width: 1.0, + color: GREENEST, + }, + }, + ..Default::default() + } +} From 8f6486aa8a65550703635007bdafeb0c33529379 Mon Sep 17 00:00:00 2001 From: lillies <103815266+lily-and-doll@users.noreply.github.com> Date: Tue, 18 Nov 2025 23:09:17 -0500 Subject: [PATCH 20/52] Update README.md screenshots --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ea104af..939b2d8 100644 --- a/README.md +++ b/README.md @@ -4,8 +4,8 @@ Supports Green Orange and White Vanilla. [DOWNLOAD](https://github.com/lily-and-doll/ZeroSplitter/releases) -zerosplitter_hjuYG7gioU -zerosplitter_1v8gZHNMn1 +zerosplitter_BjsRefSLvF +zerosplitter_vVeE4jqNXy # How to use From c56a209074871bf2ca32146990be434fc667217a Mon Sep 17 00:00:00 2001 From: lily_and_doll <103815266+lily-and-doll@users.noreply.github.com> Date: Wed, 19 Nov 2025 02:17:25 -0500 Subject: [PATCH 21/52] Add additional color coding to score diff --- splitter/build.rs | 16 ++++---- splitter/src/app.rs | 94 +++++++++++++++++++++++++------------------- splitter/src/main.rs | 17 +++----- 3 files changed, 67 insertions(+), 60 deletions(-) diff --git a/splitter/build.rs b/splitter/build.rs index c4b65d5..b6f0c0c 100644 --- a/splitter/build.rs +++ b/splitter/build.rs @@ -3,11 +3,11 @@ use std::{env, io}; use winresource::WindowsResource; fn main() -> io::Result<()> { - if env::var_os("CARGO_CFG_WINDOWS").is_some() { - WindowsResource::new() - // This path can be absolute, or relative to your crate root. - .set_icon("assets/icon.ico") - .compile()?; - } - Ok(()) -} \ No newline at end of file + if env::var_os("CARGO_CFG_WINDOWS").is_some() { + WindowsResource::new() + // This path can be absolute, or relative to your crate root. + .set_icon("assets/icon.ico") + .compile()?; + } + Ok(()) +} diff --git a/splitter/src/app.rs b/splitter/src/app.rs index dd28e65..89ee60b 100644 --- a/splitter/src/app.rs +++ b/splitter/src/app.rs @@ -4,7 +4,10 @@ use eframe::{ }; use crate::{ - Category, Gamemode, ZeroSplitter, theme::{DARK_GREEN, DARK_ORANGE, DARKER_ORANGE, GREEN, GREENEST, LIGHT_ORANGE}, ui::{category_maker_dialog, confirm_dialog}, vanilla_descriptive_split_names, vanilla_split_names + Category, Gamemode, ZeroSplitter, + theme::{DARK_GREEN, DARK_ORANGE, DARKER_GREEN, DARKER_ORANGE, GREEN, GREENEST, LIGHT_ORANGE}, + ui::{category_maker_dialog, confirm_dialog}, + vanilla_descriptive_split_names, vanilla_split_names, }; impl App for ZeroSplitter { @@ -39,7 +42,7 @@ impl App for ZeroSplitter { .on_hover_text("Display relative score per split or running total of score"); ui.toggle_value(&mut self.show_gold_split, "BEST SPLITS") .on_hover_text("Show your PB's splits or your best splits on the left"); - ui.toggle_value(&mut self.names, "NAMES") + ui.toggle_value(&mut self.names, "NAMES") .on_hover_text("Toggle descriptive or number names for WV splits"); }); ui.horizontal(|ui| { @@ -58,23 +61,19 @@ impl App for ZeroSplitter { } }); - for (i, split) in self.current_run.splits.iter().enumerate().map(|(i, &s)| { - // Switch current split score between modes - if self.relative_score { - (i, s) - } else { - // Get total score up to the current split - ( - i, - self.current_run - .splits - .iter() - .enumerate() - .take_while(|&(idx, _)| idx <= i) - .fold(0, |acc, (_, &s)| acc + s), - ) - } + // relative split: score gained during one split + // absolute split: total score during one split + for (i, rel_split, abs_split) in self.current_run.splits.iter().enumerate().map(|(i, &s)| { + (i, s, { + self.current_run + .splits + .iter() + .enumerate() + .take_while(|&(idx, _)| idx <= i) + .fold(0, |acc, (_, &s)| acc + s) + }) }) { + let split = if self.relative_score { rel_split } else { abs_split }; // translate split number to stage/loop for GO let stage_n = (i & 3) + 1; let loop_n = (i >> 2) + 1; @@ -92,16 +91,20 @@ impl App for ZeroSplitter { }; // Get relative/absolute split in the PB // PB split = score of this split in the PB run + let rel_pb_split = self.comparison.personal_best.splits[i]; + let abs_pb_split = self + .comparison + .personal_best + .splits + .iter() + .enumerate() + .take_while(|&(idx, _)| idx <= i) + .fold(0, |acc, (_, &s)| acc + s); + let pb_split = if self.relative_score { - self.comparison.personal_best.splits[i] + rel_pb_split } else { - self.comparison - .personal_best - .splits - .iter() - .enumerate() - .take_while(|&(idx, _)| idx <= i) - .fold(0, |acc, (_, &s)| acc + s) + abs_pb_split }; Sides::new().show( @@ -110,13 +113,12 @@ impl App for ZeroSplitter { match cur_category.mode { Gamemode::GreenOrange => left.label(format!("{}-{}", loop_n, stage_n)), Gamemode::WhiteVanilla => { - if self.names { - left.label(vanilla_descriptive_split_names(i)) - } else { - left.label(vanilla_split_names(i)) - } - - }, + if self.names { + left.label(vanilla_descriptive_split_names(i)) + } else { + left.label(vanilla_split_names(i)) + } + } Gamemode::BlackOnion => todo!(), }; @@ -147,14 +149,26 @@ impl App for ZeroSplitter { if i < self.current_split.unwrap_or(0) { // past split, we should show a diff let diff = split - pb_split; - let diff_color = if diff > 0 { - LIGHT_ORANGE - } else if diff == 0 { - Color32::WHITE + if self.relative_score { + let diff_color = if diff > 0 { + LIGHT_ORANGE + } else if diff == 0 { + Color32::WHITE + } else { + DARK_GREEN + }; + right.colored_label(diff_color, format!("{diff:+}")); } else { - DARK_GREEN - }; - right.colored_label(diff_color, format!("{diff:+}")); + let rel_diff = rel_split - rel_pb_split; + let diff_color = if diff > 0 { + if rel_diff > 0 { LIGHT_ORANGE } else { DARKER_ORANGE } + } else if diff == 0 { + Color32::WHITE + } else { + if rel_diff > 0 { DARK_GREEN } else { DARKER_GREEN } + }; + right.colored_label(diff_color, format!("{diff:+}")); + } } } else { right.colored_label(DARK_GREEN, "--"); diff --git a/splitter/src/main.rs b/splitter/src/main.rs index bd19e8f..36c0127 100644 --- a/splitter/src/main.rs +++ b/splitter/src/main.rs @@ -13,21 +13,18 @@ use std::{ use common::FrameData; use eframe::{ NativeOptions, - egui::{ - Context, IconData, ThemePreference, - ViewportBuilder, - }, + egui::{Context, IconData, ThemePreference, ViewportBuilder}, }; use log::{error, warn}; use serde::{Deserialize, Serialize}; use crate::theme::zeroranger_visuals; +mod app; mod hook; mod system; -mod ui; -mod app; mod theme; +mod ui; const SPLIT_DELAY_FRAMES: u32 = 20; @@ -100,7 +97,7 @@ struct ZeroSplitter { relative_score: bool, show_gold_split: bool, split_delay: Option, - names: bool + names: bool, } impl ZeroSplitter { @@ -129,7 +126,7 @@ impl ZeroSplitter { relative_score: true, show_gold_split: true, split_delay: None, - names: false + names: false, } } @@ -319,9 +316,6 @@ impl ZeroSplitter { } } - - - #[derive(PartialEq, Eq, Clone, Copy, Debug, Serialize, Deserialize)] pub enum Gamemode { GreenOrange, @@ -343,7 +337,6 @@ pub struct EntryDialogData { pub mode: Gamemode, } - /// Translation of function of the same name in ZR. /// Checkpoints in WV have the same stage/checkpoint as in GO, /// e.g. the cloudoo segment in stage 1 is technically stage 3 checkpoint 2 From 57a7f531da8ec93f1ab5564a63d84e34a82bd196 Mon Sep 17 00:00:00 2001 From: lily_and_doll <103815266+lily-and-doll@users.noreply.github.com> Date: Wed, 19 Nov 2025 02:50:26 -0500 Subject: [PATCH 22/52] Remove unnecessary feature flags --- Cargo.lock | 1549 ++----------------------------------------- splitter/Cargo.toml | 8 +- 2 files changed, 54 insertions(+), 1503 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f2931fd..0f55f6f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -18,98 +18,6 @@ version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c71b1793ee61086797f5c80b6efa2b8ffa6d5dd703f118545808a7f2e27f7046" -[[package]] -name = "accesskit" -version = "0.17.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3d3b8f9bae46a948369bc4a03e815d4ed6d616bd00de4051133a5019dc31c5a" - -[[package]] -name = "accesskit_atspi_common" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c5dd55e6e94949498698daf4d48fb5659e824d7abec0d394089656ceaf99d4f" -dependencies = [ - "accesskit", - "accesskit_consumer", - "atspi-common", - "serde", - "thiserror 1.0.69", - "zvariant", -] - -[[package]] -name = "accesskit_consumer" -version = "0.26.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f47983a1084940ba9a39c077a8c63e55c619388be5476ac04c804cfbd1e63459" -dependencies = [ - "accesskit", - "hashbrown 0.15.2", - "immutable-chunkmap", -] - -[[package]] -name = "accesskit_macos" -version = "0.18.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7329821f3bd1101e03a7d2e03bd339e3ac0dc64c70b4c9f9ae1949e3ba8dece1" -dependencies = [ - "accesskit", - "accesskit_consumer", - "hashbrown 0.15.2", - "objc2 0.5.2", - "objc2-app-kit 0.2.2", - "objc2-foundation 0.2.2", -] - -[[package]] -name = "accesskit_unix" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcee751cc20d88678c33edaf9c07e8b693cd02819fe89053776f5313492273f5" -dependencies = [ - "accesskit", - "accesskit_atspi_common", - "async-channel", - "async-executor", - "async-task", - "atspi", - "futures-lite", - "futures-util", - "serde", - "zbus", -] - -[[package]] -name = "accesskit_windows" -version = "0.24.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24fcd5d23d70670992b823e735e859374d694a3d12bfd8dd32bd3bd8bedb5d81" -dependencies = [ - "accesskit", - "accesskit_consumer", - "hashbrown 0.15.2", - "paste", - "static_assertions", - "windows 0.58.0", - "windows-core 0.58.0", -] - -[[package]] -name = "accesskit_winit" -version = "0.23.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a6a48dad5530b6deb9fc7a52cc6c3bf72cdd9eb8157ac9d32d69f2427a5e879" -dependencies = [ - "accesskit", - "accesskit_macos", - "accesskit_unix", - "accesskit_windows", - "raw-window-handle", - "winit", -] - [[package]] name = "adler2" version = "2.0.0" @@ -123,10 +31,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" dependencies = [ "cfg-if", - "getrandom 0.2.15", "once_cell", "version_check", - "zerocopy 0.7.35", + "zerocopy", ] [[package]] @@ -154,9 +61,9 @@ dependencies = [ "log", "ndk", "ndk-context", - "ndk-sys 0.6.0+11769913", + "ndk-sys", "num_enum", - "thiserror 1.0.69", + "thiserror", ] [[package]] @@ -165,15 +72,6 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc7eb209b1518d6bb87b283c20095f5228ecda460da70b44f0802523dea6da04" -[[package]] -name = "android_system_properties" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" -dependencies = [ - "libc", -] - [[package]] name = "arboard" version = "3.5.0" @@ -194,254 +92,18 @@ dependencies = [ "x11rb", ] -[[package]] -name = "arrayref" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" - -[[package]] -name = "arrayvec" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" - -[[package]] -name = "as-raw-xcb-connection" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "175571dd1d178ced59193a6fc02dde1b972eb0bc56c892cde9beeceac5bf0f6b" - -[[package]] -name = "ash" -version = "0.38.0+1.3.281" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bb44936d800fea8f016d7f2311c6a4f97aebd5dc86f09906139ec848cf3a46f" -dependencies = [ - "libloading", -] - -[[package]] -name = "async-broadcast" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" -dependencies = [ - "event-listener", - "event-listener-strategy", - "futures-core", - "pin-project-lite", -] - -[[package]] -name = "async-channel" -version = "2.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89b47800b0be77592da0afd425cc03468052844aff33b84e33cc696f64e77b6a" -dependencies = [ - "concurrent-queue", - "event-listener-strategy", - "futures-core", - "pin-project-lite", -] - -[[package]] -name = "async-executor" -version = "1.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30ca9a001c1e8ba5149f91a74362376cc6bc5b919d92d988668657bd570bdcec" -dependencies = [ - "async-task", - "concurrent-queue", - "fastrand", - "futures-lite", - "slab", -] - -[[package]] -name = "async-fs" -version = "2.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebcd09b382f40fcd159c2d695175b2ae620ffa5f3bd6f664131efff4e8b9e04a" -dependencies = [ - "async-lock", - "blocking", - "futures-lite", -] - -[[package]] -name = "async-io" -version = "2.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43a2b323ccce0a1d90b449fd71f2a06ca7faa7c54c2751f06c9bd851fc061059" -dependencies = [ - "async-lock", - "cfg-if", - "concurrent-queue", - "futures-io", - "futures-lite", - "parking", - "polling", - "rustix 0.38.44", - "slab", - "tracing", - "windows-sys 0.59.0", -] - -[[package]] -name = "async-lock" -version = "3.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff6e472cdea888a4bd64f342f09b3f50e1886d32afe8df3d663c01140b811b18" -dependencies = [ - "event-listener", - "event-listener-strategy", - "pin-project-lite", -] - -[[package]] -name = "async-process" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63255f1dc2381611000436537bbedfe83183faa303a5a0edaf191edef06526bb" -dependencies = [ - "async-channel", - "async-io", - "async-lock", - "async-signal", - "async-task", - "blocking", - "cfg-if", - "event-listener", - "futures-lite", - "rustix 0.38.44", - "tracing", -] - -[[package]] -name = "async-recursion" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "async-signal" -version = "0.2.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "637e00349800c0bdf8bfc21ebbc0b6524abea702b0da4168ac00d070d0c0b9f3" -dependencies = [ - "async-io", - "async-lock", - "atomic-waker", - "cfg-if", - "futures-core", - "futures-io", - "rustix 0.38.44", - "signal-hook-registry", - "slab", - "windows-sys 0.59.0", -] - -[[package]] -name = "async-task" -version = "4.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" - -[[package]] -name = "async-trait" -version = "0.1.88" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "atomic-waker" version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" -[[package]] -name = "atspi" -version = "0.22.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be534b16650e35237bb1ed189ba2aab86ce65e88cc84c66f4935ba38575cecbf" -dependencies = [ - "atspi-common", - "atspi-connection", - "atspi-proxies", -] - -[[package]] -name = "atspi-common" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1909ed2dc01d0a17505d89311d192518507e8a056a48148e3598fef5e7bb6ba7" -dependencies = [ - "enumflags2", - "serde", - "static_assertions", - "zbus", - "zbus-lockstep", - "zbus-lockstep-macros", - "zbus_names", - "zvariant", -] - -[[package]] -name = "atspi-connection" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "430c5960624a4baaa511c9c0fcc2218e3b58f5dbcc47e6190cafee344b873333" -dependencies = [ - "atspi-common", - "atspi-proxies", - "futures-lite", - "zbus", -] - -[[package]] -name = "atspi-proxies" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5e6c5de3e524cf967569722446bcd458d5032348554d9a17d7d72b041ab7496" -dependencies = [ - "atspi-common", - "serde", - "zbus", - "zvariant", -] - [[package]] name = "autocfg" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" -[[package]] -name = "bit-set" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" -dependencies = [ - "bit-vec", -] - -[[package]] -name = "bit-vec" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" - [[package]] name = "bitflags" version = "1.3.2" @@ -453,24 +115,6 @@ name = "bitflags" version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" -dependencies = [ - "serde", -] - -[[package]] -name = "block" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" - -[[package]] -name = "block-buffer" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" -dependencies = [ - "generic-array", -] [[package]] name = "block2" @@ -481,19 +125,6 @@ dependencies = [ "objc2 0.5.2", ] -[[package]] -name = "blocking" -version = "1.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "703f41c54fc768e63e091340b424302bb1c29ef4aa0c7f10fe849dfb114d29ea" -dependencies = [ - "async-channel", - "async-task", - "futures-io", - "futures-lite", - "piper", -] - [[package]] name = "bumpalo" version = "3.17.0" @@ -541,9 +172,9 @@ dependencies = [ "bitflags 2.9.0", "log", "polling", - "rustix 0.38.44", + "rustix", "slab", - "thiserror 1.0.69", + "thiserror", ] [[package]] @@ -553,7 +184,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95a66a987056935f7efce4ab5668920b5d0dac4a7c99991a67395f13702ddd20" dependencies = [ "calloop", - "rustix 0.38.44", + "rustix", "wayland-backend", "wayland-client", ] @@ -605,16 +236,6 @@ dependencies = [ "error-code", ] -[[package]] -name = "codespan-reporting" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" -dependencies = [ - "termcolor", - "unicode-width", -] - [[package]] name = "combine" version = "4.6.7" @@ -691,15 +312,6 @@ dependencies = [ "libc", ] -[[package]] -name = "cpufeatures" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" -dependencies = [ - "libc", -] - [[package]] name = "crc32fast" version = "1.4.2" @@ -715,32 +327,12 @@ version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" -[[package]] -name = "crypto-common" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" -dependencies = [ - "generic-array", - "typenum", -] - [[package]] name = "cursor-icon" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96a6ac251f4a2aca6b3f91340350eab87ae57c3f127ffeb585e92bd336717991" -[[package]] -name = "digest" -version = "0.10.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" -dependencies = [ - "block-buffer", - "crypto-common", -] - [[package]] name = "dispatch" version = "0.2.0" @@ -808,7 +400,6 @@ dependencies = [ "bytemuck", "document-features", "egui", - "egui-wgpu", "egui-winit", "egui_glow", "glow", @@ -840,7 +431,6 @@ version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25dd34cec49ab55d85ebf70139cb1ccd29c977ef6b6ba4fe85489d6877ee9ef3" dependencies = [ - "accesskit", "ahash", "bitflags 2.9.0", "emath", @@ -850,33 +440,12 @@ dependencies = [ "profiling", ] -[[package]] -name = "egui-wgpu" -version = "0.31.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d319dfef570f699b6e9114e235e862a2ddcf75f0d1a061de9e1328d92146d820" -dependencies = [ - "ahash", - "bytemuck", - "document-features", - "egui", - "epaint", - "log", - "profiling", - "thiserror 1.0.69", - "type-map", - "web-time", - "wgpu", - "winit", -] - [[package]] name = "egui-winit" version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d9dfbb78fe4eb9c3a39ad528b90ee5915c252e77bbab9d4ebc576541ab67e13" dependencies = [ - "accesskit_winit", "ahash", "arboard", "bytemuck", @@ -905,7 +474,6 @@ dependencies = [ "profiling", "wasm-bindgen", "web-sys", - "winit", ] [[package]] @@ -917,33 +485,6 @@ dependencies = [ "bytemuck", ] -[[package]] -name = "endi" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3d8a32ae18130a3c84dd492d4215c3d913c3b07c6b63c2eb3eb7ff1101ab7bf" - -[[package]] -name = "enumflags2" -version = "0.7.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba2f4b465f5318854c6f8dd686ede6c0a9dc67d4b1ac241cf0eb51521a309147" -dependencies = [ - "enumflags2_derive", - "serde", -] - -[[package]] -name = "enumflags2_derive" -version = "0.7.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc4caf64a58d7a6d65ab00639b046ff54399a39f5f2554728895ace4b297cd79" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "env_logger" version = "0.10.2" @@ -1003,33 +544,6 @@ version = "3.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a5d9305ccc6942a704f4335694ecd3de2ea531b114ac2d51f5f843750787a92f" -[[package]] -name = "event-listener" -version = "5.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3492acde4c3fc54c845eaab3eed8bd00c7a7d881f78bfc801e43a93dec1331ae" -dependencies = [ - "concurrent-queue", - "parking", - "pin-project-lite", -] - -[[package]] -name = "event-listener-strategy" -version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" -dependencies = [ - "event-listener", - "pin-project-lite", -] - -[[package]] -name = "fastrand" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" - [[package]] name = "fdeflate" version = "0.3.7" @@ -1049,12 +563,6 @@ dependencies = [ "miniz_oxide", ] -[[package]] -name = "foldhash" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" - [[package]] name = "foreign-types" version = "0.5.0" @@ -1091,81 +599,6 @@ dependencies = [ "percent-encoding", ] -[[package]] -name = "futures-core" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" - -[[package]] -name = "futures-io" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" - -[[package]] -name = "futures-lite" -version = "2.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5edaec856126859abb19ed65f39e90fea3a9574b9707f13539acf4abf7eb532" -dependencies = [ - "fastrand", - "futures-core", - "futures-io", - "parking", - "pin-project-lite", -] - -[[package]] -name = "futures-macro" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "futures-sink" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" - -[[package]] -name = "futures-task" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" - -[[package]] -name = "futures-util" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" -dependencies = [ - "futures-core", - "futures-io", - "futures-macro", - "futures-sink", - "futures-task", - "memchr", - "pin-project-lite", - "pin-utils", - "slab", -] - -[[package]] -name = "generic-array" -version = "0.14.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" -dependencies = [ - "typenum", - "version_check", -] - [[package]] name = "gethostname" version = "0.4.3" @@ -1176,17 +609,6 @@ dependencies = [ "windows-targets 0.48.5", ] -[[package]] -name = "getrandom" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" -dependencies = [ - "cfg-if", - "libc", - "wasi 0.11.0+wasi-snapshot-preview1", -] - [[package]] name = "getrandom" version = "0.3.2" @@ -1196,7 +618,7 @@ dependencies = [ "cfg-if", "libc", "r-efi", - "wasi 0.14.2+wasi-0.2.4", + "wasi", ] [[package]] @@ -1234,7 +656,6 @@ dependencies = [ "core-foundation 0.9.4", "dispatch", "glutin_egl_sys", - "glutin_glx_sys", "glutin_wgl_sys", "libloading", "objc2 0.5.2", @@ -1242,9 +663,7 @@ dependencies = [ "objc2-foundation 0.2.2", "once_cell", "raw-window-handle", - "wayland-sys", "windows-sys 0.52.0", - "x11-dl", ] [[package]] @@ -1269,16 +688,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "glutin_glx_sys" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a7bb2938045a88b612499fbcba375a77198e01306f52272e692f8c1f3751185" -dependencies = [ - "gl_generator", - "x11-dl", -] - [[package]] name = "glutin_wgl_sys" version = "0.6.1" @@ -1288,66 +697,12 @@ dependencies = [ "gl_generator", ] -[[package]] -name = "gpu-alloc" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbcd2dba93594b227a1f57ee09b8b9da8892c34d55aa332e034a228d0fe6a171" -dependencies = [ - "bitflags 2.9.0", - "gpu-alloc-types", -] - -[[package]] -name = "gpu-alloc-types" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98ff03b468aa837d70984d55f5d3f846f6ec31fe34bbb97c4f85219caeee1ca4" -dependencies = [ - "bitflags 2.9.0", -] - -[[package]] -name = "gpu-descriptor" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcf29e94d6d243368b7a56caa16bc213e4f9f8ed38c4d9557069527b5d5281ca" -dependencies = [ - "bitflags 2.9.0", - "gpu-descriptor-types", - "hashbrown 0.15.2", -] - -[[package]] -name = "gpu-descriptor-types" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdf242682df893b86f33a73828fb09ca4b2d3bb6cc95249707fc684d27484b91" -dependencies = [ - "bitflags 2.9.0", -] - -[[package]] -name = "hashbrown" -version = "0.15.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" -dependencies = [ - "foldhash", -] - [[package]] name = "hashbrown" version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" -[[package]] -name = "heck" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" - [[package]] name = "hermit-abi" version = "0.4.0" @@ -1360,18 +715,6 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fbd780fe5cc30f81464441920d82ac8740e2e46b29a6fad543ddd075229ce37e" -[[package]] -name = "hex" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" - -[[package]] -name = "hexf-parse" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df" - [[package]] name = "home" version = "0.5.11" @@ -1539,15 +882,6 @@ dependencies = [ "tiff", ] -[[package]] -name = "immutable-chunkmap" -version = "2.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12f97096f508d54f8f8ab8957862eee2ccd628847b6217af1a335e1c44dee578" -dependencies = [ - "arrayvec", -] - [[package]] name = "indexmap" version = "2.12.0" @@ -1555,7 +889,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f" dependencies = [ "equivalent", - "hashbrown 0.16.0", + "hashbrown", ] [[package]] @@ -1586,7 +920,7 @@ dependencies = [ "combine", "jni-sys", "log", - "thiserror 1.0.69", + "thiserror", "walkdir", "windows-sys 0.45.0", ] @@ -1603,7 +937,7 @@ version = "0.1.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a" dependencies = [ - "getrandom 0.3.2", + "getrandom", "libc", ] @@ -1617,21 +951,10 @@ checksum = "f5d4a7da358eff58addd2877a45865158f0d78c911d43a5784ceb7bbf52833b0" name = "js-sys" version = "0.3.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" -dependencies = [ - "once_cell", - "wasm-bindgen", -] - -[[package]] -name = "khronos-egl" -version = "6.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6aae1df220ece3c0ada96b8153459b67eebe9ae9212258bb0134ae60416fdf76" -dependencies = [ - "libc", - "libloading", - "pkg-config", +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +dependencies = [ + "once_cell", + "wasm-bindgen", ] [[package]] @@ -1673,12 +996,6 @@ version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" -[[package]] -name = "linux-raw-sys" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe7db12097d22ec582439daf8618b8fdd1a7bef6270e9af3b1ebcd30893cf413" - [[package]] name = "litemap" version = "0.7.5" @@ -1707,15 +1024,6 @@ version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" -[[package]] -name = "malloc_buf" -version = "0.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" -dependencies = [ - "libc", -] - [[package]] name = "memchr" version = "2.7.4" @@ -1740,21 +1048,6 @@ dependencies = [ "autocfg", ] -[[package]] -name = "metal" -version = "0.31.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f569fb946490b5743ad69813cb19629130ce9374034abe31614a36402d18f99e" -dependencies = [ - "bitflags 2.9.0", - "block", - "core-graphics-types", - "foreign-types", - "log", - "objc", - "paste", -] - [[package]] name = "miniz_oxide" version = "0.8.7" @@ -1765,28 +1058,6 @@ dependencies = [ "simd-adler32", ] -[[package]] -name = "naga" -version = "24.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e380993072e52eef724eddfcde0ed013b0c023c3f0417336ed041aa9f076994e" -dependencies = [ - "arrayvec", - "bit-set", - "bitflags 2.9.0", - "cfg_aliases", - "codespan-reporting", - "hexf-parse", - "indexmap", - "log", - "rustc-hash", - "spirv", - "strum", - "termcolor", - "thiserror 2.0.12", - "unicode-xid", -] - [[package]] name = "ndk" version = "0.9.0" @@ -1796,10 +1067,10 @@ dependencies = [ "bitflags 2.9.0", "jni-sys", "log", - "ndk-sys 0.6.0+11769913", + "ndk-sys", "num_enum", "raw-window-handle", - "thiserror 1.0.69", + "thiserror", ] [[package]] @@ -1808,15 +1079,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" -[[package]] -name = "ndk-sys" -version = "0.5.0+25.2.9519653" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c196769dd60fd4f363e11d948139556a344e79d451aeb2fa2fd040738ef7691" -dependencies = [ - "jni-sys", -] - [[package]] name = "ndk-sys" version = "0.6.0+11769913" @@ -1826,19 +1088,6 @@ dependencies = [ "jni-sys", ] -[[package]] -name = "nix" -version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" -dependencies = [ - "bitflags 2.9.0", - "cfg-if", - "cfg_aliases", - "libc", - "memoffset", -] - [[package]] name = "nohash-hasher" version = "0.2.0" @@ -1875,15 +1124,6 @@ dependencies = [ "syn", ] -[[package]] -name = "objc" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" -dependencies = [ - "malloc_buf", -] - [[package]] name = "objc-sys" version = "0.3.5" @@ -2167,25 +1407,6 @@ dependencies = [ "libredox", ] -[[package]] -name = "ordered-float" -version = "4.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7bb71e1b3fa6ca1c61f383464aaf2bb0e2f8e772a1f01d486832464de363b951" -dependencies = [ - "num-traits", -] - -[[package]] -name = "ordered-stream" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" -dependencies = [ - "futures-core", - "pin-project-lite", -] - [[package]] name = "owned_ttf_parser" version = "0.25.0" @@ -2195,12 +1416,6 @@ dependencies = [ "ttf-parser", ] -[[package]] -name = "parking" -version = "2.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" - [[package]] name = "parking_lot" version = "0.12.3" @@ -2224,18 +1439,12 @@ dependencies = [ "windows-targets 0.52.6", ] -[[package]] -name = "paste" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" - [[package]] name = "payload" version = "0.1.0" dependencies = [ "common", - "windows 0.60.0", + "windows", ] [[package]] @@ -2270,23 +1479,6 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - -[[package]] -name = "piper" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066" -dependencies = [ - "atomic-waker", - "fastrand", - "futures-io", -] - [[package]] name = "pkg-config" version = "0.3.32" @@ -2316,20 +1508,11 @@ dependencies = [ "concurrent-queue", "hermit-abi 0.4.0", "pin-project-lite", - "rustix 0.38.44", + "rustix", "tracing", "windows-sys 0.59.0", ] -[[package]] -name = "ppv-lite86" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" -dependencies = [ - "zerocopy 0.8.24", -] - [[package]] name = "pretty_env_logger" version = "0.5.0" @@ -2364,16 +1547,6 @@ version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "afbdc74edc00b6f6a218ca6a5364d6226a259d4b8ea1af4a0ea063f27e179f4d" -[[package]] -name = "quick-xml" -version = "0.30.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eff6510e86862b57b210fd8cbe8ed3f0d7d600b9c2863cd4549a2e033c66e956" -dependencies = [ - "memchr", - "serde", -] - [[package]] name = "quick-xml" version = "0.37.4" @@ -2398,36 +1571,6 @@ version = "5.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" -[[package]] -name = "rand" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" -dependencies = [ - "libc", - "rand_chacha", - "rand_core", -] - -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core", -] - -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom 0.2.15", -] - [[package]] name = "raw-window-handle" version = "0.6.2" @@ -2481,18 +1624,6 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" -[[package]] -name = "renderdoc-sys" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832" - -[[package]] -name = "rustc-hash" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" - [[package]] name = "rustix" version = "0.38.44" @@ -2502,20 +1633,7 @@ dependencies = [ "bitflags 2.9.0", "errno", "libc", - "linux-raw-sys 0.4.15", - "windows-sys 0.59.0", -] - -[[package]] -name = "rustix" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d97817398dd4bb2e6da002002db259209759911da105da92bec29ccb12cf58bf" -dependencies = [ - "bitflags 2.9.0", - "errno", - "libc", - "linux-raw-sys 0.9.3", + "linux-raw-sys", "windows-sys 0.59.0", ] @@ -2552,19 +1670,6 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" -[[package]] -name = "sctk-adwaita" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6277f0217056f77f1d8f49f2950ac6c278c0d607c45f5ee99328d792ede24ec" -dependencies = [ - "ab_glyph", - "log", - "memmap2", - "smithay-client-toolkit", - "tiny-skia", -] - [[package]] name = "serde" version = "1.0.228" @@ -2607,17 +1712,6 @@ dependencies = [ "serde", ] -[[package]] -name = "serde_repr" -version = "0.1.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "serde_spanned" version = "1.0.3" @@ -2627,32 +1721,12 @@ dependencies = [ "serde_core", ] -[[package]] -name = "sha1" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - [[package]] name = "shlex" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" -[[package]] -name = "signal-hook-registry" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" -dependencies = [ - "libc", -] - [[package]] name = "simd-adler32" version = "0.3.7" @@ -2696,8 +1770,8 @@ dependencies = [ "libc", "log", "memmap2", - "rustix 0.38.44", - "thiserror 1.0.69", + "rustix", + "thiserror", "wayland-backend", "wayland-client", "wayland-csd-frame", @@ -2728,15 +1802,6 @@ dependencies = [ "serde", ] -[[package]] -name = "spirv" -version = "0.3.0+sdk-1.3.268.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eda41003dc44290527a59b13432d4a0379379fa074b70174882adfbdfd917844" -dependencies = [ - "bitflags 2.9.0", -] - [[package]] name = "stable_deref_trait" version = "1.2.0" @@ -2749,34 +1814,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" -[[package]] -name = "strict-num" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" - -[[package]] -name = "strum" -version = "0.26.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" -dependencies = [ - "strum_macros", -] - -[[package]] -name = "strum_macros" -version = "0.26.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "rustversion", - "syn", -] - [[package]] name = "syn" version = "2.0.100" @@ -2799,19 +1836,6 @@ dependencies = [ "syn", ] -[[package]] -name = "tempfile" -version = "3.19.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7437ac7763b9b123ccf33c338a5cc1bac6f69b45a136c19bdd8a65e3916435bf" -dependencies = [ - "fastrand", - "getrandom 0.3.2", - "once_cell", - "rustix 1.0.5", - "windows-sys 0.59.0", -] - [[package]] name = "termcolor" version = "1.4.1" @@ -2827,16 +1851,7 @@ version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ - "thiserror-impl 1.0.69", -] - -[[package]] -name = "thiserror" -version = "2.0.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" -dependencies = [ - "thiserror-impl 2.0.12", + "thiserror-impl", ] [[package]] @@ -2850,17 +1865,6 @@ dependencies = [ "syn", ] -[[package]] -name = "thiserror-impl" -version = "2.0.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "tiff" version = "0.9.1" @@ -2872,31 +1876,6 @@ dependencies = [ "weezl", ] -[[package]] -name = "tiny-skia" -version = "0.11.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83d13394d44dae3207b52a326c0c85a8bf87f1541f23b0d143811088497b09ab" -dependencies = [ - "arrayref", - "arrayvec", - "bytemuck", - "cfg-if", - "log", - "tiny-skia-path", -] - -[[package]] -name = "tiny-skia-path" -version = "0.11.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c9e7fc0c2e86a30b117d0462aa261b72b7a99b7ebd7deb3a14ceda95c5bdc93" -dependencies = [ - "arrayref", - "bytemuck", - "strict-num", -] - [[package]] name = "tinystr" version = "0.7.6" @@ -2970,61 +1949,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" dependencies = [ "pin-project-lite", - "tracing-attributes", - "tracing-core", -] - -[[package]] -name = "tracing-attributes" -version = "0.1.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "tracing-core" -version = "0.1.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" -dependencies = [ - "once_cell", -] - -[[package]] -name = "ttf-parser" -version = "0.25.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31" - -[[package]] -name = "type-map" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "deb68604048ff8fa93347f02441e4487594adc20bb8a084f9e564d2b827a0a9f" -dependencies = [ - "rustc-hash", + "tracing-core", ] [[package]] -name = "typenum" -version = "1.18.0" +name = "tracing-core" +version = "0.1.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" +checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" [[package]] -name = "uds_windows" -version = "1.1.0" +name = "ttf-parser" +version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89daebc3e6fd160ac4aa9fc8b3bf71e1f74fbf92367ae71fb83a037e8bf164b9" -dependencies = [ - "memoffset", - "tempfile", - "winapi", -] +checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31" [[package]] name = "unicode-ident" @@ -3038,18 +1976,6 @@ version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" -[[package]] -name = "unicode-width" -version = "0.1.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" - -[[package]] -name = "unicode-xid" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" - [[package]] name = "url" version = "2.5.4" @@ -3089,12 +2015,6 @@ dependencies = [ "winapi-util", ] -[[package]] -name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" - [[package]] name = "wasi" version = "0.14.2+wasi-0.2.4" @@ -3183,7 +2103,7 @@ checksum = "b7208998eaa3870dad37ec8836979581506e0c5c64c20c9e79e9d2a10d6f47bf" dependencies = [ "cc", "downcast-rs", - "rustix 0.38.44", + "rustix", "scoped-tls", "smallvec", "wayland-sys", @@ -3196,7 +2116,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2120de3d33638aaef5b9f4472bff75f07c56379cf76ea320bd3a3d65ecaf73f" dependencies = [ "bitflags 2.9.0", - "rustix 0.38.44", + "rustix", "wayland-backend", "wayland-scanner", ] @@ -3218,7 +2138,7 @@ version = "0.31.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a93029cbb6650748881a00e4922b076092a6a08c11e7fbdb923f064b23968c5d" dependencies = [ - "rustix 0.38.44", + "rustix", "wayland-client", "xcursor", ] @@ -3235,19 +2155,6 @@ dependencies = [ "wayland-scanner", ] -[[package]] -name = "wayland-protocols-plasma" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ccaacc76703fefd6763022ac565b590fcade92202492381c95b2edfdf7d46b3" -dependencies = [ - "bitflags 2.9.0", - "wayland-backend", - "wayland-client", - "wayland-protocols", - "wayland-scanner", -] - [[package]] name = "wayland-protocols-wlr" version = "0.3.6" @@ -3268,7 +2175,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "896fdafd5d28145fce7958917d69f2fd44469b1d4e861cb5961bcbeebc6d1484" dependencies = [ "proc-macro2", - "quick-xml 0.37.4", + "quick-xml", "quote", ] @@ -3327,109 +2234,6 @@ version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53a85b86a771b1c87058196170769dd264f66c0782acf1ae6cc51bfd64b39082" -[[package]] -name = "wgpu" -version = "24.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35904fb00ba2d2e0a4d002fcbbb6e1b89b574d272a50e5fc95f6e81cf281c245" -dependencies = [ - "arrayvec", - "bitflags 2.9.0", - "cfg_aliases", - "document-features", - "js-sys", - "log", - "parking_lot", - "profiling", - "raw-window-handle", - "smallvec", - "static_assertions", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", - "wgpu-core", - "wgpu-hal", - "wgpu-types", -] - -[[package]] -name = "wgpu-core" -version = "24.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "671c25545d479b47d3f0a8e373aceb2060b67c6eb841b24ac8c32348151c7a0c" -dependencies = [ - "arrayvec", - "bit-vec", - "bitflags 2.9.0", - "cfg_aliases", - "document-features", - "indexmap", - "log", - "naga", - "once_cell", - "parking_lot", - "profiling", - "raw-window-handle", - "rustc-hash", - "smallvec", - "thiserror 2.0.12", - "wgpu-hal", - "wgpu-types", -] - -[[package]] -name = "wgpu-hal" -version = "24.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f112f464674ca69f3533248508ee30cb84c67cf06c25ff6800685f5e0294e259" -dependencies = [ - "android_system_properties", - "arrayvec", - "ash", - "bitflags 2.9.0", - "bytemuck", - "cfg_aliases", - "core-graphics-types", - "glow", - "glutin_wgl_sys", - "gpu-alloc", - "gpu-descriptor", - "js-sys", - "khronos-egl", - "libc", - "libloading", - "log", - "metal", - "naga", - "ndk-sys 0.5.0+25.2.9519653", - "objc", - "once_cell", - "ordered-float", - "parking_lot", - "profiling", - "raw-window-handle", - "renderdoc-sys", - "rustc-hash", - "smallvec", - "thiserror 2.0.12", - "wasm-bindgen", - "web-sys", - "wgpu-types", - "windows 0.58.0", -] - -[[package]] -name = "wgpu-types" -version = "24.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50ac044c0e76c03a0378e7786ac505d010a873665e2d51383dcff8dd227dc69c" -dependencies = [ - "bitflags 2.9.0", - "js-sys", - "log", - "web-sys", -] - [[package]] name = "winapi" version = "0.3.9" @@ -3461,16 +2265,6 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" -[[package]] -name = "windows" -version = "0.58.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6" -dependencies = [ - "windows-core 0.58.0", - "windows-targets 0.52.6", -] - [[package]] name = "windows" version = "0.60.0" @@ -3478,7 +2272,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ddf874e74c7a99773e62b1c671427abf01a425e77c3d3fb9fb1e4883ea934529" dependencies = [ "windows-collections", - "windows-core 0.60.1", + "windows-core", "windows-future", "windows-link", "windows-numerics", @@ -3490,20 +2284,7 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5467f79cc1ba3f52ebb2ed41dbb459b8e7db636cc3429458d9a852e15bc24dec" dependencies = [ - "windows-core 0.60.1", -] - -[[package]] -name = "windows-core" -version = "0.58.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99" -dependencies = [ - "windows-implement 0.58.0", - "windows-interface 0.58.0", - "windows-result 0.2.0", - "windows-strings 0.1.0", - "windows-targets 0.52.6", + "windows-core", ] [[package]] @@ -3512,11 +2293,11 @@ version = "0.60.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca21a92a9cae9bf4ccae5cf8368dce0837100ddf6e6d57936749e85f152f6247" dependencies = [ - "windows-implement 0.59.0", - "windows-interface 0.59.1", + "windows-implement", + "windows-interface", "windows-link", - "windows-result 0.3.2", - "windows-strings 0.3.1", + "windows-result", + "windows-strings", ] [[package]] @@ -3525,21 +2306,10 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a787db4595e7eb80239b74ce8babfb1363d8e343ab072f2ffe901400c03349f0" dependencies = [ - "windows-core 0.60.1", + "windows-core", "windows-link", ] -[[package]] -name = "windows-implement" -version = "0.58.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "windows-implement" version = "0.59.0" @@ -3551,17 +2321,6 @@ dependencies = [ "syn", ] -[[package]] -name = "windows-interface" -version = "0.58.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "windows-interface" version = "0.59.1" @@ -3585,19 +2344,10 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "005dea54e2f6499f2cee279b8f703b3cf3b5734a2d8d21867c8f44003182eeed" dependencies = [ - "windows-core 0.60.1", + "windows-core", "windows-link", ] -[[package]] -name = "windows-result" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" -dependencies = [ - "windows-targets 0.52.6", -] - [[package]] name = "windows-result" version = "0.3.2" @@ -3607,16 +2357,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "windows-strings" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" -dependencies = [ - "windows-result 0.2.0", - "windows-targets 0.52.6", -] - [[package]] name = "windows-strings" version = "0.3.1" @@ -3837,12 +2577,10 @@ version = "0.30.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a809eacf18c8eca8b6635091543f02a5a06ddf3dad846398795460e6e0ae3cc0" dependencies = [ - "ahash", "android-activity", "atomic-waker", "bitflags 2.9.0", "block2", - "bytemuck", "calloop", "cfg_aliases", "concurrent-queue", @@ -3852,34 +2590,24 @@ dependencies = [ "dpi", "js-sys", "libc", - "memmap2", "ndk", "objc2 0.5.2", "objc2-app-kit 0.2.2", "objc2-foundation 0.2.2", "objc2-ui-kit", "orbclient", - "percent-encoding", "pin-project", "raw-window-handle", "redox_syscall 0.4.1", - "rustix 0.38.44", - "sctk-adwaita", - "smithay-client-toolkit", + "rustix", "smol_str", "tracing", "unicode-segmentation", "wasm-bindgen", "wasm-bindgen-futures", - "wayland-backend", - "wayland-client", - "wayland-protocols", - "wayland-protocols-plasma", "web-sys", "web-time", "windows-sys 0.52.0", - "x11-dl", - "x11rb", "xkbcommon-dl", ] @@ -3923,29 +2651,14 @@ version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" -[[package]] -name = "x11-dl" -version = "2.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" -dependencies = [ - "libc", - "once_cell", - "pkg-config", -] - [[package]] name = "x11rb" version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d91ffca73ee7f68ce055750bf9f6eca0780b8c85eff9bc046a3b0da41755e12" dependencies = [ - "as-raw-xcb-connection", "gethostname", - "libc", - "libloading", - "once_cell", - "rustix 0.38.44", + "rustix", "x11rb-protocol", ] @@ -3961,16 +2674,6 @@ version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ef33da6b1660b4ddbfb3aef0ade110c8b8a781a3b6382fa5f2b5b040fd55f61" -[[package]] -name = "xdg-home" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec1cdab258fb55c0da61328dc52c8764709b249011b2cad0454c72f0bf10a1f6" -dependencies = [ - "libc", - "windows-sys 0.59.0", -] - [[package]] name = "xkbcommon-dl" version = "0.4.2" @@ -3992,9 +2695,9 @@ checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56" [[package]] name = "xml-rs" -version = "0.8.25" +version = "0.8.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5b940ebc25896e71dd073bad2dbaa2abfe97b0a391415e22ad1326d9c54e3c4" +checksum = "3ae8337f8a065cfc972643663ea4279e04e7256de865aa66fe25cec5fb912d3f" [[package]] name = "yoke" @@ -4020,121 +2723,13 @@ dependencies = [ "synstructure", ] -[[package]] -name = "zbus" -version = "4.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb97012beadd29e654708a0fdb4c84bc046f537aecfde2c3ee0a9e4b4d48c725" -dependencies = [ - "async-broadcast", - "async-executor", - "async-fs", - "async-io", - "async-lock", - "async-process", - "async-recursion", - "async-task", - "async-trait", - "blocking", - "enumflags2", - "event-listener", - "futures-core", - "futures-sink", - "futures-util", - "hex", - "nix", - "ordered-stream", - "rand", - "serde", - "serde_repr", - "sha1", - "static_assertions", - "tracing", - "uds_windows", - "windows-sys 0.52.0", - "xdg-home", - "zbus_macros", - "zbus_names", - "zvariant", -] - -[[package]] -name = "zbus-lockstep" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ca2c5dceb099bddaade154055c926bb8ae507a18756ba1d8963fd7b51d8ed1d" -dependencies = [ - "zbus_xml", - "zvariant", -] - -[[package]] -name = "zbus-lockstep-macros" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "709ab20fc57cb22af85be7b360239563209258430bccf38d8b979c5a2ae3ecce" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "zbus-lockstep", - "zbus_xml", - "zvariant", -] - -[[package]] -name = "zbus_macros" -version = "4.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "267db9407081e90bbfa46d841d3cbc60f59c0351838c4bc65199ecd79ab1983e" -dependencies = [ - "proc-macro-crate", - "proc-macro2", - "quote", - "syn", - "zvariant_utils", -] - -[[package]] -name = "zbus_names" -version = "3.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b9b1fef7d021261cc16cba64c351d291b715febe0fa10dc3a443ac5a5022e6c" -dependencies = [ - "serde", - "static_assertions", - "zvariant", -] - -[[package]] -name = "zbus_xml" -version = "4.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab3f374552b954f6abb4bd6ce979e6c9b38fb9d0cd7cc68a7d796e70c9f3a233" -dependencies = [ - "quick-xml 0.30.0", - "serde", - "static_assertions", - "zbus_names", - "zvariant", -] - [[package]] name = "zerocopy" version = "0.7.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" dependencies = [ - "zerocopy-derive 0.7.35", -] - -[[package]] -name = "zerocopy" -version = "0.8.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2586fea28e186957ef732a5f8b3be2da217d65c5969d4b1e17f973ebbe876879" -dependencies = [ - "zerocopy-derive 0.8.24", + "zerocopy-derive", ] [[package]] @@ -4148,17 +2743,6 @@ dependencies = [ "syn", ] -[[package]] -name = "zerocopy-derive" -version = "0.8.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a996a8f63c5c4448cd959ac1bab0aaa3306ccfd060472f85943ee0750f0169be" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "zerofrom" version = "0.1.6" @@ -4182,7 +2766,7 @@ dependencies = [ [[package]] name = "zerosplitter" -version = "0.1.4" +version = "0.2.4" dependencies = [ "bytemuck", "common", @@ -4191,7 +2775,7 @@ dependencies = [ "pretty_env_logger", "serde", "serde_json", - "windows 0.60.0", + "windows", "winresource", ] @@ -4216,40 +2800,3 @@ dependencies = [ "quote", "syn", ] - -[[package]] -name = "zvariant" -version = "4.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2084290ab9a1c471c38fc524945837734fbf124487e105daec2bb57fd48c81fe" -dependencies = [ - "endi", - "enumflags2", - "serde", - "static_assertions", - "zvariant_derive", -] - -[[package]] -name = "zvariant_derive" -version = "4.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73e2ba546bda683a90652bac4a279bc146adad1386f25379cf73200d2002c449" -dependencies = [ - "proc-macro-crate", - "proc-macro2", - "quote", - "syn", - "zvariant_utils", -] - -[[package]] -name = "zvariant_utils" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c51bcff7cc3dbb5055396bcf774748c3dab426b4b8659046963523cee4808340" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] diff --git a/splitter/Cargo.toml b/splitter/Cargo.toml index 2871a0e..1fde9d2 100644 --- a/splitter/Cargo.toml +++ b/splitter/Cargo.toml @@ -1,17 +1,21 @@ [package] name = "zerosplitter" -version = "0.1.4" +version = "0.2.4" edition = "2024" [dependencies] bytemuck = "1" common = {path = "../common"} -eframe = "0.31" pretty_env_logger = "0.5" log = "0.4" serde = {version = "1.0", features = ["derive"]} serde_json = "1.0" +[dependencies.eframe] +version = "0.31" +default-features = false +features = ["default_fonts", "glow"] + [dependencies.windows] version = "0.60" features = [ From ae8d5fcf32dedd83f1658481f3600b253fa10325 Mon Sep 17 00:00:00 2001 From: lily_and_doll <103815266+lily-and-doll@users.noreply.github.com> Date: Wed, 19 Nov 2025 03:31:05 -0500 Subject: [PATCH 23/52] Implement some Clippy nitpicks --- Cargo.lock | 63 ++++++++++++++++++++++++++++---------------- splitter/src/app.rs | 38 +++++++++++++------------- splitter/src/main.rs | 17 ++++++------ splitter/src/ui.rs | 10 +++---- 4 files changed, 74 insertions(+), 54 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0f55f6f..720ffe7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -172,7 +172,7 @@ dependencies = [ "bitflags 2.9.0", "log", "polling", - "rustix", + "rustix 0.38.44", "slab", "thiserror", ] @@ -184,7 +184,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95a66a987056935f7efce4ab5668920b5d0dac4a7c99991a67395f13702ddd20" dependencies = [ "calloop", - "rustix", + "rustix 0.38.44", "wayland-backend", "wayland-client", ] @@ -996,6 +996,12 @@ version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" +[[package]] +name = "linux-raw-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" + [[package]] name = "litemap" version = "0.7.5" @@ -1508,7 +1514,7 @@ dependencies = [ "concurrent-queue", "hermit-abi 0.4.0", "pin-project-lite", - "rustix", + "rustix 0.38.44", "tracing", "windows-sys 0.59.0", ] @@ -1633,7 +1639,20 @@ dependencies = [ "bitflags 2.9.0", "errno", "libc", - "linux-raw-sys", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustix" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" +dependencies = [ + "bitflags 2.9.0", + "errno", + "libc", + "linux-raw-sys 0.11.0", "windows-sys 0.59.0", ] @@ -1770,7 +1789,7 @@ dependencies = [ "libc", "log", "memmap2", - "rustix", + "rustix 0.38.44", "thiserror", "wayland-backend", "wayland-client", @@ -2097,13 +2116,13 @@ dependencies = [ [[package]] name = "wayland-backend" -version = "0.3.8" +version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7208998eaa3870dad37ec8836979581506e0c5c64c20c9e79e9d2a10d6f47bf" +checksum = "673a33c33048a5ade91a6b139580fa174e19fb0d23f396dca9fa15f2e1e49b35" dependencies = [ "cc", "downcast-rs", - "rustix", + "rustix 1.1.2", "scoped-tls", "smallvec", "wayland-sys", @@ -2111,12 +2130,12 @@ dependencies = [ [[package]] name = "wayland-client" -version = "0.31.8" +version = "0.31.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2120de3d33638aaef5b9f4472bff75f07c56379cf76ea320bd3a3d65ecaf73f" +checksum = "c66a47e840dc20793f2264eb4b3e4ecb4b75d91c0dd4af04b456128e0bdd449d" dependencies = [ "bitflags 2.9.0", - "rustix", + "rustix 1.1.2", "wayland-backend", "wayland-scanner", ] @@ -2138,16 +2157,16 @@ version = "0.31.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a93029cbb6650748881a00e4922b076092a6a08c11e7fbdb923f064b23968c5d" dependencies = [ - "rustix", + "rustix 0.38.44", "wayland-client", "xcursor", ] [[package]] name = "wayland-protocols" -version = "0.32.6" +version = "0.32.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0781cf46869b37e36928f7b432273c0995aa8aed9552c556fb18754420541efc" +checksum = "efa790ed75fbfd71283bd2521a1cfdc022aabcc28bdcff00851f9e4ae88d9901" dependencies = [ "bitflags 2.9.0", "wayland-backend", @@ -2170,9 +2189,9 @@ dependencies = [ [[package]] name = "wayland-scanner" -version = "0.31.6" +version = "0.31.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "896fdafd5d28145fce7958917d69f2fd44469b1d4e861cb5961bcbeebc6d1484" +checksum = "54cb1e9dc49da91950bdfd8b848c49330536d9d1fb03d4bfec8cae50caa50ae3" dependencies = [ "proc-macro2", "quick-xml", @@ -2181,9 +2200,9 @@ dependencies = [ [[package]] name = "wayland-sys" -version = "0.31.6" +version = "0.31.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbcebb399c77d5aa9fa5db874806ee7b4eba4e73650948e8f93963f128896615" +checksum = "34949b42822155826b41db8e5d0c1be3a2bd296c747577a43a3e6daefc296142" dependencies = [ "dlib", "log", @@ -2573,9 +2592,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winit" -version = "0.30.9" +version = "0.30.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a809eacf18c8eca8b6635091543f02a5a06ddf3dad846398795460e6e0ae3cc0" +checksum = "c66d4b9ed69c4009f6321f762d6e61ad8a2389cd431b97cb1e146812e9e6c732" dependencies = [ "android-activity", "atomic-waker", @@ -2599,7 +2618,7 @@ dependencies = [ "pin-project", "raw-window-handle", "redox_syscall 0.4.1", - "rustix", + "rustix 0.38.44", "smol_str", "tracing", "unicode-segmentation", @@ -2658,7 +2677,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d91ffca73ee7f68ce055750bf9f6eca0780b8c85eff9bc046a3b0da41755e12" dependencies = [ "gethostname", - "rustix", + "rustix 0.38.44", "x11rb-protocol", ] diff --git a/splitter/src/app.rs b/splitter/src/app.rs index 89ee60b..c09ab4f 100644 --- a/splitter/src/app.rs +++ b/splitter/src/app.rs @@ -1,11 +1,11 @@ use eframe::{ App, Frame, - egui::{Align, CentralPanel, Color32, ComboBox, Context, Id, Layout, Sides, Theme}, + egui::{Align, CentralPanel, Color32, ComboBox, Context, Id, Layout, Sides}, }; use crate::{ Category, Gamemode, ZeroSplitter, - theme::{DARK_GREEN, DARK_ORANGE, DARKER_GREEN, DARKER_ORANGE, GREEN, GREENEST, LIGHT_ORANGE}, + theme::{DARK_GREEN, DARK_ORANGE, DARKER_GREEN, DARKER_ORANGE, GREEN, LIGHT_ORANGE}, ui::{category_maker_dialog, confirm_dialog}, vanilla_descriptive_split_names, vanilla_split_names, }; @@ -19,17 +19,17 @@ impl App for ZeroSplitter { // Detect gamemode change persist between frames let prev_mode_id = Id::new("prev_mode"); let cur_mode = self.categories[self.current_category].mode; - if let Some(prev_mode) = ctx.data(|data| data.get_temp::(prev_mode_id)) { - if prev_mode != cur_mode { - let min_size = match self.categories[self.current_category].mode { - Gamemode::GreenOrange => eframe::egui::Vec2 { x: 300.0, y: 300.0 }, - Gamemode::WhiteVanilla => eframe::egui::Vec2 { x: 300.0, y: 650.0 }, - Gamemode::BlackOnion => todo!(), - }; - ctx.send_viewport_cmd(eframe::egui::ViewportCommand::MinInnerSize(min_size)); - ctx.send_viewport_cmd(eframe::egui::ViewportCommand::InnerSize(min_size)); - self.reset(); - } + if let Some(prev_mode) = ctx.data(|data| data.get_temp::(prev_mode_id)) + && prev_mode != cur_mode + { + let min_size = match self.categories[self.current_category].mode { + Gamemode::GreenOrange => eframe::egui::Vec2 { x: 300.0, y: 300.0 }, + Gamemode::WhiteVanilla => eframe::egui::Vec2 { x: 300.0, y: 650.0 }, + Gamemode::BlackOnion => todo!(), + }; + ctx.send_viewport_cmd(eframe::egui::ViewportCommand::MinInnerSize(min_size)); + ctx.send_viewport_cmd(eframe::egui::ViewportCommand::InnerSize(min_size)); + self.reset(); } ctx.data_mut(|data| data.insert_temp(prev_mode_id, cur_mode)); @@ -111,7 +111,7 @@ impl App for ZeroSplitter { ui, |left| { match cur_category.mode { - Gamemode::GreenOrange => left.label(format!("{}-{}", loop_n, stage_n)), + Gamemode::GreenOrange => left.label(format!("{loop_n}-{stage_n}")), Gamemode::WhiteVanilla => { if self.names { left.label(vanilla_descriptive_split_names(i)) @@ -126,10 +126,8 @@ impl App for ZeroSplitter { if gold_split > 0 { left.colored_label(GREEN, gold_split.to_string()); } - } else { - if pb_split > 0 { - left.colored_label(GREEN, pb_split.to_string()); - } + } else if pb_split > 0 { + left.colored_label(GREEN, pb_split.to_string()); } }, |right| { @@ -164,8 +162,10 @@ impl App for ZeroSplitter { if rel_diff > 0 { LIGHT_ORANGE } else { DARKER_ORANGE } } else if diff == 0 { Color32::WHITE + } else if rel_diff > 0 { + DARK_GREEN } else { - if rel_diff > 0 { DARK_GREEN } else { DARKER_GREEN } + DARKER_GREEN }; right.colored_label(diff_color, format!("{diff:+}")); } diff --git a/splitter/src/main.rs b/splitter/src/main.rs index 36c0127..f059a59 100644 --- a/splitter/src/main.rs +++ b/splitter/src/main.rs @@ -103,15 +103,16 @@ struct ZeroSplitter { impl ZeroSplitter { fn new(data_source: Receiver) -> Self { let (tx, rx) = mpsc::channel(); - let mut default_categories = Vec::new(); - default_categories.push(Category::new("Type-C GO".to_string(), Gamemode::GreenOrange)); - default_categories.push(Category::new("Type-B GO".to_string(), Gamemode::GreenOrange)); - default_categories.push(Category::new("Type-C WV".to_string(), Gamemode::WhiteVanilla)); - default_categories.push(Category::new("Type-B WV".to_string(), Gamemode::WhiteVanilla)); + let default_categories = vec![ + Category::new("Type-C GO".to_string(), Gamemode::GreenOrange), + Category::new("Type-B GO".to_string(), Gamemode::GreenOrange), + Category::new("Type-C WV".to_string(), Gamemode::WhiteVanilla), + Category::new("Type-B WV".to_string(), Gamemode::WhiteVanilla), + ]; Self { categories: default_categories, data_source, - last_frame: Default::default(), + last_frame: FrameData::default(), current_category: 0, current_run: Run::new(Gamemode::GreenOrange), current_split: None, @@ -139,7 +140,7 @@ impl ZeroSplitter { Ok(true) => (), Ok(false) => return Self::new(data_source), Err(e) => { - warn!("Could not tell if data file exists: {}", e); + warn!("Could not tell if data file exists: {e}"); return Self::new(data_source); } } @@ -195,7 +196,7 @@ impl ZeroSplitter { }; if let Err(err) = serde_json::to_writer_pretty(file, &self.categories) { - error!("Error writing save: {}", err); + error!("Error writing save: {err}"); } } diff --git a/splitter/src/ui.rs b/splitter/src/ui.rs index 7283c76..91d3e87 100644 --- a/splitter/src/ui.rs +++ b/splitter/src/ui.rs @@ -15,13 +15,13 @@ pub fn entry_dialog(ctx: &Context, tx: Sender, msg: &'static str) { ctx.show_viewport_deferred(ViewportId::from_hash_of("entry dialog"), vp_builder, move |ctx, _| { if ctx.input(|input| input.viewport().close_requested()) { - let _ = tx.send("".to_string()); + let _ = tx.send(String::new()); request_repaint(); return; } let text_id = Id::new("edit text"); - let mut edit_str = ctx.data_mut(|data| data.get_temp_mut_or_insert_with(text_id, || String::new()).clone()); + let mut edit_str = ctx.data_mut(|data| data.get_temp_mut_or_insert_with(text_id, String::new).clone()); CentralPanel::default().show(ctx, |ui| { ui.vertical_centered_justified(|ui| { @@ -58,7 +58,7 @@ pub fn category_maker_dialog(ctx: &Context, tx: Sender>, let text_id = Id::new("edit text"); let mode_id = Id::new("gamemode"); - let mut edit_str = ctx.data_mut(|data| data.get_temp_mut_or_insert_with(text_id, || String::new()).clone()); + let mut edit_str = ctx.data_mut(|data| data.get_temp_mut_or_insert_with(text_id, String::new).clone()); let mut mode = ctx.data_mut(|data| *data.get_temp_mut_or(mode_id, Gamemode::GreenOrange)); CentralPanel::default().show(ctx, |ui| { @@ -68,7 +68,7 @@ pub fn category_maker_dialog(ctx: &Context, tx: Sender>, if ui.button("Confirm").clicked() { let _ = tx.send(Some(EntryDialogData { textbox: edit_str.clone(), - mode: mode, + mode, })); request_repaint(); ctx.send_viewport_cmd(eframe::egui::ViewportCommand::Close); @@ -80,7 +80,7 @@ pub fn category_maker_dialog(ctx: &Context, tx: Sender>, TopBottomPanel::bottom("mode_select").show(ctx, |ui| { ui.vertical_centered_justified(|ui| { ComboBox::from_label("Mode") - .selected_text(format!("{:?}", mode)) + .selected_text(format!("{mode:?}")) .show_ui(ui, |ui| { ui.selectable_value(&mut mode, Gamemode::GreenOrange, "Green Orange"); ui.selectable_value(&mut mode, Gamemode::WhiteVanilla, "White Vanilla"); From 1002fb6e4f30e2fe561fd885231a4ed916b8f288 Mon Sep 17 00:00:00 2001 From: lily_and_doll <103815266+lily-and-doll@users.noreply.github.com> Date: Fri, 21 Nov 2025 22:03:00 -0500 Subject: [PATCH 24/52] Replace backing storage with Sqlite --- .gitignore | 2 + Cargo.lock | 70 ++++++- common/src/lib.rs | 1 + payload/src/lib.rs | 2 + splitter/Cargo.toml | 1 + splitter/sql/best_splits.sql | 7 + splitter/sql/pb_splits.sql | 13 ++ splitter/src/app.rs | 301 +++++++++++++++------------- splitter/src/database.rs | 222 +++++++++++++++++++++ splitter/src/main.rs | 369 +++++++++++++++++------------------ splitter/src/run.rs | 147 ++++++++++++++ 11 files changed, 811 insertions(+), 324 deletions(-) create mode 100644 splitter/sql/best_splits.sql create mode 100644 splitter/sql/pb_splits.sql create mode 100644 splitter/src/database.rs create mode 100644 splitter/src/run.rs diff --git a/.gitignore b/.gitignore index ea8c4bf..5ec7382 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ /target +*.db3 +/.vscode \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 720ffe7..d56cb8e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -544,6 +544,18 @@ version = "3.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a5d9305ccc6942a704f4335694ecd3de2ea531b114ac2d51f5f843750787a92f" +[[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + [[package]] name = "fdeflate" version = "0.3.7" @@ -563,6 +575,12 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + [[package]] name = "foreign-types" version = "0.5.0" @@ -697,12 +715,30 @@ dependencies = [ "gl_generator", ] +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + [[package]] name = "hashbrown" version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" +[[package]] +name = "hashlink" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" +dependencies = [ + "hashbrown 0.15.5", +] + [[package]] name = "hermit-abi" version = "0.4.0" @@ -889,7 +925,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.16.0", ] [[package]] @@ -990,6 +1026,17 @@ dependencies = [ "redox_syscall 0.5.10", ] +[[package]] +name = "libsqlite3-sys" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "133c182a6a2c87864fe97778797e46c7e999672690dc9fa3ee8e241aa4a9c13f" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + [[package]] name = "linux-raw-sys" version = "0.4.15" @@ -1630,6 +1677,20 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +[[package]] +name = "rusqlite" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "165ca6e57b20e1351573e3729b958bc62f0e48025386970b6e4d29e7a7e71f3f" +dependencies = [ + "bitflags 2.9.0", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink", + "libsqlite3-sys", + "smallvec", +] + [[package]] name = "rustix" version = "0.38.44" @@ -2018,6 +2079,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "version_check" version = "0.9.5" @@ -2792,6 +2859,7 @@ dependencies = [ "eframe", "log", "pretty_env_logger", + "rusqlite", "serde", "serde_json", "windows", diff --git a/common/src/lib.rs b/common/src/lib.rs index 42b795b..016a6ea 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -14,6 +14,7 @@ pub struct FrameData { pub realm: u8, pub checkpoint_sub: u8, pub timer_wave: u32, + pub multiplier_one: u32, } impl FrameData { diff --git a/payload/src/lib.rs b/payload/src/lib.rs index 2bb1e86..bec6161 100644 --- a/payload/src/lib.rs +++ b/payload/src/lib.rs @@ -103,6 +103,7 @@ fn get_frame_data() -> FrameData { let realm = read_var(c"realm").unwrap().value as u8; let checkpoint_sub = read_var(c"checkpoint_sub").unwrap().value as u8; let timer_wave = read_var(c"timer_wave").unwrap().value as u32; + let multiplier_one = read_var(c"multiplier_one").unwrap().value as u32; FrameData { score_p1: score_p1 as i32, score_p2: score_p2 as i32, @@ -113,6 +114,7 @@ fn get_frame_data() -> FrameData { realm, checkpoint_sub, timer_wave, + multiplier_one, } } diff --git a/splitter/Cargo.toml b/splitter/Cargo.toml index 1fde9d2..31c2d0d 100644 --- a/splitter/Cargo.toml +++ b/splitter/Cargo.toml @@ -10,6 +10,7 @@ pretty_env_logger = "0.5" log = "0.4" serde = {version = "1.0", features = ["derive"]} serde_json = "1.0" +rusqlite = { version = "0.37.0", features = ["bundled"] } [dependencies.eframe] version = "0.31" diff --git a/splitter/sql/best_splits.sql b/splitter/sql/best_splits.sql new file mode 100644 index 0000000..d0c879b --- /dev/null +++ b/splitter/sql/best_splits.sql @@ -0,0 +1,7 @@ +SELECT max(score) +FROM (SELECT split_num, score, mode +FROM splits +INNER JOIN runs +INNER JOIN categories +ON splits.run_id = runs.id AND runs.category = categories.id +WHERE categories.id = ?1) GROUP BY split_num \ No newline at end of file diff --git a/splitter/sql/pb_splits.sql b/splitter/sql/pb_splits.sql new file mode 100644 index 0000000..bb83682 --- /dev/null +++ b/splitter/sql/pb_splits.sql @@ -0,0 +1,13 @@ +SELECT score, hits, splits.run_id, mode +FROM (SELECT max(score_total), run_id, mode +FROM (SELECT sum(score) as score_total, * +FROM (SELECT score, run_id, mode +FROM splits +INNER JOIN runs +INNER JOIN categories +ON splits.run_id = runs.id AND runs.category = categories.id +WHERE categories.id = ?1) +GROUP BY run_id +ORDER BY score_total DESC)) AS sub +INNER JOIN splits +ON splits.run_id = sub.run_id \ No newline at end of file diff --git a/splitter/src/app.rs b/splitter/src/app.rs index c09ab4f..80a8658 100644 --- a/splitter/src/app.rs +++ b/splitter/src/app.rs @@ -1,10 +1,10 @@ use eframe::{ App, Frame, - egui::{Align, CentralPanel, Color32, ComboBox, Context, Id, Layout, Sides}, + egui::{Align, CentralPanel, Color32, ComboBox, Context, Id, Layout, Sides, Ui}, }; use crate::{ - Category, Gamemode, ZeroSplitter, + Category, CategoryManager, Gamemode, Run, ZeroError, ZeroSplitter, theme::{DARK_GREEN, DARK_ORANGE, DARKER_GREEN, DARKER_ORANGE, GREEN, LIGHT_ORANGE}, ui::{category_maker_dialog, confirm_dialog}, vanilla_descriptive_split_names, vanilla_split_names, @@ -18,11 +18,11 @@ impl App for ZeroSplitter { // Detect gamemode change persist between frames let prev_mode_id = Id::new("prev_mode"); - let cur_mode = self.categories[self.current_category].mode; + let cur_mode = self.categories.current().mode; if let Some(prev_mode) = ctx.data(|data| data.get_temp::(prev_mode_id)) && prev_mode != cur_mode { - let min_size = match self.categories[self.current_category].mode { + let min_size = match self.categories.current().mode { Gamemode::GreenOrange => eframe::egui::Vec2 { x: 300.0, y: 300.0 }, Gamemode::WhiteVanilla => eframe::egui::Vec2 { x: 300.0, y: 650.0 }, Gamemode::BlackOnion => todo!(), @@ -33,8 +33,6 @@ impl App for ZeroSplitter { } ctx.data_mut(|data| data.insert_temp(prev_mode_id, cur_mode)); - let cur_category = &self.categories[self.current_category]; - CentralPanel::default().show(ctx, |ui| { ui.with_layout(Layout::top_down_justified(Align::Min), |ui| { ui.horizontal(|ui| { @@ -47,9 +45,14 @@ impl App for ZeroSplitter { }); ui.horizontal(|ui| { ui.label("Category: "); - ComboBox::from_label("").show_index(ui, &mut self.current_category, self.categories.len(), |i| { - &self.categories[i].name - }); + { + let len = self.categories.len(); + let mut cat_idx = self.categories.current; + ComboBox::from_label("") + .show_index(ui, &mut cat_idx, len, |i| &self.categories.index(i).unwrap().name); + self.categories.set_current(cat_idx, &self.db).unwrap(); + } + if ui.small_button("+").clicked() { self.waiting_for_category = true; } @@ -61,133 +64,25 @@ impl App for ZeroSplitter { } }); - // relative split: score gained during one split - // absolute split: total score during one split - for (i, rel_split, abs_split) in self.current_run.splits.iter().enumerate().map(|(i, &s)| { - (i, s, { - self.current_run - .splits - .iter() - .enumerate() - .take_while(|&(idx, _)| idx <= i) - .fold(0, |acc, (_, &s)| acc + s) - }) - }) { - let split = if self.relative_score { rel_split } else { abs_split }; - // translate split number to stage/loop for GO - let stage_n = (i & 3) + 1; - let loop_n = (i >> 2) + 1; - // Get relative/absolute gold split - // Gold split = high score of this split in any run - let gold_split = if self.relative_score { - cur_category.best_splits[i] - } else { - cur_category - .best_splits - .iter() - .enumerate() - .take_while(|&(idx, _)| idx <= i) - .fold(0, |acc, (_, &s)| acc + s) - }; - // Get relative/absolute split in the PB - // PB split = score of this split in the PB run - let rel_pb_split = self.comparison.personal_best.splits[i]; - let abs_pb_split = self - .comparison - .personal_best - .splits - .iter() - .enumerate() - .take_while(|&(idx, _)| idx <= i) - .fold(0, |acc, (_, &s)| acc + s); - - let pb_split = if self.relative_score { - rel_pb_split - } else { - abs_pb_split - }; + if let Ok(data) = self.calculate_splits() { + self.display_splits(ui, data); + }; - Sides::new().show( - ui, - |left| { - match cur_category.mode { - Gamemode::GreenOrange => left.label(format!("{loop_n}-{stage_n}")), - Gamemode::WhiteVanilla => { - if self.names { - left.label(vanilla_descriptive_split_names(i)) - } else { - left.label(vanilla_split_names(i)) - } - } - Gamemode::BlackOnion => todo!(), - }; - - if self.show_gold_split { - if gold_split > 0 { - left.colored_label(GREEN, gold_split.to_string()); - } - } else if pb_split > 0 { - left.colored_label(GREEN, pb_split.to_string()); - } - }, - |right| { - // Only write splits up to the current split - if i <= self.current_split.unwrap_or(0) { - // Set color of split (rightmost number) - let split_color = if self.current_split == Some(i) && self.split_delay.is_none() { - Color32::WHITE - } else if split >= gold_split { - DARKER_ORANGE - } else { - DARK_ORANGE - }; - - right.colored_label(split_color, split.to_string()); - - if i < self.current_split.unwrap_or(0) { - // past split, we should show a diff - let diff = split - pb_split; - if self.relative_score { - let diff_color = if diff > 0 { - LIGHT_ORANGE - } else if diff == 0 { - Color32::WHITE - } else { - DARK_GREEN - }; - right.colored_label(diff_color, format!("{diff:+}")); - } else { - let rel_diff = rel_split - rel_pb_split; - let diff_color = if diff > 0 { - if rel_diff > 0 { LIGHT_ORANGE } else { DARKER_ORANGE } - } else if diff == 0 { - Color32::WHITE - } else if rel_diff > 0 { - DARK_GREEN - } else { - DARKER_GREEN - }; - right.colored_label(diff_color, format!("{diff:+}")); - } - } - } else { - right.colored_label(DARK_GREEN, "--"); - } - }, - ); - } - - ui.label(format!("Personal Best: {}", cur_category.personal_best.score)); - ui.label(format!("Sum of Best: {}", cur_category.best_splits.iter().sum::())) + ui.label(format!( + "Personal Best: {}", + self.db.get_pb_run(&self.categories).map_or(0, |r| r.1) + )); + ui.label(format!( + "Sum of Best: {}", + self.db.get_gold_splits(&self.categories).map_or(0, |s| s.iter().sum()) + )); }); }); if self.waiting_for_category { if let Ok(new_category) = self.dialog_rx.try_recv() { if let Some(data) = new_category { - self.categories.push(Category::new(data.textbox, data.mode)); - self.current_category = self.categories.len() - 1; - self.save_splits(); + self.categories.push(data.textbox, data.mode, &self.db).unwrap(); } self.waiting_for_category = false; } else { @@ -196,11 +91,9 @@ impl App for ZeroSplitter { } if self.waiting_for_rename { - if let Ok(new_category) = self.dialog_rx.try_recv() { - if let Some(data) = new_category { - self.categories[self.current_category].name = data.textbox; - self.categories[self.current_category].mode = data.mode; - self.save_splits(); + if let Ok(rename_category) = self.dialog_rx.try_recv() { + if let Some(data) = rename_category { + self.categories.rename_current(&self.db, data.textbox).unwrap(); } self.waiting_for_rename = false; } else { @@ -211,8 +104,7 @@ impl App for ZeroSplitter { if self.waiting_for_confirm { if let Ok(Some(confirmation)) = self.dialog_rx.try_recv() { if confirmation.textbox == "Deleted" { - self.categories.remove(self.current_category); - self.current_category = self.current_category.saturating_sub(1); + self.categories.delete_current(&self.db).unwrap(); } self.waiting_for_confirm = false; } else { @@ -221,7 +113,7 @@ impl App for ZeroSplitter { self.dialog_tx.clone(), format!( "Are you sure you want to delete category {}?", - self.categories[self.current_category].name + self.categories.current().name ), ); } @@ -229,6 +121,139 @@ impl App for ZeroSplitter { } fn on_exit(&mut self, _gl: Option<&eframe::glow::Context>) { - self.save_splits(); + if let Run::Active { .. } = self.run { + self.save_splits(); + } + } +} + +impl ZeroSplitter { + fn calculate_splits(&self) -> Result, ZeroError> { + // relative split: score gained during one split + // absolute split: total score during one split + let raw_splits = self.run.splits()?; + let mut ret = Vec::new(); + for (i, rel_split, abs_split) in raw_splits.iter().enumerate().map(|(i, &s)| { + (i, s, { + raw_splits + .clone() + .iter() + .enumerate() + .take_while(|&(idx, _)| idx <= i) + .fold(0, |acc, (_, &s)| acc + s) + }) + }) { + let split = if self.relative_score { rel_split } else { abs_split }; + // Get relative/absolute gold split + // Gold split = high score of this split in any run + let best_splits = self.db.get_gold_splits(&self.categories); + let gold_split = match (best_splits, self.relative_score) { + (Ok(splits), true) => splits[i], + (Ok(splits), false) => splits + .iter() + .enumerate() + .take_while(|&(idx, _)| idx <= i) + .fold(0, |acc, (_, &s)| acc + s), + _ => 0, + }; + // Get relative/absolute split in the PB + // PB split = score of this split in the PB run + let compare = self.categories.get_comparison(); + let rel_pb_split = compare[i]; + let abs_pb_split = compare + .iter() + .enumerate() + .take_while(|&(idx, _)| idx <= i) + .fold(0, |acc, (_, &s)| acc + s); + + let pb_split = if self.relative_score { + rel_pb_split + } else { + abs_pb_split + }; + + ret.push((gold_split, split, pb_split)); + } + Ok(ret) + } + + fn display_splits(&self, ui: &mut Ui, split_data: Vec<(i32, i32, i32)>) { + let current_split = self.run.current_split().unwrap_or(0); + + for (n, &(gold_score, current_score, compare_score)) in split_data.iter().enumerate() { + // translate split number to stage/loop for GO + let stage_n = (n & 3) + 1; + let loop_n = (n >> 2) + 1; + Sides::new().show( + ui, + |left| { + match self.categories.current().mode { + Gamemode::GreenOrange => left.label(format!("{loop_n}-{stage_n}")), + Gamemode::WhiteVanilla => { + if self.names { + left.label(vanilla_descriptive_split_names(n)) + } else { + left.label(vanilla_split_names(n)) + } + } + Gamemode::BlackOnion => todo!(), + }; + + if self.show_gold_split { + if gold_score > 0 { + left.colored_label(GREEN, gold_score.to_string()); + } + } else if compare_score > 0 { + left.colored_label(GREEN, compare_score.to_string()); + } + }, + |right| { + // Only write splits up to the current split + if n <= self.run.current_split().unwrap() { + // Set color of split (rightmost number) + let split_color = if current_split == n && self.split_delay.is_none() { + Color32::WHITE + } else if current_score >= gold_score { + DARKER_ORANGE + } else { + DARK_ORANGE + }; + + right.colored_label(split_color, current_score.to_string()); + + if n < current_split { + // past split, we should show a diff + let diff = current_score - compare_score; + if self.relative_score { + let diff_color = if diff > 0 { + LIGHT_ORANGE + } else if diff == 0 { + Color32::WHITE + } else { + DARK_GREEN + }; + right.colored_label(diff_color, format!("{diff:+}")); + } else { + let &(_, prev_score, prev_compare) = + n.checked_sub(1).map_or(&(0, 0, 0), |n| split_data.get(n).unwrap()); + let rel_diff = (current_score - prev_score) - (compare_score - prev_compare); + let diff_color = if diff > 0 { + if rel_diff > 0 { LIGHT_ORANGE } else { DARKER_ORANGE } + } else if diff == 0 { + Color32::WHITE + } else if rel_diff > 0 { + DARK_GREEN + } else { + DARKER_GREEN + }; + right.colored_label(diff_color, format!("{diff:+}")); + } + } + } else { + right.colored_label(DARK_GREEN, "--"); + } + }, + ); + } } } diff --git a/splitter/src/database.rs b/splitter/src/database.rs new file mode 100644 index 0000000..0f67ea4 --- /dev/null +++ b/splitter/src/database.rs @@ -0,0 +1,222 @@ +use std::{fmt::Error, sync::Arc}; + +use log::error; +use rusqlite::{ + Connection, Result, ToSql, params, + types::{FromSql, ValueRef}, +}; + +use crate::{Category, CategoryManager, Gamemode, Run}; + +#[derive(Clone)] +pub struct Database { + conn: Arc, +} + +impl Database { + pub fn init() -> Result { + let database = Database { + conn: Arc::new(Connection::open("./sqlite.db3")?), + }; + + let schema_version = database + .conn + .query_one("PRAGMA user_version", (), |row| row.get::<_, i32>(0)) + .unwrap(); + // #[cfg(debug_assertions)] + // { + // database.conn.execute("DROP TABLE IF EXISTS splits", ()).unwrap(); + // database.conn.execute("DROP TABLE IF EXISTS runs", ()).unwrap(); + // database.conn.execute("DROP TABLE IF EXISTS categories", ()).unwrap(); + // } + if !database.conn.table_exists(Some("main"), "categories")? { + if let Err(err) = database.create_tables() { + error!("Error creating tables: {}", err) + }; + database.insert_new_category("default".to_owned(), Gamemode::GreenOrange)?; + } + + Ok(database) + } + #[must_use] + pub fn create_tables(&self) -> Result<()> { + self.conn.execute( + " + CREATE TABLE IF NOT EXISTS categories ( + id INTEGER PRIMARY KEY, + name TEXT UNIQUE NOT NULL, + mode INTEGER NOT NULL + ) + ", + (), + )?; + + self.conn.execute( + " + CREATE TABLE IF NOT EXISTS runs ( + id INTEGER PRIMARY KEY, + category INTEGER NOT NULL REFERENCES categories(id) ON DELETE CASCADE + ) + ", + (), + )?; + + self.conn.execute( + " + CREATE TABLE IF NOT EXISTS splits ( + id INTEGER PRIMARY KEY, + split_num INTEGER NOT NULL, + score INTEGER NOT NULL, + hits INTEGER, + mult REAL, + run_id INTEGER NOT NULL REFERENCES runs(id) ON DELETE CASCADE + ) + ", + (), + )?; + + Ok(()) + } + + #[must_use] + pub fn insert_current_category(&self, category: &CategoryManager) -> Result { + let category = category.current(); + self.conn.execute( + "INSERT OR IGNORE INTO categories VALUES(NULL, ?1, ?2)", + params![category.name, category.mode], + ) + } + #[must_use] + pub fn insert_new_category(&self, name: String, mode: Gamemode) -> Result { + self.conn + .execute("INSERT INTO categories VALUES(NULL, ?1, ?2)", params![name, mode])?; + + Ok(self.conn.last_insert_rowid()) + } + #[must_use] + pub fn delete_category(&self, category: Category) -> Result { + self.conn + .execute("DELETE FROM categories WHERE id = ?1", params![category.id]) + } + #[must_use] + pub fn rename_category(&self, category: &Category, new_name: String) -> Result { + self.conn.execute( + "UPDATE categories SET name='?1' WHERE id=?2", + params![new_name, category.id], + ) + } + #[must_use] + pub fn get_categories(&self) -> Result> { + let mut statement = self.conn.prepare("SELECT name, mode, id FROM categories")?; + let rows = statement.query_map((), |row| { + Ok(( + row.get::<_, String>(0)?, + row.get::<_, Gamemode>(1)?, + row.get::<_, i64>(2)?, + )) + })?; + let categories = rows + .map(|r| r.unwrap()) + .map(|(name, mode, id)| Category { id, mode, name }) + .collect::>(); + Ok(categories) + } + #[must_use] + pub fn insert_run(&self, category: &CategoryManager, run: &Run) -> Result<()> { + let category = category.current(); + self.conn.execute("BEGIN TRANSACTION", ())?; + + match (|| { + let mut stmt = self.conn.prepare("SELECT id FROM categories WHERE name = ?1")?; + let res = stmt.query_one(params![category.name], |row| row.get::(0))?; + + self.conn.execute("INSERT INTO runs VALUES(NULL, ?1)", params![res])?; + + let run_id = self.conn.last_insert_rowid(); + + for (num, &split) in run.splits().unwrap().iter().enumerate() { + let mult = run.mults().unwrap()[num]; + self.conn.execute( + "INSERT INTO splits (id, split_num, score, hits, mult, run_id) VALUES(NULL, ?1, ?2, ?3, ?4, ?5)", + params![num, split, 0, mult, run_id], + )?; + } + + Ok::<(), rusqlite::Error>(()) + })() { + Ok(_) => { + self.conn.execute("COMMIT", ())?; + Ok(()) + } + Err(err) => { + self.conn.execute("ROLLBACK", ())?; + Err(err) + } + } + } + #[must_use] + pub fn get_pb_run(&self, category: &CategoryManager) -> Result<(Vec, i32, Gamemode)> { + let category = category.current(); + let mut statement = self.conn.prepare(include_str!("../sql/pb_splits.sql"))?; + let rows = statement.query_map(params![category.id], |row| { + Ok(( + row.get::(0)?, + row.get::(1)?, + row.get::(2)?, + row.get::(3)?, + )) + })?; + let splits: Vec<(i32, i32, i32, Gamemode)> = rows.map(|r| r.unwrap()).collect(); + + if splits.len() > 0 { + let scores: Vec = splits.iter().map(|s| s.0).collect(); + let hits: Vec = splits.iter().map(|s| s.1).collect(); + let run_id = splits[0].2; + let mode = splits[0].3; + + let total = scores.iter().sum(); + + Ok((scores, total, mode)) + } else { + Err(rusqlite::Error::QueryReturnedNoRows) + } + } + + /// Get the highest core of each split for the category + #[must_use] + pub fn get_gold_splits(&self, category: &CategoryManager) -> Result> { + let mut statement = self.conn.prepare(include_str!("../sql/best_splits.sql"))?; + statement + .query_map(params![category.current().id], |rows| rows.get(0))? + .collect::>>() + .map(|v| { + if v.len() > 0 { + Ok(v) + } else { + Err(rusqlite::Error::QueryReturnedNoRows) + } + })? + } +} + +impl ToSql for Gamemode { + fn to_sql(&self) -> Result> { + match self { + Gamemode::GreenOrange => Ok(0u8.into()), + Gamemode::WhiteVanilla => Ok(1u8.into()), + Gamemode::BlackOnion => Ok(2u8.into()), + } + } +} + +impl FromSql for Gamemode { + fn column_result(value: rusqlite::types::ValueRef<'_>) -> rusqlite::types::FromSqlResult { + match value { + ValueRef::Integer(0) => Ok(Self::GreenOrange), + ValueRef::Integer(1) => Ok(Self::WhiteVanilla), + ValueRef::Integer(2) => Ok(Self::BlackOnion), + ValueRef::Integer(i) => Err(rusqlite::types::FromSqlError::OutOfRange(i)), + _ => Err(rusqlite::types::FromSqlError::InvalidType), + } + } +} diff --git a/splitter/src/main.rs b/splitter/src/main.rs index f059a59..200ac2e 100644 --- a/splitter/src/main.rs +++ b/splitter/src/main.rs @@ -3,7 +3,7 @@ use std::{ fs::{self, File}, net::UdpSocket, sync::{ - OnceLock, + LazyLock, OnceLock, mpsc::{self, Receiver, Sender}, }, thread, @@ -15,13 +15,15 @@ use eframe::{ NativeOptions, egui::{Context, IconData, ThemePreference, ViewportBuilder}, }; -use log::{error, warn}; +use log::{debug, error, warn}; use serde::{Deserialize, Serialize}; -use crate::theme::zeroranger_visuals; +use crate::{database::Database, run::Run, theme::zeroranger_visuals}; mod app; +mod database; mod hook; +mod run; mod system; mod theme; mod ui; @@ -31,6 +33,11 @@ const SPLIT_DELAY_FRAMES: u32 = 20; static EGUI_CTX: OnceLock = OnceLock::new(); fn main() { + #[cfg(debug_assertions)] + unsafe { + env::set_var("RUST_BACKTRACE", "1"); + } + pretty_env_logger::init(); let options = NativeOptions { @@ -80,123 +87,60 @@ fn ipc_thread(channel: Sender) { } struct ZeroSplitter { - categories: Vec, - current_category: usize, + categories: CategoryManager, data_source: Receiver, last_frame: FrameData, - current_run: Run, - current_split: Option, - current_split_score_offset: i32, + run: Run, waiting_for_category: bool, waiting_for_rename: bool, waiting_for_confirm: bool, dialog_rx: Receiver>, dialog_tx: Sender>, - comparison: Category, - active: bool, relative_score: bool, show_gold_split: bool, split_delay: Option, names: bool, + db: Database, } impl ZeroSplitter { - fn new(data_source: Receiver) -> Self { + fn new(data_source: Receiver, db: Database) -> Self { let (tx, rx) = mpsc::channel(); - let default_categories = vec![ - Category::new("Type-C GO".to_string(), Gamemode::GreenOrange), - Category::new("Type-B GO".to_string(), Gamemode::GreenOrange), - Category::new("Type-C WV".to_string(), Gamemode::WhiteVanilla), - Category::new("Type-B WV".to_string(), Gamemode::WhiteVanilla), - ]; - Self { - categories: default_categories, + let mut zerosplitter = Self { + categories: CategoryManager::init(), data_source, last_frame: FrameData::default(), - current_category: 0, - current_run: Run::new(Gamemode::GreenOrange), - current_split: None, - current_split_score_offset: 0, + run: Run::Inactive, dialog_rx: rx, dialog_tx: tx, waiting_for_category: false, waiting_for_rename: false, waiting_for_confirm: false, - comparison: Category::new("".to_string(), Gamemode::GreenOrange), - active: false, relative_score: true, show_gold_split: true, split_delay: None, names: false, - } + db, + }; + + zerosplitter.categories.load(&zerosplitter.db).unwrap(); + + zerosplitter } fn load(data_source: Receiver) -> Self { - let data_path = env::current_exe() - .expect("Could not get program directory") - .with_file_name("zs_data.json"); - - match fs::exists(&data_path) { - Ok(true) => (), - Ok(false) => return Self::new(data_source), - Err(e) => { - warn!("Could not tell if data file exists: {e}"); - return Self::new(data_source); - } - } + let db = Database::init().unwrap(); - match File::open(&data_path) { - Ok(file) => { - let try_new_cat = serde_json::from_reader::<_, Vec>(&file); - if let Ok(data) = try_new_cat { - if data.is_empty() { - Self::new(data_source) - } else { - Self { - current_category: 0, - categories: data, - ..Self::new(data_source) - } - } - } else { - // An attempt at old-save migration. Probably doesn't work. - let try_old_cat = serde_json::from_reader::<_, Vec>(&file); - if let Ok(data) = try_old_cat { - Self { - current_category: 0, - categories: data.iter().map(|c| c.to_new(Gamemode::GreenOrange)).collect(), - ..Self::new(data_source) - } - } else { - panic!( - "Data failed to parse as Category or OldCategory at {:?}: {} and {}", - &data_path, - try_new_cat.unwrap_err(), - try_old_cat.unwrap_err() - ) - } - } - } - Err(e) => panic!("Could not open extant data file at {:?}: {}", &data_path, e), - } + Self::new(data_source, db.clone()) } fn save_splits(&mut self) { - self.categories[self.current_category].update_from_run(&self.current_run); - - let data_path = env::current_exe() - .expect("Could not get program directory") - .with_file_name("zs_data.json"); - let file = match File::create(&data_path) { - Ok(file) => file, - Err(err) => { - error!("Could not save: Could not open data file {:?}: {}", &data_path, err); - return; - } - }; + if self.run.is_active() { + debug!("Saving splits"); - if let Err(err) = serde_json::to_writer_pretty(file, &self.categories) { - error!("Error writing save: {err}"); + if let Err(err) = self.db.insert_run(&self.categories, &self.run) { + error!("Error writing run to database: {err}"); + } } } @@ -213,17 +157,19 @@ impl ZeroSplitter { } fn update_greenorange(&mut self, frame: FrameData) { - // Skip update if current category isn't Green Orange - if self.categories[self.current_category].mode != Gamemode::GreenOrange { + // Skip update if current category isn't Green Orange or if on menu + if self.categories.current().mode != Gamemode::GreenOrange || frame.is_menu() { return; } // Reset if we just left the menu or returned to 1-1 if frame.stage != self.last_frame.stage && (self.last_frame.is_menu() || frame.is_first_stage()) { self.reset(); + self.run.start(frame); + self.categories.refresh_comparison(&self.db).unwrap(); } - if !frame.is_menu() { + if !frame.is_menu() && self.run.is_active() { let frame_split = (frame.stage - 1 - frame.game_loop) as usize; if frame_split >= 8 { @@ -232,21 +178,12 @@ impl ZeroSplitter { } // Split if necessary - if frame.stage != self.last_frame.stage { - self.current_split = Some(frame_split); - self.current_split_score_offset = self.last_frame.total_score(); - self.save_splits(); - } - - // If our score got reset by a continue, fix the score offset. - if self.current_split_score_offset > frame.total_score() { - self.current_split_score_offset = 0; + if (frame.stage != self.last_frame.stage) && !self.last_frame.is_menu() { + self.run.split().unwrap(); } // Update run and split scores - self.current_run.score = frame.total_score(); - let split_score = frame.total_score() - self.current_split_score_offset; - self.current_run.splits[frame_split] = split_score; + self.run.update(frame).unwrap(); } else { // End the run if we're back on the menu self.end_run(); @@ -255,24 +192,28 @@ impl ZeroSplitter { fn update_whitevanilla(&mut self, frame: FrameData) { // Skip update if current category isn't White Vanilla or if on menu - if self.categories[self.current_category].mode != Gamemode::WhiteVanilla || frame.is_menu() { + if self.categories.current().mode != Gamemode::WhiteVanilla || frame.is_menu() { return; } // Reset if we returned to 1-1 if frame.total_score() == 0 && self.last_frame.total_score() > 0 || self.last_frame.is_menu() { self.reset(); - self.current_split = match frame.stage { - 1 => Some(0), - 2 => Some(5), - 3 => Some(12), - 4 => Some(19), - _ => panic!("Stage out of bounds! {}", frame.stage), - }; + self.run.start(frame); + self.run + .set_split(match frame.stage { + 1 => 0, + 2 => 5, + 3 => 12, + 4 => 19, + _ => panic!("Stage out of bounds! {}", frame.stage), + }) + .unwrap(); + self.categories.refresh_comparison(&self.db).unwrap(); return; } - if !frame.is_menu() && self.active { + if !frame.is_menu() && !(self.run == Run::Inactive) { // Split if necessary; score requirement prevents spurious splits after a reset if frame.timer_wave == 0 && self.last_frame.timer_wave != 0 && frame.total_score() > 0 { self.split_delay = Some(SPLIT_DELAY_FRAMES); @@ -282,19 +223,13 @@ impl ZeroSplitter { if split_delay > 1 { self.split_delay = Some(split_delay - 1) } else { - self.current_split = self.current_split.or(Some(0)).map(|s| s + 1); - self.current_split_score_offset = self.last_frame.total_score(); - self.save_splits(); + self.run.split().unwrap(); self.split_delay = None } } - // TODO: reimplement continue support - // Update run and split scores - self.current_run.score = frame.total_score(); - let split_score = frame.total_score() - self.current_split_score_offset; - self.current_run.splits[self.current_split.unwrap_or(0)] = split_score; + self.run.update(frame).unwrap(); } else if frame.is_menu() { // End the run if we're back on the menu self.end_run(); @@ -302,18 +237,13 @@ impl ZeroSplitter { } fn reset(&mut self) { - self.end_run(); - self.current_run = Run::new(self.categories[self.current_category].mode); - self.comparison = self.categories[self.current_category].clone(); - self.current_split = Some(0); - self.active = true; - self.current_split_score_offset = 0; + self.save_splits(); + self.run.reset(); } fn end_run(&mut self) { self.save_splits(); - self.current_split = None; - self.active = false; + self.run.stop() } } @@ -333,6 +263,17 @@ impl Gamemode { } } } + +impl From for Gamemode { + fn from(value: i8) -> Self { + match value { + -1 => Self::WhiteVanilla, + 0 => Self::GreenOrange, + 1 => panic!("illegal black onion detected"), + _ => panic!(), + } + } +} pub struct EntryDialogData { pub textbox: String, pub mode: Gamemode, @@ -495,82 +436,140 @@ struct OldRun { score: i32, } -#[derive(Debug, Serialize, Deserialize, Clone)] -struct Run { - splits: Vec, - score: i32, - mode: Gamemode, +// #[derive(Debug, Serialize, Deserialize, Clone)] +// struct Run { +// splits: Vec, +// score: i32, +// mode: Gamemode, +// active: bool +// } + +// impl Run { +// fn new(mode: Gamemode) -> Self { +// Run { +// splits: vec![0; mode.splits()], +// score: 0, +// mode, +// active: false, +// } +// } + +// fn start(&mut self, category: &CategoryManager) { +// if category.current().mode == self.mode { +// self.splits = vec![0; self.mode.splits()]; +// self.score = 0; +// self.active = true +// } +// } + +// fn stop(&mut self) { +// self.active = false +// } +// } + +struct CategoryManager { + categories: Vec, + current: usize, + comparison_cache: Vec, } -impl Run { - fn new(mode: Gamemode) -> Self { - Run { - splits: vec![0; mode.splits()], - score: 0, - mode, +impl CategoryManager { + fn init() -> Self { + CategoryManager { + categories: Vec::new(), + current: 0, + comparison_cache: Vec::new(), } } -} -#[derive(Debug, Serialize, Deserialize, Clone)] -struct OldCategory { - personal_best: OldRun, - best_splits: [i32; 8], - name: String, -} + fn current(&self) -> &Category { + &self.categories.get(self.current).unwrap() + } -impl OldRun { - fn to_new(self) -> Run { - Run { - splits: self.splits.to_vec(), - score: self.score, - mode: Gamemode::GreenOrange, - } + fn current_mut(&mut self) -> &mut Category { + &mut self.categories[self.current] } -} -impl OldCategory { - fn to_new(&self, mode: Gamemode) -> Category { - Category { - personal_best: self.personal_best.to_new(), - best_splits: self.best_splits.to_vec(), - name: self.name.clone(), - mode, - } + #[must_use] + pub fn push(&mut self, name: String, mode: Gamemode, db: &Database) -> Result<(), ZeroError> { + let id = db + .insert_new_category(name.clone(), mode) + .map_err(ZeroError::DatabaseError)?; + self.categories.push(Category { name, mode, id }); + Ok(()) } -} -#[derive(Debug, Serialize, Deserialize, Clone)] -struct Category { - personal_best: Run, - best_splits: Vec, - name: String, - mode: Gamemode, -} + pub fn index(&self, index: usize) -> Option<&Category> { + self.categories.get(index) + } -impl Category { - fn new(name: String, mode: Gamemode) -> Self { - Category { - personal_best: Run::new(mode), - best_splits: vec![0; mode.splits()], - name, - mode, - } + pub fn len(&self) -> usize { + self.categories.len() } - fn update_from_run(&mut self, run: &Run) { - if run.mode != self.mode { - return; + /// Populate the CategoryManager with data from the database + pub fn load(&mut self, db: &Database) -> Result<(), ZeroError> { + self.categories = db.get_categories().map_err(ZeroError::DatabaseError)?; + Ok(()) + } + + pub fn delete_current(&mut self, db: &Database) -> Result { + if self.categories.len() > 1 { + db.delete_category(self.categories.remove(self.current)) + .map_err(ZeroError::DatabaseError) + } else { + Err(ZeroError::Illegal) } + } - if run.score > self.personal_best.score { - self.personal_best = run.clone(); + pub fn rename_current(&mut self, db: &Database, new_name: String) -> Result { + self.current_mut().name = new_name.clone(); + db.rename_category(self.current(), new_name) + .map_err(ZeroError::DatabaseError) + } + + pub fn set_current(&mut self, new_idx: usize, db: &Database) -> Result<(), ZeroError> { + if new_idx >= self.categories.len() { + return Err(ZeroError::CategoryOutOfRange); } + self.current = new_idx; + self.refresh_comparison(db)?; - for (best, new) in self.best_splits.iter_mut().zip(run.splits.iter()) { - if *new > *best { - *best = *new; - } + Ok(()) + } + + pub fn get_comparison(&self) -> &Vec { + if self.comparison_cache.len() == 0 { + panic!() } + &self.comparison_cache + } + + pub fn refresh_comparison(&mut self, db: &Database) -> Result<(), ZeroError> { + self.comparison_cache = match db.get_pb_run(self) { + Ok((scores, total, mode)) if mode == self.current().mode => scores, + Ok(_) => return Err(ZeroError::DifficultyMismatch), + Err(rusqlite::Error::QueryReturnedNoRows) => vec![0; self.current().mode.splits()], + Err(e) => return Err(ZeroError::DatabaseError(e)), + }; + Ok(()) } } + +#[derive(Debug, Serialize, Deserialize, Clone)] +struct Category { + name: String, + mode: Gamemode, + id: i64, +} + +#[derive(Debug)] +#[non_exhaustive] +enum ZeroError { + Illegal, + DatabaseError(rusqlite::Error), + RunInactive, + DifficultyMismatch, + SplitOutOfRange, + CategoryOutOfRange, +} diff --git a/splitter/src/run.rs b/splitter/src/run.rs new file mode 100644 index 0000000..692aabc --- /dev/null +++ b/splitter/src/run.rs @@ -0,0 +1,147 @@ +use common::FrameData; + +use crate::{Gamemode, ZeroError}; + +#[derive(Debug, PartialEq)] +pub enum Run { + Inactive, + Active { + difficulty: Gamemode, + splits: Vec, + mults: Vec, + score: i32, + current_split: usize, + split_base_score: i32, + }, +} + +impl Run { + pub fn score(&self) -> Option { + match *self { + Run::Inactive => None, + Run::Active { score, .. } => Some(score), + } + } + + pub fn splits(&self) -> Result, ZeroError> { + match self { + Run::Inactive => Err(ZeroError::RunInactive), + Run::Active { splits, .. } => Ok(splits.to_vec()), + } + } + + pub fn mults(&self) -> Result, ZeroError> { + match self { + Run::Inactive => Err(ZeroError::RunInactive), + Run::Active { mults, .. } => Ok(mults.to_vec()), + } + } + + pub fn start(&mut self, frame: FrameData) { + let mode = frame.difficulty.into(); + *self = Self::Active { + difficulty: mode, + splits: vec![0; mode.splits()], + mults: vec![0; mode.splits()], + score: 0, + current_split: 0, + split_base_score: 0, + }; + } + + pub fn stop(&mut self) { + *self = Self::Inactive + } + + pub fn reset(&mut self) { + *self = match *self { + Run::Inactive => Run::Inactive, + Run::Active { difficulty, .. } => Run::Active { + difficulty: difficulty, + splits: vec![0; difficulty.splits()], + mults: vec![0; difficulty.splits()], + score: 0, + current_split: 0, + split_base_score: 0, + }, + } + } + + pub fn update(&mut self, frame: FrameData) -> Result<(), ZeroError> { + if let Self::Active { + difficulty, + splits, + score, + current_split, + split_base_score, + mults, + } = self + { + if *difficulty == Gamemode::from(frame.difficulty) { + *score = frame.total_score(); + if *split_base_score > *score { + *split_base_score = 0 + } + splits[*current_split] = frame.total_score() - *split_base_score; + mults[*current_split] = frame.multiplier_one; + Ok(()) + } else { + Err(ZeroError::DifficultyMismatch) + } + } else { + Err(ZeroError::RunInactive) + } + } + + pub fn split(&mut self) -> Result<(), ZeroError> { + if let Self::Active { + current_split, + difficulty, + score, + split_base_score, + .. + } = self + { + if *current_split < difficulty.splits() { + *current_split += 1; + *split_base_score = *score; + Ok(()) + } else { + Err(ZeroError::SplitOutOfRange) + } + } else { + Err(ZeroError::RunInactive) + } + } + + pub fn set_split(&mut self, new_split: usize) -> Result<(), ZeroError> { + if let Self::Active { + current_split, + difficulty, + .. + } = self + { + if new_split < difficulty.splits() { + *current_split = new_split; + Ok(()) + } else { + Err(ZeroError::SplitOutOfRange) + } + } else { + Err(ZeroError::RunInactive) + } + } + pub fn current_split(&self) -> Result { + match self { + Run::Inactive => Err(ZeroError::RunInactive), + Run::Active { current_split, .. } => Ok(*current_split), + } + } + + pub fn is_active(&self) -> bool { + match self { + Run::Inactive => false, + Run::Active { .. } => true, + } + } +} From 90e5bc6c193dec515cf1e9b4a4c1ef080786ecc1 Mon Sep 17 00:00:00 2001 From: lily_and_doll <103815266+lily-and-doll@users.noreply.github.com> Date: Sat, 22 Nov 2025 01:00:25 -0500 Subject: [PATCH 25/52] Add "final split" flag to the split schema --- splitter/src/database.rs | 70 +++++++++++++++++++++++++++++++++------- splitter/src/main.rs | 2 +- 2 files changed, 59 insertions(+), 13 deletions(-) diff --git a/splitter/src/database.rs b/splitter/src/database.rs index 0f67ea4..f4328d6 100644 --- a/splitter/src/database.rs +++ b/splitter/src/database.rs @@ -13,6 +13,8 @@ pub struct Database { conn: Arc, } +const CURRENT_SCHEMA_VERSION: i32 = 1; + impl Database { pub fn init() -> Result { let database = Database { @@ -23,6 +25,14 @@ impl Database { .conn .query_one("PRAGMA user_version", (), |row| row.get::<_, i32>(0)) .unwrap(); + + if schema_version < CURRENT_SCHEMA_VERSION { + database.migrate(schema_version) + } else if schema_version > CURRENT_SCHEMA_VERSION { + panic!( + "Schema version beyond program version!\n Schema version {schema_version}, program version {CURRENT_SCHEMA_VERSION}" + ) + } // #[cfg(debug_assertions)] // { // database.conn.execute("DROP TABLE IF EXISTS splits", ()).unwrap(); @@ -38,8 +48,9 @@ impl Database { Ok(database) } - #[must_use] pub fn create_tables(&self) -> Result<()> { + self.conn + .pragma_update(Some("main"), "user_version", CURRENT_SCHEMA_VERSION)?; self.conn.execute( " CREATE TABLE IF NOT EXISTS categories ( @@ -68,8 +79,9 @@ impl Database { split_num INTEGER NOT NULL, score INTEGER NOT NULL, hits INTEGER, - mult REAL, + mult INTEGER, run_id INTEGER NOT NULL REFERENCES runs(id) ON DELETE CASCADE + final BOOLEAN ) ", (), @@ -78,7 +90,6 @@ impl Database { Ok(()) } - #[must_use] pub fn insert_current_category(&self, category: &CategoryManager) -> Result { let category = category.current(); self.conn.execute( @@ -86,26 +97,25 @@ impl Database { params![category.name, category.mode], ) } - #[must_use] + pub fn insert_new_category(&self, name: String, mode: Gamemode) -> Result { self.conn .execute("INSERT INTO categories VALUES(NULL, ?1, ?2)", params![name, mode])?; Ok(self.conn.last_insert_rowid()) } - #[must_use] + pub fn delete_category(&self, category: Category) -> Result { self.conn .execute("DELETE FROM categories WHERE id = ?1", params![category.id]) } - #[must_use] + pub fn rename_category(&self, category: &Category, new_name: String) -> Result { self.conn.execute( "UPDATE categories SET name='?1' WHERE id=?2", params![new_name, category.id], ) } - #[must_use] pub fn get_categories(&self) -> Result> { let mut statement = self.conn.prepare("SELECT name, mode, id FROM categories")?; let rows = statement.query_map((), |row| { @@ -121,7 +131,7 @@ impl Database { .collect::>(); Ok(categories) } - #[must_use] + pub fn insert_run(&self, category: &CategoryManager, run: &Run) -> Result<()> { let category = category.current(); self.conn.execute("BEGIN TRANSACTION", ())?; @@ -135,10 +145,11 @@ impl Database { let run_id = self.conn.last_insert_rowid(); for (num, &split) in run.splits().unwrap().iter().enumerate() { + let final_split = num == run.current_split().unwrap(); let mult = run.mults().unwrap()[num]; self.conn.execute( - "INSERT INTO splits (id, split_num, score, hits, mult, run_id) VALUES(NULL, ?1, ?2, ?3, ?4, ?5)", - params![num, split, 0, mult, run_id], + "INSERT INTO splits (id, split_num, score, hits, mult, run_id, final) VALUES(NULL, ?1, ?2, ?3, ?4, ?5, ?6)", + params![num, split, 0, mult, run_id, final_split], )?; } @@ -154,7 +165,7 @@ impl Database { } } } - #[must_use] + pub fn get_pb_run(&self, category: &CategoryManager) -> Result<(Vec, i32, Gamemode)> { let category = category.current(); let mut statement = self.conn.prepare(include_str!("../sql/pb_splits.sql"))?; @@ -183,7 +194,6 @@ impl Database { } /// Get the highest core of each split for the category - #[must_use] pub fn get_gold_splits(&self, category: &CategoryManager) -> Result> { let mut statement = self.conn.prepare(include_str!("../sql/best_splits.sql"))?; statement @@ -197,6 +207,42 @@ impl Database { } })? } + + fn migrate(&self, schema_version: i32) { + println!("Migrating database from {schema_version} to {CURRENT_SCHEMA_VERSION}"); + + self.conn.execute("BEGIN TRANSACTION", ()).unwrap(); + + let mut current_schema = schema_version; + + match (|| { + while current_schema < CURRENT_SCHEMA_VERSION { + match current_schema { + 0 => { + self.migrate0to1()?; + current_schema += 1 + } + _ => Err(rusqlite::Error::InvalidQuery)?, + }; + } + Ok::<(), rusqlite::Error>(()) + })() { + Ok(_) => { + self.conn.execute("COMMIT", ()).unwrap(); + println!("Migration successful") + } + Err(err) => { + self.conn.execute("ROLLBACK", ()).unwrap(); + panic!("Migration failed! {err}") + } + } + } + + fn migrate0to1(&self) -> Result { + println!("Migrating schema 0 to 1..."); + self.conn.pragma_update(Some("main"), "user_version", 1)?; + self.conn.execute("ALTER TABLE splits ADD COLUMN final BOOLEAN", ()) + } } impl ToSql for Gamemode { diff --git a/splitter/src/main.rs b/splitter/src/main.rs index 200ac2e..a5df954 100644 --- a/splitter/src/main.rs +++ b/splitter/src/main.rs @@ -135,7 +135,7 @@ impl ZeroSplitter { } fn save_splits(&mut self) { - if self.run.is_active() { + if self.run.is_active() && self.run.splits().unwrap().iter().sum::() > 0 { debug!("Saving splits"); if let Err(err) = self.db.insert_run(&self.categories, &self.run) { From 5a23894a93c31b6a8a3a2265daf40a2bc620a089 Mon Sep 17 00:00:00 2001 From: lily_and_doll <103815266+lily-and-doll@users.noreply.github.com> Date: Sat, 22 Nov 2025 03:43:08 -0500 Subject: [PATCH 26/52] Fix broken table initialization --- splitter/src/database.rs | 76 ++++++++++++++++++++++------------------ 1 file changed, 42 insertions(+), 34 deletions(-) diff --git a/splitter/src/database.rs b/splitter/src/database.rs index f4328d6..554b42d 100644 --- a/splitter/src/database.rs +++ b/splitter/src/database.rs @@ -21,73 +21,81 @@ impl Database { conn: Arc::new(Connection::open("./sqlite.db3")?), }; - let schema_version = database - .conn - .query_one("PRAGMA user_version", (), |row| row.get::<_, i32>(0)) - .unwrap(); - - if schema_version < CURRENT_SCHEMA_VERSION { - database.migrate(schema_version) - } else if schema_version > CURRENT_SCHEMA_VERSION { - panic!( - "Schema version beyond program version!\n Schema version {schema_version}, program version {CURRENT_SCHEMA_VERSION}" - ) - } - // #[cfg(debug_assertions)] - // { - // database.conn.execute("DROP TABLE IF EXISTS splits", ()).unwrap(); - // database.conn.execute("DROP TABLE IF EXISTS runs", ()).unwrap(); - // database.conn.execute("DROP TABLE IF EXISTS categories", ()).unwrap(); - // } if !database.conn.table_exists(Some("main"), "categories")? { if let Err(err) = database.create_tables() { error!("Error creating tables: {}", err) }; database.insert_new_category("default".to_owned(), Gamemode::GreenOrange)?; + } else { + let schema_version = database + .conn + .query_one("PRAGMA user_version", (), |row| row.get::<_, i32>(0)) + .unwrap(); + + if schema_version < CURRENT_SCHEMA_VERSION { + database.migrate(schema_version) + } else if schema_version > CURRENT_SCHEMA_VERSION { + panic!( + "Schema version beyond program version!\n Schema version {schema_version}, program version {CURRENT_SCHEMA_VERSION}" + ) + } } Ok(database) } pub fn create_tables(&self) -> Result<()> { - self.conn - .pragma_update(Some("main"), "user_version", CURRENT_SCHEMA_VERSION)?; - self.conn.execute( - " + self.conn.execute("BEGIN TRANSACTION", ())?; + + match (|| { + self.conn + .pragma_update(Some("main"), "user_version", CURRENT_SCHEMA_VERSION)?; + self.conn.execute( + " CREATE TABLE IF NOT EXISTS categories ( id INTEGER PRIMARY KEY, name TEXT UNIQUE NOT NULL, mode INTEGER NOT NULL ) ", - (), - )?; + (), + )?; - self.conn.execute( - " + self.conn.execute( + " CREATE TABLE IF NOT EXISTS runs ( id INTEGER PRIMARY KEY, category INTEGER NOT NULL REFERENCES categories(id) ON DELETE CASCADE ) ", - (), - )?; + (), + )?; - self.conn.execute( - " + self.conn.execute( + " CREATE TABLE IF NOT EXISTS splits ( id INTEGER PRIMARY KEY, split_num INTEGER NOT NULL, score INTEGER NOT NULL, hits INTEGER, mult INTEGER, - run_id INTEGER NOT NULL REFERENCES runs(id) ON DELETE CASCADE + run_id INTEGER NOT NULL REFERENCES runs(id) ON DELETE CASCADE, final BOOLEAN ) ", - (), - )?; + (), + )?; - Ok(()) + Ok::<(), rusqlite::Error>(()) + })() { + Ok(_) => { + self.conn.execute("COMMIT", ())?; + Ok(()) + } + Err(err) => { + self.conn.execute("ROLLBACK", ())?; + Err(err) + } + } } pub fn insert_current_category(&self, category: &CategoryManager) -> Result { From 88767f61faa012652b944d0a6b3d944cdf0742f7 Mon Sep 17 00:00:00 2001 From: lily_and_doll <103815266+lily-and-doll@users.noreply.github.com> Date: Sat, 22 Nov 2025 22:26:07 -0500 Subject: [PATCH 27/52] Add a configuration file --- .gitignore | 3 +- Cargo.lock | 1 + splitter/Cargo.toml | 1 + splitter/assets/default_config.toml | 7 +++++ splitter/src/app.rs | 12 ++++++-- splitter/src/config.rs | 48 +++++++++++++++++++++++++++++ splitter/src/main.rs | 11 +++++-- 7 files changed, 78 insertions(+), 5 deletions(-) create mode 100644 splitter/assets/default_config.toml create mode 100644 splitter/src/config.rs diff --git a/.gitignore b/.gitignore index 5ec7382..e7a5685 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /target *.db3 -/.vscode \ No newline at end of file +/.vscode +config.toml \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index d56cb8e..84043d0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2862,6 +2862,7 @@ dependencies = [ "rusqlite", "serde", "serde_json", + "toml", "windows", "winresource", ] diff --git a/splitter/Cargo.toml b/splitter/Cargo.toml index 31c2d0d..cabdf82 100644 --- a/splitter/Cargo.toml +++ b/splitter/Cargo.toml @@ -11,6 +11,7 @@ log = "0.4" serde = {version = "1.0", features = ["derive"]} serde_json = "1.0" rusqlite = { version = "0.37.0", features = ["bundled"] } +toml = "0.9.8" [dependencies.eframe] version = "0.31" diff --git a/splitter/assets/default_config.toml b/splitter/assets/default_config.toml new file mode 100644 index 0000000..d49b26f --- /dev/null +++ b/splitter/assets/default_config.toml @@ -0,0 +1,7 @@ +# Configuration for ZeroSplitter. Edit this file to change the settings +# (If you're unfamiliar with the format, this is a TOML file) +# https://en.wikipedia.org/wiki/TOML + +# Scale factor for the entire UI. 2.0 = twice as large as normal. +# The program is 300x300 in GO mode and 300x650 at 1.0 zoom, for reference +zoom_level = 1.0 \ No newline at end of file diff --git a/splitter/src/app.rs b/splitter/src/app.rs index 80a8658..398bb19 100644 --- a/splitter/src/app.rs +++ b/splitter/src/app.rs @@ -5,6 +5,7 @@ use eframe::{ use crate::{ Category, CategoryManager, Gamemode, Run, ZeroError, ZeroSplitter, + config::CONFIG, theme::{DARK_GREEN, DARK_ORANGE, DARKER_GREEN, DARKER_ORANGE, GREEN, LIGHT_ORANGE}, ui::{category_maker_dialog, confirm_dialog}, vanilla_descriptive_split_names, vanilla_split_names, @@ -22,9 +23,16 @@ impl App for ZeroSplitter { if let Some(prev_mode) = ctx.data(|data| data.get_temp::(prev_mode_id)) && prev_mode != cur_mode { + let zoom_level = CONFIG.get().unwrap().zoom_level; let min_size = match self.categories.current().mode { - Gamemode::GreenOrange => eframe::egui::Vec2 { x: 300.0, y: 300.0 }, - Gamemode::WhiteVanilla => eframe::egui::Vec2 { x: 300.0, y: 650.0 }, + Gamemode::GreenOrange => eframe::egui::Vec2 { + x: 300.0 * zoom_level, + y: 300.0 * zoom_level, + }, + Gamemode::WhiteVanilla => eframe::egui::Vec2 { + x: 300.0 * zoom_level, + y: 650.0 * zoom_level, + }, Gamemode::BlackOnion => todo!(), }; ctx.send_viewport_cmd(eframe::egui::ViewportCommand::MinInnerSize(min_size)); diff --git a/splitter/src/config.rs b/splitter/src/config.rs new file mode 100644 index 0000000..d40b115 --- /dev/null +++ b/splitter/src/config.rs @@ -0,0 +1,48 @@ +use std::{ + fs::{File, read_to_string}, + io::{Read, Write}, + sync::OnceLock, +}; + +use toml::{Table, Value}; + +use crate::ZeroError; + +pub static CONFIG: OnceLock = OnceLock::new(); + +const CONFIG_PATH: &'static str = "config.toml"; + +pub fn load_config() -> Result<(), ZeroError> { + let config_str = match read_to_string(CONFIG_PATH) { + Ok(s) => s, + Err(e) => match e.kind() { + std::io::ErrorKind::NotFound => create_config()?, + _ => return Err(ZeroError::IOError(e)), + }, + }; + let table = config_str.parse::().map_err(ZeroError::TOMLError)?; + + let config = Config { + zoom_level: match table.get("zoom_level") { + Some(Value::Float(f)) => *f as f32, + _ => 1.0, + }, + }; + + CONFIG.set(config).map_err(|_| ZeroError::StaticAlreadyInit)?; + Ok(()) +} + +fn create_config() -> Result { + let mut file = File::create_new(CONFIG_PATH).map_err(ZeroError::IOError)?; + file.write_all(include_bytes!("../assets/default_config.toml")) + .map_err(ZeroError::IOError)?; + let mut ret = String::new(); + file.read_to_string(&mut ret).map_err(ZeroError::IOError)?; + + Ok(ret) +} + +pub struct Config { + pub zoom_level: f32, +} diff --git a/splitter/src/main.rs b/splitter/src/main.rs index a5df954..3311ce2 100644 --- a/splitter/src/main.rs +++ b/splitter/src/main.rs @@ -17,10 +17,12 @@ use eframe::{ }; use log::{debug, error, warn}; use serde::{Deserialize, Serialize}; +use toml::de::Error; -use crate::{database::Database, run::Run, theme::zeroranger_visuals}; +use crate::{config::CONFIG, database::Database, run::Run, theme::zeroranger_visuals}; mod app; +mod config; mod database; mod hook; mod run; @@ -33,6 +35,7 @@ const SPLIT_DELAY_FRAMES: u32 = 20; static EGUI_CTX: OnceLock = OnceLock::new(); fn main() { + config::load_config().unwrap(); #[cfg(debug_assertions)] unsafe { env::set_var("RUST_BACKTRACE", "1"); @@ -42,7 +45,7 @@ fn main() { let options = NativeOptions { viewport: ViewportBuilder::default() - .with_inner_size([300., 290.]) + .with_inner_size([300., 300.]) .with_icon(IconData::default()) .with_title("ZeroSplitter"), @@ -60,6 +63,7 @@ fn main() { let _ = EGUI_CTX.set(c.egui_ctx.clone()); c.egui_ctx.set_theme(ThemePreference::Dark); c.egui_ctx.set_visuals(zeroranger_visuals()); + c.egui_ctx.set_zoom_factor(CONFIG.get().unwrap().zoom_level); Ok(Box::new(ZeroSplitter::load(rx))) }), ) @@ -572,4 +576,7 @@ enum ZeroError { DifficultyMismatch, SplitOutOfRange, CategoryOutOfRange, + IOError(std::io::Error), + TOMLError(toml::de::Error), + StaticAlreadyInit, } From 54b6b8dbf6235c034e8e45ec67b413c8c9eb98e7 Mon Sep 17 00:00:00 2001 From: lily_and_doll <103815266+lily-and-doll@users.noreply.github.com> Date: Sat, 22 Nov 2025 23:19:21 -0500 Subject: [PATCH 28/52] Add a datetime column to Runs table --- splitter/src/database.rs | 51 ++++++++++++++++++++++++++-------------- 1 file changed, 33 insertions(+), 18 deletions(-) diff --git a/splitter/src/database.rs b/splitter/src/database.rs index 554b42d..e9e2843 100644 --- a/splitter/src/database.rs +++ b/splitter/src/database.rs @@ -13,7 +13,7 @@ pub struct Database { conn: Arc, } -const CURRENT_SCHEMA_VERSION: i32 = 1; +const CURRENT_SCHEMA_VERSION: i32 = 2; impl Database { pub fn init() -> Result { @@ -21,24 +21,26 @@ impl Database { conn: Arc::new(Connection::open("./sqlite.db3")?), }; + // create tables if they don't exist if !database.conn.table_exists(Some("main"), "categories")? { if let Err(err) = database.create_tables() { error!("Error creating tables: {}", err) }; database.insert_new_category("default".to_owned(), Gamemode::GreenOrange)?; - } else { - let schema_version = database - .conn - .query_one("PRAGMA user_version", (), |row| row.get::<_, i32>(0)) - .unwrap(); - - if schema_version < CURRENT_SCHEMA_VERSION { - database.migrate(schema_version) - } else if schema_version > CURRENT_SCHEMA_VERSION { - panic!( - "Schema version beyond program version!\n Schema version {schema_version}, program version {CURRENT_SCHEMA_VERSION}" - ) - } + } + // check version of schema (0 if they were just created) + let schema_version = database + .conn + .query_one("PRAGMA user_version", (), |row| row.get::<_, i32>(0)) + .unwrap(); + + // migrate schema version if necessary + if schema_version < CURRENT_SCHEMA_VERSION { + database.migrate(schema_version) + } else if schema_version > CURRENT_SCHEMA_VERSION { + panic!( + "Schema version beyond program version!\n Schema version {schema_version}, program version {CURRENT_SCHEMA_VERSION}" + ) } Ok(database) @@ -79,7 +81,6 @@ impl Database { hits INTEGER, mult INTEGER, run_id INTEGER NOT NULL REFERENCES runs(id) ON DELETE CASCADE, - final BOOLEAN ) ", (), @@ -148,11 +149,14 @@ impl Database { let mut stmt = self.conn.prepare("SELECT id FROM categories WHERE name = ?1")?; let res = stmt.query_one(params![category.name], |row| row.get::(0))?; - self.conn.execute("INSERT INTO runs VALUES(NULL, ?1)", params![res])?; + self.conn.execute( + "INSERT INTO runs (id, category, datetime) VALUES(NULL, ?1, datetime('now'))", + params![res], + )?; let run_id = self.conn.last_insert_rowid(); - for (num, &split) in run.splits().unwrap().iter().enumerate() { + for (num, &split) in run.splits().unwrap().iter().take_while(|&&s| s > 0).enumerate() { let final_split = num == run.current_split().unwrap(); let mult = run.mults().unwrap()[num]; self.conn.execute( @@ -165,6 +169,7 @@ impl Database { })() { Ok(_) => { self.conn.execute("COMMIT", ())?; + println!("Committing run with score {} to database", run.score().unwrap()); Ok(()) } Err(err) => { @@ -228,7 +233,11 @@ impl Database { match current_schema { 0 => { self.migrate0to1()?; - current_schema += 1 + current_schema = 1 + } + 1 => { + self.migrate1to2()?; + current_schema = 2 } _ => Err(rusqlite::Error::InvalidQuery)?, }; @@ -251,6 +260,12 @@ impl Database { self.conn.pragma_update(Some("main"), "user_version", 1)?; self.conn.execute("ALTER TABLE splits ADD COLUMN final BOOLEAN", ()) } + + fn migrate1to2(&self) -> Result { + println!("Migrating schema 1 to 2..."); + self.conn.pragma_update(Some("main"), "user_version", 2)?; + self.conn.execute("ALTER TABLE runs ADD COLUMN datetime INTEGER", ()) + } } impl ToSql for Gamemode { From 5dd407fd5299685c84921f2cac0e6f3caa9daa1f Mon Sep 17 00:00:00 2001 From: lily_and_doll <103815266+lily-and-doll@users.noreply.github.com> Date: Sun, 23 Nov 2025 01:34:07 -0500 Subject: [PATCH 29/52] Clean up all warnings --- splitter/src/app.rs | 2 +- splitter/src/database.rs | 14 +---- splitter/src/main.rs | 122 ++------------------------------------- splitter/src/system.rs | 8 +-- splitter/src/ui.rs | 37 +----------- 5 files changed, 13 insertions(+), 170 deletions(-) diff --git a/splitter/src/app.rs b/splitter/src/app.rs index 398bb19..ba50ab9 100644 --- a/splitter/src/app.rs +++ b/splitter/src/app.rs @@ -4,7 +4,7 @@ use eframe::{ }; use crate::{ - Category, CategoryManager, Gamemode, Run, ZeroError, ZeroSplitter, + Gamemode, Run, ZeroError, ZeroSplitter, config::CONFIG, theme::{DARK_GREEN, DARK_ORANGE, DARKER_GREEN, DARKER_ORANGE, GREEN, LIGHT_ORANGE}, ui::{category_maker_dialog, confirm_dialog}, diff --git a/splitter/src/database.rs b/splitter/src/database.rs index e9e2843..2991678 100644 --- a/splitter/src/database.rs +++ b/splitter/src/database.rs @@ -1,4 +1,4 @@ -use std::{fmt::Error, sync::Arc}; +use std::sync::Arc; use log::error; use rusqlite::{ @@ -99,14 +99,6 @@ impl Database { } } - pub fn insert_current_category(&self, category: &CategoryManager) -> Result { - let category = category.current(); - self.conn.execute( - "INSERT OR IGNORE INTO categories VALUES(NULL, ?1, ?2)", - params![category.name, category.mode], - ) - } - pub fn insert_new_category(&self, name: String, mode: Gamemode) -> Result { self.conn .execute("INSERT INTO categories VALUES(NULL, ?1, ?2)", params![name, mode])?; @@ -194,8 +186,8 @@ impl Database { if splits.len() > 0 { let scores: Vec = splits.iter().map(|s| s.0).collect(); - let hits: Vec = splits.iter().map(|s| s.1).collect(); - let run_id = splits[0].2; + let _hits: Vec = splits.iter().map(|s| s.1).collect(); + let _run_id = splits[0].2; let mode = splits[0].3; let total = scores.iter().sum(); diff --git a/splitter/src/main.rs b/splitter/src/main.rs index 3311ce2..9967a54 100644 --- a/splitter/src/main.rs +++ b/splitter/src/main.rs @@ -1,9 +1,8 @@ use std::{ env, - fs::{self, File}, net::UdpSocket, sync::{ - LazyLock, OnceLock, + OnceLock, mpsc::{self, Receiver, Sender}, }, thread, @@ -15,9 +14,8 @@ use eframe::{ NativeOptions, egui::{Context, IconData, ThemePreference, ViewportBuilder}, }; -use log::{debug, error, warn}; +use log::{debug, error}; use serde::{Deserialize, Serialize}; -use toml::de::Error; use crate::{config::CONFIG, database::Database, run::Run, theme::zeroranger_visuals}; @@ -283,119 +281,6 @@ pub struct EntryDialogData { pub mode: Gamemode, } -/// Translation of function of the same name in ZR. -/// Checkpoints in WV have the same stage/checkpoint as in GO, -/// e.g. the cloudoo segment in stage 1 is technically stage 3 checkpoint 2 -/// just like it is in GO. Also, some segments have multiple checkpoints. -/// This function gets the segment number in WV from the GO data. -/// "Realm" seems to indicate when the ship is changed, like in TLB, the dream, or bonus stages -/// -/// Someone much smarter than me could try to call this function directly from the -/// game instead of using this. -fn vanilla_get_simstage(stage: u8, checkpoint: u8, checkpoint_sub: u8, realm: u8) -> Result<(u32, u32), ()> { - Ok(match (stage, checkpoint) { - // GO stage/check => WV stage/check - (1, _) if realm == 3 => (1, 4), - (1, 0 | 1) => (1, 0), - (1, 2) => (2, 4), - (1, 3) => (1, 2), - (1, 4 | 5 | 6) => (1, 3), - - (2, _) if realm == 3 => (2, 6), - (2, 0 | 1) => (2, 0), - (2, 2) => (3, 1), - (2, 3) => (2, 1), - (2, 4) => (2, 3), - (2, 5) => (4, 1), - (2, 6 | 7 | 8) => (2, 5), - - (3, 0) => (3, 0), - (3, 1) => (1, 1), - (3, 2) => (4, 4), - (3, 3) => (3, 2), - (3, 4) if checkpoint_sub < 1 => (2, 2), - (3, 4) => (3, 3), - - (3, 6) => (4, 3), - (3, 7) => (3, 4), - (3, 8 | 9) => (3, 5), - - (4, _) if realm == 3 => (3, 6), - (4, 3) => (3, 6), - (4, 5 | 6) => (4, 0), - (4, 7 | 8) => (4, 2), - (4, 9 | 10) => (4, 5), - _ => return Err(()), - }) -} - -/// Translate sim stage and checkpoint to split count -/// e.g. snake is split 7 (0 indexed) -fn vanilla_get_split_count(simstage: u32, simcheckpoint: u32) -> usize { - [ - (1, 1), - (1, 2), - (1, 3), - (1, 4), - (1, 5), - (2, 1), - (2, 2), - (2, 3), - (2, 4), - (2, 5), - (2, 6), - (2, 7), - (3, 1), - (3, 2), - (3, 3), - (3, 4), - (3, 5), - (3, 6), - (3, 7), - (4, 1), - (4, 2), - (4, 3), - (4, 4), - (4, 5), - (4, 6), - (4, 7), - ] - .iter() - .position(|&x| x == (simstage, simcheckpoint + 1)) - .unwrap() -} - -fn vanilla_stage_from_split(split: usize) -> (u32, u32) { - [ - (1, 1), - (1, 2), - (1, 3), - (1, 4), - (1, 5), - (2, 1), - (2, 2), - (2, 3), - (2, 4), - (2, 5), - (2, 6), - (2, 7), - (3, 1), - (3, 2), - (3, 3), - (3, 4), - (3, 5), - (3, 6), - (3, 7), - (4, 1), - (4, 2), - (4, 3), - (4, 4), - (4, 5), - (4, 6), - (4, 7), - ][split] -} - fn vanilla_split_names(split: usize) -> &'static str { [ "1-1", "1-2", "1-3", "1-4", "Bonus 1", "2-1", "2-2", "2-3", "2-4", "2-5", "2-6", "Bonus 2", "3-1", "3-2", @@ -551,7 +436,7 @@ impl CategoryManager { pub fn refresh_comparison(&mut self, db: &Database) -> Result<(), ZeroError> { self.comparison_cache = match db.get_pb_run(self) { - Ok((scores, total, mode)) if mode == self.current().mode => scores, + Ok((scores, _, mode)) if mode == self.current().mode => scores, Ok(_) => return Err(ZeroError::DifficultyMismatch), Err(rusqlite::Error::QueryReturnedNoRows) => vec![0; self.current().mode.splits()], Err(e) => return Err(ZeroError::DatabaseError(e)), @@ -569,6 +454,7 @@ struct Category { #[derive(Debug)] #[non_exhaustive] +#[allow(dead_code)] enum ZeroError { Illegal, DatabaseError(rusqlite::Error), diff --git a/splitter/src/system.rs b/splitter/src/system.rs index d1f3b91..92fb1b6 100644 --- a/splitter/src/system.rs +++ b/splitter/src/system.rs @@ -49,7 +49,7 @@ impl ProcessHandle { pub unsafe fn from_raw(raw: HANDLE) -> Self { ProcessHandle { raw } } - + #[allow(dead_code)] pub fn as_raw(&self) -> HANDLE { self.raw } @@ -62,7 +62,7 @@ impl ProcessHandle { }; OsString::from_wide(&buffer[0..size as usize]).into() } - + #[allow(dead_code)] pub unsafe fn read_memory(&self, base_addr: *const c_void, buffer: &mut [u8]) -> Result<(), Error> { unsafe { ReadProcessMemory(self.raw, base_addr, buffer.as_mut_ptr().cast(), buffer.len(), None) } } @@ -101,7 +101,7 @@ impl ProcessHandle { }; res } - + #[allow(dead_code)] pub fn enumerate_modules(&self) -> Vec { let mut needed = 0; unsafe { @@ -121,7 +121,7 @@ impl ProcessHandle { modules } - + #[allow(dead_code)] pub fn get_module_file_name_ex(&self, module: HMODULE) -> OsString { let mut buffer = [0; 256]; let size = unsafe { GetModuleFileNameExW(Some(self.raw), Some(module), &mut buffer) }; diff --git a/splitter/src/ui.rs b/splitter/src/ui.rs index 91d3e87..210b499 100644 --- a/splitter/src/ui.rs +++ b/splitter/src/ui.rs @@ -1,44 +1,9 @@ use std::{sync::mpsc::Sender, time::Duration}; -use eframe::egui::{CentralPanel, ComboBox, Context, Id, Key, TopBottomPanel, ViewportBuilder, ViewportId}; +use eframe::egui::{CentralPanel, ComboBox, Context, Id, TopBottomPanel, ViewportBuilder, ViewportId}; use crate::{EGUI_CTX, EntryDialogData, Gamemode}; -pub fn entry_dialog(ctx: &Context, tx: Sender, msg: &'static str) { - let vp_builder = ViewportBuilder::default() - .with_title("ZeroSplitter") - .with_active(true) - .with_resizable(false) - .with_minimize_button(false) - .with_maximize_button(false) - .with_inner_size([200., 100.]); - - ctx.show_viewport_deferred(ViewportId::from_hash_of("entry dialog"), vp_builder, move |ctx, _| { - if ctx.input(|input| input.viewport().close_requested()) { - let _ = tx.send(String::new()); - request_repaint(); - return; - } - - let text_id = Id::new("edit text"); - let mut edit_str = ctx.data_mut(|data| data.get_temp_mut_or_insert_with(text_id, String::new).clone()); - - CentralPanel::default().show(ctx, |ui| { - ui.vertical_centered_justified(|ui| { - ui.label(msg); - if ui.text_edit_singleline(&mut edit_str).lost_focus() && ui.input(|i| i.key_pressed(Key::Enter)) { - let _ = tx.send(edit_str.clone()); - request_repaint(); - } - }); - }); - - ctx.data_mut(|data| { - data.insert_temp(text_id, edit_str); - }); - }); -} - /// Category entry dialoge menu with a gamemode dropdown. pub fn category_maker_dialog(ctx: &Context, tx: Sender>, msg: &'static str, mode_select: bool) { let vp_builder = ViewportBuilder::default() From 2f7e4c97f1db4cb7fdb7fce2af97f57ec5adc381 Mon Sep 17 00:00:00 2001 From: lily_and_doll <103815266+lily-and-doll@users.noreply.github.com> Date: Sun, 23 Nov 2025 02:59:41 -0500 Subject: [PATCH 30/52] Fix switching category between runs --- splitter/src/app.rs | 5 ++++- splitter/src/main.rs | 10 +++++++--- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/splitter/src/app.rs b/splitter/src/app.rs index ba50ab9..7d30dfd 100644 --- a/splitter/src/app.rs +++ b/splitter/src/app.rs @@ -58,7 +58,10 @@ impl App for ZeroSplitter { let mut cat_idx = self.categories.current; ComboBox::from_label("") .show_index(ui, &mut cat_idx, len, |i| &self.categories.index(i).unwrap().name); - self.categories.set_current(cat_idx, &self.db).unwrap(); + if self.categories.current != cat_idx { + self.end_run(); + self.categories.set_current(cat_idx, &self.db).unwrap(); + } } if ui.small_button("+").clicked() { diff --git a/splitter/src/main.rs b/splitter/src/main.rs index 9967a54..0770c04 100644 --- a/splitter/src/main.rs +++ b/splitter/src/main.rs @@ -417,14 +417,18 @@ impl CategoryManager { .map_err(ZeroError::DatabaseError) } - pub fn set_current(&mut self, new_idx: usize, db: &Database) -> Result<(), ZeroError> { - if new_idx >= self.categories.len() { + /// Sets the current selected category by index. + /// Returns true if the category changed + pub fn set_current(&mut self, new_idx: usize, db: &Database) -> Result { + if new_idx == self.current { + return Ok(false); + } else if new_idx >= self.categories.len() { return Err(ZeroError::CategoryOutOfRange); } self.current = new_idx; self.refresh_comparison(db)?; - Ok(()) + Ok(true) } pub fn get_comparison(&self) -> &Vec { From a426316d7559959c68a58f40d92b244e3fd400b4 Mon Sep 17 00:00:00 2001 From: lily_and_doll <103815266+lily-and-doll@users.noreply.github.com> Date: Sun, 23 Nov 2025 03:06:40 -0500 Subject: [PATCH 31/52] Fix broken table initialization again --- splitter/src/database.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/splitter/src/database.rs b/splitter/src/database.rs index 2991678..3bdc1e0 100644 --- a/splitter/src/database.rs +++ b/splitter/src/database.rs @@ -80,7 +80,7 @@ impl Database { score INTEGER NOT NULL, hits INTEGER, mult INTEGER, - run_id INTEGER NOT NULL REFERENCES runs(id) ON DELETE CASCADE, + run_id INTEGER NOT NULL REFERENCES runs(id) ON DELETE CASCADE ) ", (), From 85b24baca44b961270a5b11d41607bee43f11958 Mon Sep 17 00:00:00 2001 From: lily_and_doll <103815266+lily-and-doll@users.noreply.github.com> Date: Sun, 23 Nov 2025 04:13:39 -0500 Subject: [PATCH 32/52] Fix erroneous splits starting loop 2 in GO --- splitter/src/app.rs | 2 +- splitter/src/main.rs | 12 +++++++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/splitter/src/app.rs b/splitter/src/app.rs index 7d30dfd..f2baeae 100644 --- a/splitter/src/app.rs +++ b/splitter/src/app.rs @@ -170,7 +170,7 @@ impl ZeroSplitter { // Get relative/absolute split in the PB // PB split = score of this split in the PB run let compare = self.categories.get_comparison(); - let rel_pb_split = compare[i]; + let rel_pb_split = *compare.get(i).unwrap_or(&0); let abs_pb_split = compare .iter() .enumerate() diff --git a/splitter/src/main.rs b/splitter/src/main.rs index 0770c04..53f9354 100644 --- a/splitter/src/main.rs +++ b/splitter/src/main.rs @@ -164,23 +164,29 @@ impl ZeroSplitter { return; } + let frame_split = frame + .stage + .checked_sub(1) + .unwrap_or(0) + .checked_sub(frame.game_loop) + .unwrap_or(0) as usize; + // Reset if we just left the menu or returned to 1-1 if frame.stage != self.last_frame.stage && (self.last_frame.is_menu() || frame.is_first_stage()) { self.reset(); self.run.start(frame); + self.run.set_split(frame_split).unwrap(); self.categories.refresh_comparison(&self.db).unwrap(); } if !frame.is_menu() && self.run.is_active() { - let frame_split = (frame.stage - 1 - frame.game_loop) as usize; - if frame_split >= 8 { // TLB or credits return; } // Split if necessary - if (frame.stage != self.last_frame.stage) && !self.last_frame.is_menu() { + if (frame_split > self.run.current_split().unwrap()) && !self.last_frame.is_menu() { self.run.split().unwrap(); } From 9ffb25c1ef0b90c25f576991fa1c8e13dcf71893 Mon Sep 17 00:00:00 2001 From: lillies <103815266+lily-and-doll@users.noreply.github.com> Date: Sun, 23 Nov 2025 17:19:13 -0500 Subject: [PATCH 33/52] Update README.md --- README.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 939b2d8..08a478b 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,9 @@ doesn't show up as the correct split, just go back to the main menu and launch f Continues should be tracked properly in Green Orange but not White Vanilla. -Your data is stored in the `zs_data.json` next to the .exe. +Co-op should work, but has not been tested. If you find co-op to work, let me know. + +Your data is stored in the `sqlite.db3` next to the .exe. You can manually insert, delete, or modify any data in the database if you like. A "category" is a set of splits and personal bests to run against. ZeroSplitter will try to detect which mode you are playing and not overwrite scores from one mode with another - but don't push your luck: have the right @@ -23,7 +25,7 @@ category selected before you take off. Press the plus button to add a new category - don't click Black Onion for the mode (it won't work at all). -Currently the only way to delete categories is by manually deleting them in the `zs_data.json`, but you can rename them. +Currently the only way to delete categories is by manually dropping them from the database, but you can rename them. The "relative" button switches the display between showing your score per split or your running total up to each split. Turn on relative mode to see how much better or worse you did each split versus your PB run. Turn off relative mode @@ -31,7 +33,7 @@ to see how far ahead or behind you are versus your PB run. The relative button only changes the display, not how the data is saved: toggle it as much as you like, even mid-run. -If you want to move the program to another folder, just copy `zerosplitter.exe`, `payload.dll`, and `zs_data.json`. +If you want to move the program to another folder, just copy all the files in the folder. # How to build from source Just run `cargo run --release` in the top level of the repository, next to this `README.md`. `build.sh` will zip `zerosplitter.exe` From d2c87b03ed738514906cdff4e66b18359f3aa632 Mon Sep 17 00:00:00 2001 From: lily_and_doll <103815266+lily-and-doll@users.noreply.github.com> Date: Sun, 23 Nov 2025 17:48:14 -0500 Subject: [PATCH 34/52] Fix broken table initialization again*2 --- splitter/src/app.rs | 2 +- splitter/src/database.rs | 7 +++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/splitter/src/app.rs b/splitter/src/app.rs index f2baeae..3c6ca19 100644 --- a/splitter/src/app.rs +++ b/splitter/src/app.rs @@ -159,7 +159,7 @@ impl ZeroSplitter { // Gold split = high score of this split in any run let best_splits = self.db.get_gold_splits(&self.categories); let gold_split = match (best_splits, self.relative_score) { - (Ok(splits), true) => splits[i], + (Ok(splits), true) => *splits.get(i).unwrap_or(&0), (Ok(splits), false) => splits .iter() .enumerate() diff --git a/splitter/src/database.rs b/splitter/src/database.rs index 3bdc1e0..e80fe76 100644 --- a/splitter/src/database.rs +++ b/splitter/src/database.rs @@ -23,7 +23,7 @@ impl Database { // create tables if they don't exist if !database.conn.table_exists(Some("main"), "categories")? { - if let Err(err) = database.create_tables() { + if let Err(err) = database.create_tables0() { error!("Error creating tables: {}", err) }; database.insert_new_category("default".to_owned(), Gamemode::GreenOrange)?; @@ -45,12 +45,11 @@ impl Database { Ok(database) } - pub fn create_tables(&self) -> Result<()> { + pub fn create_tables0(&self) -> Result<()> { self.conn.execute("BEGIN TRANSACTION", ())?; match (|| { - self.conn - .pragma_update(Some("main"), "user_version", CURRENT_SCHEMA_VERSION)?; + self.conn.pragma_update(Some("main"), "user_version", 0)?; self.conn.execute( " CREATE TABLE IF NOT EXISTS categories ( From 4a9804f1ef2cf1b4ac6913e22969001273676def Mon Sep 17 00:00:00 2001 From: lily_and_doll <103815266+lily-and-doll@users.noreply.github.com> Date: Mon, 24 Nov 2025 16:41:58 -0500 Subject: [PATCH 35/52] Grab pattern and dynamic rank per split --- common/src/lib.rs | 2 ++ payload/src/lib.rs | 4 +++ splitter/src/database.rs | 20 ++++++++++++--- splitter/src/run.rs | 54 ++++++++++++++++++++++++++++++++-------- 4 files changed, 66 insertions(+), 14 deletions(-) diff --git a/common/src/lib.rs b/common/src/lib.rs index 016a6ea..ee7c865 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -15,6 +15,8 @@ pub struct FrameData { pub checkpoint_sub: u8, pub timer_wave: u32, pub multiplier_one: u32, + pub dynamic_rank: f32, + pub pattern_rank: f32, } impl FrameData { diff --git a/payload/src/lib.rs b/payload/src/lib.rs index bec6161..8f816c1 100644 --- a/payload/src/lib.rs +++ b/payload/src/lib.rs @@ -104,6 +104,8 @@ fn get_frame_data() -> FrameData { let checkpoint_sub = read_var(c"checkpoint_sub").unwrap().value as u8; let timer_wave = read_var(c"timer_wave").unwrap().value as u32; let multiplier_one = read_var(c"multiplier_one").unwrap().value as u32; + let pattern_rank = read_var(c"pattern_rank").unwrap().value as f32; + let dynamic_rank = read_var(c"dynamic_rank").unwrap().value as f32; FrameData { score_p1: score_p1 as i32, score_p2: score_p2 as i32, @@ -115,6 +117,8 @@ fn get_frame_data() -> FrameData { checkpoint_sub, timer_wave, multiplier_one, + pattern_rank, + dynamic_rank, } } diff --git a/splitter/src/database.rs b/splitter/src/database.rs index e80fe76..959b655 100644 --- a/splitter/src/database.rs +++ b/splitter/src/database.rs @@ -13,7 +13,7 @@ pub struct Database { conn: Arc, } -const CURRENT_SCHEMA_VERSION: i32 = 2; +const CURRENT_SCHEMA_VERSION: i32 = 3; impl Database { pub fn init() -> Result { @@ -151,8 +151,8 @@ impl Database { let final_split = num == run.current_split().unwrap(); let mult = run.mults().unwrap()[num]; self.conn.execute( - "INSERT INTO splits (id, split_num, score, hits, mult, run_id, final) VALUES(NULL, ?1, ?2, ?3, ?4, ?5, ?6)", - params![num, split, 0, mult, run_id, final_split], + "INSERT INTO splits (id, split_num, score, hits, mult, run_id, final, pattern_rank, dynamic_rank) VALUES(NULL, ?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)", + params![num, split, 0, mult, run_id, final_split, ], )?; } @@ -230,6 +230,10 @@ impl Database { self.migrate1to2()?; current_schema = 2 } + 2 => { + self.migrate2to3()?; + current_schema = 3 + } _ => Err(rusqlite::Error::InvalidQuery)?, }; } @@ -257,6 +261,16 @@ impl Database { self.conn.pragma_update(Some("main"), "user_version", 2)?; self.conn.execute("ALTER TABLE runs ADD COLUMN datetime INTEGER", ()) } + + fn migrate2to3(&self) -> Result<()> { + println!("Migrating schema 2 to 3..."); + self.conn.pragma_update(Some("main"), "user_version", 3)?; + self.conn.execute_batch( + "ALTER TABLE splits ADD COLUMN pattern_rank REAL; + ALTER TABLE splits ADD COLUMN dynamic_rank REAL;" + + ) + } } impl ToSql for Gamemode { diff --git a/splitter/src/run.rs b/splitter/src/run.rs index 692aabc..6ae2e49 100644 --- a/splitter/src/run.rs +++ b/splitter/src/run.rs @@ -7,8 +7,7 @@ pub enum Run { Inactive, Active { difficulty: Gamemode, - splits: Vec, - mults: Vec, + splits: Vec, score: i32, current_split: usize, split_base_score: i32, @@ -26,14 +25,14 @@ impl Run { pub fn splits(&self) -> Result, ZeroError> { match self { Run::Inactive => Err(ZeroError::RunInactive), - Run::Active { splits, .. } => Ok(splits.to_vec()), + Run::Active { splits, .. } => Ok(splits.iter().map(|s| s.score).collect()), } } pub fn mults(&self) -> Result, ZeroError> { match self { Run::Inactive => Err(ZeroError::RunInactive), - Run::Active { mults, .. } => Ok(mults.to_vec()), + Run::Active { splits, .. } => Ok(splits.iter().map(|s| s.mult).collect()), } } @@ -41,8 +40,7 @@ impl Run { let mode = frame.difficulty.into(); *self = Self::Active { difficulty: mode, - splits: vec![0; mode.splits()], - mults: vec![0; mode.splits()], + splits: vec![Default::default(); mode.splits()], score: 0, current_split: 0, split_base_score: 0, @@ -58,8 +56,7 @@ impl Run { Run::Inactive => Run::Inactive, Run::Active { difficulty, .. } => Run::Active { difficulty: difficulty, - splits: vec![0; difficulty.splits()], - mults: vec![0; difficulty.splits()], + splits: vec![Default::default(); difficulty.splits()], score: 0, current_split: 0, split_base_score: 0, @@ -74,7 +71,6 @@ impl Run { score, current_split, split_base_score, - mults, } = self { if *difficulty == Gamemode::from(frame.difficulty) { @@ -82,8 +78,14 @@ impl Run { if *split_base_score > *score { *split_base_score = 0 } - splits[*current_split] = frame.total_score() - *split_base_score; - mults[*current_split] = frame.multiplier_one; + let data = SplitData::new( + frame.total_score(), + frame.multiplier_one, + frame.pattern_rank, + frame.dynamic_rank, + ); + splits.insert(*current_split, data); + Ok(()) } else { Err(ZeroError::DifficultyMismatch) @@ -145,3 +147,33 @@ impl Run { } } } + +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct SplitData { + score: i32, + mult: u32, + pattern_rank: f32, + dynamic_rank: f32, +} + +impl SplitData { + fn new(score: i32, mult: u32, pattern_rank: f32, dynamic_rank: f32) -> Self { + Self { + score, + mult, + pattern_rank, + dynamic_rank, + } + } +} + +impl Default for SplitData { + fn default() -> Self { + Self { + score: Default::default(), + mult: Default::default(), + pattern_rank: Default::default(), + dynamic_rank: Default::default(), + } + } +} From 496eac03e96da66bb99898760e0a132e601ee3fa Mon Sep 17 00:00:00 2001 From: lily_and_doll <103815266+lily-and-doll@users.noreply.github.com> Date: Mon, 24 Nov 2025 16:42:22 -0500 Subject: [PATCH 36/52] Add window decorations toggle --- splitter/src/app.rs | 37 ++++++++++++++++++++++++++++--------- splitter/src/database.rs | 3 +-- splitter/src/main.rs | 15 ++++++++------- 3 files changed, 37 insertions(+), 18 deletions(-) diff --git a/splitter/src/app.rs b/splitter/src/app.rs index 3c6ca19..5e04950 100644 --- a/splitter/src/app.rs +++ b/splitter/src/app.rs @@ -11,6 +11,13 @@ use crate::{ vanilla_descriptive_split_names, vanilla_split_names, }; +pub struct Toggles { + pub names: bool, + pub relative_score: bool, + pub show_gold_split: bool, + pub decorations: bool, +} + impl App for ZeroSplitter { fn update(&mut self, ctx: &Context, _frame: &mut Frame) { while let Ok(data) = self.data_source.try_recv() { @@ -44,12 +51,20 @@ impl App for ZeroSplitter { CentralPanel::default().show(ctx, |ui| { ui.with_layout(Layout::top_down_justified(Align::Min), |ui| { ui.horizontal(|ui| { - ui.toggle_value(&mut self.relative_score, "RELATIVE") + ui.toggle_value(&mut self.toggles.relative_score, "RELATIVE") .on_hover_text("Display relative score per split or running total of score"); - ui.toggle_value(&mut self.show_gold_split, "BEST SPLITS") + ui.toggle_value(&mut self.toggles.show_gold_split, "BEST SPLITS") .on_hover_text("Show your PB's splits or your best splits on the left"); - ui.toggle_value(&mut self.names, "NAMES") + ui.toggle_value(&mut self.toggles.names, "NAMES") .on_hover_text("Toggle descriptive or number names for WV splits"); + let deco_toggle = ui + .toggle_value(&mut self.toggles.decorations, "TITLE") + .on_hover_text("Toggle the titlebar of the program"); + if deco_toggle.changed() && self.toggles.decorations { + ctx.send_viewport_cmd(eframe::egui::ViewportCommand::Decorations(true)); + } else if deco_toggle.changed() { + ctx.send_viewport_cmd(eframe::egui::ViewportCommand::Decorations(false)); + } }); ui.horizontal(|ui| { ui.label("Category: "); @@ -154,11 +169,15 @@ impl ZeroSplitter { .fold(0, |acc, (_, &s)| acc + s) }) }) { - let split = if self.relative_score { rel_split } else { abs_split }; + let split = if self.toggles.relative_score { + rel_split + } else { + abs_split + }; // Get relative/absolute gold split // Gold split = high score of this split in any run let best_splits = self.db.get_gold_splits(&self.categories); - let gold_split = match (best_splits, self.relative_score) { + let gold_split = match (best_splits, self.toggles.relative_score) { (Ok(splits), true) => *splits.get(i).unwrap_or(&0), (Ok(splits), false) => splits .iter() @@ -177,7 +196,7 @@ impl ZeroSplitter { .take_while(|&(idx, _)| idx <= i) .fold(0, |acc, (_, &s)| acc + s); - let pb_split = if self.relative_score { + let pb_split = if self.toggles.relative_score { rel_pb_split } else { abs_pb_split @@ -201,7 +220,7 @@ impl ZeroSplitter { match self.categories.current().mode { Gamemode::GreenOrange => left.label(format!("{loop_n}-{stage_n}")), Gamemode::WhiteVanilla => { - if self.names { + if self.toggles.names { left.label(vanilla_descriptive_split_names(n)) } else { left.label(vanilla_split_names(n)) @@ -210,7 +229,7 @@ impl ZeroSplitter { Gamemode::BlackOnion => todo!(), }; - if self.show_gold_split { + if self.toggles.show_gold_split { if gold_score > 0 { left.colored_label(GREEN, gold_score.to_string()); } @@ -235,7 +254,7 @@ impl ZeroSplitter { if n < current_split { // past split, we should show a diff let diff = current_score - compare_score; - if self.relative_score { + if self.toggles.relative_score { let diff_color = if diff > 0 { LIGHT_ORANGE } else if diff == 0 { diff --git a/splitter/src/database.rs b/splitter/src/database.rs index 959b655..e458d60 100644 --- a/splitter/src/database.rs +++ b/splitter/src/database.rs @@ -267,8 +267,7 @@ impl Database { self.conn.pragma_update(Some("main"), "user_version", 3)?; self.conn.execute_batch( "ALTER TABLE splits ADD COLUMN pattern_rank REAL; - ALTER TABLE splits ADD COLUMN dynamic_rank REAL;" - + ALTER TABLE splits ADD COLUMN dynamic_rank REAL;", ) } } diff --git a/splitter/src/main.rs b/splitter/src/main.rs index 53f9354..b0c8fef 100644 --- a/splitter/src/main.rs +++ b/splitter/src/main.rs @@ -17,7 +17,7 @@ use eframe::{ use log::{debug, error}; use serde::{Deserialize, Serialize}; -use crate::{config::CONFIG, database::Database, run::Run, theme::zeroranger_visuals}; +use crate::{app::Toggles, config::CONFIG, database::Database, run::Run, theme::zeroranger_visuals}; mod app; mod config; @@ -98,11 +98,9 @@ struct ZeroSplitter { waiting_for_confirm: bool, dialog_rx: Receiver>, dialog_tx: Sender>, - relative_score: bool, - show_gold_split: bool, split_delay: Option, - names: bool, db: Database, + toggles: Toggles, } impl ZeroSplitter { @@ -118,11 +116,14 @@ impl ZeroSplitter { waiting_for_category: false, waiting_for_rename: false, waiting_for_confirm: false, - relative_score: true, - show_gold_split: true, split_delay: None, - names: false, db, + toggles: Toggles { + names: false, + relative_score: true, + show_gold_split: true, + decorations: true, + }, }; zerosplitter.categories.load(&zerosplitter.db).unwrap(); From b678c1a05dccabf89321b9ee6aceba1788a35155 Mon Sep 17 00:00:00 2001 From: lily_and_doll <103815266+lily-and-doll@users.noreply.github.com> Date: Mon, 24 Nov 2025 18:42:01 -0500 Subject: [PATCH 37/52] Add window decoration toggle toggle to config --- splitter/assets/default_config.toml | 12 +++++++++++- splitter/src/app.rs | 25 +++++++++++++++++-------- splitter/src/config.rs | 5 +++++ 3 files changed, 33 insertions(+), 9 deletions(-) diff --git a/splitter/assets/default_config.toml b/splitter/assets/default_config.toml index d49b26f..be55179 100644 --- a/splitter/assets/default_config.toml +++ b/splitter/assets/default_config.toml @@ -4,4 +4,14 @@ # Scale factor for the entire UI. 2.0 = twice as large as normal. # The program is 300x300 in GO mode and 300x650 at 1.0 zoom, for reference -zoom_level = 1.0 \ No newline at end of file +zoom_level = 1.0 + + +# Adds a toggle in the top bar that turns off window decorations, e.g. +# the title bar and side bars. When the decorations are hidden, you +# can move the window by dragging it anywhere but cannot resize it +# manually. +# Known issue: right clicking the window when decorations are hidden +# makes dragging no longer work. Turn them back on and drag the +# title bar to fix it. +decoration_button = false \ No newline at end of file diff --git a/splitter/src/app.rs b/splitter/src/app.rs index 5e04950..24ed7fc 100644 --- a/splitter/src/app.rs +++ b/splitter/src/app.rs @@ -1,6 +1,6 @@ use eframe::{ App, Frame, - egui::{Align, CentralPanel, Color32, ComboBox, Context, Id, Layout, Sides, Ui}, + egui::{Align, CentralPanel, Color32, ComboBox, Context, Id, Layout, Sense, Sides, Ui}, }; use crate::{ @@ -49,6 +49,13 @@ impl App for ZeroSplitter { ctx.data_mut(|data| data.insert_temp(prev_mode_id, cur_mode)); CentralPanel::default().show(ctx, |ui| { + if !self.toggles.decorations + && ui + .interact(ui.max_rect(), Id::new("window_drag"), Sense::drag()) + .dragged() + { + ctx.send_viewport_cmd(eframe::egui::ViewportCommand::StartDrag); + } ui.with_layout(Layout::top_down_justified(Align::Min), |ui| { ui.horizontal(|ui| { ui.toggle_value(&mut self.toggles.relative_score, "RELATIVE") @@ -57,13 +64,15 @@ impl App for ZeroSplitter { .on_hover_text("Show your PB's splits or your best splits on the left"); ui.toggle_value(&mut self.toggles.names, "NAMES") .on_hover_text("Toggle descriptive or number names for WV splits"); - let deco_toggle = ui - .toggle_value(&mut self.toggles.decorations, "TITLE") - .on_hover_text("Toggle the titlebar of the program"); - if deco_toggle.changed() && self.toggles.decorations { - ctx.send_viewport_cmd(eframe::egui::ViewportCommand::Decorations(true)); - } else if deco_toggle.changed() { - ctx.send_viewport_cmd(eframe::egui::ViewportCommand::Decorations(false)); + if CONFIG.get().unwrap().decoration_button { + let deco_toggle = ui + .toggle_value(&mut self.toggles.decorations, "DECOR") + .on_hover_text("Toggle the decoration (title bar...)"); + if deco_toggle.changed() && self.toggles.decorations { + ctx.send_viewport_cmd(eframe::egui::ViewportCommand::Decorations(true)); + } else if deco_toggle.changed() { + ctx.send_viewport_cmd(eframe::egui::ViewportCommand::Decorations(false)); + } } }); ui.horizontal(|ui| { diff --git a/splitter/src/config.rs b/splitter/src/config.rs index d40b115..014229b 100644 --- a/splitter/src/config.rs +++ b/splitter/src/config.rs @@ -27,6 +27,10 @@ pub fn load_config() -> Result<(), ZeroError> { Some(Value::Float(f)) => *f as f32, _ => 1.0, }, + decoration_button: match table.get("decoration_button") { + Some(Value::Boolean(b)) => *b, + _ => false, + }, }; CONFIG.set(config).map_err(|_| ZeroError::StaticAlreadyInit)?; @@ -45,4 +49,5 @@ fn create_config() -> Result { pub struct Config { pub zoom_level: f32, + pub decoration_button: bool, } From 212bf21fc5f0db4c13c173963a1a8d94d7b8e7f9 Mon Sep 17 00:00:00 2001 From: lily_and_doll <103815266+lily-and-doll@users.noreply.github.com> Date: Mon, 24 Nov 2025 18:44:56 -0500 Subject: [PATCH 38/52] Fix some misc. nitpicks --- README.md | 2 +- TODO.txt | 3 +++ splitter/src/app.rs | 2 ++ splitter/src/ui.rs | 2 +- 4 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 TODO.txt diff --git a/README.md b/README.md index 08a478b..73a0834 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ A "category" is a set of splits and personal bests to run against. ZeroSplitter you are playing and not overwrite scores from one mode with another - but don't push your luck: have the right category selected before you take off. -Press the plus button to add a new category - don't click Black Onion for the mode (it won't work at all). +Press the plus button to add a new category. Currently the only way to delete categories is by manually dropping them from the database, but you can rename them. diff --git a/TODO.txt b/TODO.txt new file mode 100644 index 0000000..9d979a4 --- /dev/null +++ b/TODO.txt @@ -0,0 +1,3 @@ +Add deleting categories +Rework the pop up menus +Add a settings menu? \ No newline at end of file diff --git a/splitter/src/app.rs b/splitter/src/app.rs index 24ed7fc..a72165d 100644 --- a/splitter/src/app.rs +++ b/splitter/src/app.rs @@ -101,6 +101,8 @@ impl App for ZeroSplitter { if let Ok(data) = self.calculate_splits() { self.display_splits(ui, data); + } else { + ui.centered_and_justified(|ui| ui.label("Waiting for a run to start...")); }; ui.label(format!( diff --git a/splitter/src/ui.rs b/splitter/src/ui.rs index 210b499..c987078 100644 --- a/splitter/src/ui.rs +++ b/splitter/src/ui.rs @@ -49,7 +49,7 @@ pub fn category_maker_dialog(ctx: &Context, tx: Sender>, .show_ui(ui, |ui| { ui.selectable_value(&mut mode, Gamemode::GreenOrange, "Green Orange"); ui.selectable_value(&mut mode, Gamemode::WhiteVanilla, "White Vanilla"); - ui.selectable_value(&mut mode, Gamemode::BlackOnion, "Black Onion"); + // ui.selectable_value(&mut mode, Gamemode::BlackOnion, "Black Onion"); }); }) }); From c6aed063541b3102e8d1add634a7764ebffa438b Mon Sep 17 00:00:00 2001 From: lily_and_doll <103815266+lily-and-doll@users.noreply.github.com> Date: Mon, 24 Nov 2025 20:59:16 -0500 Subject: [PATCH 39/52] Fix the rank collection --- splitter/src/app.rs | 7 +++++-- splitter/src/database.rs | 5 ++--- splitter/src/main.rs | 2 +- splitter/src/run.rs | 23 +++++++++++++++-------- 4 files changed, 23 insertions(+), 14 deletions(-) diff --git a/splitter/src/app.rs b/splitter/src/app.rs index a72165d..90ef8f8 100644 --- a/splitter/src/app.rs +++ b/splitter/src/app.rs @@ -102,7 +102,10 @@ impl App for ZeroSplitter { if let Ok(data) = self.calculate_splits() { self.display_splits(ui, data); } else { - ui.centered_and_justified(|ui| ui.label("Waiting for a run to start...")); + ui.centered_and_justified(|ui| { + ui.style_mut().interaction.selectable_labels = false; + ui.label("Waiting for a run to start...") + }); }; ui.label(format!( @@ -168,7 +171,7 @@ impl ZeroSplitter { fn calculate_splits(&self) -> Result, ZeroError> { // relative split: score gained during one split // absolute split: total score during one split - let raw_splits = self.run.splits()?; + let raw_splits = self.run.scores()?; let mut ret = Vec::new(); for (i, rel_split, abs_split) in raw_splits.iter().enumerate().map(|(i, &s)| { (i, s, { diff --git a/splitter/src/database.rs b/splitter/src/database.rs index e458d60..b6c850b 100644 --- a/splitter/src/database.rs +++ b/splitter/src/database.rs @@ -147,12 +147,11 @@ impl Database { let run_id = self.conn.last_insert_rowid(); - for (num, &split) in run.splits().unwrap().iter().take_while(|&&s| s > 0).enumerate() { + for (num, &split) in run.splits().unwrap().iter().take_while(|&&s| s.score > 0).enumerate() { let final_split = num == run.current_split().unwrap(); - let mult = run.mults().unwrap()[num]; self.conn.execute( "INSERT INTO splits (id, split_num, score, hits, mult, run_id, final, pattern_rank, dynamic_rank) VALUES(NULL, ?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)", - params![num, split, 0, mult, run_id, final_split, ], + params![num, split.score, 0, split.mult, run_id, final_split, split.pattern_rank, split.dynamic_rank], )?; } diff --git a/splitter/src/main.rs b/splitter/src/main.rs index b0c8fef..1ade1b3 100644 --- a/splitter/src/main.rs +++ b/splitter/src/main.rs @@ -138,7 +138,7 @@ impl ZeroSplitter { } fn save_splits(&mut self) { - if self.run.is_active() && self.run.splits().unwrap().iter().sum::() > 0 { + if self.run.is_active() && self.run.scores().unwrap().iter().sum::() > 0 { debug!("Saving splits"); if let Err(err) = self.db.insert_run(&self.categories, &self.run) { diff --git a/splitter/src/run.rs b/splitter/src/run.rs index 6ae2e49..0252ba2 100644 --- a/splitter/src/run.rs +++ b/splitter/src/run.rs @@ -22,10 +22,17 @@ impl Run { } } - pub fn splits(&self) -> Result, ZeroError> { + pub fn scores(&self) -> Result, ZeroError> { match self { Run::Inactive => Err(ZeroError::RunInactive), - Run::Active { splits, .. } => Ok(splits.iter().map(|s| s.score).collect()), + Run::Active { splits, .. } => Ok(splits.iter().map(|x| x.score).collect()), + } + } + + pub fn splits(&self) -> Result, ZeroError> { + match self { + Run::Inactive => Err(ZeroError::RunInactive), + Run::Active { splits, .. } => Ok(splits.iter().map(|&s| s).collect()), } } @@ -79,12 +86,12 @@ impl Run { *split_base_score = 0 } let data = SplitData::new( - frame.total_score(), + frame.total_score() - *split_base_score, frame.multiplier_one, frame.pattern_rank, frame.dynamic_rank, ); - splits.insert(*current_split, data); + *splits.get_mut(*current_split).unwrap() = data; Ok(()) } else { @@ -150,10 +157,10 @@ impl Run { #[derive(Clone, Copy, Debug, PartialEq)] pub struct SplitData { - score: i32, - mult: u32, - pattern_rank: f32, - dynamic_rank: f32, + pub score: i32, + pub mult: u32, + pub pattern_rank: f32, + pub dynamic_rank: f32, } impl SplitData { From 3ac948f3acc11801bb63ad7c4ebf3c22482a42c0 Mon Sep 17 00:00:00 2001 From: lily_and_doll <103815266+lily-and-doll@users.noreply.github.com> Date: Mon, 24 Nov 2025 22:43:06 -0500 Subject: [PATCH 40/52] Remove dead function --- splitter/src/run.rs | 7 ------- 1 file changed, 7 deletions(-) diff --git a/splitter/src/run.rs b/splitter/src/run.rs index 0252ba2..47d2567 100644 --- a/splitter/src/run.rs +++ b/splitter/src/run.rs @@ -36,13 +36,6 @@ impl Run { } } - pub fn mults(&self) -> Result, ZeroError> { - match self { - Run::Inactive => Err(ZeroError::RunInactive), - Run::Active { splits, .. } => Ok(splits.iter().map(|s| s.mult).collect()), - } - } - pub fn start(&mut self, frame: FrameData) { let mode = frame.difficulty.into(); *self = Self::Active { From acad3ce51e0fef60e569439bf1ff1d93cc40787d Mon Sep 17 00:00:00 2001 From: lily_and_doll <103815266+lily-and-doll@users.noreply.github.com> Date: Wed, 26 Nov 2025 02:58:03 -0500 Subject: [PATCH 41/52] Add settings menu and run importer --- Cargo.lock | 6 ++-- TODO.txt | 2 +- splitter/Cargo.toml | 2 +- splitter/sql/import_run.sql | 1 + splitter/sql/import_split.sql | 1 + splitter/src/app.rs | 29 ++++++++++++++-- splitter/src/config.rs | 58 ++++++++++++++++++++++++++++++- splitter/src/database.rs | 64 +++++++++++++++++++++++++++++++++-- splitter/src/main.rs | 10 ++---- splitter/src/theme.rs | 8 +++-- 10 files changed, 161 insertions(+), 20 deletions(-) create mode 100644 splitter/sql/import_run.sql create mode 100644 splitter/sql/import_split.sql diff --git a/Cargo.lock b/Cargo.lock index 84043d0..2f4e7ab 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -229,9 +229,9 @@ dependencies = [ [[package]] name = "clipboard-win" -version = "5.4.0" +version = "5.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15efe7a882b08f34e38556b14f2fb3daa98769d06c7f0c1b076dfd0d983bc892" +checksum = "bde03770d3df201d4fb868f2c9c59e66a3e4e2bd06692a0fe701e7103c7e84d4" dependencies = [ "error-code", ] @@ -2852,7 +2852,7 @@ dependencies = [ [[package]] name = "zerosplitter" -version = "0.2.4" +version = "0.3.1" dependencies = [ "bytemuck", "common", diff --git a/TODO.txt b/TODO.txt index 9d979a4..8901c46 100644 --- a/TODO.txt +++ b/TODO.txt @@ -1,3 +1,3 @@ Add deleting categories Rework the pop up menus -Add a settings menu? \ No newline at end of file +Add log file and remove terminal window \ No newline at end of file diff --git a/splitter/Cargo.toml b/splitter/Cargo.toml index cabdf82..e526161 100644 --- a/splitter/Cargo.toml +++ b/splitter/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "zerosplitter" -version = "0.2.4" +version = "0.3.1" edition = "2024" [dependencies] diff --git a/splitter/sql/import_run.sql b/splitter/sql/import_run.sql new file mode 100644 index 0000000..7a3620a --- /dev/null +++ b/splitter/sql/import_run.sql @@ -0,0 +1 @@ +INSERT INTO runs (category, imported) VALUES (?1, true) \ No newline at end of file diff --git a/splitter/sql/import_split.sql b/splitter/sql/import_split.sql new file mode 100644 index 0000000..3107628 --- /dev/null +++ b/splitter/sql/import_split.sql @@ -0,0 +1 @@ +INSERT INTO splits (split_num, score, run_id) VALUES (?1, ?2, ?3) \ No newline at end of file diff --git a/splitter/src/app.rs b/splitter/src/app.rs index 90ef8f8..d34f336 100644 --- a/splitter/src/app.rs +++ b/splitter/src/app.rs @@ -5,7 +5,7 @@ use eframe::{ use crate::{ Gamemode, Run, ZeroError, ZeroSplitter, - config::CONFIG, + config::{CONFIG, options_menu}, theme::{DARK_GREEN, DARK_ORANGE, DARKER_GREEN, DARKER_ORANGE, GREEN, LIGHT_ORANGE}, ui::{category_maker_dialog, confirm_dialog}, vanilla_descriptive_split_names, vanilla_split_names, @@ -16,6 +16,19 @@ pub struct Toggles { pub relative_score: bool, pub show_gold_split: bool, pub decorations: bool, + pub show_options_menu: bool, +} + +impl Default for Toggles { + fn default() -> Self { + Self { + names: false, + relative_score: true, + show_gold_split: true, + decorations: true, + show_options_menu: false, + } + } } impl App for ZeroSplitter { @@ -42,10 +55,10 @@ impl App for ZeroSplitter { }, Gamemode::BlackOnion => todo!(), }; - ctx.send_viewport_cmd(eframe::egui::ViewportCommand::MinInnerSize(min_size)); ctx.send_viewport_cmd(eframe::egui::ViewportCommand::InnerSize(min_size)); self.reset(); } + ctx.data_mut(|data| data.insert_temp(prev_mode_id, cur_mode)); CentralPanel::default().show(ctx, |ui| { @@ -56,7 +69,19 @@ impl App for ZeroSplitter { { ctx.send_viewport_cmd(eframe::egui::ViewportCommand::StartDrag); } + + if self.toggles.show_options_menu { + options_menu(ctx, &self.db, &mut self.toggles.show_options_menu); + }; + ui.with_layout(Layout::top_down_justified(Align::Min), |ui| { + ui.horizontal_top(|ui| { + ui.with_layout(Layout::right_to_left(Align::Min), |ui| { + if ui.button("⚙").clicked() { + self.toggles.show_options_menu = true; + } + }); + }); ui.horizontal(|ui| { ui.toggle_value(&mut self.toggles.relative_score, "RELATIVE") .on_hover_text("Display relative score per split or running total of score"); diff --git a/splitter/src/config.rs b/splitter/src/config.rs index 014229b..472083c 100644 --- a/splitter/src/config.rs +++ b/splitter/src/config.rs @@ -4,9 +4,12 @@ use std::{ sync::OnceLock, }; +use eframe::egui::{ + Context, Id, Modal, ModalResponse, Response, RichText, Separator, TextEdit, Ui, ViewportBuilder, ViewportId, +}; use toml::{Table, Value}; -use crate::ZeroError; +use crate::{ZeroError, theme::GREEN}; pub static CONFIG: OnceLock = OnceLock::new(); @@ -51,3 +54,56 @@ pub struct Config { pub zoom_level: f32, pub decoration_button: bool, } + +pub fn options_menu(ctx: &Context, db: &crate::database::Database, open: &mut bool) -> () { + ctx.show_viewport_immediate( + ViewportId::from_hash_of("options_menu_viewport"), + ViewportBuilder::default().with_title("Options"), + |ctx, _| { + eframe::egui::CentralPanel::default().show(ctx, |ui| { + // IMPORTER + ui.horizontal(|ui| { + ui.label(RichText::new("Importer").color(GREEN).heading()); + ui.add(Separator::default().horizontal()) + }); + // Category name + let category_name_id = ui.label("Category name").id; + let mut category_name = ctx + .data(|data| data.get_temp::(category_name_id)) + .unwrap_or_default(); + ui.add(TextEdit::singleline(&mut category_name)); + ctx.data_mut(|data| data.insert_temp(category_name_id, category_name.clone())); + + // Split data + let splits_data_id = ui.label("Split scores").id; + let mut run_string = ctx + .data(|data| data.get_temp::(splits_data_id)) + .unwrap_or_default(); + ui.add(TextEdit::multiline(&mut run_string).hint_text("111, 222, 333, 444, 555, 666, 777, 888")); + ctx.data_mut(|data| data.insert_temp(splits_data_id, run_string.clone())); + + if ui.button("IMPORT").clicked() { + let splits = run_string + .split(", ") + .map(|s| s.parse().unwrap_or_default()) + .collect::>(); + match db.import_run(splits.clone(), &category_name) { + Ok(_) => { + println!( + "Successfully imported run with {} splits and total score {}", + splits.len(), + splits.iter().sum::() + ); + run_string.clear(); + } + Err(err) => println!("Import failed: {err}"), + }; + }; + }); + + if ctx.input(|i| i.viewport().close_requested()) { + *open = false + }; + }, + ); +} diff --git a/splitter/src/database.rs b/splitter/src/database.rs index b6c850b..3bdb2c1 100644 --- a/splitter/src/database.rs +++ b/splitter/src/database.rs @@ -6,14 +6,37 @@ use rusqlite::{ types::{FromSql, ValueRef}, }; -use crate::{Category, CategoryManager, Gamemode, Run}; +use crate::{Category, CategoryManager, Gamemode, Run, config::CONFIG}; #[derive(Clone)] pub struct Database { conn: Arc, } -const CURRENT_SCHEMA_VERSION: i32 = 3; +macro_rules! transaction { + ($conn:expr, $inner:expr) => {{ + $conn.execute("BEGIN TRANSACTION", ())?; + + match (|| { + { + $inner + } + + Ok::<(), rusqlite::Error>(()) + })() { + Ok(_) => { + $conn.execute("COMMIT", ())?; + Ok(()) + } + Err(err) => { + $conn.execute("ROLLBACK", ())?; + Err(err) + } + } + }}; +} + +const CURRENT_SCHEMA_VERSION: i32 = 4; impl Database { pub fn init() -> Result { @@ -141,7 +164,7 @@ impl Database { let res = stmt.query_one(params![category.name], |row| row.get::(0))?; self.conn.execute( - "INSERT INTO runs (id, category, datetime) VALUES(NULL, ?1, datetime('now'))", + "INSERT INTO runs (id, category, datetime, imported) VALUES(NULL, ?1, datetime('now'), false)", params![res], )?; @@ -233,6 +256,10 @@ impl Database { self.migrate2to3()?; current_schema = 3 } + 3 => { + self.migrate3to4()?; + current_schema = 4 + } _ => Err(rusqlite::Error::InvalidQuery)?, }; } @@ -269,6 +296,37 @@ impl Database { ALTER TABLE splits ADD COLUMN dynamic_rank REAL;", ) } + + fn migrate3to4(&self) -> Result { + println!("Migrating schema 3 to 4..."); + self.conn.pragma_update(Some("main"), "user_version", 4)?; + self.conn.execute("ALTER TABLE runs ADD COLUMN imported BOOLEAN", ()) + } + + pub fn import_run(&self, splits: Vec, category_name: &String) -> Result<()> { + transaction!(self.conn, { + let category_id = + self.conn + .query_one("SELECT id FROM categories WHERE name=?1", params![category_name], |r| { + r.get::<_, i32>(0) + })?; + self.conn + .execute( + "INSERT INTO runs (category, imported) VALUES (?1, true)", + params![category_id], + ) + .unwrap(); + let last = self.conn.last_insert_rowid(); + for (idx, score) in splits.iter().enumerate() { + self.conn + .execute( + "INSERT INTO splits (split_num, score, run_id) VALUES (?1, ?2, ?3)", + params![idx as i32, score, last], + ) + .unwrap(); + } + }) + } } impl ToSql for Gamemode { diff --git a/splitter/src/main.rs b/splitter/src/main.rs index 1ade1b3..43a2437 100644 --- a/splitter/src/main.rs +++ b/splitter/src/main.rs @@ -34,6 +34,7 @@ static EGUI_CTX: OnceLock = OnceLock::new(); fn main() { config::load_config().unwrap(); + let zoom_level = CONFIG.get().unwrap().zoom_level; #[cfg(debug_assertions)] unsafe { env::set_var("RUST_BACKTRACE", "1"); @@ -43,7 +44,7 @@ fn main() { let options = NativeOptions { viewport: ViewportBuilder::default() - .with_inner_size([300., 300.]) + .with_inner_size([300.0 * zoom_level, 300.0 * zoom_level]) .with_icon(IconData::default()) .with_title("ZeroSplitter"), @@ -118,12 +119,7 @@ impl ZeroSplitter { waiting_for_confirm: false, split_delay: None, db, - toggles: Toggles { - names: false, - relative_score: true, - show_gold_split: true, - decorations: true, - }, + toggles: Default::default(), }; zerosplitter.categories.load(&zerosplitter.db).unwrap(); diff --git a/splitter/src/theme.rs b/splitter/src/theme.rs index 324d667..453b346 100644 --- a/splitter/src/theme.rs +++ b/splitter/src/theme.rs @@ -27,7 +27,7 @@ pub const BLACK: Color32 = Color32::BLACK; pub fn zeroranger_visuals() -> egui::Visuals { egui::Visuals { window_fill: BLACK, - extreme_bg_color: BLACK, + extreme_bg_color: GREENEST, panel_fill: BLACK, widgets: egui::style::Widgets { inactive: egui::style::WidgetVisuals { @@ -66,7 +66,11 @@ pub fn zeroranger_visuals() -> egui::Visuals { noninteractive: WidgetVisuals { bg_fill: LIGHT_ORANGE, weak_bg_fill: LIGHT_ORANGE, - bg_stroke: Stroke { ..Default::default() }, + bg_stroke: Stroke { + color: GREEN, + width: 1.0, + ..Default::default() + }, corner_radius: CornerRadius::ZERO, fg_stroke: Stroke { width: 1.0, From 7f066f6d4c7b1a3aaa061f244d0ac000bbc78faf Mon Sep 17 00:00:00 2001 From: lillies <103815266+lily-and-doll@users.noreply.github.com> Date: Wed, 26 Nov 2025 03:11:47 -0500 Subject: [PATCH 42/52] Update README.md --- README.md | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 73a0834..be80f69 100644 --- a/README.md +++ b/README.md @@ -17,8 +17,14 @@ Continues should be tracked properly in Green Orange but not White Vanilla. Co-op should work, but has not been tested. If you find co-op to work, let me know. +# Your Data Your data is stored in the `sqlite.db3` next to the .exe. You can manually insert, delete, or modify any data in the database if you like. +When updating the program, just put the new `zerosplitter.exe` and `payload.dll` in the same folder as your old `sqlite.db3` and `config.toml` files. +The database file will automatically be updated and will not be able to be used with older versions of the program. +If you want to move the program to another folder, just copy all the files in the folder. + +# Categories A "category" is a set of splits and personal bests to run against. ZeroSplitter will try to detect which mode you are playing and not overwrite scores from one mode with another - but don't push your luck: have the right category selected before you take off. @@ -27,13 +33,21 @@ Press the plus button to add a new category. Currently the only way to delete categories is by manually dropping them from the database, but you can rename them. +# Toggles The "relative" button switches the display between showing your score per split or your running total up to each split. Turn on relative mode to see how much better or worse you did each split versus your PB run. Turn off relative mode to see how far ahead or behind you are versus your PB run. The relative button only changes the display, not how the data is saved: toggle it as much as you like, even mid-run. -If you want to move the program to another folder, just copy all the files in the folder. +The Best Splits button switches the left-hand score display between your best score for a split or the splits of your PB. + +The Names button switches the split names between numbers (1-2) and names (Cloudoos). White Vanilla only. + +# Options +The gear button in the top right opens up the options menu. +You can import previously recorded runs by putting a category name and a list of scores into the two boxes. +Seperate each score with a comma and space like shown in the hint. # How to build from source Just run `cargo run --release` in the top level of the repository, next to this `README.md`. `build.sh` will zip `zerosplitter.exe` From 49c688f930c6125d7636f21b15b2bbfca7da64ab Mon Sep 17 00:00:00 2001 From: lily_and_doll <103815266+lily-and-doll@users.noreply.github.com> Date: Wed, 26 Nov 2025 23:35:49 -0500 Subject: [PATCH 43/52] Add update checker to the options menu --- Cargo.lock | 825 +++++++++++++++++- splitter/Cargo.toml | 4 +- .../config_sections/check_for_updates.toml | 4 + .../decoration_button.toml} | 12 +- splitter/assets/config_sections/heading.toml | 4 + .../assets/config_sections/zoom_level.toml | 4 + splitter/src/config.rs | 66 +- splitter/src/main.rs | 36 +- splitter/src/update.rs | 50 ++ 9 files changed, 956 insertions(+), 49 deletions(-) create mode 100644 splitter/assets/config_sections/check_for_updates.toml rename splitter/assets/{default_config.toml => config_sections/decoration_button.toml} (51%) create mode 100644 splitter/assets/config_sections/heading.toml create mode 100644 splitter/assets/config_sections/zoom_level.toml create mode 100644 splitter/src/update.rs diff --git a/Cargo.lock b/Cargo.lock index 2f4e7ab..270d516 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -104,6 +104,12 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + [[package]] name = "bitflags" version = "1.3.2" @@ -297,7 +303,7 @@ dependencies = [ "bitflags 1.3.2", "core-foundation 0.9.4", "core-graphics-types", - "foreign-types", + "foreign-types 0.5.0", "libc", ] @@ -485,6 +491,15 @@ dependencies = [ "bytemuck", ] +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + [[package]] name = "env_logger" version = "0.10.2" @@ -556,6 +571,12 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + [[package]] name = "fdeflate" version = "0.3.7" @@ -575,12 +596,27 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + [[package]] name = "foldhash" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared 0.1.1", +] + [[package]] name = "foreign-types" version = "0.5.0" @@ -588,7 +624,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" dependencies = [ "foreign-types-macros", - "foreign-types-shared", + "foreign-types-shared 0.3.1", ] [[package]] @@ -602,6 +638,12 @@ dependencies = [ "syn", ] +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + [[package]] name = "foreign-types-shared" version = "0.3.1" @@ -617,6 +659,56 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + [[package]] name = "gethostname" version = "0.4.3" @@ -627,6 +719,17 @@ dependencies = [ "windows-targets 0.48.5", ] +[[package]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", +] + [[package]] name = "getrandom" version = "0.3.2" @@ -636,7 +739,7 @@ dependencies = [ "cfg-if", "libc", "r-efi", - "wasi", + "wasi 0.14.2+wasi-0.2.4", ] [[package]] @@ -715,6 +818,25 @@ dependencies = [ "gl_generator", ] +[[package]] +name = "h2" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "hashbrown" version = "0.15.5" @@ -752,20 +874,130 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fbd780fe5cc30f81464441920d82ac8740e2e46b29a6fad543ddd075229ce37e" [[package]] -name = "home" -version = "0.5.11" +name = "http" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" dependencies = [ - "windows-sys 0.59.0", + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", ] +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + [[package]] name = "humantime" version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b112acc8b3adf4b107a8ec20977da0273a8c386765a3ec0229bd500a1443f9f" +[[package]] +name = "hyper" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "pin-utils", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52e9a2a24dc5c6821e71a7030e1e14b7b632acac55c40e9d2e082c621261bb56" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "system-configuration", + "tokio", + "tower-service", + "tracing", + "windows-registry", +] + [[package]] name = "icu_collections" version = "1.5.0" @@ -928,6 +1160,22 @@ dependencies = [ "hashbrown 0.16.0", ] +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + +[[package]] +name = "iri-string" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f867b9d1d896b67beb18518eda36fdb77a32ea590de864f1325b294a6d14397" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "is-terminal" version = "0.4.16" @@ -973,7 +1221,7 @@ version = "0.1.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a" dependencies = [ - "getrandom", + "getrandom 0.3.2", "libc", ] @@ -1001,9 +1249,9 @@ checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" [[package]] name = "libc" -version = "0.2.171" +version = "0.2.177" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6" +checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" [[package]] name = "libloading" @@ -1101,6 +1349,12 @@ dependencies = [ "autocfg", ] +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + [[package]] name = "miniz_oxide" version = "0.8.7" @@ -1111,6 +1365,34 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "mio" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873" +dependencies = [ + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", + "windows-sys 0.61.2", +] + +[[package]] +name = "native-tls" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + [[package]] name = "ndk" version = "0.9.0" @@ -1451,6 +1733,50 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "openssl" +version = "0.10.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328" +dependencies = [ + "bitflags 2.9.0", + "cfg-if", + "foreign-types 0.3.2", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "openssl-probe" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" + +[[package]] +name = "openssl-sys" +version = "0.9.111" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "orbclient" version = "0.3.48" @@ -1532,6 +1858,12 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + [[package]] name = "pkg-config" version = "0.3.32" @@ -1677,6 +2009,62 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +[[package]] +name = "reqwest" +version = "0.12.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d0946410b9f7b082a427e4ef5c8ff541a88b357bc6c637c40db3a68ac70a36f" +dependencies = [ + "base64", + "bytes", + "encoding_rs", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-tls", + "hyper-util", + "js-sys", + "log", + "mime", + "native-tls", + "percent-encoding", + "pin-project-lite", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-native-tls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.16", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + [[package]] name = "rusqlite" version = "0.37.0" @@ -1714,7 +2102,40 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.11.0", - "windows-sys 0.59.0", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f" +dependencies = [ + "once_cell", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94182ad936a0c91c324cd46c6511b9510ed16af436d7b5bab34beab0afd55f7a" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", ] [[package]] @@ -1738,6 +2159,15 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "schannel" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "scoped-tls" version = "1.0.1" @@ -1750,6 +2180,35 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags 2.9.0", + "core-foundation 0.9.4", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + [[package]] name = "serde" version = "1.0.228" @@ -1801,6 +2260,18 @@ dependencies = [ "serde_core", ] +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + [[package]] name = "shlex" version = "1.3.0" @@ -1882,6 +2353,16 @@ dependencies = [ "serde", ] +[[package]] +name = "socket2" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + [[package]] name = "stable_deref_trait" version = "1.2.0" @@ -1894,6 +2375,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + [[package]] name = "syn" version = "2.0.100" @@ -1905,6 +2392,15 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + [[package]] name = "synstructure" version = "0.13.1" @@ -1916,6 +2412,40 @@ dependencies = [ "syn", ] +[[package]] +name = "system-configuration" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +dependencies = [ + "bitflags 2.9.0", + "core-foundation 0.9.4", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "tempfile" +version = "3.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" +dependencies = [ + "fastrand", + "getrandom 0.3.2", + "once_cell", + "rustix 1.1.2", + "windows-sys 0.61.2", +] + [[package]] name = "termcolor" version = "1.4.1" @@ -1966,6 +2496,53 @@ dependencies = [ "zerovec", ] +[[package]] +name = "tokio" +version = "1.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + [[package]] name = "toml" version = "0.9.8" @@ -2022,6 +2599,51 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df8b2b54733674ad286d16267dcfc7a71ed5c776e4ac7aa3c3e2561f7c637bf2" +[[package]] +name = "tower" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cf146f99d442e8e68e585f5d798ccd3cad9a7835b917e09728880a862706456" +dependencies = [ + "bitflags 2.9.0", + "bytes", + "futures-util", + "http", + "http-body", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + [[package]] name = "tracing" version = "0.1.41" @@ -2037,6 +2659,15 @@ name = "tracing-core" version = "0.1.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "ttf-parser" @@ -2056,6 +2687,12 @@ version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + [[package]] name = "url" version = "2.5.4" @@ -2101,6 +2738,21 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + [[package]] name = "wasi" version = "0.14.2+wasi-0.2.4" @@ -2299,12 +2951,11 @@ dependencies = [ [[package]] name = "webbrowser" -version = "1.0.4" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5df295f8451142f1856b1bd86a606dfe9587d439bc036e319c827700dbd555e" +checksum = "00f1243ef785213e3a32fa0396093424a3a6ea566f9948497e5a2309261a4c97" dependencies = [ "core-foundation 0.10.0", - "home", "jni", "log", "ndk-context", @@ -2360,7 +3011,7 @@ dependencies = [ "windows-collections", "windows-core", "windows-future", - "windows-link", + "windows-link 0.1.1", "windows-numerics", ] @@ -2381,9 +3032,9 @@ checksum = "ca21a92a9cae9bf4ccae5cf8368dce0837100ddf6e6d57936749e85f152f6247" dependencies = [ "windows-implement", "windows-interface", - "windows-link", - "windows-result", - "windows-strings", + "windows-link 0.1.1", + "windows-result 0.3.2", + "windows-strings 0.3.1", ] [[package]] @@ -2393,7 +3044,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a787db4595e7eb80239b74ce8babfb1363d8e343ab072f2ffe901400c03349f0" dependencies = [ "windows-core", - "windows-link", + "windows-link 0.1.1", ] [[package]] @@ -2424,6 +3075,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + [[package]] name = "windows-numerics" version = "0.1.1" @@ -2431,7 +3088,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "005dea54e2f6499f2cee279b8f703b3cf3b5734a2d8d21867c8f44003182eeed" dependencies = [ "windows-core", - "windows-link", + "windows-link 0.1.1", +] + +[[package]] +name = "windows-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" +dependencies = [ + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", ] [[package]] @@ -2440,7 +3108,16 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c64fd11a4fd95df68efcfee5f44a294fe71b8bc6a91993e2791938abcc712252" dependencies = [ - "windows-link", + "windows-link 0.1.1", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link 0.2.1", ] [[package]] @@ -2449,7 +3126,16 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319" dependencies = [ - "windows-link", + "windows-link 0.1.1", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link 0.2.1", ] [[package]] @@ -2479,6 +3165,24 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link 0.2.1", +] + [[package]] name = "windows-targets" version = "0.42.2" @@ -2518,13 +3222,30 @@ dependencies = [ "windows_aarch64_gnullvm 0.52.6", "windows_aarch64_msvc 0.52.6", "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm", + "windows_i686_gnullvm 0.52.6", "windows_i686_msvc 0.52.6", "windows_x86_64_gnu 0.52.6", "windows_x86_64_gnullvm 0.52.6", "windows_x86_64_msvc 0.52.6", ] +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link 0.2.1", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.42.2" @@ -2543,6 +3264,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + [[package]] name = "windows_aarch64_msvc" version = "0.42.2" @@ -2561,6 +3288,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + [[package]] name = "windows_i686_gnu" version = "0.42.2" @@ -2579,12 +3312,24 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + [[package]] name = "windows_i686_msvc" version = "0.42.2" @@ -2603,6 +3348,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + [[package]] name = "windows_x86_64_gnu" version = "0.42.2" @@ -2621,6 +3372,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + [[package]] name = "windows_x86_64_gnullvm" version = "0.42.2" @@ -2639,6 +3396,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + [[package]] name = "windows_x86_64_msvc" version = "0.42.2" @@ -2657,6 +3420,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + [[package]] name = "winit" version = "0.30.12" @@ -2850,16 +3619,24 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + [[package]] name = "zerosplitter" -version = "0.3.1" +version = "0.3.2" dependencies = [ "bytemuck", "common", "eframe", "log", "pretty_env_logger", + "reqwest", "rusqlite", + "semver", "serde", "serde_json", "toml", diff --git a/splitter/Cargo.toml b/splitter/Cargo.toml index e526161..85aacd9 100644 --- a/splitter/Cargo.toml +++ b/splitter/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "zerosplitter" -version = "0.3.1" +version = "0.3.2" edition = "2024" [dependencies] @@ -12,6 +12,8 @@ serde = {version = "1.0", features = ["derive"]} serde_json = "1.0" rusqlite = { version = "0.37.0", features = ["bundled"] } toml = "0.9.8" +reqwest = { version = "0.12.24", features = ["blocking", "json"] } +semver = "1.0.27" [dependencies.eframe] version = "0.31" diff --git a/splitter/assets/config_sections/check_for_updates.toml b/splitter/assets/config_sections/check_for_updates.toml new file mode 100644 index 0000000..f29d5b0 --- /dev/null +++ b/splitter/assets/config_sections/check_for_updates.toml @@ -0,0 +1,4 @@ +# Checks for updates on launch. If an update is avaliable, a link will +# be in the Options menu. +check_for_updates = true + diff --git a/splitter/assets/default_config.toml b/splitter/assets/config_sections/decoration_button.toml similarity index 51% rename from splitter/assets/default_config.toml rename to splitter/assets/config_sections/decoration_button.toml index be55179..5ffc263 100644 --- a/splitter/assets/default_config.toml +++ b/splitter/assets/config_sections/decoration_button.toml @@ -1,12 +1,3 @@ -# Configuration for ZeroSplitter. Edit this file to change the settings -# (If you're unfamiliar with the format, this is a TOML file) -# https://en.wikipedia.org/wiki/TOML - -# Scale factor for the entire UI. 2.0 = twice as large as normal. -# The program is 300x300 in GO mode and 300x650 at 1.0 zoom, for reference -zoom_level = 1.0 - - # Adds a toggle in the top bar that turns off window decorations, e.g. # the title bar and side bars. When the decorations are hidden, you # can move the window by dragging it anywhere but cannot resize it @@ -14,4 +5,5 @@ zoom_level = 1.0 # Known issue: right clicking the window when decorations are hidden # makes dragging no longer work. Turn them back on and drag the # title bar to fix it. -decoration_button = false \ No newline at end of file +decoration_button = false + diff --git a/splitter/assets/config_sections/heading.toml b/splitter/assets/config_sections/heading.toml new file mode 100644 index 0000000..fe2be02 --- /dev/null +++ b/splitter/assets/config_sections/heading.toml @@ -0,0 +1,4 @@ +# Configuration for ZeroSplitter. Edit this file to change the settings +# (If you're unfamiliar with the format, this is a TOML file) +# https://en.wikipedia.org/wiki/TOML + diff --git a/splitter/assets/config_sections/zoom_level.toml b/splitter/assets/config_sections/zoom_level.toml new file mode 100644 index 0000000..93c6137 --- /dev/null +++ b/splitter/assets/config_sections/zoom_level.toml @@ -0,0 +1,4 @@ +# Scale factor for the entire UI. 2.0 = twice as large as normal. +# The program is 300x300 in GO mode and 300x650 at 1.0 zoom, for reference +zoom_level = 1.0 + diff --git a/splitter/src/config.rs b/splitter/src/config.rs index 472083c..492fb46 100644 --- a/splitter/src/config.rs +++ b/splitter/src/config.rs @@ -1,15 +1,17 @@ use std::{ - fs::{File, read_to_string}, + fs::{File, OpenOptions, read_to_string}, io::{Read, Write}, sync::OnceLock, }; +use crate::VERSION; + use eframe::egui::{ - Context, Id, Modal, ModalResponse, Response, RichText, Separator, TextEdit, Ui, ViewportBuilder, ViewportId, + Context, Id, RichText, Separator, TextEdit, ViewportBuilder, ViewportId, }; use toml::{Table, Value}; -use crate::{ZeroError, theme::GREEN}; +use crate::{ZeroError, theme::GREEN, update::check_for_updates}; pub static CONFIG: OnceLock = OnceLock::new(); @@ -23,16 +25,34 @@ pub fn load_config() -> Result<(), ZeroError> { _ => return Err(ZeroError::IOError(e)), }, }; - let table = config_str.parse::
().map_err(ZeroError::TOMLError)?; + let table = config_str.parse::
()?; + + let mut writer = OpenOptions::new().append(true).open(CONFIG_PATH)?; let config = Config { zoom_level: match table.get("zoom_level") { Some(Value::Float(f)) => *f as f32, - _ => 1.0, + None => { + writer.write_all(include_bytes!("../assets/config_sections/zoom_level.toml"))?; + 1.0 + } + _ => return Err(ZeroError::ConfigError("zoom_level".to_owned())), }, decoration_button: match table.get("decoration_button") { Some(Value::Boolean(b)) => *b, - _ => false, + None => { + writer.write_all(include_bytes!("../assets/config_sections/decoration_button.toml"))?; + false + } + _ => return Err(ZeroError::ConfigError("decoration_button".to_owned())), + }, + check_for_updates: match table.get("check_for_updates") { + Some(Value::Boolean(b)) => *b, + None => { + writer.write_all(include_bytes!("../assets/config_sections/check_for_updates.toml"))?; + true + } + _ => return Err(ZeroError::ConfigError("check_for_updates".to_owned())), }, }; @@ -41,11 +61,10 @@ pub fn load_config() -> Result<(), ZeroError> { } fn create_config() -> Result { - let mut file = File::create_new(CONFIG_PATH).map_err(ZeroError::IOError)?; - file.write_all(include_bytes!("../assets/default_config.toml")) - .map_err(ZeroError::IOError)?; + let mut file = File::create_new(CONFIG_PATH)?; + file.write_all(include_bytes!("../assets/config_sections/heading.toml"))?; let mut ret = String::new(); - file.read_to_string(&mut ret).map_err(ZeroError::IOError)?; + file.read_to_string(&mut ret)?; Ok(ret) } @@ -53,6 +72,7 @@ fn create_config() -> Result { pub struct Config { pub zoom_level: f32, pub decoration_button: bool, + pub check_for_updates: bool, } pub fn options_menu(ctx: &Context, db: &crate::database::Database, open: &mut bool) -> () { @@ -61,6 +81,32 @@ pub fn options_menu(ctx: &Context, db: &crate::database::Database, open: &mut bo ViewportBuilder::default().with_title("Options"), |ctx, _| { eframe::egui::CentralPanel::default().show(ctx, |ui| { + if CONFIG.get().unwrap().check_for_updates { + // UPDATER + ui.horizontal(|ui| { + ui.label(RichText::new("Updater").color(GREEN).heading()); + ui.add(Separator::default().horizontal()) + }); + ui.horizontal(|ui| { + let update_label_id = Id::new("update_label"); + if ui.button("Check for updates").clicked() { + ctx.data_mut(|data| { + data.insert_temp( + update_label_id, + match check_for_updates() { + Ok(Some(url)) => format!("Update avaliable - {url}"), + Ok(None) => format!("Up to date - {VERSION}"), + Err(e) => format!("Failed to check for update - {e:?}"), + }, + ) + }); + } + ui.label( + ctx.data(|data| data.get_temp::(update_label_id)) + .unwrap_or_default(), + ); + }); + }; // IMPORTER ui.horizontal(|ui| { ui.label(RichText::new("Importer").color(GREEN).heading()); diff --git a/splitter/src/main.rs b/splitter/src/main.rs index 43a2437..d4a17b8 100644 --- a/splitter/src/main.rs +++ b/splitter/src/main.rs @@ -27,9 +27,12 @@ mod run; mod system; mod theme; mod ui; +mod update; const SPLIT_DELAY_FRAMES: u32 = 20; +const VERSION: &str = env!("CARGO_PKG_VERSION"); + static EGUI_CTX: OnceLock = OnceLock::new(); fn main() { @@ -384,9 +387,7 @@ impl CategoryManager { #[must_use] pub fn push(&mut self, name: String, mode: Gamemode, db: &Database) -> Result<(), ZeroError> { - let id = db - .insert_new_category(name.clone(), mode) - .map_err(ZeroError::DatabaseError)?; + let id = db.insert_new_category(name.clone(), mode)?; self.categories.push(Category { name, mode, id }); Ok(()) } @@ -401,7 +402,7 @@ impl CategoryManager { /// Populate the CategoryManager with data from the database pub fn load(&mut self, db: &Database) -> Result<(), ZeroError> { - self.categories = db.get_categories().map_err(ZeroError::DatabaseError)?; + self.categories = db.get_categories()?; Ok(()) } @@ -472,4 +473,31 @@ enum ZeroError { IOError(std::io::Error), TOMLError(toml::de::Error), StaticAlreadyInit, + ReqwestError(reqwest::Error), + ParseError, + ConfigError(String), +} + +impl From for ZeroError { + fn from(value: reqwest::Error) -> Self { + ZeroError::ReqwestError(value) + } +} + +impl From for ZeroError { + fn from(value: rusqlite::Error) -> Self { + ZeroError::DatabaseError(value) + } +} + +impl From for ZeroError { + fn from(value: std::io::Error) -> Self { + ZeroError::IOError(value) + } +} + +impl From for ZeroError { + fn from(value: toml::de::Error) -> Self { + ZeroError::TOMLError(value) + } } diff --git a/splitter/src/update.rs b/splitter/src/update.rs new file mode 100644 index 0000000..5ff0690 --- /dev/null +++ b/splitter/src/update.rs @@ -0,0 +1,50 @@ +use reqwest::{Url, header::ACCEPT}; +use semver::Version; +use serde::Deserialize; + +use crate::{VERSION, ZeroError}; + +const LATEST_URL: &'static str = "https://api.github.com/repos/lily-and-doll/zerosplitter/releases/latest"; + +pub fn check_for_updates() -> Result, ZeroError> { + let response = reqwest::blocking::Client::builder() + .user_agent("ZeroSplitter") + .build()? + .get(LATEST_URL) + .header(ACCEPT, "application/vnd.github+json") + .send()?; + + let json = response.json::()?; + + if Version::parse(&json.tag_name).expect("Malformed tag on release!") + > VERSION.parse().expect("No package version found!") + { + let download_url = json + .assets + .zero + .browser_download_url + .parse::() + .map_err(|_| ZeroError::ParseError)?; + + Ok(Some(download_url)) + } else { + Ok(None) + } +} + +#[derive(Deserialize)] +struct LatestResponse { + tag_name: String, + assets: Assets, +} + +#[derive(Deserialize)] +struct Assets { + #[serde(rename = "0")] + zero: Asset, +} + +#[derive(Deserialize)] +struct Asset { + browser_download_url: String, +} From 1e71f9e74a8062e1e9c2510ed4bb5e21e40dcce2 Mon Sep 17 00:00:00 2001 From: lily_and_doll <103815266+lily-and-doll@users.noreply.github.com> Date: Sat, 29 Nov 2025 05:41:52 -0500 Subject: [PATCH 44/52] Fix PB retrieval failing on null mult --- splitter/src/database.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/splitter/src/database.rs b/splitter/src/database.rs index 3bdb2c1..4f20d78 100644 --- a/splitter/src/database.rs +++ b/splitter/src/database.rs @@ -6,7 +6,7 @@ use rusqlite::{ types::{FromSql, ValueRef}, }; -use crate::{Category, CategoryManager, Gamemode, Run, config::CONFIG}; +use crate::{Category, CategoryManager, Gamemode, Run}; #[derive(Clone)] pub struct Database { @@ -197,17 +197,17 @@ impl Database { let mut statement = self.conn.prepare(include_str!("../sql/pb_splits.sql"))?; let rows = statement.query_map(params![category.id], |row| { Ok(( - row.get::(0)?, - row.get::(1)?, - row.get::(2)?, + row.get::(0)?, //score + row.get::>(1)?, //mult + row.get::(2)?, //run_id row.get::(3)?, )) })?; - let splits: Vec<(i32, i32, i32, Gamemode)> = rows.map(|r| r.unwrap()).collect(); + let splits: Vec<(i32, Option, i32, Gamemode)> = rows.map(|r| r.unwrap()).collect(); if splits.len() > 0 { let scores: Vec = splits.iter().map(|s| s.0).collect(); - let _hits: Vec = splits.iter().map(|s| s.1).collect(); + let _hits: Vec = splits.iter().map(|s| s.1.unwrap_or(0)).collect(); let _run_id = splits[0].2; let mode = splits[0].3; From 9cd0f09ddacd7e79dc7598b64f5150ca90831af5 Mon Sep 17 00:00:00 2001 From: lily_and_doll <103815266+lily-and-doll@users.noreply.github.com> Date: Sat, 29 Nov 2025 16:42:15 -0500 Subject: [PATCH 45/52] Add testsing for database --- splitter/src/database.rs | 44 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 42 insertions(+), 2 deletions(-) diff --git a/splitter/src/database.rs b/splitter/src/database.rs index 4f20d78..3ec00f1 100644 --- a/splitter/src/database.rs +++ b/splitter/src/database.rs @@ -41,7 +41,10 @@ const CURRENT_SCHEMA_VERSION: i32 = 4; impl Database { pub fn init() -> Result { let database = Database { + #[cfg(not(test))] conn: Arc::new(Connection::open("./sqlite.db3")?), + #[cfg(test)] + conn: Arc::new(Connection::open_in_memory()?), }; // create tables if they don't exist @@ -197,9 +200,9 @@ impl Database { let mut statement = self.conn.prepare(include_str!("../sql/pb_splits.sql"))?; let rows = statement.query_map(params![category.id], |row| { Ok(( - row.get::(0)?, //score + row.get::(0)?, //score row.get::>(1)?, //mult - row.get::(2)?, //run_id + row.get::(2)?, //run_id row.get::(3)?, )) })?; @@ -350,3 +353,40 @@ impl FromSql for Gamemode { } } } + +#[cfg(test)] +#[allow(dead_code, unused)] +mod tests { + use std::sync::Arc; + + use rusqlite::Connection; + + use rusqlite::Result; + + use crate::Gamemode; + use crate::{ + Category, + database::{self, Database}, + }; + + #[test] + fn import_and_get_pb() -> Result<()> { + let db = Database::init()?; + + println!("Importing run..."); + db.import_run(vec![10, 20, 30, 40], &"default".to_string())?; + + let mut categories = crate::CategoryManager { + categories: vec![], + current: 0, + comparison_cache: vec![], + }; + categories.load(&db); + + println!("Getting PB..."); + let pb = db.get_pb_run(&categories)?; + + assert!(pb.0 == vec![10, 20, 30, 40]); + Ok(()) + } +} From 19b50c291220b350559fa950f2933a5257fcea0e Mon Sep 17 00:00:00 2001 From: lily_and_doll <103815266+lily-and-doll@users.noreply.github.com> Date: Sat, 29 Nov 2025 16:42:48 -0500 Subject: [PATCH 46/52] Make misc. changes --- TODO.txt | 4 +++- splitter/sql/pass_rate.sql | 8 ++++++++ splitter/src/config.rs | 4 +--- splitter/src/main.rs | 8 ++++---- 4 files changed, 16 insertions(+), 8 deletions(-) create mode 100644 splitter/sql/pass_rate.sql diff --git a/TODO.txt b/TODO.txt index 8901c46..2f03d1d 100644 --- a/TODO.txt +++ b/TODO.txt @@ -1,3 +1,5 @@ Add deleting categories Rework the pop up menus -Add log file and remove terminal window \ No newline at end of file +Add log file and remove terminal window +Make it more clear that importing uses relative scores or make it accept either +Fix the reset stage/full reset split detection \ No newline at end of file diff --git a/splitter/sql/pass_rate.sql b/splitter/sql/pass_rate.sql new file mode 100644 index 0000000..8fbb23d --- /dev/null +++ b/splitter/sql/pass_rate.sql @@ -0,0 +1,8 @@ +SELECT +split_num, +count(case when final = false then 1 end) as pass_count, +count(case when final = false or final=true then 1 end) as run_count, +count(case when final = false then 1 end) * 100.0 / count(case when final = false or final=true then 1 end) as percentage +FROM splits INNER JOIN runs ON runs.id = splits.run_id +WHERE score > 0 and runs.category = 3 +GROUP BY split_num \ No newline at end of file diff --git a/splitter/src/config.rs b/splitter/src/config.rs index 492fb46..2bc5e22 100644 --- a/splitter/src/config.rs +++ b/splitter/src/config.rs @@ -6,9 +6,7 @@ use std::{ use crate::VERSION; -use eframe::egui::{ - Context, Id, RichText, Separator, TextEdit, ViewportBuilder, ViewportId, -}; +use eframe::egui::{Context, Id, RichText, Separator, TextEdit, ViewportBuilder, ViewportId}; use toml::{Table, Value}; use crate::{ZeroError, theme::GREEN, update::check_for_updates}; diff --git a/splitter/src/main.rs b/splitter/src/main.rs index d4a17b8..35a3703 100644 --- a/splitter/src/main.rs +++ b/splitter/src/main.rs @@ -296,26 +296,26 @@ fn vanilla_split_names(split: usize) -> &'static str { fn vanilla_descriptive_split_names(split: usize) -> &'static str { [ - "Stage 1 Start", + "Stage 1 Start", //0 "Cloudoos", "Arc Adder", "Catastrophy", "Bonus 1", - "Stage 2 Start", + "Stage 2 Start", //5 "Box Blockade", "Snake", "Artypo", "Skull Taxis", "2nd Apocalypse", "Bonus 2", - "Stage 3 Start", + "Stage 3 Start", //12 "Crab Landing", "Plane", "Crab", "Tank", "Grapefruit", "Bonus 3", - "Stage 4 Start", + "Stage 4 Start", //19 "Left Tunnel", "Maze", "Trains", From b7be043524214a4469503105b0da6afdd0830f1a Mon Sep 17 00:00:00 2001 From: lily_and_doll <103815266+lily-and-doll@users.noreply.github.com> Date: Sat, 29 Nov 2025 18:22:37 -0500 Subject: [PATCH 47/52] Fix reset stage/full reset split detection --- splitter/src/main.rs | 37 ++++++++++++++++++++++++------------- 1 file changed, 24 insertions(+), 13 deletions(-) diff --git a/splitter/src/main.rs b/splitter/src/main.rs index 35a3703..e196d87 100644 --- a/splitter/src/main.rs +++ b/splitter/src/main.rs @@ -103,6 +103,7 @@ struct ZeroSplitter { dialog_rx: Receiver>, dialog_tx: Sender>, split_delay: Option, + start_delay: Option, db: Database, toggles: Toggles, } @@ -121,6 +122,7 @@ impl ZeroSplitter { waiting_for_rename: false, waiting_for_confirm: false, split_delay: None, + start_delay: None, db, toggles: Default::default(), }; @@ -207,18 +209,27 @@ impl ZeroSplitter { // Reset if we returned to 1-1 if frame.total_score() == 0 && self.last_frame.total_score() > 0 || self.last_frame.is_menu() { self.reset(); - self.run.start(frame); - self.run - .set_split(match frame.stage { - 1 => 0, - 2 => 5, - 3 => 12, - 4 => 19, - _ => panic!("Stage out of bounds! {}", frame.stage), - }) - .unwrap(); - self.categories.refresh_comparison(&self.db).unwrap(); - return; + self.start_delay = Some(1); + } + + if let Some(start_delay) = self.start_delay { + if start_delay >= 1 { + self.start_delay = Some(start_delay - 1) + } else { + self.run.start(frame); + self.run + .set_split(match frame.stage { + 1 => 0, + 2 => 5, + 3 => 12, + 4 => 19, + _ => panic!("Stage out of bounds! {}", frame.stage), + }) + .unwrap(); + self.categories.refresh_comparison(&self.db).unwrap(); + self.start_delay = None; + return; + } } if !frame.is_menu() && !(self.run == Run::Inactive) { @@ -228,7 +239,7 @@ impl ZeroSplitter { } if let Some(split_delay) = self.split_delay { - if split_delay > 1 { + if split_delay >= 1 { self.split_delay = Some(split_delay - 1) } else { self.run.split().unwrap(); From 19409bba254758c9a10ee3b9d513e3683f08cd04 Mon Sep 17 00:00:00 2001 From: lily_and_doll <103815266+lily-and-doll@users.noreply.github.com> Date: Sat, 29 Nov 2025 18:23:34 -0500 Subject: [PATCH 48/52] Bump version --- Cargo.lock | 2 +- splitter/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 270d516..817769d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3627,7 +3627,7 @@ checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" [[package]] name = "zerosplitter" -version = "0.3.2" +version = "0.3.3" dependencies = [ "bytemuck", "common", diff --git a/splitter/Cargo.toml b/splitter/Cargo.toml index 85aacd9..359a89e 100644 --- a/splitter/Cargo.toml +++ b/splitter/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "zerosplitter" -version = "0.3.2" +version = "0.3.3" edition = "2024" [dependencies] From b6d2dfcdf7aa6d6356868a293f9d2216ae13175c Mon Sep 17 00:00:00 2001 From: lily_and_doll <103815266+lily-and-doll@users.noreply.github.com> Date: Tue, 9 Dec 2025 22:43:16 -0500 Subject: [PATCH 49/52] Fix end-of-run detection --- Cargo.lock | 2 +- TODO.txt | 11 ++++++++++- common/src/lib.rs | 2 ++ payload/src/lib.rs | 3 +++ splitter/Cargo.toml | 2 +- splitter/src/main.rs | 9 +++++++-- splitter/src/run.rs | 37 ++++++++++++++++++++++++++++++++++++- 7 files changed, 60 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 817769d..74d1fe5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3627,7 +3627,7 @@ checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" [[package]] name = "zerosplitter" -version = "0.3.3" +version = "0.3.4" dependencies = [ "bytemuck", "common", diff --git a/TODO.txt b/TODO.txt index 2f03d1d..8303d50 100644 --- a/TODO.txt +++ b/TODO.txt @@ -2,4 +2,13 @@ Add deleting categories Rework the pop up menus Add log file and remove terminal window Make it more clear that importing uses relative scores or make it accept either -Fix the reset stage/full reset split detection \ No newline at end of file +Fix the reset stage/full reset split detection + +Extract functionality from UI + Pull UI state (comparisons, toggles) into single top-level child struct + Ui modifies state via self access/reads from state + no DB calls in UI - all data accessed via get-or-insert cache in UI state struct + +RunData {SplitData} struct + Get all run/split data through a central channel that gets all data + Pare down data as needed later \ No newline at end of file diff --git a/common/src/lib.rs b/common/src/lib.rs index ee7c865..c362bb8 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -17,6 +17,8 @@ pub struct FrameData { pub multiplier_one: u32, pub dynamic_rank: f32, pub pattern_rank: f32, + pub stage_current: u8, + pub padding: u8, } impl FrameData { diff --git a/payload/src/lib.rs b/payload/src/lib.rs index 8f816c1..8ad6075 100644 --- a/payload/src/lib.rs +++ b/payload/src/lib.rs @@ -106,6 +106,7 @@ fn get_frame_data() -> FrameData { let multiplier_one = read_var(c"multiplier_one").unwrap().value as u32; let pattern_rank = read_var(c"pattern_rank").unwrap().value as f32; let dynamic_rank = read_var(c"dynamic_rank").unwrap().value as f32; + let stage_current = read_var(c"stage_current").unwrap().value as u8; FrameData { score_p1: score_p1 as i32, score_p2: score_p2 as i32, @@ -119,6 +120,8 @@ fn get_frame_data() -> FrameData { multiplier_one, pattern_rank, dynamic_rank, + stage_current, + padding: 0, } } diff --git a/splitter/Cargo.toml b/splitter/Cargo.toml index 359a89e..eb3f38a 100644 --- a/splitter/Cargo.toml +++ b/splitter/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "zerosplitter" -version = "0.3.3" +version = "0.3.4" edition = "2024" [dependencies] diff --git a/splitter/src/main.rs b/splitter/src/main.rs index e196d87..01546de 100644 --- a/splitter/src/main.rs +++ b/splitter/src/main.rs @@ -139,7 +139,7 @@ impl ZeroSplitter { } fn save_splits(&mut self) { - if self.run.is_active() && self.run.scores().unwrap().iter().sum::() > 0 { + if self.run.is_active() && self.run.scores().unwrap()[0] > 0 { debug!("Saving splits"); if let Err(err) = self.db.insert_run(&self.categories, &self.run) { @@ -149,6 +149,9 @@ impl ZeroSplitter { } fn update_frame(&mut self, frame: FrameData) { + if self.run.is_active() && frame.is_menu() { + self.end_run(); + } // Difficulty is ZR-speak for gamemode if frame.difficulty == 0 { self.update_greenorange(frame); @@ -196,6 +199,7 @@ impl ZeroSplitter { self.run.update(frame).unwrap(); } else { // End the run if we're back on the menu + // TODO make certain this is unreachable? self.end_run(); } } @@ -251,6 +255,7 @@ impl ZeroSplitter { self.run.update(frame).unwrap(); } else if frame.is_menu() { // End the run if we're back on the menu + // TODO make certain this is unreachable? self.end_run(); } } @@ -262,7 +267,7 @@ impl ZeroSplitter { fn end_run(&mut self) { self.save_splits(); - self.run.stop() + self.run.end(); } } diff --git a/splitter/src/run.rs b/splitter/src/run.rs index 47d2567..94d554a 100644 --- a/splitter/src/run.rs +++ b/splitter/src/run.rs @@ -12,6 +12,11 @@ pub enum Run { current_split: usize, split_base_score: i32, }, + Residual { + difficulty: Gamemode, + splits: Vec, + score: i32, + }, } impl Run { @@ -19,6 +24,7 @@ impl Run { match *self { Run::Inactive => None, Run::Active { score, .. } => Some(score), + Run::Residual { score, .. } => Some(score), } } @@ -26,6 +32,7 @@ impl Run { match self { Run::Inactive => Err(ZeroError::RunInactive), Run::Active { splits, .. } => Ok(splits.iter().map(|x| x.score).collect()), + Run::Residual { splits, .. } => Ok(splits.iter().map(|x| x.score).collect()), } } @@ -33,9 +40,11 @@ impl Run { match self { Run::Inactive => Err(ZeroError::RunInactive), Run::Active { splits, .. } => Ok(splits.iter().map(|&s| s).collect()), + Run::Residual { splits, .. } => Ok(splits.iter().map(|&s| s).collect()), } } + /// Start a new run with the difficulty of the frame pub fn start(&mut self, frame: FrameData) { let mode = frame.difficulty.into(); *self = Self::Active { @@ -47,10 +56,32 @@ impl Run { }; } - pub fn stop(&mut self) { + /// End the run; turning an active run into a residual + pub fn end(&mut self) { + *self = match self { + Run::Inactive => Run::Inactive, + Run::Active { + difficulty, + splits, + score, + .. + } => Run::Residual { + difficulty: *difficulty, + splits: splits.clone(), + score: *score, + }, + Run::Residual { .. } => Run::Inactive, + } + } + + /// Abruptly cancel the run with no closing behavior. This destroys the data from the run. + /// Use Zerosplitter::end_run() to write data to the database + pub fn deactivate(&mut self) { *self = Self::Inactive } + /// Reset the data of the run while keeping its inactive/active status. + /// Deactivates a residual run pub fn reset(&mut self) { *self = match *self { Run::Inactive => Run::Inactive, @@ -61,9 +92,11 @@ impl Run { current_split: 0, split_base_score: 0, }, + Run::Residual { .. } => Run::Inactive, } } + /// Integrate the data from the frame into the Run data. pub fn update(&mut self, frame: FrameData) -> Result<(), ZeroError> { if let Self::Active { difficulty, @@ -137,6 +170,7 @@ impl Run { match self { Run::Inactive => Err(ZeroError::RunInactive), Run::Active { current_split, .. } => Ok(*current_split), + Run::Residual { difficulty, .. } => Ok(difficulty.splits() + 1), } } @@ -144,6 +178,7 @@ impl Run { match self { Run::Inactive => false, Run::Active { .. } => true, + Run::Residual { .. } => false, } } } From ee97159608408fd21c78b35dee6a21b2072bb77b Mon Sep 17 00:00:00 2001 From: lily_and_doll <103815266+lily-and-doll@users.noreply.github.com> Date: Fri, 12 Dec 2025 22:50:04 -0500 Subject: [PATCH 50/52] Capture time, time bonus, and coldframes --- Cargo.lock | 2 +- common/Cargo.toml | 2 +- common/src/lib.rs | 4 ++- payload/src/lib.rs | 7 +++++- splitter/Cargo.toml | 2 +- splitter/src/database.rs | 20 ++++++++++++--- splitter/src/main.rs | 6 +++-- splitter/src/run.rs | 53 +++++++++++++++++++++++++++++++++------- 8 files changed, 77 insertions(+), 19 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 74d1fe5..1293922 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3627,7 +3627,7 @@ checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" [[package]] name = "zerosplitter" -version = "0.3.4" +version = "0.3.5" dependencies = [ "bytemuck", "common", diff --git a/common/Cargo.toml b/common/Cargo.toml index ebb7a7f..eabea06 100644 --- a/common/Cargo.toml +++ b/common/Cargo.toml @@ -5,4 +5,4 @@ edition = "2024" [dependencies.bytemuck] version = "1" -features = ["derive"] \ No newline at end of file +features = ["derive", "min_const_generics"] \ No newline at end of file diff --git a/common/src/lib.rs b/common/src/lib.rs index c362bb8..a3a377d 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -18,7 +18,9 @@ pub struct FrameData { pub dynamic_rank: f32, pub pattern_rank: f32, pub stage_current: u8, - pub padding: u8, + pub timer_wave_frames: u8, + pub timer_wave_hotframes: u32, + pub timer_wave_bonus: u32, } impl FrameData { diff --git a/payload/src/lib.rs b/payload/src/lib.rs index 8ad6075..a2d9e9c 100644 --- a/payload/src/lib.rs +++ b/payload/src/lib.rs @@ -107,6 +107,9 @@ fn get_frame_data() -> FrameData { let pattern_rank = read_var(c"pattern_rank").unwrap().value as f32; let dynamic_rank = read_var(c"dynamic_rank").unwrap().value as f32; let stage_current = read_var(c"stage_current").unwrap().value as u8; + let timer_wave_frames = read_var(c"timer_wave_frames").unwrap().value as u8; + let timer_wave_hotframes = read_var(c"timer_wave_hotframes").unwrap().value as u32; + let timer_wave_bonus = read_var(c"timer_wave_bonus").unwrap().value as u32; FrameData { score_p1: score_p1 as i32, score_p2: score_p2 as i32, @@ -121,7 +124,9 @@ fn get_frame_data() -> FrameData { pattern_rank, dynamic_rank, stage_current, - padding: 0, + timer_wave_frames, + timer_wave_hotframes, + timer_wave_bonus, } } diff --git a/splitter/Cargo.toml b/splitter/Cargo.toml index eb3f38a..dcb052b 100644 --- a/splitter/Cargo.toml +++ b/splitter/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "zerosplitter" -version = "0.3.4" +version = "0.3.5" edition = "2024" [dependencies] diff --git a/splitter/src/database.rs b/splitter/src/database.rs index 3ec00f1..3634f43 100644 --- a/splitter/src/database.rs +++ b/splitter/src/database.rs @@ -36,7 +36,7 @@ macro_rules! transaction { }}; } -const CURRENT_SCHEMA_VERSION: i32 = 4; +const CURRENT_SCHEMA_VERSION: i32 = 5; impl Database { pub fn init() -> Result { @@ -176,8 +176,8 @@ impl Database { for (num, &split) in run.splits().unwrap().iter().take_while(|&&s| s.score > 0).enumerate() { let final_split = num == run.current_split().unwrap(); self.conn.execute( - "INSERT INTO splits (id, split_num, score, hits, mult, run_id, final, pattern_rank, dynamic_rank) VALUES(NULL, ?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)", - params![num, split.score, 0, split.mult, run_id, final_split, split.pattern_rank, split.dynamic_rank], + "INSERT INTO splits (id, split_num, score, hits, mult, run_id, final, pattern_rank, dynamic_rank, time_frames, time_bonus, time_coldframes) VALUES(NULL, ?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11)", + params![num, split.score, 0, split.mult, run_id, final_split, split.pattern_rank, split.dynamic_rank, split.time_frames, split.time_bonus, split.coldframes], )?; } @@ -263,6 +263,10 @@ impl Database { self.migrate3to4()?; current_schema = 4 } + 4 => { + self.migrate4to5()?; + current_schema = 5 + } _ => Err(rusqlite::Error::InvalidQuery)?, }; } @@ -306,6 +310,16 @@ impl Database { self.conn.execute("ALTER TABLE runs ADD COLUMN imported BOOLEAN", ()) } + fn migrate4to5(&self) -> Result<()> { + println!("Migrating schema 4 to 5..."); + self.conn.pragma_update(Some("main"), "user_version", 5)?; + self.conn.execute_batch( + "ALTER TABLE splits ADD COLUMN time_frames INTEGER; + ALTER TABLE splits ADD COLUMN time_bonus INTEGER; + ALTER TABLE splits ADD COLUMN time_coldframes INTEGER;", + ) + } + pub fn import_run(&self, splits: Vec, category_name: &String) -> Result<()> { transaction!(self.conn, { let category_id = diff --git a/splitter/src/main.rs b/splitter/src/main.rs index 01546de..7d80564 100644 --- a/splitter/src/main.rs +++ b/splitter/src/main.rs @@ -196,7 +196,7 @@ impl ZeroSplitter { } // Update run and split scores - self.run.update(frame).unwrap(); + self.run.store_end_of_split(frame).unwrap(); } else { // End the run if we're back on the menu // TODO make certain this is unreachable? @@ -239,6 +239,8 @@ impl ZeroSplitter { if !frame.is_menu() && !(self.run == Run::Inactive) { // Split if necessary; score requirement prevents spurious splits after a reset if frame.timer_wave == 0 && self.last_frame.timer_wave != 0 && frame.total_score() > 0 { + // get time from right before it resets + self.run.store_time(self.last_frame).unwrap(); self.split_delay = Some(SPLIT_DELAY_FRAMES); } @@ -252,7 +254,7 @@ impl ZeroSplitter { } // Update run and split scores - self.run.update(frame).unwrap(); + self.run.store_end_of_split(frame).unwrap(); } else if frame.is_menu() { // End the run if we're back on the menu // TODO make certain this is unreachable? diff --git a/splitter/src/run.rs b/splitter/src/run.rs index 94d554a..43d290f 100644 --- a/splitter/src/run.rs +++ b/splitter/src/run.rs @@ -97,7 +97,9 @@ impl Run { } /// Integrate the data from the frame into the Run data. - pub fn update(&mut self, frame: FrameData) -> Result<(), ZeroError> { + /// Call this one at the end of the split + /// Stores score, mult, and rank + pub fn store_end_of_split(&mut self, frame: FrameData) -> Result<(), ZeroError> { if let Self::Active { difficulty, splits, @@ -111,13 +113,11 @@ impl Run { if *split_base_score > *score { *split_base_score = 0 } - let data = SplitData::new( - frame.total_score() - *split_base_score, - frame.multiplier_one, - frame.pattern_rank, - frame.dynamic_rank, - ); - *splits.get_mut(*current_split).unwrap() = data; + let current_data = &mut splits[*current_split]; + current_data.score = frame.total_score() - *split_base_score; + current_data.mult = frame.multiplier_one; + current_data.pattern_rank = frame.pattern_rank; + current_data.dynamic_rank = frame.dynamic_rank; Ok(()) } else { @@ -128,6 +128,24 @@ impl Run { } } + pub fn store_time(&mut self, frame: FrameData) -> Result<(), ZeroError> { + if let Self::Active { + splits, current_split, .. + } = self + { + let current_data = &mut splits[*current_split]; + + current_data.time_frames = frame.timer_wave * 60 + frame.timer_wave_frames as u32; + current_data.coldframes = current_data.time_frames - frame.timer_wave_hotframes; + let factr = 1_f32.max(1_f32 + ((frame.timer_wave_hotframes as f32 / 60_f32) - 1_f32)); + current_data.time_bonus = (0.5_f32 + (frame.timer_wave_bonus as f32 / factr) as f32).floor() as u32; + + Ok(()) + } else { + Err(ZeroError::RunInactive) + } + } + pub fn split(&mut self) -> Result<(), ZeroError> { if let Self::Active { current_split, @@ -189,15 +207,29 @@ pub struct SplitData { pub mult: u32, pub pattern_rank: f32, pub dynamic_rank: f32, + pub time_frames: u32, + pub time_bonus: u32, + pub coldframes: u32, } impl SplitData { - fn new(score: i32, mult: u32, pattern_rank: f32, dynamic_rank: f32) -> Self { + fn new( + score: i32, + mult: u32, + pattern_rank: f32, + dynamic_rank: f32, + time_frames: u32, + time_bonus: u32, + cool_frames: u32, + ) -> Self { Self { score, mult, pattern_rank, dynamic_rank, + time_frames, + time_bonus, + coldframes: cool_frames, } } } @@ -209,6 +241,9 @@ impl Default for SplitData { mult: Default::default(), pattern_rank: Default::default(), dynamic_rank: Default::default(), + time_frames: Default::default(), + time_bonus: Default::default(), + coldframes: Default::default(), } } } From c8df26b3448ec8e96750f112b4d95d5a29e21424 Mon Sep 17 00:00:00 2001 From: lily_and_doll <103815266+lily-and-doll@users.noreply.github.com> Date: Mon, 29 Dec 2025 04:13:38 -0500 Subject: [PATCH 51/52] Add columns and column selector --- Cargo.lock | 409 ++++++++++----- splitter/Cargo.toml | 7 +- splitter/assets/config_sections/min_col.toml | 5 + splitter/src/app.rs | 511 +++++++++++++------ splitter/src/config.rs | 192 ++++--- splitter/src/database.rs | 1 + splitter/src/main.rs | 82 ++- splitter/src/run.rs | 39 +- 8 files changed, 861 insertions(+), 385 deletions(-) create mode 100644 splitter/assets/config_sections/min_col.toml diff --git a/Cargo.lock b/Cargo.lock index 1293922..d7cd80e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,9 +4,9 @@ version = 4 [[package]] name = "ab_glyph" -version = "0.2.29" +version = "0.2.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec3672c180e71eeaaac3a541fbbc5f5ad4def8b747c595ad30d674e43049f7b0" +checksum = "01c0457472c38ea5bd1c3b5ada5e368271cb550be7a4ca4a0b4634e9913f6cc2" dependencies = [ "ab_glyph_rasterizer", "owned_ttf_parser", @@ -26,9 +26,9 @@ checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" [[package]] name = "ahash" -version = "0.8.11" +version = "0.8.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" dependencies = [ "cfg-if", "once_cell", @@ -52,7 +52,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef6978589202a00cd7e118380c448a08b6ed394c3a8df3a430d0898e3a42d046" dependencies = [ "android-properties", - "bitflags 2.9.0", + "bitflags 2.10.0", "cc", "cesu8", "jni", @@ -74,21 +74,21 @@ checksum = "fc7eb209b1518d6bb87b283c20095f5228ecda460da70b44f0802523dea6da04" [[package]] name = "arboard" -version = "3.5.0" +version = "3.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1df21f715862ede32a0c525ce2ca4d52626bb0007f8c18b87a384503ac33e70" +checksum = "0348a1c054491f4bfe6ab86a7b6ab1e44e45d899005de92f58b3df180b36ddaf" dependencies = [ "clipboard-win", "image", "log", - "objc2 0.6.0", - "objc2-app-kit 0.3.0", + "objc2 0.6.3", + "objc2-app-kit 0.3.2", "objc2-core-foundation", "objc2-core-graphics", - "objc2-foundation 0.3.0", + "objc2-foundation 0.3.2", "parking_lot", "percent-encoding", - "windows-sys 0.59.0", + "windows-sys 0.60.2", "x11rb", ] @@ -118,9 +118,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.9.0" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" [[package]] name = "block2" @@ -139,18 +139,18 @@ checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" [[package]] name = "bytemuck" -version = "1.22.0" +version = "1.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6b1fc10dbac614ebc03540c9dbd60e83887fda27794998c6528f1782047d540" +checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4" dependencies = [ "bytemuck_derive", ] [[package]] name = "bytemuck_derive" -version = "1.9.3" +version = "1.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ecc273b49b3205b83d648f0690daa588925572cc5063745bfe547fe7ec8e1a1" +checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff" dependencies = [ "proc-macro2", "quote", @@ -175,7 +175,7 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b99da2f8558ca23c71f4fd15dc57c906239752dd27ff3c00a1d56b685b7cbfec" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.10.0", "log", "polling", "rustix 0.38.44", @@ -345,6 +345,16 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" +[[package]] +name = "dispatch2" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" +dependencies = [ + "bitflags 2.10.0", + "objc2 0.6.3", +] + [[package]] name = "displaydoc" version = "0.2.5" @@ -388,9 +398,9 @@ checksum = "f25c0e292a7ca6d6498557ff1df68f32c99850012b6ea401cf8daf771f22ff53" [[package]] name = "ecolor" -version = "0.31.1" +version = "0.33.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc4feb366740ded31a004a0e4452fbf84e80ef432ecf8314c485210229672fd1" +checksum = "71ddb8ac7643d1dba1bb02110e804406dd459a838efcb14011ced10556711a8e" dependencies = [ "bytemuck", "emath", @@ -398,9 +408,9 @@ dependencies = [ [[package]] name = "eframe" -version = "0.31.1" +version = "0.33.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0dfe0859f3fb1bc6424c57d41e10e9093fe938f426b691e42272c2f336d915c" +checksum = "457481173e6db5ca9fa2be93a58df8f4c7be639587aeb4853b526c6cf87db4e6" dependencies = [ "ahash", "bytemuck", @@ -426,37 +436,40 @@ dependencies = [ "wasm-bindgen-futures", "web-sys", "web-time", - "winapi", - "windows-sys 0.59.0", + "windows-sys 0.61.2", "winit", ] [[package]] name = "egui" -version = "0.31.1" +version = "0.33.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25dd34cec49ab55d85ebf70139cb1ccd29c977ef6b6ba4fe85489d6877ee9ef3" +checksum = "6a9b567d356674e9a5121ed3fedfb0a7c31e059fe71f6972b691bcd0bfc284e3" dependencies = [ "ahash", - "bitflags 2.9.0", + "bitflags 2.10.0", "emath", "epaint", "log", "nohash-hasher", "profiling", + "smallvec", + "unicode-segmentation", ] [[package]] name = "egui-winit" -version = "0.31.1" +version = "0.33.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d9dfbb78fe4eb9c3a39ad528b90ee5915c252e77bbab9d4ebc576541ab67e13" +checksum = "ec6687e5bb551702f4ad10ac428bab12acf9d53047ebb1082d4a0ed8c6251a29" dependencies = [ - "ahash", "arboard", "bytemuck", "egui", "log", + "objc2 0.5.2", + "objc2-foundation 0.2.2", + "objc2-ui-kit", "profiling", "raw-window-handle", "smithay-clipboard", @@ -466,12 +479,25 @@ dependencies = [ ] [[package]] -name = "egui_glow" -version = "0.31.1" +name = "egui_extras" +version = "0.33.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "910906e3f042ea6d2378ec12a6fd07698e14ddae68aed2d819ffe944a73aab9e" +checksum = "d01d34e845f01c62e3fded726961092e70417d66570c499b9817ab24674ca4ed" dependencies = [ "ahash", + "egui", + "enum-map", + "log", + "mime_guess2", + "profiling", +] + +[[package]] +name = "egui_glow" +version = "0.33.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6420863ea1d90e750f75075231a260030ad8a9f30a7cef82cdc966492dc4c4eb" +dependencies = [ "bytemuck", "egui", "glow", @@ -482,11 +508,17 @@ dependencies = [ "web-sys", ] +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + [[package]] name = "emath" -version = "0.31.1" +version = "0.33.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e4cadcff7a5353ba72b7fea76bf2122b5ebdbc68e8155aa56dfdea90083fe1b" +checksum = "491bdf728bf25ddd9ad60d4cf1c48588fa82c013a2440b91aa7fc43e34a07c32" dependencies = [ "bytemuck", ] @@ -500,6 +532,26 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "enum-map" +version = "2.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6866f3bfdf8207509a033af1a75a7b08abda06bbaaeae6669323fd5a097df2e9" +dependencies = [ + "enum-map-derive", +] + +[[package]] +name = "enum-map-derive" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f282cfdfe92516eb26c2af8589c274c7c17681f5ecc03c18255fe741c6aa64eb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "env_logger" version = "0.10.2" @@ -515,9 +567,9 @@ dependencies = [ [[package]] name = "epaint" -version = "0.31.1" +version = "0.33.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41fcc0f5a7c613afd2dee5e4b30c3e6acafb8ad6f0edb06068811f708a67c562" +checksum = "009d0dd3c2163823a0abdb899451ecbc78798dec545ee91b43aff1fa790bab62" dependencies = [ "ab_glyph", "ahash", @@ -533,9 +585,9 @@ dependencies = [ [[package]] name = "epaint_default_fonts" -version = "0.31.1" +version = "0.33.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc7e7a64c02cf7a5b51e745a9e45f60660a286f151c238b9d397b3e923f5082f" +checksum = "5c4fbe202b6578d3d56428fa185cdf114a05e49da05f477b3c7f0fbb221f1862" [[package]] name = "equivalent" @@ -767,21 +819,21 @@ dependencies = [ [[package]] name = "glutin" -version = "0.32.2" +version = "0.32.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03642b8b0cce622392deb0ee3e88511f75df2daac806102597905c3ea1974848" +checksum = "12124de845cacfebedff80e877bb37b5b75c34c5a4c89e47e1cdd67fb6041325" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.10.0", "cfg_aliases", "cgl", - "core-foundation 0.9.4", - "dispatch", + "dispatch2", "glutin_egl_sys", "glutin_wgl_sys", "libloading", - "objc2 0.5.2", - "objc2-app-kit 0.2.2", - "objc2-foundation 0.2.2", + "objc2 0.6.3", + "objc2-app-kit 0.3.2", + "objc2-core-foundation", + "objc2-foundation 0.3.2", "once_cell", "raw-window-handle", "windows-sys 0.52.0", @@ -1187,6 +1239,15 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.15" @@ -1269,7 +1330,7 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.10.0", "libc", "redox_syscall 0.5.10", ] @@ -1311,19 +1372,18 @@ checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5" [[package]] name = "lock_api" -version = "0.4.12" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" dependencies = [ - "autocfg", "scopeguard", ] [[package]] name = "log" -version = "0.4.27" +version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "memchr" @@ -1355,6 +1415,18 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "mime_guess2" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1706dc14a2e140dec0a7a07109d9a3d5890b81e85bd6c60b906b249a77adf0ca" +dependencies = [ + "mime", + "phf", + "phf_shared", + "unicase", +] + [[package]] name = "miniz_oxide" version = "0.8.7" @@ -1399,7 +1471,7 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.10.0", "jni-sys", "log", "ndk-sys", @@ -1477,9 +1549,9 @@ dependencies = [ [[package]] name = "objc2" -version = "0.6.0" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3531f65190d9cff863b77a99857e74c314dd16bf56c538c4b57c7cbc3f3a6e59" +checksum = "b7c2599ce0ec54857b29ce62166b0ed9b4f6f1a70ccc9a71165b6154caca8c05" dependencies = [ "objc2-encode", ] @@ -1490,7 +1562,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.10.0", "block2", "libc", "objc2 0.5.2", @@ -1502,14 +1574,15 @@ dependencies = [ [[package]] name = "objc2-app-kit" -version = "0.3.0" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5906f93257178e2f7ae069efb89fbd6ee94f0592740b5f8a1512ca498814d0fb" +checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" dependencies = [ - "bitflags 2.9.0", - "objc2 0.6.0", + "bitflags 2.10.0", + "objc2 0.6.3", + "objc2-core-foundation", "objc2-core-graphics", - "objc2-foundation 0.3.0", + "objc2-foundation 0.3.2", ] [[package]] @@ -1518,7 +1591,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.10.0", "block2", "objc2 0.5.2", "objc2-core-location", @@ -1542,7 +1615,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.10.0", "block2", "objc2 0.5.2", "objc2-foundation 0.2.2", @@ -1550,22 +1623,24 @@ dependencies = [ [[package]] name = "objc2-core-foundation" -version = "0.3.0" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daeaf60f25471d26948a1c2f840e3f7d86f4109e3af4e8e4b5cd70c39690d925" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ - "bitflags 2.9.0", - "objc2 0.6.0", + "bitflags 2.10.0", + "dispatch2", + "objc2 0.6.3", ] [[package]] name = "objc2-core-graphics" -version = "0.3.0" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8dca602628b65356b6513290a21a6405b4d4027b8b250f0b98dddbb28b7de02" +checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" dependencies = [ - "bitflags 2.9.0", - "objc2 0.6.0", + "bitflags 2.10.0", + "dispatch2", + "objc2 0.6.3", "objc2-core-foundation", "objc2-io-surface", ] @@ -1606,7 +1681,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.10.0", "block2", "dispatch", "libc", @@ -1615,23 +1690,23 @@ dependencies = [ [[package]] name = "objc2-foundation" -version = "0.3.0" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a21c6c9014b82c39515db5b396f91645182611c97d24637cf56ac01e5f8d998" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" dependencies = [ - "bitflags 2.9.0", - "objc2 0.6.0", + "bitflags 2.10.0", + "objc2 0.6.3", "objc2-core-foundation", ] [[package]] name = "objc2-io-surface" -version = "0.3.0" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "161a8b87e32610086e1a7a9e9ec39f84459db7b3a0881c1f16ca5a2605581c19" +checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" dependencies = [ - "bitflags 2.9.0", - "objc2 0.6.0", + "bitflags 2.10.0", + "objc2 0.6.3", "objc2-core-foundation", ] @@ -1653,7 +1728,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.10.0", "block2", "objc2 0.5.2", "objc2-foundation 0.2.2", @@ -1665,7 +1740,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.10.0", "block2", "objc2 0.5.2", "objc2-foundation 0.2.2", @@ -1688,7 +1763,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.10.0", "block2", "objc2 0.5.2", "objc2-cloud-kit", @@ -1720,7 +1795,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.10.0", "block2", "objc2 0.5.2", "objc2-core-location", @@ -1739,7 +1814,7 @@ version = "0.10.75" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.10.0", "cfg-if", "foreign-types 0.3.2", "libc", @@ -1797,9 +1872,9 @@ dependencies = [ [[package]] name = "parking_lot" -version = "0.12.3" +version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" dependencies = [ "lock_api", "parking_lot_core", @@ -1807,15 +1882,15 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.10" +version = "0.9.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", "redox_syscall 0.5.10", "smallvec", - "windows-targets 0.52.6", + "windows-link 0.2.1", ] [[package]] @@ -1828,9 +1903,53 @@ dependencies = [ [[package]] name = "percent-encoding" -version = "2.3.1" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "phf" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" +dependencies = [ + "phf_macros", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" +dependencies = [ + "phf_shared", + "rand", +] + +[[package]] +name = "phf_macros" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn", + "unicase", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher", + "unicase", +] [[package]] name = "pin-project" @@ -1928,9 +2047,9 @@ dependencies = [ [[package]] name = "profiling" -version = "1.0.16" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afbdc74edc00b6f6a218ca6a5364d6226a259d4b8ea1af4a0ea063f27e179f4d" +checksum = "3eb8486b569e12e2c32ad3e204dbaba5e4b5b216e9367044f25f1dba42341773" [[package]] name = "quick-xml" @@ -1956,6 +2075,21 @@ version = "5.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" + [[package]] name = "raw-window-handle" version = "0.6.2" @@ -1977,7 +2111,7 @@ version = "0.5.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b8c0c260b63a8219631167be35e6a988e9554dbd323f8bd08439c8ed1302bd1" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.10.0", ] [[package]] @@ -2071,7 +2205,7 @@ version = "0.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "165ca6e57b20e1351573e3729b958bc62f0e48025386970b6e4d29e7a7e71f3f" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.10.0", "fallible-iterator", "fallible-streaming-iterator", "hashlink", @@ -2085,7 +2219,7 @@ version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.10.0", "errno", "libc", "linux-raw-sys 0.4.15", @@ -2098,7 +2232,7 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.10.0", "errno", "libc", "linux-raw-sys 0.11.0", @@ -2186,7 +2320,7 @@ version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.10.0", "core-foundation 0.9.4", "core-foundation-sys", "libc", @@ -2284,6 +2418,12 @@ version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" +[[package]] +name = "siphasher" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" + [[package]] name = "slab" version = "0.4.9" @@ -2304,9 +2444,9 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.14.0" +version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "smithay-client-toolkit" @@ -2314,7 +2454,7 @@ version = "0.19.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3457dea1f0eb631b4034d61d4d8c32074caa6cd1ab2d59f2327bd8461e2c0016" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.10.0", "calloop", "calloop-wayland-source", "cursor-icon", @@ -2418,7 +2558,7 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.10.0", "core-foundation 0.9.4", "system-configuration-sys", ] @@ -2620,7 +2760,7 @@ version = "0.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9cf146f99d442e8e68e585f5d798ccd3cad9a7835b917e09728880a862706456" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.10.0", "bytes", "futures-util", "http", @@ -2675,6 +2815,12 @@ version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31" +[[package]] +name = "unicase" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" + [[package]] name = "unicode-ident" version = "1.0.18" @@ -2853,7 +2999,7 @@ version = "0.31.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c66a47e840dc20793f2264eb4b3e4ecb4b75d91c0dd4af04b456128e0bdd449d" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.10.0", "rustix 1.1.2", "wayland-backend", "wayland-scanner", @@ -2865,7 +3011,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "625c5029dbd43d25e6aa9615e88b829a5cad13b2819c4ae129fdbb7c31ab4c7e" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.10.0", "cursor-icon", "wayland-backend", ] @@ -2887,7 +3033,7 @@ version = "0.32.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "efa790ed75fbfd71283bd2521a1cfdc022aabcc28bdcff00851f9e4ae88d9901" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.10.0", "wayland-backend", "wayland-client", "wayland-scanner", @@ -2899,7 +3045,7 @@ version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "248a02e6f595aad796561fa82d25601bd2c8c3b145b1c7453fc8f94c1a58f8b2" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.10.0", "wayland-backend", "wayland-client", "wayland-protocols", @@ -2959,8 +3105,8 @@ dependencies = [ "jni", "log", "ndk-context", - "objc2 0.6.0", - "objc2-foundation 0.3.0", + "objc2 0.6.3", + "objc2-foundation 0.3.2", "url", "web-sys", ] @@ -2971,22 +3117,6 @@ version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53a85b86a771b1c87058196170769dd264f66c0782acf1ae6cc51bfd64b39082" -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - [[package]] name = "winapi-util" version = "0.1.9" @@ -2996,12 +3126,6 @@ dependencies = [ "windows-sys 0.59.0", ] -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - [[package]] name = "windows" version = "0.60.0" @@ -3434,7 +3558,7 @@ checksum = "c66d4b9ed69c4009f6321f762d6e61ad8a2389cd431b97cb1e146812e9e6c732" dependencies = [ "android-activity", "atomic-waker", - "bitflags 2.9.0", + "bitflags 2.10.0", "block2", "calloop", "cfg_aliases", @@ -3491,7 +3615,7 @@ version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.10.0", ] [[package]] @@ -3535,7 +3659,7 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d039de8032a9a8856a6be89cea3e5d12fdd82306ab7c94d74e6deab2460651c5" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.10.0", "dlib", "log", "once_cell", @@ -3580,18 +3704,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.7.35" +version = "0.8.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +checksum = "fd74ec98b9250adb3ca554bdde269adf631549f51d8a8f8f0a10b50f1cb298c3" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.7.35" +version = "0.8.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +checksum = "d8a8d209fdf45cf5138cbb5a506f6b52522a25afccc534d1475dad8e31105c6a" dependencies = [ "proc-macro2", "quote", @@ -3627,11 +3751,14 @@ checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" [[package]] name = "zerosplitter" -version = "0.3.5" +version = "0.4.0" dependencies = [ "bytemuck", "common", "eframe", + "egui", + "egui_extras", + "itertools", "log", "pretty_env_logger", "reqwest", diff --git a/splitter/Cargo.toml b/splitter/Cargo.toml index dcb052b..e10ada0 100644 --- a/splitter/Cargo.toml +++ b/splitter/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "zerosplitter" -version = "0.3.5" +version = "0.4.0" edition = "2024" [dependencies] @@ -14,9 +14,12 @@ rusqlite = { version = "0.37.0", features = ["bundled"] } toml = "0.9.8" reqwest = { version = "0.12.24", features = ["blocking", "json"] } semver = "1.0.27" +egui_extras = "0.33.3" +egui = "0.33.3" +itertools = "0.14.0" [dependencies.eframe] -version = "0.31" +version = "0.33.3" default-features = false features = ["default_fonts", "glow"] diff --git a/splitter/assets/config_sections/min_col.toml b/splitter/assets/config_sections/min_col.toml new file mode 100644 index 0000000..f6b88f8 --- /dev/null +++ b/splitter/assets/config_sections/min_col.toml @@ -0,0 +1,5 @@ +# Minimum width of each column in the main UI +# Columns will expand automatically as they are filled with data, but you can give them extra breathing room if you want +# A very small number may cause columns to frequently get wider as your run progresses +minimum_column_width = 50.0 + diff --git a/splitter/src/app.rs b/splitter/src/app.rs index d34f336..cfe985d 100644 --- a/splitter/src/app.rs +++ b/splitter/src/app.rs @@ -1,25 +1,31 @@ +use std::{fmt::Display, ops::Add}; + use eframe::{ App, Frame, - egui::{Align, CentralPanel, Color32, ComboBox, Context, Id, Layout, Sense, Sides, Ui}, + egui::{Align, CentralPanel, Color32, ComboBox, Context, Id, Layout, Sense, Ui}, }; +use egui::TopBottomPanel; +use egui_extras::{Column, TableBuilder}; use crate::{ Gamemode, Run, ZeroError, ZeroSplitter, - config::{CONFIG, options_menu}, + config::CONFIG, + run::SplitData, theme::{DARK_GREEN, DARK_ORANGE, DARKER_GREEN, DARKER_ORANGE, GREEN, LIGHT_ORANGE}, ui::{category_maker_dialog, confirm_dialog}, vanilla_descriptive_split_names, vanilla_split_names, }; -pub struct Toggles { +pub struct Options { pub names: bool, pub relative_score: bool, pub show_gold_split: bool, pub decorations: bool, pub show_options_menu: bool, + pub columns_order: Vec, } -impl Default for Toggles { +impl Default for Options { fn default() -> Self { Self { names: false, @@ -27,6 +33,7 @@ impl Default for Toggles { show_gold_split: true, decorations: true, show_options_menu: false, + columns_order: vec![0, 1, 3, 7], } } } @@ -45,24 +52,19 @@ impl App for ZeroSplitter { { let zoom_level = CONFIG.get().unwrap().zoom_level; let min_size = match self.categories.current().mode { - Gamemode::GreenOrange => eframe::egui::Vec2 { - x: 300.0 * zoom_level, - y: 300.0 * zoom_level, - }, - Gamemode::WhiteVanilla => eframe::egui::Vec2 { - x: 300.0 * zoom_level, - y: 650.0 * zoom_level, - }, + Gamemode::GreenOrange => eframe::egui::Vec2 { x: 300.0, y: 300.0 }, + Gamemode::WhiteVanilla => eframe::egui::Vec2 { x: 300.0, y: 650.0 }, Gamemode::BlackOnion => todo!(), }; - ctx.send_viewport_cmd(eframe::egui::ViewportCommand::InnerSize(min_size)); + let size = ctx.used_size(); + ctx.send_viewport_cmd(eframe::egui::ViewportCommand::InnerSize(size)); self.reset(); } ctx.data_mut(|data| data.insert_temp(prev_mode_id, cur_mode)); CentralPanel::default().show(ctx, |ui| { - if !self.toggles.decorations + if !self.options.decorations && ui .interact(ui.max_rect(), Id::new("window_drag"), Sense::drag()) .dragged() @@ -70,30 +72,30 @@ impl App for ZeroSplitter { ctx.send_viewport_cmd(eframe::egui::ViewportCommand::StartDrag); } - if self.toggles.show_options_menu { - options_menu(ctx, &self.db, &mut self.toggles.show_options_menu); + if self.options.show_options_menu { + self.options_menu(ctx); }; ui.with_layout(Layout::top_down_justified(Align::Min), |ui| { ui.horizontal_top(|ui| { ui.with_layout(Layout::right_to_left(Align::Min), |ui| { if ui.button("⚙").clicked() { - self.toggles.show_options_menu = true; + self.options.show_options_menu = true; } }); }); ui.horizontal(|ui| { - ui.toggle_value(&mut self.toggles.relative_score, "RELATIVE") + ui.toggle_value(&mut self.options.relative_score, "RELATIVE") .on_hover_text("Display relative score per split or running total of score"); - ui.toggle_value(&mut self.toggles.show_gold_split, "BEST SPLITS") + ui.toggle_value(&mut self.options.show_gold_split, "BEST SPLITS") .on_hover_text("Show your PB's splits or your best splits on the left"); - ui.toggle_value(&mut self.toggles.names, "NAMES") + ui.toggle_value(&mut self.options.names, "NAMES") .on_hover_text("Toggle descriptive or number names for WV splits"); if CONFIG.get().unwrap().decoration_button { let deco_toggle = ui - .toggle_value(&mut self.toggles.decorations, "DECOR") + .toggle_value(&mut self.options.decorations, "DECOR") .on_hover_text("Toggle the decoration (title bar...)"); - if deco_toggle.changed() && self.toggles.decorations { + if deco_toggle.changed() && self.options.decorations { ctx.send_viewport_cmd(eframe::egui::ViewportCommand::Decorations(true)); } else if deco_toggle.changed() { ctx.send_viewport_cmd(eframe::egui::ViewportCommand::Decorations(false)); @@ -124,23 +126,33 @@ impl App for ZeroSplitter { } }); - if let Ok(data) = self.calculate_splits() { - self.display_splits(ui, data); + TopBottomPanel::bottom("footer") + .show_separator_line(false) + .show(ctx, |ui| { + ui.label(format!( + "Personal Best: {}", + self.db.get_pb_run(&self.categories).map_or(0, |r| r.1) + )); + ui.label(format!( + "Sum of Best: {}", + self.db.get_gold_splits(&self.categories).map_or(0, |s| s.iter().sum()) + )); + }); + + if let Ok(data) = self.calculate_synthetic_data() { + self.display_splits( + ui, + self.run.splits().unwrap(), + self.run.current_split().unwrap_or_default(), + data, + self.categories.current().mode, + ); } else { ui.centered_and_justified(|ui| { ui.style_mut().interaction.selectable_labels = false; ui.label("Waiting for a run to start...") }); }; - - ui.label(format!( - "Personal Best: {}", - self.db.get_pb_run(&self.categories).map_or(0, |r| r.1) - )); - ui.label(format!( - "Sum of Best: {}", - self.db.get_gold_splits(&self.categories).map_or(0, |s| s.iter().sum()) - )); }); }); @@ -183,6 +195,9 @@ impl App for ZeroSplitter { ); } } + + let size = ctx.used_size(); + ctx.send_viewport_cmd(eframe::egui::ViewportCommand::InnerSize(size)); } fn on_exit(&mut self, _gl: Option<&eframe::glow::Context>) { @@ -192,137 +207,337 @@ impl App for ZeroSplitter { } } +#[derive(Clone, Copy)] +struct SyntheticData { + gold_split: i32, + pb_split: i32, + summed_score: i32, + summed_gold_split: i32, + summed_pb_split: i32, +} + impl ZeroSplitter { - fn calculate_splits(&self) -> Result, ZeroError> { - // relative split: score gained during one split - // absolute split: total score during one split + fn calculate_synthetic_data(&self) -> Result, ZeroError> { let raw_splits = self.run.scores()?; - let mut ret = Vec::new(); - for (i, rel_split, abs_split) in raw_splits.iter().enumerate().map(|(i, &s)| { - (i, s, { - raw_splits - .clone() - .iter() - .enumerate() - .take_while(|&(idx, _)| idx <= i) - .fold(0, |acc, (_, &s)| acc + s) - }) - }) { - let split = if self.toggles.relative_score { - rel_split - } else { - abs_split - }; - // Get relative/absolute gold split - // Gold split = high score of this split in any run - let best_splits = self.db.get_gold_splits(&self.categories); - let gold_split = match (best_splits, self.toggles.relative_score) { - (Ok(splits), true) => *splits.get(i).unwrap_or(&0), - (Ok(splits), false) => splits - .iter() - .enumerate() - .take_while(|&(idx, _)| idx <= i) - .fold(0, |acc, (_, &s)| acc + s), - _ => 0, - }; - // Get relative/absolute split in the PB - // PB split = score of this split in the PB run - let compare = self.categories.get_comparison(); - let rel_pb_split = *compare.get(i).unwrap_or(&0); - let abs_pb_split = compare - .iter() - .enumerate() - .take_while(|&(idx, _)| idx <= i) - .fold(0, |acc, (_, &s)| acc + s); - - let pb_split = if self.toggles.relative_score { - rel_pb_split - } else { - abs_pb_split - }; - ret.push((gold_split, split, pb_split)); - } - Ok(ret) + let summed_score = running_total(raw_splits); + + let gold_splits = self.categories.get_golds().clone(); + let summed_gold_splits = running_total(gold_splits.clone()); + + let pb_splits = self.categories.get_comparison().clone(); + let summed_pb_splits = running_total(pb_splits.clone()); + + Ok(itertools::multizip(( + summed_score, + gold_splits, + summed_gold_splits, + pb_splits, + summed_pb_splits, + )) + .map( + |(summed_score, gold_split, summed_gold_split, pb_split, summed_pb_split)| SyntheticData { + gold_split, + pb_split, + summed_score, + summed_gold_split, + summed_pb_split, + }, + ) + .collect()) } - fn display_splits(&self, ui: &mut Ui, split_data: Vec<(i32, i32, i32)>) { - let current_split = self.run.current_split().unwrap_or(0); - - for (n, &(gold_score, current_score, compare_score)) in split_data.iter().enumerate() { - // translate split number to stage/loop for GO - let stage_n = (n & 3) + 1; - let loop_n = (n >> 2) + 1; - Sides::new().show( - ui, - |left| { - match self.categories.current().mode { - Gamemode::GreenOrange => left.label(format!("{loop_n}-{stage_n}")), - Gamemode::WhiteVanilla => { - if self.toggles.names { - left.label(vanilla_descriptive_split_names(n)) - } else { - left.label(vanilla_split_names(n)) + /// Display data recieved from params. Only use the self reference for options/config if possible + fn display_splits( + &self, + ui: &mut Ui, + run_data: Vec, + current_split: usize, + synthetic_data: Vec, + current_mode: Gamemode, + ) { + ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend); + + let min_col = CONFIG.get().unwrap().min_col; + + TableBuilder::new(ui) + .columns(Column::auto().at_least(min_col), self.options.columns_order.len()) + .cell_layout(Layout::right_to_left(Align::Min)) + .scroll_bar_visibility(egui::scroll_area::ScrollBarVisibility::AlwaysHidden) + .header(20.0, |mut row| { + let headings = [ + "names", + "best", + "sum best", + "diff", + "sum diff", + "gold diff", + "sum gold diff", + "score", + "sum score", + "P rank", + "D rank", + "mult", + "bonus", + "time", + "frames", + "cold", + "drops", + ]; + + for idx in self.options.columns_order.iter() { + row.col(|ui| { + ui.label(headings[*idx]); + }); + } + }) + .body(|body| { + body.rows(20.0, run_data.len(), |mut row| { + let n = row.index(); + let this_current = run_data[n]; + let this_synthetic = synthetic_data[n]; + + // translate split number to stage/loop for GO + let stage_n = (n & 3) + 1; + let loop_n = (n >> 2) + 1; + + let names_col = Box::new(|ui: &mut Ui| { + match current_mode { + Gamemode::GreenOrange => ui.label(format!("{loop_n}-{stage_n}")), + Gamemode::WhiteVanilla => { + if self.options.names { + ui.label(vanilla_descriptive_split_names(n)) + } else { + ui.label(vanilla_split_names(n)) + } } + Gamemode::BlackOnion => todo!(), + }; + }); + + let gold_col = Box::new(|ui: &mut Ui| { + if this_synthetic.gold_split > 0 { + ui.colored_label(GREEN, this_synthetic.gold_split.to_string()); } - Gamemode::BlackOnion => todo!(), - }; + }); - if self.toggles.show_gold_split { - if gold_score > 0 { - left.colored_label(GREEN, gold_score.to_string()); + let summed_gold_col = Box::new(|ui: &mut Ui| { + if this_synthetic.summed_gold_split > 0 { + ui.colored_label(GREEN, this_synthetic.summed_gold_split.to_string()); } - } else if compare_score > 0 { - left.colored_label(GREEN, compare_score.to_string()); - } - }, - |right| { - // Only write splits up to the current split - if n <= self.run.current_split().unwrap() { - // Set color of split (rightmost number) - let split_color = if current_split == n && self.split_delay.is_none() { - Color32::WHITE - } else if current_score >= gold_score { - DARKER_ORANGE - } else { - DARK_ORANGE - }; + }); + + let diff_col = Box::new(|ui: &mut Ui| { + if n < current_split { + // past split, we should show a diff + let diff = this_current.score - this_synthetic.pb_split; + let diff_color = if diff > 0 { + LIGHT_ORANGE + } else if diff == 0 { + Color32::WHITE + } else { + DARK_GREEN + }; + ui.colored_label(diff_color, format!("{diff:+}")); + } + }); - right.colored_label(split_color, current_score.to_string()); + let summed_diff_col = Box::new(|ui: &mut Ui| { + if n < current_split { + // past split, we should show a diff + let summed_diff = this_synthetic.summed_score - this_synthetic.summed_pb_split; + let diff = this_current.score - this_synthetic.pb_split; + let diff_color = if summed_diff > 0 { + if diff >= 0 { LIGHT_ORANGE } else { DARK_ORANGE } + } else if summed_diff == 0 { + Color32::WHITE + } else { + if diff >= 0 { DARK_GREEN } else { DARKER_GREEN } + }; + ui.colored_label(diff_color, format!("{summed_diff:+}")); //TODO: re-add subcolors + } + }); + let gold_diff_col = Box::new(|ui: &mut Ui| { if n < current_split { // past split, we should show a diff - let diff = current_score - compare_score; - if self.toggles.relative_score { - let diff_color = if diff > 0 { - LIGHT_ORANGE - } else if diff == 0 { - Color32::WHITE - } else { - DARK_GREEN - }; - right.colored_label(diff_color, format!("{diff:+}")); + let diff = this_current.score - this_synthetic.gold_split; + let diff_color = if diff > 0 { + LIGHT_ORANGE + } else if diff == 0 { + Color32::WHITE } else { - let &(_, prev_score, prev_compare) = - n.checked_sub(1).map_or(&(0, 0, 0), |n| split_data.get(n).unwrap()); - let rel_diff = (current_score - prev_score) - (compare_score - prev_compare); - let diff_color = if diff > 0 { - if rel_diff > 0 { LIGHT_ORANGE } else { DARKER_ORANGE } - } else if diff == 0 { - Color32::WHITE - } else if rel_diff > 0 { - DARK_GREEN + DARK_GREEN + }; + ui.colored_label(diff_color, format!("{diff:+}")); + } + }); + + let summed_gold_diff_col = Box::new(|ui: &mut Ui| { + if n < current_split { + // past split, we should show a diff + let summed_diff = this_synthetic.summed_score - this_synthetic.summed_gold_split; + let diff = this_current.score - this_synthetic.gold_split; + let diff_color = if summed_diff > 0 { + if diff >= 0 { LIGHT_ORANGE } else { DARK_ORANGE } + } else if summed_diff == 0 { + Color32::WHITE + } else { + if diff >= 0 { DARK_GREEN } else { DARKER_GREEN } + }; + ui.colored_label(diff_color, format!("{summed_diff:+}")); //TODO: re-add subcolors + } + }); + + let scores_col = Box::new(|ui: &mut Ui| { + ui.horizontal(|ui| { + ui.with_layout(Layout::right_to_left(Align::Min), |ui| { + // Only write splits up to the current split + if n <= current_split { + // Set color of split (rightmost number) + let split_color = if current_split == n && self.split_delay.is_none() { + Color32::WHITE + } else if this_current.score >= this_synthetic.gold_split { + DARKER_ORANGE + } else { + DARK_ORANGE + }; + + ui.colored_label(split_color, this_current.score.to_string()); } else { - DARKER_GREEN - }; - right.colored_label(diff_color, format!("{diff:+}")); - } + ui.colored_label(DARK_GREEN, "--"); + } + }); + }); + }); + + let summed_scores_col = Box::new(|ui: &mut Ui| { + ui.horizontal(|ui| { + ui.with_layout(Layout::right_to_left(Align::Min), |ui| { + // Only write splits up to the current split + if n <= current_split { + let split_color = if current_split == n && self.split_delay.is_none() { + Color32::WHITE + } else if this_synthetic.summed_score >= this_synthetic.summed_gold_split { + DARKER_ORANGE + } else { + DARK_ORANGE + }; + + ui.colored_label(split_color, this_synthetic.summed_score.to_string()); + } else { + ui.colored_label(DARK_GREEN, "--"); + } + }); + }); + }); + + let prank_col = Box::new(|ui: &mut Ui| { + if this_current.pattern_rank > 0.0 { + ui.colored_label(GREEN, format!("{}", this_current.pattern_rank)); + } else { + ui.colored_label(DARK_GREEN, "--"); } - } else { - right.colored_label(DARK_GREEN, "--"); + }); + + let drank_col = Box::new(|ui: &mut Ui| { + if this_current.dynamic_rank > 0.0 { + ui.colored_label(GREEN, format!("{:.4}", this_current.dynamic_rank)); + } else { + ui.colored_label(DARK_GREEN, "--"); + } + }); + + let timebonus_col = Box::new(|ui: &mut Ui| { + if this_current.time_bonus > 0 { + ui.colored_label(GREEN, format!("{}", this_current.time_bonus)); + } else { + ui.colored_label(DARK_GREEN, "--"); + } + }); + + let time_col = Box::new(|ui: &mut Ui| { + if this_current.time_frames > 0 { + ui.colored_label(GREEN, format!("{:.2}", this_current.time_frames as f64 / 60.0)); + } else { + ui.colored_label(DARK_GREEN, "--"); + } + }); + + let time_frames_col = Box::new(|ui: &mut Ui| { + if this_current.time_frames > 0 { + ui.colored_label(GREEN, format!("{}", this_current.time_frames)); + } else { + ui.colored_label(DARK_GREEN, "--"); + } + }); + + let coldframes_col = Box::new(|ui: &mut Ui| { + if this_current.coldframes > 0 { + ui.colored_label(GREEN, format!("{}", this_current.coldframes)); + } else { + ui.colored_label(DARK_GREEN, "--"); + } + }); + + let mult_drops_col = Box::new(|ui: &mut Ui| { + if this_current.mult_drops > 0 { + ui.colored_label(GREEN, format!("{}", this_current.mult_drops)); + } else { + ui.colored_label(DARK_GREEN, "--"); + } + }); + + let mult_col = simple_col(this_current.mult); + + let mut columns: Vec> = vec![ + names_col, + gold_col, + summed_gold_col, + diff_col, + summed_diff_col, + gold_diff_col, + summed_gold_diff_col, + scores_col, + summed_scores_col, + prank_col, + drank_col, + mult_col, + timebonus_col, + time_col, + time_frames_col, + coldframes_col, + mult_drops_col, + ]; + + for idx in self.options.columns_order.iter() { + row.col(columns.get_mut(*idx).unwrap()); } - }, - ); - } + }); + }); + } +} + +fn running_total(mut input: Vec) -> Vec +where + T: Add + Copy + Default, +{ + for i in 0..input.len() { + input[i] = input[i] + i.checked_sub(1).map_or(T::default(), |j| input[j]) } + + input +} + +fn simple_col(value: T) -> Box +where + T: Display + PartialOrd + Default + Copy, +{ + Box::new(move |ui: &mut Ui| { + if value > T::default() { + ui.colored_label(GREEN, format!("{}", value)); + } else { + ui.colored_label(DARK_GREEN, "--"); + } + }) } diff --git a/splitter/src/config.rs b/splitter/src/config.rs index 2bc5e22..a7ebf39 100644 --- a/splitter/src/config.rs +++ b/splitter/src/config.rs @@ -4,7 +4,7 @@ use std::{ sync::OnceLock, }; -use crate::VERSION; +use crate::{VERSION, ZeroSplitter}; use eframe::egui::{Context, Id, RichText, Separator, TextEdit, ViewportBuilder, ViewportId}; use toml::{Table, Value}; @@ -52,6 +52,14 @@ pub fn load_config() -> Result<(), ZeroError> { } _ => return Err(ZeroError::ConfigError("check_for_updates".to_owned())), }, + min_col: match table.get("minimum_column_width") { + Some(Value::Float(f)) => *f as f32, + None => { + writer.write_all(include_bytes!("../assets/config_sections/min_col.toml"))?; + 50.0 + } + _ => return Err(ZeroError::ConfigError("min_col".to_owned())), + }, }; CONFIG.set(config).map_err(|_| ZeroError::StaticAlreadyInit)?; @@ -71,83 +79,125 @@ pub struct Config { pub zoom_level: f32, pub decoration_button: bool, pub check_for_updates: bool, + pub min_col: f32, } -pub fn options_menu(ctx: &Context, db: &crate::database::Database, open: &mut bool) -> () { - ctx.show_viewport_immediate( - ViewportId::from_hash_of("options_menu_viewport"), - ViewportBuilder::default().with_title("Options"), - |ctx, _| { - eframe::egui::CentralPanel::default().show(ctx, |ui| { - if CONFIG.get().unwrap().check_for_updates { - // UPDATER +impl ZeroSplitter { + pub fn options_menu(&mut self, ctx: &Context) -> () { + ctx.show_viewport_immediate( + ViewportId::from_hash_of("options_menu_viewport"), + ViewportBuilder::default().with_title("Options"), + |ctx, _| { + eframe::egui::CentralPanel::default().show(ctx, |ui| { + if CONFIG.get().unwrap().check_for_updates { + // UPDATER + ui.horizontal(|ui| { + ui.label(RichText::new("Updater").color(GREEN).heading()); + ui.add(Separator::default().horizontal()) + }); + ui.horizontal(|ui| { + let update_label_id = Id::new("update_label"); + if ui.button("Check for updates").clicked() { + ctx.data_mut(|data| { + data.insert_temp( + update_label_id, + match check_for_updates() { + Ok(Some(url)) => format!("Update avaliable - {url}"), + Ok(None) => format!("Up to date - {VERSION}"), + Err(e) => format!("Failed to check for update - {e:?}"), + }, + ) + }); + } + ui.label( + ctx.data(|data| data.get_temp::(update_label_id)) + .unwrap_or_default(), + ); + }); + }; + // EXTRA COLUMNS ui.horizontal(|ui| { - ui.label(RichText::new("Updater").color(GREEN).heading()); + ui.label(RichText::new("Column Selector").color(GREEN).heading()); ui.add(Separator::default().horizontal()) }); - ui.horizontal(|ui| { - let update_label_id = Id::new("update_label"); - if ui.button("Check for updates").clicked() { - ctx.data_mut(|data| { - data.insert_temp( - update_label_id, - match check_for_updates() { - Ok(Some(url)) => format!("Update avaliable - {url}"), - Ok(None) => format!("Up to date - {VERSION}"), - Err(e) => format!("Failed to check for update - {e:?}"), - }, - ) - }); - } - ui.label( - ctx.data(|data| data.get_temp::(update_label_id)) - .unwrap_or_default(), + + let mut col_checks = vec![false; 17]; + for col in self.options.columns_order.iter() { + col_checks[*col] = true + } + + // TODO: don't do this + ui.checkbox(col_checks.get_mut(0).unwrap(), "names"); + ui.checkbox(col_checks.get_mut(1).unwrap(), "best"); + ui.checkbox(col_checks.get_mut(2).unwrap(), "summed best"); + ui.checkbox(col_checks.get_mut(3).unwrap(), "diff"); + ui.checkbox(col_checks.get_mut(4).unwrap(), "summed diff"); + ui.checkbox(col_checks.get_mut(5).unwrap(), "gold diff"); + ui.checkbox(col_checks.get_mut(6).unwrap(), "summed gold diff"); + ui.checkbox(col_checks.get_mut(7).unwrap(), "score"); + ui.checkbox(col_checks.get_mut(8).unwrap(), "summed score"); + ui.checkbox(col_checks.get_mut(9).unwrap(), "pattern rank"); + ui.checkbox(col_checks.get_mut(10).unwrap(), "dynamic rank"); + ui.checkbox(col_checks.get_mut(11).unwrap(), "mult (WV)"); + ui.checkbox(col_checks.get_mut(12).unwrap(), "time bonus (WV)"); + ui.checkbox(col_checks.get_mut(13).unwrap(), "time (WV)"); + ui.checkbox(col_checks.get_mut(14).unwrap(), "time (frames) (WV)"); + ui.checkbox(col_checks.get_mut(15).unwrap(), "coldframes (WV)") + .on_hover_text( + "The number of frames the time bonus countdown was paused due to killing all enemies", ); + ui.checkbox(col_checks.get_mut(16).unwrap(), "mult drops (GO)"); + + self.options.columns_order = col_checks + .iter() + .enumerate() + .filter_map(|(i, b)| b.then_some(i)) + .collect(); + + // IMPORTER + ui.horizontal(|ui| { + ui.label(RichText::new("Importer").color(GREEN).heading()); + ui.add(Separator::default().horizontal()) }); - }; - // IMPORTER - ui.horizontal(|ui| { - ui.label(RichText::new("Importer").color(GREEN).heading()); - ui.add(Separator::default().horizontal()) - }); - // Category name - let category_name_id = ui.label("Category name").id; - let mut category_name = ctx - .data(|data| data.get_temp::(category_name_id)) - .unwrap_or_default(); - ui.add(TextEdit::singleline(&mut category_name)); - ctx.data_mut(|data| data.insert_temp(category_name_id, category_name.clone())); - - // Split data - let splits_data_id = ui.label("Split scores").id; - let mut run_string = ctx - .data(|data| data.get_temp::(splits_data_id)) - .unwrap_or_default(); - ui.add(TextEdit::multiline(&mut run_string).hint_text("111, 222, 333, 444, 555, 666, 777, 888")); - ctx.data_mut(|data| data.insert_temp(splits_data_id, run_string.clone())); - - if ui.button("IMPORT").clicked() { - let splits = run_string - .split(", ") - .map(|s| s.parse().unwrap_or_default()) - .collect::>(); - match db.import_run(splits.clone(), &category_name) { - Ok(_) => { - println!( - "Successfully imported run with {} splits and total score {}", - splits.len(), - splits.iter().sum::() - ); - run_string.clear(); - } - Err(err) => println!("Import failed: {err}"), + // Category name + let category_name_id = ui.label("Category name").id; + let mut category_name = ctx + .data(|data| data.get_temp::(category_name_id)) + .unwrap_or_default(); + ui.add(TextEdit::singleline(&mut category_name)); + ctx.data_mut(|data| data.insert_temp(category_name_id, category_name.clone())); + + // Split data + let splits_data_id = ui.label("Split scores").id; + let mut run_string = ctx + .data(|data| data.get_temp::(splits_data_id)) + .unwrap_or_default(); + ui.add(TextEdit::multiline(&mut run_string).hint_text("111, 222, 333, 444, 555, 666, 777, 888")); + ctx.data_mut(|data| data.insert_temp(splits_data_id, run_string.clone())); + + if ui.button("IMPORT").clicked() { + let splits = run_string + .split(", ") + .map(|s| s.parse().unwrap_or_default()) + .collect::>(); + match self.db.import_run(splits.clone(), &category_name) { + Ok(_) => { + println!( + "Successfully imported run with {} splits and total score {}", + splits.len(), + splits.iter().sum::() + ); + run_string.clear(); + } + Err(err) => println!("Import failed: {err}"), + }; }; - }; - }); + }); - if ctx.input(|i| i.viewport().close_requested()) { - *open = false - }; - }, - ); + if ctx.input(|i| i.viewport().close_requested()) { + self.options.show_options_menu = false + }; + }, + ); + } } diff --git a/splitter/src/database.rs b/splitter/src/database.rs index 3634f43..0303057 100644 --- a/splitter/src/database.rs +++ b/splitter/src/database.rs @@ -394,6 +394,7 @@ mod tests { categories: vec![], current: 0, comparison_cache: vec![], + golds_cache: vec![], }; categories.load(&db); diff --git a/splitter/src/main.rs b/splitter/src/main.rs index 7d80564..739fb46 100644 --- a/splitter/src/main.rs +++ b/splitter/src/main.rs @@ -1,8 +1,9 @@ use std::{ env, net::UdpSocket, + path::PathBuf, sync::{ - OnceLock, + Arc, OnceLock, mpsc::{self, Receiver, Sender}, }, thread, @@ -14,10 +15,11 @@ use eframe::{ NativeOptions, egui::{Context, IconData, ThemePreference, ViewportBuilder}, }; +use egui::{FontData, FontDefinitions}; use log::{debug, error}; use serde::{Deserialize, Serialize}; -use crate::{app::Toggles, config::CONFIG, database::Database, run::Run, theme::zeroranger_visuals}; +use crate::{app::Options, config::CONFIG, database::Database, run::Run, theme::zeroranger_visuals}; mod app; mod config; @@ -105,7 +107,7 @@ struct ZeroSplitter { split_delay: Option, start_delay: Option, db: Database, - toggles: Toggles, + options: Options, } impl ZeroSplitter { @@ -124,7 +126,7 @@ impl ZeroSplitter { split_delay: None, start_delay: None, db, - toggles: Default::default(), + options: Default::default(), }; zerosplitter.categories.load(&zerosplitter.db).unwrap(); @@ -181,7 +183,7 @@ impl ZeroSplitter { self.reset(); self.run.start(frame); self.run.set_split(frame_split).unwrap(); - self.categories.refresh_comparison(&self.db).unwrap(); + self.categories.refresh_caches(&self.db).unwrap(); } if !frame.is_menu() && self.run.is_active() { @@ -190,13 +192,21 @@ impl ZeroSplitter { return; } + // check for mult halving, second conditional is for odd multiplier + if frame.multiplier_one + 10 == (self.last_frame.multiplier_one + 10) / 2 + || frame.multiplier_one + 9 == (self.last_frame.multiplier_one + 10) / 2 + { + dbg!(frame.multiplier_one, self.last_frame.multiplier_one); + self.run.bump_mult_drops(); + } + // Split if necessary if (frame_split > self.run.current_split().unwrap()) && !self.last_frame.is_menu() { self.run.split().unwrap(); } // Update run and split scores - self.run.store_end_of_split(frame).unwrap(); + self.run.update(frame).unwrap(); } else { // End the run if we're back on the menu // TODO make certain this is unreachable? @@ -205,17 +215,19 @@ impl ZeroSplitter { } fn update_whitevanilla(&mut self, frame: FrameData) { + let last_frame = self.last_frame; // Skip update if current category isn't White Vanilla or if on menu if self.categories.current().mode != Gamemode::WhiteVanilla || frame.is_menu() { return; } // Reset if we returned to 1-1 - if frame.total_score() == 0 && self.last_frame.total_score() > 0 || self.last_frame.is_menu() { + if frame.total_score() == 0 && last_frame.total_score() > 0 || last_frame.is_menu() { self.reset(); self.start_delay = Some(1); } + // Start run the frame after reset if let Some(start_delay) = self.start_delay { if start_delay >= 1 { self.start_delay = Some(start_delay - 1) @@ -230,31 +242,36 @@ impl ZeroSplitter { _ => panic!("Stage out of bounds! {}", frame.stage), }) .unwrap(); - self.categories.refresh_comparison(&self.db).unwrap(); + self.categories.refresh_caches(&self.db).unwrap(); self.start_delay = None; return; } } + // Run in progress if !frame.is_menu() && !(self.run == Run::Inactive) { - // Split if necessary; score requirement prevents spurious splits after a reset - if frame.timer_wave == 0 && self.last_frame.timer_wave != 0 && frame.total_score() > 0 { - // get time from right before it resets - self.run.store_time(self.last_frame).unwrap(); - self.split_delay = Some(SPLIT_DELAY_FRAMES); - } - + // Operate within the 20-frame inconsistent window between splits + // ~Frame 18: time bonus score applied if let Some(split_delay) = self.split_delay { - if split_delay >= 1 { - self.split_delay = Some(split_delay - 1) - } else { + self.split_delay = Some(split_delay + 1); + // wait after the timer reset to get time bonus + if split_delay == 20 { self.run.split().unwrap(); + } else if split_delay == 60 { + self.run.store_mult(frame).unwrap(); self.split_delay = None } + } else { + self.run.store_time(last_frame).unwrap(); + } + + // Split if necessary; score requirement prevents spurious splits after a reset + if frame.timer_wave == 0 && self.last_frame.timer_wave != 0 && frame.total_score() > 0 { + self.split_delay = Some(0); } // Update run and split scores - self.run.store_end_of_split(frame).unwrap(); + self.run.update(frame).unwrap(); } else if frame.is_menu() { // End the run if we're back on the menu // TODO make certain this is unreachable? @@ -384,6 +401,7 @@ struct CategoryManager { categories: Vec, current: usize, comparison_cache: Vec, + golds_cache: Vec, } impl CategoryManager { @@ -392,6 +410,7 @@ impl CategoryManager { categories: Vec::new(), current: 0, comparison_cache: Vec::new(), + golds_cache: Vec::new(), } } @@ -403,7 +422,6 @@ impl CategoryManager { &mut self.categories[self.current] } - #[must_use] pub fn push(&mut self, name: String, mode: Gamemode, db: &Database) -> Result<(), ZeroError> { let id = db.insert_new_category(name.clone(), mode)?; self.categories.push(Category { name, mode, id }); @@ -449,6 +467,7 @@ impl CategoryManager { } self.current = new_idx; self.refresh_comparison(db)?; + self.refresh_golds(db)?; Ok(true) } @@ -460,7 +479,19 @@ impl CategoryManager { &self.comparison_cache } - pub fn refresh_comparison(&mut self, db: &Database) -> Result<(), ZeroError> { + pub fn get_golds(&self) -> &Vec { + if self.golds_cache.len() == 0 { + panic!() + } + &self.golds_cache + } + + pub fn refresh_caches(&mut self, db: &Database) -> Result<(), ZeroError> { + self.refresh_comparison(db)?; + self.refresh_golds(db) + } + + fn refresh_comparison(&mut self, db: &Database) -> Result<(), ZeroError> { self.comparison_cache = match db.get_pb_run(self) { Ok((scores, _, mode)) if mode == self.current().mode => scores, Ok(_) => return Err(ZeroError::DifficultyMismatch), @@ -469,6 +500,15 @@ impl CategoryManager { }; Ok(()) } + + fn refresh_golds(&mut self, db: &Database) -> Result<(), ZeroError> { + self.golds_cache = match db.get_gold_splits(self) { + Ok(scores) => scores, + Err(rusqlite::Error::QueryReturnedNoRows) => vec![0; self.current().mode.splits()], + Err(e) => return Err(ZeroError::DatabaseError(e)), + }; + Ok(()) + } } #[derive(Debug, Serialize, Deserialize, Clone)] diff --git a/splitter/src/run.rs b/splitter/src/run.rs index 43d290f..978ec9f 100644 --- a/splitter/src/run.rs +++ b/splitter/src/run.rs @@ -20,6 +20,17 @@ pub enum Run { } impl Run { + fn current_data_mut(&mut self) -> &mut SplitData { + if let Run::Active { + current_split, splits, .. + } = self + { + &mut splits[*current_split] + } else { + panic!() + } + } + pub fn score(&self) -> Option { match *self { Run::Inactive => None, @@ -99,7 +110,7 @@ impl Run { /// Integrate the data from the frame into the Run data. /// Call this one at the end of the split /// Stores score, mult, and rank - pub fn store_end_of_split(&mut self, frame: FrameData) -> Result<(), ZeroError> { + pub fn update(&mut self, frame: FrameData) -> Result<(), ZeroError> { if let Self::Active { difficulty, splits, @@ -115,7 +126,6 @@ impl Run { } let current_data = &mut splits[*current_split]; current_data.score = frame.total_score() - *split_base_score; - current_data.mult = frame.multiplier_one; current_data.pattern_rank = frame.pattern_rank; current_data.dynamic_rank = frame.dynamic_rank; @@ -146,6 +156,21 @@ impl Run { } } + pub fn store_mult(&mut self, frame: FrameData) -> Result<(), ZeroError> { + if let Self::Active { + splits, current_split, .. + } = self + { + let current_data = &mut splits[*current_split]; + + current_data.mult = frame.multiplier_one; + + Ok(()) + } else { + Err(ZeroError::RunInactive) + } + } + pub fn split(&mut self) -> Result<(), ZeroError> { if let Self::Active { current_split, @@ -199,6 +224,12 @@ impl Run { Run::Residual { .. } => false, } } + + pub fn bump_mult_drops(&mut self) { + if let Run::Active { .. } = self { + self.current_data_mut().mult_drops += 1 + } + } } #[derive(Clone, Copy, Debug, PartialEq)] @@ -210,6 +241,7 @@ pub struct SplitData { pub time_frames: u32, pub time_bonus: u32, pub coldframes: u32, + pub mult_drops: u32, } impl SplitData { @@ -221,6 +253,7 @@ impl SplitData { time_frames: u32, time_bonus: u32, cool_frames: u32, + mult_drops: u32, ) -> Self { Self { score, @@ -230,6 +263,7 @@ impl SplitData { time_frames, time_bonus, coldframes: cool_frames, + mult_drops, } } } @@ -244,6 +278,7 @@ impl Default for SplitData { time_frames: Default::default(), time_bonus: Default::default(), coldframes: Default::default(), + mult_drops: Default::default(), } } } From ae8665c5120e453308586b488e1896ca1334dcf2 Mon Sep 17 00:00:00 2001 From: lily_and_doll <103815266+lily-and-doll@users.noreply.github.com> Date: Thu, 1 Jan 2026 02:34:48 -0500 Subject: [PATCH 52/52] Fix crash with no complete PB --- splitter/src/app.rs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/splitter/src/app.rs b/splitter/src/app.rs index cfe985d..19c7112 100644 --- a/splitter/src/app.rs +++ b/splitter/src/app.rs @@ -6,6 +6,7 @@ use eframe::{ }; use egui::TopBottomPanel; use egui_extras::{Column, TableBuilder}; +use itertools::Itertools; use crate::{ Gamemode, Run, ZeroError, ZeroSplitter, @@ -228,12 +229,13 @@ impl ZeroSplitter { let pb_splits = self.categories.get_comparison().clone(); let summed_pb_splits = running_total(pb_splits.clone()); + // Every iterator needs to be at least the length of the run; having extra should be OK Ok(itertools::multizip(( - summed_score, - gold_splits, - summed_gold_splits, - pb_splits, - summed_pb_splits, + summed_score.into_iter().pad_using(26, |_| 0), + gold_splits.into_iter().pad_using(26, |_| 0), + summed_gold_splits.into_iter().pad_using(26, |_| 0), + pb_splits.into_iter().pad_using(26, |_| 0), + summed_pb_splits.into_iter().pad_using(26, |_| 0), )) .map( |(summed_score, gold_split, summed_gold_split, pb_split, summed_pb_split)| SyntheticData {