From dcd9adc435b25a21aa7d946738a79542b62c170f Mon Sep 17 00:00:00 2001 From: Eddio0141 Date: Sun, 17 Nov 2024 01:12:18 +0000 Subject: [PATCH 01/19] initial work for splitting on hide --- src/modules/tas_studio/editor/db.rs | 10 ++ src/modules/tas_studio/editor/mod.rs | 191 ++++++++++++++++++++++++++- 2 files changed, 198 insertions(+), 3 deletions(-) diff --git a/src/modules/tas_studio/editor/db.rs b/src/modules/tas_studio/editor/db.rs index 66d52cde..e10fa8be 100644 --- a/src/modules/tas_studio/editor/db.rs +++ b/src/modules/tas_studio/editor/db.rs @@ -21,6 +21,8 @@ pub struct Branch { pub is_hidden: bool, pub script: HLTAS, + pub split_idx: Option, + pub full_script: HLTAS, pub stop_frame: u32, } @@ -190,11 +192,15 @@ impl Db { let script = HLTAS::from_str(&buffer) .map_err(|err| eyre!("invalid script value, cannot parse: {err:?}"))?; + let full_script = script.clone(); + Ok(Branch { branch_id, name, is_hidden, script, + full_script, + split_idx: None, stop_frame, }) } @@ -220,12 +226,16 @@ impl Db { let script = HLTAS::from_str(&buffer) .map_err(|err| eyre!("invalid script value, cannot parse: {err:?}"))?; + let full_script = script.clone(); + branches.push(Branch { branch_id, name, is_hidden, script, stop_frame, + full_script, + split_idx: None, }) } stmt.finalize()?; diff --git a/src/modules/tas_studio/editor/mod.rs b/src/modules/tas_studio/editor/mod.rs index bd7ff7e2..2f987d90 100644 --- a/src/modules/tas_studio/editor/mod.rs +++ b/src/modules/tas_studio/editor/mod.rs @@ -11,7 +11,7 @@ use bxt_strafe::{Hull, Trace}; use color_eyre::eyre::{self, ensure}; use glam::{IVec2, Vec2, Vec3}; use hltas::types::{ - AutoMovement, Change, ChangeTarget, Line, StrafeDir, StrafeSettings, StrafeType, + AutoMovement, Change, ChangeTarget, FrameBulk, Line, StrafeDir, StrafeSettings, StrafeType, VectorialStrafingConstraints, }; use hltas::HLTAS; @@ -3460,6 +3460,7 @@ impl Editor { return Err(ManualOpError::CannotDoDuringAdjustment); } + // TODO: apply split modifications let script = self.script(); if new_script == *script { return Ok(()); @@ -3660,12 +3661,19 @@ impl Editor { return Err(ManualOpError::CannotDoDuringAdjustment); } + let unhide = |editor: &mut Editor| { + editor.first_shown_frame_idx = 0; + let br = &mut editor.branch_mut().branch; + br.split_idx = None; + br.script = br.full_script.clone(); + }; + match self.hovered_frame_idx { - None => self.first_shown_frame_idx = 0, + None => unhide(self), // If we're pressing hide again on the first visible frame, unhide instead. This is // nicer than struggling to look away to unhide. Some(frame_idx) if frame_idx == self.first_shown_frame_idx => { - self.first_shown_frame_idx = 0; + unhide(self); } Some(frame_idx) => { let frame_count = self @@ -3678,6 +3686,132 @@ impl Editor { self.first_shown_frame_idx = min(frame_idx, frame_count.saturating_sub(1)); + // find split + let mut split_found_cnt = 0usize; + let mut split_line_last_idx = 0; // line index that is split + let mut frame_count = 0usize; + let lines = &self.branch().branch.script.lines; + for (i, line) in lines.iter().enumerate() { + match line { + Line::Save(_) => todo!(), + Line::Reset { non_shared_seed } => todo!(), + Line::Comment(comment) => { + if comment.trim() != "bxt-rs-split" { + continue; + } + + if i + 1 >= lines.len() { + continue; + } + + split_line_last_idx = i + 1; + split_found_cnt += 1; + } + Line::FrameBulk(fb) => { + let new_frame_count = frame_count + fb.frame_count.get() as usize; + if new_frame_count > self.first_shown_frame_idx { + break; + } + // to later determine if accurate frame has caught up with split + // TODO: share save from remote to main + frame_count = new_frame_count; + } + _ => (), + } + } + + // do we split? if no save is created then don't split, split later when the save is created at split + // TODO: split later logic impl + if split_found_cnt > 0 + && frame_count.saturating_sub(1) <= self.branch().first_predicted_frame + { + let branch = &mut self.branch_mut().branch; + + const PADDING_LOAD_FRAMES: u32 = 10; + + let full_script_split = match branch.split_idx { + Some(split_idx) => { + if split_idx < split_line_last_idx { + // previous split is before current split index, so we can just trim some frames off script + let mut after_padded_idx = 0usize; + let mut padded_count = PADDING_LOAD_FRAMES; + for (i, line) in branch.script.lines.iter().enumerate() { + let Line::FrameBulk(fb) = line else { + continue; + }; + + let fb_count = fb.frame_count.get(); + if fb_count > padded_count { + // split this framebulk, since padding is too big + let mut fb_split = fb.clone(); + fb_split.frame_count = + NonZeroU32::new(fb_count - padded_count).unwrap(); + branch + .script + .lines + .insert(i + 1, Line::FrameBulk(fb_split)); + after_padded_idx = i + 1; + break; + } else { + padded_count -= fb_count; + if padded_count == 0 { + after_padded_idx = i + 1; + break; + } + } + } + + for _ in 0..split_line_last_idx - split_idx { + // remove contents after the padding frames, which are the actual lines we're interested in + branch.script.lines.remove(after_padded_idx); + } + false + } else { + true + } + } + None => true, + }; + + // start new split from full script + if full_script_split { + branch.script.lines = + branch.full_script.lines[split_line_last_idx..].to_owned(); + let last_fb = branch.full_script.lines[..split_line_last_idx] + .iter() + .rev() + .filter_map(Line::frame_bulk) + .next() + .unwrap(); + let mut empty_fb = FrameBulk::with_frame_time(last_fb.frame_time.clone()); + empty_fb.frame_count = (PADDING_LOAD_FRAMES - 1).try_into().unwrap(); + branch.script.lines.insert(0, Line::FrameBulk(empty_fb)); + + // requires 1 frame from when save command is ran + let mut last_fb = last_fb.clone(); + last_fb.frame_count = NonZeroU32::new(1).unwrap(); + branch.script.lines.insert(1, Line::FrameBulk(last_fb)); + } + + // always a fb + let Line::FrameBulk(fb) = &mut branch.script.lines[1] else { + unreachable!(); + }; + match &mut fb.console_command { + Some(command) => command.push_str(";unpause"), + None => fb.console_command = Some("unpause".to_owned()), + } + + // TODO: what is the save name + // TODO: detect and remove previous loads, or just clear load command? is there a good reason to keep commands + branch.script.properties.load_command = + Some("_bxt_load split;bxt_autopause 1".to_owned()); + + // TODO: insert save in file on split sections + + branch.split_idx = Some(split_line_last_idx); + } + // Check if we need to unselect or unhover anything now hidden. let hovered_frame_bulk_idx = bulk_idx_and_repeat_at_frame(self.script(), self.first_shown_frame_idx) @@ -5147,4 +5281,55 @@ mod tests { prop_assert_eq!(old_script, new_script); } } + + #[test] + fn hide_and_split() { + let original_script = HLTAS::from_str( + "version 1\nframes\n\ + ----------|------|------|0.002|-|-|10\n\ + // bxt-rs-split\n\ + ----------|------|------|0.001|-|-|10", + ) + .unwrap(); + let mut editor = Editor::create_in_memory(&original_script).unwrap(); + + // right before split + editor.hovered_frame_idx = Some(9); + editor.hide_frames_up_to_hovered().unwrap(); + + let br = &editor.branch().branch; + assert_eq!(br.script, original_script); + assert_eq!(br.full_script, original_script); + + // right on split + editor.hovered_frame_idx = Some(10); + editor.branch_mut().first_predicted_frame = 20; + editor.hide_frames_up_to_hovered().unwrap(); + + let script = HLTAS::from_str( + "version 1\n\ + load_command _bxt_load split;bxt_autopause 1\n\ + frames\n\ + ----------|------|------|0.002|-|-|9\n\ + ----------|------|------|0.002|-|-|1|unpause\n\ + ----------|------|------|0.001|-|-|10", + ) + .unwrap(); + + let br = &editor.branch().branch; + assert_eq!(br.script, script); + + // after split, but already split + editor.hovered_frame_idx = Some(11); + editor.hide_frames_up_to_hovered().unwrap(); + + let br = &editor.branch().branch; + assert_eq!(br.script, script); + + // unhide (undo split) + editor.hide_frames_up_to_hovered().unwrap(); + + let br = &editor.branch().branch; + assert_eq!(br.script, original_script); + } } From 46308517a79941679a13390cb51e72fb5d8af0fc Mon Sep 17 00:00:00 2001 From: Eddio0141 Date: Tue, 19 Nov 2024 03:56:22 +0000 Subject: [PATCH 02/19] Revert "initial work for splitting on hide" This reverts commit dcd9adc435b25a21aa7d946738a79542b62c170f. --- src/modules/tas_studio/editor/db.rs | 10 -- src/modules/tas_studio/editor/mod.rs | 191 +-------------------------- 2 files changed, 3 insertions(+), 198 deletions(-) diff --git a/src/modules/tas_studio/editor/db.rs b/src/modules/tas_studio/editor/db.rs index e10fa8be..66d52cde 100644 --- a/src/modules/tas_studio/editor/db.rs +++ b/src/modules/tas_studio/editor/db.rs @@ -21,8 +21,6 @@ pub struct Branch { pub is_hidden: bool, pub script: HLTAS, - pub split_idx: Option, - pub full_script: HLTAS, pub stop_frame: u32, } @@ -192,15 +190,11 @@ impl Db { let script = HLTAS::from_str(&buffer) .map_err(|err| eyre!("invalid script value, cannot parse: {err:?}"))?; - let full_script = script.clone(); - Ok(Branch { branch_id, name, is_hidden, script, - full_script, - split_idx: None, stop_frame, }) } @@ -226,16 +220,12 @@ impl Db { let script = HLTAS::from_str(&buffer) .map_err(|err| eyre!("invalid script value, cannot parse: {err:?}"))?; - let full_script = script.clone(); - branches.push(Branch { branch_id, name, is_hidden, script, stop_frame, - full_script, - split_idx: None, }) } stmt.finalize()?; diff --git a/src/modules/tas_studio/editor/mod.rs b/src/modules/tas_studio/editor/mod.rs index 2f987d90..bd7ff7e2 100644 --- a/src/modules/tas_studio/editor/mod.rs +++ b/src/modules/tas_studio/editor/mod.rs @@ -11,7 +11,7 @@ use bxt_strafe::{Hull, Trace}; use color_eyre::eyre::{self, ensure}; use glam::{IVec2, Vec2, Vec3}; use hltas::types::{ - AutoMovement, Change, ChangeTarget, FrameBulk, Line, StrafeDir, StrafeSettings, StrafeType, + AutoMovement, Change, ChangeTarget, Line, StrafeDir, StrafeSettings, StrafeType, VectorialStrafingConstraints, }; use hltas::HLTAS; @@ -3460,7 +3460,6 @@ impl Editor { return Err(ManualOpError::CannotDoDuringAdjustment); } - // TODO: apply split modifications let script = self.script(); if new_script == *script { return Ok(()); @@ -3661,19 +3660,12 @@ impl Editor { return Err(ManualOpError::CannotDoDuringAdjustment); } - let unhide = |editor: &mut Editor| { - editor.first_shown_frame_idx = 0; - let br = &mut editor.branch_mut().branch; - br.split_idx = None; - br.script = br.full_script.clone(); - }; - match self.hovered_frame_idx { - None => unhide(self), + None => self.first_shown_frame_idx = 0, // If we're pressing hide again on the first visible frame, unhide instead. This is // nicer than struggling to look away to unhide. Some(frame_idx) if frame_idx == self.first_shown_frame_idx => { - unhide(self); + self.first_shown_frame_idx = 0; } Some(frame_idx) => { let frame_count = self @@ -3686,132 +3678,6 @@ impl Editor { self.first_shown_frame_idx = min(frame_idx, frame_count.saturating_sub(1)); - // find split - let mut split_found_cnt = 0usize; - let mut split_line_last_idx = 0; // line index that is split - let mut frame_count = 0usize; - let lines = &self.branch().branch.script.lines; - for (i, line) in lines.iter().enumerate() { - match line { - Line::Save(_) => todo!(), - Line::Reset { non_shared_seed } => todo!(), - Line::Comment(comment) => { - if comment.trim() != "bxt-rs-split" { - continue; - } - - if i + 1 >= lines.len() { - continue; - } - - split_line_last_idx = i + 1; - split_found_cnt += 1; - } - Line::FrameBulk(fb) => { - let new_frame_count = frame_count + fb.frame_count.get() as usize; - if new_frame_count > self.first_shown_frame_idx { - break; - } - // to later determine if accurate frame has caught up with split - // TODO: share save from remote to main - frame_count = new_frame_count; - } - _ => (), - } - } - - // do we split? if no save is created then don't split, split later when the save is created at split - // TODO: split later logic impl - if split_found_cnt > 0 - && frame_count.saturating_sub(1) <= self.branch().first_predicted_frame - { - let branch = &mut self.branch_mut().branch; - - const PADDING_LOAD_FRAMES: u32 = 10; - - let full_script_split = match branch.split_idx { - Some(split_idx) => { - if split_idx < split_line_last_idx { - // previous split is before current split index, so we can just trim some frames off script - let mut after_padded_idx = 0usize; - let mut padded_count = PADDING_LOAD_FRAMES; - for (i, line) in branch.script.lines.iter().enumerate() { - let Line::FrameBulk(fb) = line else { - continue; - }; - - let fb_count = fb.frame_count.get(); - if fb_count > padded_count { - // split this framebulk, since padding is too big - let mut fb_split = fb.clone(); - fb_split.frame_count = - NonZeroU32::new(fb_count - padded_count).unwrap(); - branch - .script - .lines - .insert(i + 1, Line::FrameBulk(fb_split)); - after_padded_idx = i + 1; - break; - } else { - padded_count -= fb_count; - if padded_count == 0 { - after_padded_idx = i + 1; - break; - } - } - } - - for _ in 0..split_line_last_idx - split_idx { - // remove contents after the padding frames, which are the actual lines we're interested in - branch.script.lines.remove(after_padded_idx); - } - false - } else { - true - } - } - None => true, - }; - - // start new split from full script - if full_script_split { - branch.script.lines = - branch.full_script.lines[split_line_last_idx..].to_owned(); - let last_fb = branch.full_script.lines[..split_line_last_idx] - .iter() - .rev() - .filter_map(Line::frame_bulk) - .next() - .unwrap(); - let mut empty_fb = FrameBulk::with_frame_time(last_fb.frame_time.clone()); - empty_fb.frame_count = (PADDING_LOAD_FRAMES - 1).try_into().unwrap(); - branch.script.lines.insert(0, Line::FrameBulk(empty_fb)); - - // requires 1 frame from when save command is ran - let mut last_fb = last_fb.clone(); - last_fb.frame_count = NonZeroU32::new(1).unwrap(); - branch.script.lines.insert(1, Line::FrameBulk(last_fb)); - } - - // always a fb - let Line::FrameBulk(fb) = &mut branch.script.lines[1] else { - unreachable!(); - }; - match &mut fb.console_command { - Some(command) => command.push_str(";unpause"), - None => fb.console_command = Some("unpause".to_owned()), - } - - // TODO: what is the save name - // TODO: detect and remove previous loads, or just clear load command? is there a good reason to keep commands - branch.script.properties.load_command = - Some("_bxt_load split;bxt_autopause 1".to_owned()); - - // TODO: insert save in file on split sections - - branch.split_idx = Some(split_line_last_idx); - } - // Check if we need to unselect or unhover anything now hidden. let hovered_frame_bulk_idx = bulk_idx_and_repeat_at_frame(self.script(), self.first_shown_frame_idx) @@ -5281,55 +5147,4 @@ mod tests { prop_assert_eq!(old_script, new_script); } } - - #[test] - fn hide_and_split() { - let original_script = HLTAS::from_str( - "version 1\nframes\n\ - ----------|------|------|0.002|-|-|10\n\ - // bxt-rs-split\n\ - ----------|------|------|0.001|-|-|10", - ) - .unwrap(); - let mut editor = Editor::create_in_memory(&original_script).unwrap(); - - // right before split - editor.hovered_frame_idx = Some(9); - editor.hide_frames_up_to_hovered().unwrap(); - - let br = &editor.branch().branch; - assert_eq!(br.script, original_script); - assert_eq!(br.full_script, original_script); - - // right on split - editor.hovered_frame_idx = Some(10); - editor.branch_mut().first_predicted_frame = 20; - editor.hide_frames_up_to_hovered().unwrap(); - - let script = HLTAS::from_str( - "version 1\n\ - load_command _bxt_load split;bxt_autopause 1\n\ - frames\n\ - ----------|------|------|0.002|-|-|9\n\ - ----------|------|------|0.002|-|-|1|unpause\n\ - ----------|------|------|0.001|-|-|10", - ) - .unwrap(); - - let br = &editor.branch().branch; - assert_eq!(br.script, script); - - // after split, but already split - editor.hovered_frame_idx = Some(11); - editor.hide_frames_up_to_hovered().unwrap(); - - let br = &editor.branch().branch; - assert_eq!(br.script, script); - - // unhide (undo split) - editor.hide_frames_up_to_hovered().unwrap(); - - let br = &editor.branch().branch; - assert_eq!(br.script, original_script); - } } From e4fa0e2fc128cb404ad4c81ded375aee000c502a Mon Sep 17 00:00:00 2001 From: Eddio0141 Date: Tue, 19 Nov 2024 06:37:43 +0000 Subject: [PATCH 03/19] way to track split markers in script --- src/modules/tas_studio/editor/db.rs | 229 +++++++++++++++++++++++++++- 1 file changed, 227 insertions(+), 2 deletions(-) diff --git a/src/modules/tas_studio/editor/db.rs b/src/modules/tas_studio/editor/db.rs index 66d52cde..0397104c 100644 --- a/src/modules/tas_studio/editor/db.rs +++ b/src/modules/tas_studio/editor/db.rs @@ -1,9 +1,10 @@ -use std::fmt; use std::path::Path; +use std::{collections::HashSet, fmt}; use bincode::Options; use color_eyre::eyre::{self, ensure, eyre}; -use hltas::HLTAS; +use hltas::{types::Line, HLTAS}; +use nom::FindSubstring; use rusqlite::{params, Connection, OpenFlags, OptionalExtension}; use serde::{Deserialize, Serialize}; @@ -21,6 +22,9 @@ pub struct Branch { pub is_hidden: bool, pub script: HLTAS, + pub splits: Vec, + pub split_idx: Option, + pub full_script: HLTAS, pub stop_frame: u32, } @@ -35,6 +39,141 @@ impl fmt::Debug for Branch { } } +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SplitInfo { + // start_idx is very literal on where it starts from + // meaning, there could be anything after this split, framebulk, special property line, etc + pub start_idx: usize, + // a split could have no name + // this could also be duplicate names, which becomes `None` + pub name: Option, + pub split_type: SplitType, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum SplitType { + Comment, + Reset, + Save, +} + +impl SplitInfo { + pub fn split_hltas(hltas: &HLTAS) -> Vec { + if hltas.lines.is_empty() { + return Vec::new(); + } + + let mut splits = Vec::new(); + + let mut i = 0usize; + // skip till there's at least 1 framebulk + while i < hltas.lines.len() { + if matches!(hltas.lines[i], Line::FrameBulk(_)) { + break; + } + + i += 1; + } + + while i < hltas.lines.len() { + // this is correct, previous search would place i on the framebulk + // so we are only interested on what comes after for the first split + i += 1; + + let line = &hltas.lines[i]; + + const SPLIT_MARKER: &str = "bxt-rs-split"; + + let name; + let start_idx; + let split_type; + let no_framebulks_left = |i| { + for line in hltas.lines[i..].iter() { + if matches!(line, Line::FrameBulk(_)) { + return false; + } + } + true + }; + + match line { + // TODO: save name;load name console + // TODO: handle setting shared rng, and what property lines do i bring over? + // TODO: handle completely invalid back to back splits + Line::Save(save_name) => { + i += 1; + if no_framebulks_left(i) { + break; + } + + name = Some(save_name.to_owned()); + start_idx = i; + split_type = SplitType::Save; + } + // this reset doesn't have a name, one with comment attached is handled below + Line::Reset { .. } => { + i += 1; + if no_framebulks_left(i) { + break; + } + + name = None; + start_idx = i; + split_type = SplitType::Reset; + } + Line::Comment(comment) => { + let comment = comment.trim(); + + if !comment.starts_with(SPLIT_MARKER) { + continue; + } + + let comment = &comment[SPLIT_MARKER.len()..]; + + if !comment.is_empty() && !comment.chars().next().unwrap().is_whitespace() { + continue; + } + + i += 1; + + // linked to reset? + split_type = + if i < hltas.lines.len() && matches!(hltas.lines[i], Line::Reset { .. }) { + i += 1; + if no_framebulks_left(i) { + break; + } + + SplitType::Reset + } else { + if no_framebulks_left(i) { + break; + } + + SplitType::Comment + }; + start_idx = i; + let comment = comment.trim_start(); + if comment.is_empty() { + name = None; + } else { + name = Some(comment.to_owned()); + } + } + _ => continue, + } + + splits.push(SplitInfo { + start_idx, + name, + split_type, + }); + } + + splits + } +} + #[derive(Debug, Clone)] pub struct GlobalSettings { pub current_branch_id: i64, @@ -190,11 +329,17 @@ impl Db { let script = HLTAS::from_str(&buffer) .map_err(|err| eyre!("invalid script value, cannot parse: {err:?}"))?; + let full_script = script.clone(); + let splits = SplitInfo::split_hltas(&full_script); + Ok(Branch { branch_id, name, is_hidden, script, + full_script, + split_idx: None, + splits, stop_frame, }) } @@ -220,12 +365,18 @@ impl Db { let script = HLTAS::from_str(&buffer) .map_err(|err| eyre!("invalid script value, cannot parse: {err:?}"))?; + let full_script = script.clone(); + let splits = SplitInfo::split_hltas(&full_script); + branches.push(Branch { branch_id, name, is_hidden, script, stop_frame, + full_script, + split_idx: None, + splits, }) } stmt.finalize()?; @@ -506,3 +657,77 @@ fn update_branch(conn: &Connection, branch: &Branch) -> eyre::Result<()> { Ok(()) } + +#[cfg(test)] +mod tests { + use hltas::HLTAS; + + use crate::modules::tas_studio::editor::db::{SplitInfo, SplitType}; + + #[test] + fn split_by_markers() { + // TODO: complete, duplicate names + let script = HLTAS::from_str( + "version 1\nframes\n\ + ----------|------|------|0.001|-|-|10\n\ + // bxt-rs-split name\n\ + ----------|------|------|0.002|-|-|10\n\ + ----------|------|------|0.002|-|-|10\n\ + // bxt-rs-split\n\ + ----------|------|------|0.003|-|-|10\n\ + ----------|------|------|0.003|-|-|10\n\ + ----------|------|------|0.003|-|-|10\n\ + save name2 + ----------|------|------|0.004|-|-|10\n\ + ----------|------|------|0.004|-|-|10\n\ + ----------|------|------|0.004|-|-|10\n\ + ----------|------|------|0.004|-|-|10\n\ + reset 0 + ----------|------|------|0.005|-|-|10\n\ + ----------|------|------|0.005|-|-|10\n\ + ----------|------|------|0.005|-|-|10\n\ + ----------|------|------|0.005|-|-|10\n\ + ----------|------|------|0.005|-|-|10\n\ + // bxt-rs-split name4 + reset 1 + ----------|------|------|0.006|-|-|10\n\ + ----------|------|------|0.006|-|-|10\n\ + ----------|------|------|0.006|-|-|10\n\ + ----------|------|------|0.006|-|-|10\n\ + ----------|------|------|0.006|-|-|10\n\ + ----------|------|------|0.006|-|-|10\n", + ) + .unwrap(); + + let splits = SplitInfo::split_hltas(&script); + let expected = vec![ + SplitInfo { + start_idx: 2, + name: Some("name".to_string()), + split_type: SplitType::Comment, + }, + SplitInfo { + start_idx: 5, + name: None, + split_type: SplitType::Comment, + }, + SplitInfo { + start_idx: 9, + name: Some("name2".to_string()), + split_type: SplitType::Save, + }, + SplitInfo { + start_idx: 14, + name: Some("name3".to_string()), + split_type: SplitType::Reset, + }, + SplitInfo { + start_idx: 21, + name: Some("name4".to_string()), + split_type: SplitType::Comment, + }, + ]; + + assert_eq!(splits, expected); + } +} From d0f6579b08ebd12569bf6634e4fc5e3c55487e3c Mon Sep 17 00:00:00 2001 From: Eddio0141 Date: Sun, 24 Nov 2024 06:25:43 +0000 Subject: [PATCH 04/19] Editor::rewrite does splits again Untested --- src/modules/tas_studio/editor/db.rs | 115 ++++++++++++++++----------- src/modules/tas_studio/editor/mod.rs | 46 ++++++++++- 2 files changed, 111 insertions(+), 50 deletions(-) diff --git a/src/modules/tas_studio/editor/db.rs b/src/modules/tas_studio/editor/db.rs index 0397104c..2cd01c2e 100644 --- a/src/modules/tas_studio/editor/db.rs +++ b/src/modules/tas_studio/editor/db.rs @@ -1,10 +1,10 @@ +use std::fmt; use std::path::Path; -use std::{collections::HashSet, fmt}; use bincode::Options; use color_eyre::eyre::{self, ensure, eyre}; use hltas::{types::Line, HLTAS}; -use nom::FindSubstring; +use itertools::{Itertools, MultiPeek}; use rusqlite::{params, Connection, OpenFlags, OptionalExtension}; use serde::{Deserialize, Serialize}; @@ -23,7 +23,6 @@ pub struct Branch { pub script: HLTAS, pub splits: Vec, - pub split_idx: Option, pub full_script: HLTAS, pub stop_frame: u32, } @@ -41,13 +40,15 @@ impl fmt::Debug for Branch { #[derive(Debug, Clone, PartialEq, Eq)] pub struct SplitInfo { - // start_idx is very literal on where it starts from - // meaning, there could be anything after this split, framebulk, special property line, etc + // this is 1 index right after the split marker + // it is guaranteed to have a framebulk somewhere after this pub start_idx: usize, // a split could have no name // this could also be duplicate names, which becomes `None` pub name: Option, pub split_type: SplitType, + // ready as in, there's a save created, and lines before and including start_idx is still unchanged + pub ready: bool, } #[derive(Debug, Clone, PartialEq, Eq)] @@ -58,8 +59,20 @@ pub enum SplitType { } impl SplitInfo { - pub fn split_hltas(hltas: &HLTAS) -> Vec { - if hltas.lines.is_empty() { + #[must_use] + pub fn split_lines<'a, T: Iterator>(lines: T) -> Vec { + Self::split_lines_with_stop(lines, usize::MAX) + } + + // TODO: test stop_idx + #[must_use] + pub fn split_lines_with_stop<'a, T: Iterator>( + lines: T, + stop_idx: usize, + ) -> Vec { + let mut lines = lines.into_iter().multipeek(); + + if lines.peek().is_none() { return Vec::new(); } @@ -67,44 +80,39 @@ impl SplitInfo { let mut i = 0usize; // skip till there's at least 1 framebulk - while i < hltas.lines.len() { - if matches!(hltas.lines[i], Line::FrameBulk(_)) { + for line in lines.by_ref() { + if matches!(line, Line::FrameBulk(_)) { break; } i += 1; + if i >= stop_idx { + return splits; + } } - while i < hltas.lines.len() { - // this is correct, previous search would place i on the framebulk - // so we are only interested on what comes after for the first split + while let Some(line) = lines.next() { + // this is correct, if FrameBulk is at index 0, we are searching from index 1 i += 1; - - let line = &hltas.lines[i]; + if i >= stop_idx { + return splits; + } const SPLIT_MARKER: &str = "bxt-rs-split"; let name; let start_idx; let split_type; - let no_framebulks_left = |i| { - for line in hltas.lines[i..].iter() { - if matches!(line, Line::FrameBulk(_)) { - return false; - } - } - true - }; match line { // TODO: save name;load name console // TODO: handle setting shared rng, and what property lines do i bring over? // TODO: handle completely invalid back to back splits Line::Save(save_name) => { - i += 1; - if no_framebulks_left(i) { + if Self::no_framebulks_left(&mut lines) { break; } + i += 1; name = Some(save_name.to_owned()); start_idx = i; @@ -112,10 +120,10 @@ impl SplitInfo { } // this reset doesn't have a name, one with comment attached is handled below Line::Reset { .. } => { - i += 1; - if no_framebulks_left(i) { + if Self::no_framebulks_left(&mut lines) { break; } + i += 1; name = None; start_idx = i; @@ -134,24 +142,24 @@ impl SplitInfo { continue; } + // linked to reset? + split_type = if matches!(lines.peek(), Some(Line::Reset { .. })) { + lines.next(); // consume reset + if Self::no_framebulks_left(&mut lines) { + break; + } + i += 1; + + SplitType::Reset + } else { + if Self::no_framebulks_left(&mut lines) { + break; + } + + SplitType::Comment + }; i += 1; - // linked to reset? - split_type = - if i < hltas.lines.len() && matches!(hltas.lines[i], Line::Reset { .. }) { - i += 1; - if no_framebulks_left(i) { - break; - } - - SplitType::Reset - } else { - if no_framebulks_left(i) { - break; - } - - SplitType::Comment - }; start_idx = i; let comment = comment.trim_start(); if comment.is_empty() { @@ -167,11 +175,21 @@ impl SplitInfo { start_idx, name, split_type, + ready: false, }); } splits } + + fn no_framebulks_left<'a, T: Iterator>(lines: &mut MultiPeek) -> bool { + while let Some(line) = lines.peek() { + if matches!(line, Line::FrameBulk(_)) { + return false; + } + } + true + } } #[derive(Debug, Clone)] @@ -330,7 +348,7 @@ impl Db { .map_err(|err| eyre!("invalid script value, cannot parse: {err:?}"))?; let full_script = script.clone(); - let splits = SplitInfo::split_hltas(&full_script); + let splits = SplitInfo::split_lines(full_script.lines.iter()); Ok(Branch { branch_id, @@ -338,7 +356,6 @@ impl Db { is_hidden, script, full_script, - split_idx: None, splits, stop_frame, }) @@ -366,7 +383,7 @@ impl Db { .map_err(|err| eyre!("invalid script value, cannot parse: {err:?}"))?; let full_script = script.clone(); - let splits = SplitInfo::split_hltas(&full_script); + let splits = SplitInfo::split_lines(full_script.lines.iter()); branches.push(Branch { branch_id, @@ -375,7 +392,6 @@ impl Db { script, stop_frame, full_script, - split_idx: None, splits, }) } @@ -699,32 +715,37 @@ mod tests { ) .unwrap(); - let splits = SplitInfo::split_hltas(&script); + let splits = SplitInfo::split_lines(script.lines.iter()); let expected = vec![ SplitInfo { start_idx: 2, name: Some("name".to_string()), split_type: SplitType::Comment, + ready: false, }, SplitInfo { start_idx: 5, name: None, split_type: SplitType::Comment, + ready: false, }, SplitInfo { start_idx: 9, name: Some("name2".to_string()), split_type: SplitType::Save, + ready: false, }, SplitInfo { start_idx: 14, name: Some("name3".to_string()), split_type: SplitType::Reset, + ready: false, }, SplitInfo { start_idx: 21, name: Some("name4".to_string()), split_type: SplitType::Comment, + ready: false, }, ]; diff --git a/src/modules/tas_studio/editor/mod.rs b/src/modules/tas_studio/editor/mod.rs index bd7ff7e2..7afd0ba3 100644 --- a/src/modules/tas_studio/editor/mod.rs +++ b/src/modules/tas_studio/editor/mod.rs @@ -9,6 +9,7 @@ use std::time::Instant; use bxt_ipc_types::Frame; use bxt_strafe::{Hull, Trace}; use color_eyre::eyre::{self, ensure}; +use db::SplitInfo; use glam::{IVec2, Vec2, Vec3}; use hltas::types::{ AutoMovement, Change, ChangeTarget, Line, StrafeDir, StrafeSettings, StrafeType, @@ -3308,13 +3309,49 @@ impl Editor { let mut buffer = Vec::new(); hltas::write::gen_lines(&mut buffer, to) .expect("writing to an in-memory buffer should never fail"); - let to = String::from_utf8(buffer) + let to_str = String::from_utf8(buffer) .expect("Line serialization should never produce invalid UTF-8"); + // if modified before or on split idx, it is invalid + // simply find the first split that is covered + // TODO: test + let splits = &mut self.branch_mut().branch.splits; + let last_line_idx = first_line_idx + count - 1; + let invalid_split_idx = + splits.partition_point(|s| s.start_idx < first_line_idx && s.start_idx > last_line_idx); + + // offsets are applied for after the last_line_idx range + let offset_split_idx = + splits[invalid_split_idx..].partition_point(|s| s.start_idx < last_line_idx); + // TODO: unlikely, but what to do if overflow, or count is too big + let split_offset_cnt = isize::try_from(to_str.len()) + .expect("Length of the replacement line is too huge to be isize") + - isize::try_from(count).expect("Number of lines to replace is too huge to be isize"); + for split in splits[offset_split_idx..].iter_mut() { + split + .start_idx + .checked_add_signed(split_offset_cnt) + .expect("Split marker index is out of range to be usable, which shouldn't happen"); + } + + // handle `to` lines split info + // TODO: test + let to_splits = SplitInfo::split_lines_with_stop( + to.iter() + .chain(self.script().lines[offset_split_idx..].iter()), + to.len(), + ); + let splits = &mut self.branch_mut().branch.splits; + for (i, split) in to_splits.into_iter().enumerate() { + let mut split = split; + split.start_idx += first_line_idx; + splits.insert(invalid_split_idx + i, split); + } + let op = Operation::ReplaceMultiple { first_line_idx, from, - to, + to: to_str, }; self.apply_operation(op) } @@ -3460,7 +3497,8 @@ impl Editor { return Err(ManualOpError::CannotDoDuringAdjustment); } - let script = self.script(); + let branch = &mut self.branch_mut().branch; + let script = &branch.script; if new_script == *script { return Ok(()); } @@ -3471,6 +3509,8 @@ impl Editor { return Ok(()); } + branch.splits = SplitInfo::split_lines(new_script.lines.iter()); + let mut buffer = Vec::new(); script .to_writer(&mut buffer) From 1a5ee7fd42218878d6109ffeed88cede0eb76e6f Mon Sep 17 00:00:00 2001 From: Eddio0141 Date: Wed, 27 Nov 2024 06:13:19 +0000 Subject: [PATCH 05/19] split marker invalidations --- src/modules/tas_studio/editor/db.rs | 46 +++++++++++-------- src/modules/tas_studio/editor/mod.rs | 69 +++++++++++++++++++++++----- 2 files changed, 85 insertions(+), 30 deletions(-) diff --git a/src/modules/tas_studio/editor/db.rs b/src/modules/tas_studio/editor/db.rs index 2cd01c2e..88ff4d48 100644 --- a/src/modules/tas_studio/editor/db.rs +++ b/src/modules/tas_studio/editor/db.rs @@ -23,7 +23,6 @@ pub struct Branch { pub script: HLTAS, pub splits: Vec, - pub full_script: HLTAS, pub stop_frame: u32, } @@ -43,6 +42,8 @@ pub struct SplitInfo { // this is 1 index right after the split marker // it is guaranteed to have a framebulk somewhere after this pub start_idx: usize, + // for the sake of easier searching, the last framebulk index is stored + pub bulk_idx: usize, // a split could have no name // this could also be duplicate names, which becomes `None` pub name: Option, @@ -78,23 +79,24 @@ impl SplitInfo { let mut splits = Vec::new(); - let mut i = 0usize; + let mut line_idx = 0usize; + let mut bulk_idx = 1usize; // skip till there's at least 1 framebulk for line in lines.by_ref() { if matches!(line, Line::FrameBulk(_)) { break; } - i += 1; - if i >= stop_idx { + line_idx += 1; + if line_idx >= stop_idx { return splits; } } while let Some(line) = lines.next() { // this is correct, if FrameBulk is at index 0, we are searching from index 1 - i += 1; - if i >= stop_idx { + line_idx += 1; + if line_idx >= stop_idx { return splits; } @@ -112,10 +114,10 @@ impl SplitInfo { if Self::no_framebulks_left(&mut lines) { break; } - i += 1; + line_idx += 1; name = Some(save_name.to_owned()); - start_idx = i; + start_idx = line_idx; split_type = SplitType::Save; } // this reset doesn't have a name, one with comment attached is handled below @@ -123,10 +125,10 @@ impl SplitInfo { if Self::no_framebulks_left(&mut lines) { break; } - i += 1; + line_idx += 1; name = None; - start_idx = i; + start_idx = line_idx; split_type = SplitType::Reset; } Line::Comment(comment) => { @@ -148,7 +150,7 @@ impl SplitInfo { if Self::no_framebulks_left(&mut lines) { break; } - i += 1; + line_idx += 1; SplitType::Reset } else { @@ -158,9 +160,9 @@ impl SplitInfo { SplitType::Comment }; - i += 1; + line_idx += 1; - start_idx = i; + start_idx = line_idx; let comment = comment.trim_start(); if comment.is_empty() { name = None; @@ -168,6 +170,10 @@ impl SplitInfo { name = Some(comment.to_owned()); } } + Line::FrameBulk(_) => { + bulk_idx += 1; + continue; + } _ => continue, } @@ -175,6 +181,7 @@ impl SplitInfo { start_idx, name, split_type, + bulk_idx, ready: false, }); } @@ -347,15 +354,13 @@ impl Db { let script = HLTAS::from_str(&buffer) .map_err(|err| eyre!("invalid script value, cannot parse: {err:?}"))?; - let full_script = script.clone(); - let splits = SplitInfo::split_lines(full_script.lines.iter()); + let splits = SplitInfo::split_lines(script.lines.iter()); Ok(Branch { branch_id, name, is_hidden, script, - full_script, splits, stop_frame, }) @@ -382,8 +387,7 @@ impl Db { let script = HLTAS::from_str(&buffer) .map_err(|err| eyre!("invalid script value, cannot parse: {err:?}"))?; - let full_script = script.clone(); - let splits = SplitInfo::split_lines(full_script.lines.iter()); + let splits = SplitInfo::split_lines(script.lines.iter()); branches.push(Branch { branch_id, @@ -391,7 +395,6 @@ impl Db { is_hidden, script, stop_frame, - full_script, splits, }) } @@ -719,30 +722,35 @@ mod tests { let expected = vec![ SplitInfo { start_idx: 2, + bulk_idx: 1, name: Some("name".to_string()), split_type: SplitType::Comment, ready: false, }, SplitInfo { start_idx: 5, + bulk_idx: 3, name: None, split_type: SplitType::Comment, ready: false, }, SplitInfo { start_idx: 9, + bulk_idx: 6, name: Some("name2".to_string()), split_type: SplitType::Save, ready: false, }, SplitInfo { start_idx: 14, + bulk_idx: 10, name: Some("name3".to_string()), split_type: SplitType::Reset, ready: false, }, SplitInfo { start_idx: 21, + bulk_idx: 15, name: Some("name4".to_string()), split_type: SplitType::Comment, ready: false, diff --git a/src/modules/tas_studio/editor/mod.rs b/src/modules/tas_studio/editor/mod.rs index 7afd0ba3..c7960264 100644 --- a/src/modules/tas_studio/editor/mod.rs +++ b/src/modules/tas_studio/editor/mod.rs @@ -673,6 +673,8 @@ impl Editor { self.recompute_extra_camera_frame_data_if_needed(); self.generation = self.generation.wrapping_add(1); + + self.invalidate_splits_fb_idx(frame_idx); } pub fn recompute_extra_camera_frame_data_if_needed(&mut self) { @@ -3315,7 +3317,7 @@ impl Editor { // if modified before or on split idx, it is invalid // simply find the first split that is covered // TODO: test - let splits = &mut self.branch_mut().branch.splits; + let splits = self.split_markers(); let last_line_idx = first_line_idx + count - 1; let invalid_split_idx = splits.partition_point(|s| s.start_idx < first_line_idx && s.start_idx > last_line_idx); @@ -3323,15 +3325,37 @@ impl Editor { // offsets are applied for after the last_line_idx range let offset_split_idx = splits[invalid_split_idx..].partition_point(|s| s.start_idx < last_line_idx); - // TODO: unlikely, but what to do if overflow, or count is too big - let split_offset_cnt = isize::try_from(to_str.len()) - .expect("Length of the replacement line is too huge to be isize") - - isize::try_from(count).expect("Number of lines to replace is too huge to be isize"); - for split in splits[offset_split_idx..].iter_mut() { - split - .start_idx - .checked_add_signed(split_offset_cnt) - .expect("Split marker index is out of range to be usable, which shouldn't happen"); + if offset_split_idx < splits.len() { + let split_offset_cnt = isize::try_from(to.len()) + .expect("Length of the replacement line is too huge to be isize") + - isize::try_from(count) + .expect("Number of lines to replace is too huge to be isize"); + // same thing as above, but for framebulks + let fb_to_cnt = to + .iter() + .filter(|l| matches!(l, Line::FrameBulk(_))) + .count(); + let fb_count_cnt = from_lines + .iter() + .filter(|l| matches!(l, Line::FrameBulk(_))) + .count(); + let bulk_offset_cnt = isize::try_from(fb_to_cnt) + .expect("Index of framebulk is too large for calculating as isize") + - isize::try_from(fb_count_cnt) + .expect("Index of framebulk is too large for calculating as isize"); + + // apply offsets + let splits = self.split_markers_mut(); + for split in splits[offset_split_idx..].iter_mut() { + split + .start_idx + .checked_add_signed(split_offset_cnt) + .expect("Split marker index is out of range"); + split + .bulk_idx + .checked_add_signed(bulk_offset_cnt) + .expect("Split marker framebulk index is out of range"); + } } // handle `to` lines split info @@ -3341,7 +3365,7 @@ impl Editor { .chain(self.script().lines[offset_split_idx..].iter()), to.len(), ); - let splits = &mut self.branch_mut().branch.splits; + let splits = self.split_markers_mut(); for (i, split) in to_splits.into_iter().enumerate() { let mut split = split; split.start_idx += first_line_idx; @@ -4592,6 +4616,29 @@ impl Editor { tri.end(); } + + pub fn split_markers(&self) -> &[SplitInfo] { + &self.branch().branch.splits + } + + pub fn split_markers_mut(&mut self) -> &mut Vec { + &mut self.branch_mut().branch.splits + } + + /// Invalidates split markers with a framebulk index + pub fn invalidate_splits_fb_idx(&mut self, fb_idx: usize) { + let invalid_split_idx = self.invalid_split_idx_from_fb_idx(fb_idx); + let splits = self.split_markers_mut(); + for split in splits[invalid_split_idx..].iter_mut() { + split.ready = false; + } + } + + fn invalid_split_idx_from_fb_idx(&self, fb_idx: usize) -> usize { + // splits are invalid if bulk_idx is equals to fb_idx or higher + self.split_markers() + .partition_point(|s| s.bulk_idx < fb_idx) + } } fn perpendicular(prev: Vec3, next: Vec3) -> Vec3 { From f6fdd1134a8e416dca2cd59f2dd2d85303632f26 Mon Sep 17 00:00:00 2001 From: Eddio0141 Date: Wed, 27 Nov 2024 06:50:52 +0000 Subject: [PATCH 06/19] update split marker ready-ness from accurate frames --- src/modules/tas_studio/editor/mod.rs | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/src/modules/tas_studio/editor/mod.rs b/src/modules/tas_studio/editor/mod.rs index c7960264..077b3ccc 100644 --- a/src/modules/tas_studio/editor/mod.rs +++ b/src/modules/tas_studio/editor/mod.rs @@ -3824,7 +3824,16 @@ impl Editor { return None; } - branch.first_predicted_frame = max(frame.frame_idx + 1, branch.first_predicted_frame); + if frame.frame_idx + 1 > branch.first_predicted_frame { + branch.first_predicted_frame = frame.frame_idx + 1; + + let splits = &mut branch.branch.splits; + let split_valid_to = + splits.partition_point(|s| s.bulk_idx >= branch.first_predicted_frame); + for split in splits[..split_valid_to].iter_mut() { + split.ready = true; + } + } if branch.frames.len() == frame.frame_idx { branch.frames.push(frame.frame); @@ -4627,18 +4636,24 @@ impl Editor { /// Invalidates split markers with a framebulk index pub fn invalidate_splits_fb_idx(&mut self, fb_idx: usize) { - let invalid_split_idx = self.invalid_split_idx_from_fb_idx(fb_idx); + let invalid_split_idx = self.split_idx_from_fb_idx(fb_idx); let splits = self.split_markers_mut(); for split in splits[invalid_split_idx..].iter_mut() { split.ready = false; } } - fn invalid_split_idx_from_fb_idx(&self, fb_idx: usize) -> usize { - // splits are invalid if bulk_idx is equals to fb_idx or higher + /// Gets the split index equals or higher than fb_idx + fn split_idx_from_fb_idx(&self, fb_idx: usize) -> usize { self.split_markers() .partition_point(|s| s.bulk_idx < fb_idx) } + + /// Gets the split index before fb_idx + fn split_idx_before_fb_idx(&self, fb_idx: usize) -> usize { + self.split_markers() + .partition_point(|s| s.bulk_idx >= fb_idx) + } } fn perpendicular(prev: Vec3, next: Vec3) -> Vec3 { From eb1121d9691f1cd31c0f299283da0a0ce1596f57 Mon Sep 17 00:00:00 2001 From: Eddio0141 Date: Wed, 27 Nov 2024 07:45:12 +0000 Subject: [PATCH 07/19] split markers assumed to be ready if save exists --- src/modules/tas_studio/editor/db.rs | 52 +++++++++++++++++++++++++--- src/modules/tas_studio/editor/mod.rs | 1 + 2 files changed, 48 insertions(+), 5 deletions(-) diff --git a/src/modules/tas_studio/editor/db.rs b/src/modules/tas_studio/editor/db.rs index 88ff4d48..b541663f 100644 --- a/src/modules/tas_studio/editor/db.rs +++ b/src/modules/tas_studio/editor/db.rs @@ -1,5 +1,6 @@ -use std::fmt; +use std::ffi::CStr; use std::path::Path; +use std::{collections::HashSet, fmt}; use bincode::Options; use color_eyre::eyre::{self, ensure, eyre}; @@ -8,6 +9,9 @@ use itertools::{Itertools, MultiPeek}; use rusqlite::{params, Connection, OpenFlags, OptionalExtension}; use serde::{Deserialize, Serialize}; +use crate::hooks::engine; +use crate::utils::MainThreadMarker; + use super::operation::Operation; #[derive(Debug)] @@ -46,6 +50,7 @@ pub struct SplitInfo { pub bulk_idx: usize, // a split could have no name // this could also be duplicate names, which becomes `None` + // TODO: could probably get away with &str pub name: Option, pub split_type: SplitType, // ready as in, there's a save created, and lines before and including start_idx is still unchanged @@ -93,6 +98,8 @@ impl SplitInfo { } } + let mut used_save_names = HashSet::new(); + while let Some(line) = lines.next() { // this is correct, if FrameBulk is at index 0, we are searching from index 1 line_idx += 1; @@ -116,7 +123,7 @@ impl SplitInfo { } line_idx += 1; - name = Some(save_name.to_owned()); + name = Some(save_name.as_str()); start_idx = line_idx; split_type = SplitType::Save; } @@ -167,7 +174,7 @@ impl SplitInfo { if comment.is_empty() { name = None; } else { - name = Some(comment.to_owned()); + name = Some(comment); } } Line::FrameBulk(_) => { @@ -177,6 +184,17 @@ impl SplitInfo { _ => continue, } + let name = if let Some(name) = name { + if used_save_names.contains(name) { + None + } else { + used_save_names.insert(name.to_owned()); + Some(name.to_owned()) + } + } else { + None + }; + splits.push(SplitInfo { start_idx, name, @@ -197,6 +215,22 @@ impl SplitInfo { } true } + + pub fn validate_all_by_saves(splits: &mut Vec, marker: MainThreadMarker) { + for split in splits { + let Some(name) = &split.name else { + return; + }; + + let game_dir = Path::new( + unsafe { CStr::from_ptr(engine::com_gamedir.get(marker).cast()) } + .to_str() + .unwrap(), + ); + let save_path = game_dir.join("SAVE").join(name); + split.ready = save_path.is_file(); + } + } } #[derive(Debug, Clone)] @@ -354,7 +388,11 @@ impl Db { let script = HLTAS::from_str(&buffer) .map_err(|err| eyre!("invalid script value, cannot parse: {err:?}"))?; - let splits = SplitInfo::split_lines(script.lines.iter()); + let mut splits = SplitInfo::split_lines(script.lines.iter()); + // TODO: is this fine? not sure how else to get MainThreadMarker for game directory + unsafe { + SplitInfo::validate_all_by_saves(&mut splits, MainThreadMarker::new()); + } Ok(Branch { branch_id, @@ -387,7 +425,11 @@ impl Db { let script = HLTAS::from_str(&buffer) .map_err(|err| eyre!("invalid script value, cannot parse: {err:?}"))?; - let splits = SplitInfo::split_lines(script.lines.iter()); + let mut splits = SplitInfo::split_lines(script.lines.iter()); + // TODO: is this fine? not sure how else to get MainThreadMarker for game directory + unsafe { + SplitInfo::validate_all_by_saves(&mut splits, MainThreadMarker::new()); + } branches.push(Branch { branch_id, diff --git a/src/modules/tas_studio/editor/mod.rs b/src/modules/tas_studio/editor/mod.rs index 077b3ccc..09d88b4c 100644 --- a/src/modules/tas_studio/editor/mod.rs +++ b/src/modules/tas_studio/editor/mod.rs @@ -3533,6 +3533,7 @@ impl Editor { return Ok(()); } + // don't validate by checking save file, its a rewrite branch.splits = SplitInfo::split_lines(new_script.lines.iter()); let mut buffer = Vec::new(); From 23dbd4f53949ed307285b938a71c21932ec5f2df Mon Sep 17 00:00:00 2001 From: Eddio0141 Date: Wed, 27 Nov 2024 07:47:51 +0000 Subject: [PATCH 08/19] fixed wrong filename --- src/modules/tas_studio/editor/db.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/tas_studio/editor/db.rs b/src/modules/tas_studio/editor/db.rs index b541663f..84379c31 100644 --- a/src/modules/tas_studio/editor/db.rs +++ b/src/modules/tas_studio/editor/db.rs @@ -227,7 +227,7 @@ impl SplitInfo { .to_str() .unwrap(), ); - let save_path = game_dir.join("SAVE").join(name); + let save_path = game_dir.join("SAVE").join(format!("{name}.sav")); split.ready = save_path.is_file(); } } From 8edb5506335d168b8472b188b001cb713beef52b Mon Sep 17 00:00:00 2001 From: Eddio0141 Date: Thu, 28 Nov 2024 04:46:34 +0000 Subject: [PATCH 09/19] split hltas via SplitInfo --- src/modules/tas_studio/editor/db.rs | 55 +++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/src/modules/tas_studio/editor/db.rs b/src/modules/tas_studio/editor/db.rs index 84379c31..41438a79 100644 --- a/src/modules/tas_studio/editor/db.rs +++ b/src/modules/tas_studio/editor/db.rs @@ -1,9 +1,11 @@ use std::ffi::CStr; +use std::num::NonZeroU32; use std::path::Path; use std::{collections::HashSet, fmt}; use bincode::Options; use color_eyre::eyre::{self, ensure, eyre}; +use hltas::types::FrameBulk; use hltas::{types::Line, HLTAS}; use itertools::{Itertools, MultiPeek}; use rusqlite::{params, Connection, OpenFlags, OptionalExtension}; @@ -231,6 +233,59 @@ impl SplitInfo { split.ready = save_path.is_file(); } } + + pub fn split_hltas_for_remote_use(splits: &[SplitInfo], hltas: &HLTAS, fb_idx: usize) -> HLTAS { + let Some(split) = splits + .iter() + .rev() + .find(|s| s.bulk_idx < fb_idx && s.name.is_some()) + else { + // Cow could be used if return doesn't need to be owned + return hltas.clone(); + }; + + split.split_hltas(hltas) + } + + // TODO: test + fn split_hltas(&self, hltas: &HLTAS) -> HLTAS { + let properties = hltas.properties.clone(); + let lines = hltas.lines[self.start_idx..].to_owned(); + + let frame_time = lines + .iter() + .find_map(|l| { + if let Line::FrameBulk(b) = l { + Some(&b.frame_time) + } else { + None + } + }) + .expect("There is a frame bulk after a split marker, this should never happen") + .clone(); + + let mut hltas = HLTAS { properties, lines }; + // TODO: apply shared rng, properties, etc, which would be stored in split info + // TODO: if reset or no autopause with manual save load, do not enable autopause + hltas.properties.load_command = Some(format!( + "bxt_autopause 1; _bxt_load \"{}\"", + self.name.as_ref().unwrap() + )); + + // TODO: could the loading frames be figured out + let padding = FrameBulk { + auto_actions: Default::default(), + movement_keys: Default::default(), + action_keys: Default::default(), + frame_time, + pitch: Default::default(), + frame_count: NonZeroU32::try_from(15).unwrap(), + console_command: Default::default(), + }; + hltas.lines.insert(0, Line::FrameBulk(padding)); + + todo!() + } } #[derive(Debug, Clone)] From 654dfda942917203d3e3ca5428965313a66c376b Mon Sep 17 00:00:00 2001 From: Eddio0141 Date: Tue, 3 Dec 2024 01:30:29 +0000 Subject: [PATCH 10/19] use split hltas for accurate frames --- src/modules/tas_studio/editor/db.rs | 91 +++++++++++---------- src/modules/tas_studio/editor/mod.rs | 117 +++++++++++++++++++++++---- src/modules/tas_studio/mod.rs | 6 +- 3 files changed, 154 insertions(+), 60 deletions(-) diff --git a/src/modules/tas_studio/editor/db.rs b/src/modules/tas_studio/editor/db.rs index 41438a79..1b6b979f 100644 --- a/src/modules/tas_studio/editor/db.rs +++ b/src/modules/tas_studio/editor/db.rs @@ -1,6 +1,6 @@ use std::ffi::CStr; use std::num::NonZeroU32; -use std::path::Path; +use std::path::{Path, PathBuf}; use std::{collections::HashSet, fmt}; use bincode::Options; @@ -29,6 +29,8 @@ pub struct Branch { pub script: HLTAS, pub splits: Vec, + // actual split script, with frame index of where it is + pub split: Option<(HLTAS, usize)>, pub stop_frame: u32, } @@ -84,10 +86,8 @@ impl SplitInfo { return Vec::new(); } - let mut splits = Vec::new(); - let mut line_idx = 0usize; - let mut bulk_idx = 1usize; + let mut bulk_idx = 0usize; // skip till there's at least 1 framebulk for line in lines.by_ref() { if matches!(line, Line::FrameBulk(_)) { @@ -96,10 +96,11 @@ impl SplitInfo { line_idx += 1; if line_idx >= stop_idx { - return splits; + return Vec::new(); } } + let mut splits = Vec::new(); let mut used_save_names = HashSet::new(); while let Some(line) = lines.next() { @@ -123,10 +124,9 @@ impl SplitInfo { if Self::no_framebulks_left(&mut lines) { break; } - line_idx += 1; name = Some(save_name.as_str()); - start_idx = line_idx; + start_idx = line_idx + 1; split_type = SplitType::Save; } // this reset doesn't have a name, one with comment attached is handled below @@ -134,10 +134,9 @@ impl SplitInfo { if Self::no_framebulks_left(&mut lines) { break; } - line_idx += 1; name = None; - start_idx = line_idx; + start_idx = line_idx + 1; split_type = SplitType::Reset; } Line::Comment(comment) => { @@ -156,10 +155,10 @@ impl SplitInfo { // linked to reset? split_type = if matches!(lines.peek(), Some(Line::Reset { .. })) { lines.next(); // consume reset + line_idx += 1; if Self::no_framebulks_left(&mut lines) { break; } - line_idx += 1; SplitType::Reset } else { @@ -169,9 +168,8 @@ impl SplitInfo { SplitType::Comment }; - line_idx += 1; - start_idx = line_idx; + start_idx = line_idx + 1; let comment = comment.trim_start(); if comment.is_empty() { name = None; @@ -218,37 +216,34 @@ impl SplitInfo { true } - pub fn validate_all_by_saves(splits: &mut Vec, marker: MainThreadMarker) { + #[cfg(test)] + pub fn validate_all_by_saves(splits: &mut Vec, _marker: MainThreadMarker) { for split in splits { - let Some(name) = &split.name else { - return; - }; - - let game_dir = Path::new( - unsafe { CStr::from_ptr(engine::com_gamedir.get(marker).cast()) } - .to_str() - .unwrap(), - ); - let save_path = game_dir.join("SAVE").join(format!("{name}.sav")); - split.ready = save_path.is_file(); + split.ready = false; } } - pub fn split_hltas_for_remote_use(splits: &[SplitInfo], hltas: &HLTAS, fb_idx: usize) -> HLTAS { - let Some(split) = splits - .iter() - .rev() - .find(|s| s.bulk_idx < fb_idx && s.name.is_some()) - else { - // Cow could be used if return doesn't need to be owned - return hltas.clone(); - }; + #[cfg(not(test))] + pub fn validate_all_by_saves(splits: &mut Vec, marker: MainThreadMarker) { + let game_dir = Path::new( + unsafe { CStr::from_ptr(engine::com_gamedir.get(marker).cast()) } + .to_str() + .unwrap(), + ); + + for split in splits { + split.validate(game_dir); + } + } - split.split_hltas(hltas) + pub fn save_path(&self, game_dir: &Path) -> Option { + self.name + .as_ref() + .map(|name| game_dir.join("SAVE").join(format!("{name}.sav"))) } // TODO: test - fn split_hltas(&self, hltas: &HLTAS) -> HLTAS { + pub fn split_hltas(&self, hltas: &HLTAS) -> HLTAS { let properties = hltas.properties.clone(); let lines = hltas.lines[self.start_idx..].to_owned(); @@ -284,7 +279,15 @@ impl SplitInfo { }; hltas.lines.insert(0, Line::FrameBulk(padding)); - todo!() + hltas + } + + pub fn validate(&mut self, game_dir: &Path) { + // TODO: what should i do with unnamed / invalid names + self.ready = self + .save_path(game_dir) + .map(|save_path| save_path.is_file()) + .unwrap_or(false); } } @@ -456,6 +459,7 @@ impl Db { script, splits, stop_frame, + split: None, }) } @@ -493,6 +497,7 @@ impl Db { script, stop_frame, splits, + split: None, }) } stmt.finalize()?; @@ -819,37 +824,37 @@ mod tests { let expected = vec![ SplitInfo { start_idx: 2, - bulk_idx: 1, + bulk_idx: 0, name: Some("name".to_string()), split_type: SplitType::Comment, ready: false, }, SplitInfo { start_idx: 5, - bulk_idx: 3, + bulk_idx: 2, name: None, split_type: SplitType::Comment, ready: false, }, SplitInfo { start_idx: 9, - bulk_idx: 6, + bulk_idx: 5, name: Some("name2".to_string()), split_type: SplitType::Save, ready: false, }, SplitInfo { start_idx: 14, - bulk_idx: 10, - name: Some("name3".to_string()), + bulk_idx: 9, + name: None, split_type: SplitType::Reset, ready: false, }, SplitInfo { start_idx: 21, - bulk_idx: 15, + bulk_idx: 14, name: Some("name4".to_string()), - split_type: SplitType::Comment, + split_type: SplitType::Reset, ready: false, }, ]; diff --git a/src/modules/tas_studio/editor/mod.rs b/src/modules/tas_studio/editor/mod.rs index 09d88b4c..2051b074 100644 --- a/src/modules/tas_studio/editor/mod.rs +++ b/src/modules/tas_studio/editor/mod.rs @@ -1,5 +1,7 @@ -use std::cmp::{max, min}; +use std::cmp::min; +use std::ffi::CStr; use std::fmt::Write; +use std::fs; use std::iter::{self, zip}; use std::num::NonZeroU32; use std::ops::ControlFlow; @@ -28,6 +30,8 @@ use self::utils::{ FrameBulkExt, MaxAccelOffsetValuesMut, }; use super::remote::{AccurateFrame, PlayRequest}; +use super::MainThreadMarker; +use crate::hooks::engine; use crate::hooks::sdl::MouseState; use crate::modules::tas_optimizer::simulator::Simulator; use crate::modules::tas_studio::editor::utils::MaxAccelOffsetValues; @@ -674,7 +678,12 @@ impl Editor { self.generation = self.generation.wrapping_add(1); - self.invalidate_splits_fb_idx(frame_idx); + // TODO: is this fine + unsafe { + let marker = MainThreadMarker::new(); + self.invalidate_splits_fb_idx(frame_idx, marker); + self.update_split_hltas(marker); + } } pub fn recompute_extra_camera_frame_data_if_needed(&mut self) { @@ -3778,8 +3787,10 @@ impl Editor { return None; } + let split_frame_idx = self.split_hltas().map(|split| split.1).unwrap_or_default(); + let actual_frame_idx = frame.frame_idx + split_frame_idx; // TODO: make this nicer somehow maybe? - if frame.frame_idx == 0 { + if actual_frame_idx == 0 { // Initial frame is the same for all branches and between smoothed/unsmoothed. for branch_idx in 0..self.branches.len() { let branch = &mut self.branches[branch_idx]; @@ -3800,7 +3811,7 @@ impl Editor { let branch = &mut self.branches[frame.branch_idx]; - if frame.frame_idx > branch.first_predicted_frame { + if actual_frame_idx > branch.first_predicted_frame { // TODO: we can still use newer frames. return None; } @@ -3812,21 +3823,21 @@ impl Editor { let frames = &mut branch.auto_smoothing.frames; - if frames.len() == frame.frame_idx { + if frames.len() == actual_frame_idx { frames.push(frame.frame); } else { - let current_frame = &mut frames[frame.frame_idx]; + let current_frame = &mut frames[actual_frame_idx]; if *current_frame != frame.frame { *current_frame = frame.frame; - frames.truncate(frame.frame_idx + 1); + frames.truncate(actual_frame_idx + 1); } } return None; } - if frame.frame_idx + 1 > branch.first_predicted_frame { - branch.first_predicted_frame = frame.frame_idx + 1; + if actual_frame_idx + 1 > branch.first_predicted_frame { + branch.first_predicted_frame = actual_frame_idx + 1; let splits = &mut branch.branch.splits; let split_valid_to = @@ -3836,20 +3847,20 @@ impl Editor { } } - if branch.frames.len() == frame.frame_idx { + if branch.frames.len() == actual_frame_idx { branch.frames.push(frame.frame); branch.extra_cam.clear(); } else { - let current_frame = &mut branch.frames[frame.frame_idx]; + let current_frame = &mut branch.frames[actual_frame_idx]; if *current_frame != frame.frame { *current_frame = frame.frame; branch.first_predicted_frame = - min(branch.first_predicted_frame, frame.frame_idx + 1); + min(branch.first_predicted_frame, actual_frame_idx + 1); branch.extra_cam.clear(); if truncate_on_mismatch { - branch.frames.truncate(frame.frame_idx + 1); + branch.frames.truncate(actual_frame_idx + 1); } } } @@ -3863,8 +3874,18 @@ impl Editor { .map(|bulk| bulk.frame_count.get() as usize) .sum::(); - if frame.frame_idx + 1 == frame_count { - let mut smoothed_script = branch.branch.script.clone(); + if actual_frame_idx + 1 == frame_count { + unsafe { + self.update_split_hltas(MainThreadMarker::new()); + } + + let branch = &mut self.branches[frame.branch_idx]; + let mut smoothed_script = branch + .branch + .split + .as_ref() + .map(|split| split.0.clone()) + .unwrap_or_else(|| branch.branch.script.clone()); // Enable vectorial strafing if it wasn't enabled. smoothed_script @@ -3923,6 +3944,10 @@ impl Editor { } branch.auto_smoothing.script = Some(smoothed_script.clone()); + if let Some(split) = &branch.branch.split { + branch.branch.split = Some((smoothed_script.clone(), split.1)); + } + return Some(PlayRequest { script: smoothed_script, generation: self.generation, @@ -4636,12 +4661,68 @@ impl Editor { } /// Invalidates split markers with a framebulk index - pub fn invalidate_splits_fb_idx(&mut self, fb_idx: usize) { + pub fn invalidate_splits_fb_idx(&mut self, fb_idx: usize, _marker: MainThreadMarker) { let invalid_split_idx = self.split_idx_from_fb_idx(fb_idx); let splits = self.split_markers_mut(); + + #[cfg(not(test))] + let game_dir = Path::new( + unsafe { CStr::from_ptr(engine::com_gamedir.get(_marker).cast()) } + .to_str() + .unwrap(), + ); + for split in splits[invalid_split_idx..].iter_mut() { split.ready = false; + + #[cfg(not(test))] + { + let Some(save_path) = split.save_path(game_dir) else { + continue; + }; + // TODO: propagate error, print outside. + if let Err(err) = fs::remove_file(save_path) { + error!("error receiving request from server: {err:?}"); + } + } + } + } + + // TODO: test + /// Updates split state + pub fn update_split_hltas(&mut self, _marker: MainThreadMarker) { + #[cfg(not(test))] + let game_dir = Path::new( + unsafe { CStr::from_ptr(engine::com_gamedir.get(_marker).cast()) } + .to_str() + .unwrap(), + ); + + let mut split_found = None; + let fb_idx = self.branch().first_predicted_frame; + + for (i, split) in self.split_markers_mut().iter_mut().enumerate().rev() { + // uses >= comparison because, if first predicted frame is passed in and it is 0 + // it could match with a split where there's 1 framebulk before split (bulk_idx is 0) + if split.bulk_idx >= fb_idx { + continue; + } + #[cfg(not(test))] + split.validate(game_dir); + if split.ready { + split_found = Some(i); + break; + } } + + let Some(split) = split_found else { + self.branch_mut().branch.split = None; + return; + }; + let split = &self.split_markers()[split]; + + let split_script = split.split_hltas(self.script()); + self.branch_mut().branch.split = Some((split_script, fb_idx)); } /// Gets the split index equals or higher than fb_idx @@ -4655,6 +4736,10 @@ impl Editor { self.split_markers() .partition_point(|s| s.bulk_idx >= fb_idx) } + + pub fn split_hltas(&self) -> Option<&(HLTAS, usize)> { + self.branch().branch.split.as_ref() + } } fn perpendicular(prev: Vec3, next: Vec3) -> Vec3 { diff --git a/src/modules/tas_studio/mod.rs b/src/modules/tas_studio/mod.rs index 73fc6f6c..312f8a9e 100644 --- a/src/modules/tas_studio/mod.rs +++ b/src/modules/tas_studio/mod.rs @@ -2041,8 +2041,12 @@ pub fn draw(marker: MainThreadMarker, tri: &TriangleApi) { if let Some(at) = *simulate_at { if Instant::now() > at { *simulate_at = None; + editor.update_split_hltas(marker); remote::maybe_send_request_to_client(PlayRequest { - script: editor.script().clone(), + script: editor + .split_hltas() + .map(|split| split.0.clone()) + .unwrap_or_else(|| editor.script().clone()), generation: *last_generation, branch_idx: *last_branch_idx, is_smoothed: false, From 23a989291622ae239ed2ca5c52467dc87daedb81 Mon Sep 17 00:00:00 2001 From: Eddio0141 Date: Sat, 7 Dec 2024 02:33:27 +0000 Subject: [PATCH 11/19] complete test and fixed split marker generation --- src/modules/tas_studio/editor/db.rs | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/src/modules/tas_studio/editor/db.rs b/src/modules/tas_studio/editor/db.rs index 1b6b979f..913a9d2f 100644 --- a/src/modules/tas_studio/editor/db.rs +++ b/src/modules/tas_studio/editor/db.rs @@ -70,6 +70,7 @@ pub enum SplitType { impl SplitInfo { #[must_use] + #[inline(always)] pub fn split_lines<'a, T: Iterator>(lines: T) -> Vec { Self::split_lines_with_stop(lines, usize::MAX) } @@ -162,6 +163,7 @@ impl SplitInfo { SplitType::Reset } else { + lines.reset_peek(); if Self::no_framebulks_left(&mut lines) { break; } @@ -787,7 +789,6 @@ mod tests { #[test] fn split_by_markers() { - // TODO: complete, duplicate names let script = HLTAS::from_str( "version 1\nframes\n\ ----------|------|------|0.001|-|-|10\n\ @@ -809,14 +810,18 @@ mod tests { ----------|------|------|0.005|-|-|10\n\ ----------|------|------|0.005|-|-|10\n\ ----------|------|------|0.005|-|-|10\n\ - // bxt-rs-split name4 + // bxt-rs-split name3 reset 1 ----------|------|------|0.006|-|-|10\n\ ----------|------|------|0.006|-|-|10\n\ ----------|------|------|0.006|-|-|10\n\ ----------|------|------|0.006|-|-|10\n\ ----------|------|------|0.006|-|-|10\n\ - ----------|------|------|0.006|-|-|10\n", + ----------|------|------|0.006|-|-|10\n + // bxt-rs-split name4 + ----------|------|------|0.007|-|-|10\n\ + // bxt-rs-split name4 + ----------|------|------|0.008|-|-|10\n", ) .unwrap(); @@ -853,10 +858,24 @@ mod tests { SplitInfo { start_idx: 21, bulk_idx: 14, - name: Some("name4".to_string()), + name: Some("name3".to_string()), split_type: SplitType::Reset, ready: false, }, + SplitInfo { + start_idx: 28, + bulk_idx: 20, + name: Some("name4".to_string()), + split_type: SplitType::Comment, + ready: false, + }, + SplitInfo { + start_idx: 30, + bulk_idx: 21, + name: None, + split_type: SplitType::Comment, + ready: false, + }, ]; assert_eq!(splits, expected); From 11d5cd1cac1f5f9077664372916d6c34a153fbdf Mon Sep 17 00:00:00 2001 From: Eddio0141 Date: Sat, 7 Dec 2024 14:46:00 +0000 Subject: [PATCH 12/19] changed split to require name --- src/modules/tas_studio/editor/db.rs | 175 +++++++++++++++------------ src/modules/tas_studio/editor/mod.rs | 4 +- 2 files changed, 96 insertions(+), 83 deletions(-) diff --git a/src/modules/tas_studio/editor/db.rs b/src/modules/tas_studio/editor/db.rs index 913a9d2f..596c88d0 100644 --- a/src/modules/tas_studio/editor/db.rs +++ b/src/modules/tas_studio/editor/db.rs @@ -52,10 +52,8 @@ pub struct SplitInfo { pub start_idx: usize, // for the sake of easier searching, the last framebulk index is stored pub bulk_idx: usize, - // a split could have no name - // this could also be duplicate names, which becomes `None` // TODO: could probably get away with &str - pub name: Option, + pub name: String, pub split_type: SplitType, // ready as in, there's a save created, and lines before and including start_idx is still unchanged pub ready: bool, @@ -126,20 +124,10 @@ impl SplitInfo { break; } - name = Some(save_name.as_str()); + name = save_name.as_str(); start_idx = line_idx + 1; split_type = SplitType::Save; } - // this reset doesn't have a name, one with comment attached is handled below - Line::Reset { .. } => { - if Self::no_framebulks_left(&mut lines) { - break; - } - - name = None; - start_idx = line_idx + 1; - split_type = SplitType::Reset; - } Line::Comment(comment) => { let comment = comment.trim(); @@ -174,10 +162,9 @@ impl SplitInfo { start_idx = line_idx + 1; let comment = comment.trim_start(); if comment.is_empty() { - name = None; - } else { - name = Some(comment); + continue; } + name = comment; } Line::FrameBulk(_) => { bulk_idx += 1; @@ -186,20 +173,14 @@ impl SplitInfo { _ => continue, } - let name = if let Some(name) = name { - if used_save_names.contains(name) { - None - } else { - used_save_names.insert(name.to_owned()); - Some(name.to_owned()) - } - } else { - None - }; + if used_save_names.contains(name) { + continue; + } + used_save_names.insert(name); splits.push(SplitInfo { start_idx, - name, + name: name.to_owned(), split_type, bulk_idx, ready: false, @@ -238,10 +219,8 @@ impl SplitInfo { } } - pub fn save_path(&self, game_dir: &Path) -> Option { - self.name - .as_ref() - .map(|name| game_dir.join("SAVE").join(format!("{name}.sav"))) + pub fn save_path(&self, game_dir: &Path) -> PathBuf { + game_dir.join("SAVE").join(format!("{}.sav", self.name)) } // TODO: test @@ -249,47 +228,38 @@ impl SplitInfo { let properties = hltas.properties.clone(); let lines = hltas.lines[self.start_idx..].to_owned(); - let frame_time = lines - .iter() - .find_map(|l| { - if let Line::FrameBulk(b) = l { - Some(&b.frame_time) - } else { - None - } - }) - .expect("There is a frame bulk after a split marker, this should never happen") - .clone(); + let last_fb = hltas.lines[self.bulk_idx].frame_bulk().unwrap(); let mut hltas = HLTAS { properties, lines }; // TODO: apply shared rng, properties, etc, which would be stored in split info // TODO: if reset or no autopause with manual save load, do not enable autopause - hltas.properties.load_command = Some(format!( - "bxt_autopause 1; _bxt_load \"{}\"", - self.name.as_ref().unwrap() - )); + hltas.properties.load_command = + Some(format!("bxt_autopause 1;_bxt_load \"{}\"", self.name)); // TODO: could the loading frames be figured out let padding = FrameBulk { auto_actions: Default::default(), movement_keys: Default::default(), action_keys: Default::default(), - frame_time, + frame_time: last_fb.frame_time.to_owned(), pitch: Default::default(), - frame_count: NonZeroU32::try_from(15).unwrap(), + frame_count: NonZeroU32::try_from(14).unwrap(), console_command: Default::default(), }; hltas.lines.insert(0, Line::FrameBulk(padding)); + let mut last_fb = last_fb.clone(); + last_fb.frame_count = NonZeroU32::MIN; + last_fb.console_command = match last_fb.console_command { + Some(console_command) => Some(format!("{console_command};unpause")), + None => Some("unpause".to_owned()), + }; + hltas.lines.insert(1, Line::FrameBulk(last_fb)); hltas } pub fn validate(&mut self, game_dir: &Path) { - // TODO: what should i do with unnamed / invalid names - self.ready = self - .save_path(game_dir) - .map(|save_path| save_path.is_file()) - .unwrap_or(false); + self.ready = self.save_path(game_dir).is_file(); } } @@ -788,7 +758,7 @@ mod tests { use crate::modules::tas_studio::editor::db::{SplitInfo, SplitType}; #[test] - fn split_by_markers() { + fn markers_from_hltas() { let script = HLTAS::from_str( "version 1\nframes\n\ ----------|------|------|0.001|-|-|10\n\ @@ -830,49 +800,28 @@ mod tests { SplitInfo { start_idx: 2, bulk_idx: 0, - name: Some("name".to_string()), - split_type: SplitType::Comment, - ready: false, - }, - SplitInfo { - start_idx: 5, - bulk_idx: 2, - name: None, + name: "name".to_string(), split_type: SplitType::Comment, ready: false, }, SplitInfo { start_idx: 9, bulk_idx: 5, - name: Some("name2".to_string()), + name: "name2".to_string(), split_type: SplitType::Save, ready: false, }, - SplitInfo { - start_idx: 14, - bulk_idx: 9, - name: None, - split_type: SplitType::Reset, - ready: false, - }, SplitInfo { start_idx: 21, bulk_idx: 14, - name: Some("name3".to_string()), + name: "name3".to_string(), split_type: SplitType::Reset, ready: false, }, SplitInfo { start_idx: 28, bulk_idx: 20, - name: Some("name4".to_string()), - split_type: SplitType::Comment, - ready: false, - }, - SplitInfo { - start_idx: 30, - bulk_idx: 21, - name: None, + name: "name4".to_string(), split_type: SplitType::Comment, ready: false, }, @@ -880,4 +829,70 @@ mod tests { assert_eq!(splits, expected); } + + #[test] + fn split_by_markers() { + let script = HLTAS::from_str( + "version 1\nframes\n\ + ----------|------|------|0.001|-|-|10\n\ + // bxt-rs-split name\n\ + ----------|------|------|0.002|-|-|10\n", + ) + .unwrap(); + let splits = SplitInfo::split_lines(script.lines.iter()); + let split = splits[0].split_hltas(&script); + let script = HLTAS::from_str( + "version 1\n\ + load_command bxt_autopause 1;_bxt_load \"name\"\n\ + frames\n\ + ----------|------|------|0.001|-|-|14\n\ + ----------|------|------|0.001|-|-|1|unpause\n\ + ----------|------|------|0.002|-|-|10\n", + ) + .unwrap(); + assert_eq!(script, split); + + // TODO: idk save load timing for `save` command + // let script = HLTAS::from_str( + // "version 1\nframes\n\ + // ----------|------|------|0.003|180|15|15|echo foo\n\ + // save name2 + // ----------|------|------|0.004|-|-|10\n\ + // ----------|------|------|0.004|-|-|10\n", + // ) + // .unwrap(); + // let split = splits[0].split_hltas(&script); + // let script = HLTAS::from_str( + // "version 1\n\ + // load_command bxt_autopause 1;_bxt_load \"name2\"\n\ + // frames\n\ + // ----------|------|------|0.003|-|-|14\n\ + // ----------|------|------|0.003|180|15|1|echo foo;unpause\n\ + // ----------|------|------|0.004|-|-|10\n\ + // ----------|------|------|0.004|-|-|10\n", + // ) + // .unwrap(); + // assert_eq!(script, split); + + let script = HLTAS::from_str( + "version 1\nframes\n\ + ----------|------|------|0.005|-|-|10\n\ + // bxt-rs-split name3 + reset 1 + ----------|------|------|0.006|-|-|10\n", + ) + .unwrap(); + let splits = SplitInfo::split_lines(script.lines.iter()); + let split = splits[0].split_hltas(&script); + let script = HLTAS::from_str( + "version 1\n\ + load_command _bxt_load \"name3\"\n\ + frames\n\ + ----------|------|------|0.005|-|-|14\n\ + ----------|------|------|0.005|-|-|1\n\ + ----------|------|------|0.006|-|-|10\n", + ) + .unwrap(); + assert_eq!(script, split); + } } diff --git a/src/modules/tas_studio/editor/mod.rs b/src/modules/tas_studio/editor/mod.rs index 2051b074..ead69d93 100644 --- a/src/modules/tas_studio/editor/mod.rs +++ b/src/modules/tas_studio/editor/mod.rs @@ -4677,9 +4677,7 @@ impl Editor { #[cfg(not(test))] { - let Some(save_path) = split.save_path(game_dir) else { - continue; - }; + let save_path = split.save_path(game_dir); // TODO: propagate error, print outside. if let Err(err) = fs::remove_file(save_path) { error!("error receiving request from server: {err:?}"); From 1e6d2572349a6f1411ecca3eadfac91ce0646561 Mon Sep 17 00:00:00 2001 From: Eddio0141 Date: Sat, 7 Dec 2024 15:35:59 +0000 Subject: [PATCH 13/19] test splitting hltas --- src/modules/tas_studio/editor/db.rs | 36 ++++++++++++++++++++--------- 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/src/modules/tas_studio/editor/db.rs b/src/modules/tas_studio/editor/db.rs index 596c88d0..916bb5a0 100644 --- a/src/modules/tas_studio/editor/db.rs +++ b/src/modules/tas_studio/editor/db.rs @@ -223,7 +223,6 @@ impl SplitInfo { game_dir.join("SAVE").join(format!("{}.sav", self.name)) } - // TODO: test pub fn split_hltas(&self, hltas: &HLTAS) -> HLTAS { let properties = hltas.properties.clone(); let lines = hltas.lines[self.start_idx..].to_owned(); @@ -247,13 +246,28 @@ impl SplitInfo { console_command: Default::default(), }; hltas.lines.insert(0, Line::FrameBulk(padding)); - let mut last_fb = last_fb.clone(); - last_fb.frame_count = NonZeroU32::MIN; - last_fb.console_command = match last_fb.console_command { - Some(console_command) => Some(format!("{console_command};unpause")), - None => Some("unpause".to_owned()), - }; - hltas.lines.insert(1, Line::FrameBulk(last_fb)); + if matches!(self.split_type, SplitType::Reset) { + // TODO: for reset, its same as doing save load without autopause, is this correct + // TODO: this exists because I don't know how to generate load time frames + let padding = FrameBulk { + auto_actions: Default::default(), + movement_keys: Default::default(), + action_keys: Default::default(), + frame_time: last_fb.frame_time.to_owned(), + pitch: Default::default(), + frame_count: NonZeroU32::MIN, + console_command: Some("unpause".to_owned()), + }; + hltas.lines.insert(1, Line::FrameBulk(padding)); + } else { + let mut last_fb = last_fb.clone(); + last_fb.frame_count = NonZeroU32::MIN; + last_fb.console_command = match last_fb.console_command { + Some(console_command) => Some(format!("{console_command};unpause")), + None => Some("unpause".to_owned()), + }; + hltas.lines.insert(1, Line::FrameBulk(last_fb)); + } hltas } @@ -876,7 +890,7 @@ mod tests { let script = HLTAS::from_str( "version 1\nframes\n\ - ----------|------|------|0.005|-|-|10\n\ + ----------|------|------|0.005|90|-|10\n\ // bxt-rs-split name3 reset 1 ----------|------|------|0.006|-|-|10\n", @@ -886,10 +900,10 @@ mod tests { let split = splits[0].split_hltas(&script); let script = HLTAS::from_str( "version 1\n\ - load_command _bxt_load \"name3\"\n\ + load_command bxt_autopause 1;_bxt_load \"name3\"\n\ frames\n\ ----------|------|------|0.005|-|-|14\n\ - ----------|------|------|0.005|-|-|1\n\ + ----------|------|------|0.005|-|-|1|unpause\n\ ----------|------|------|0.006|-|-|10\n", ) .unwrap(); From e0d3aca5984ffb7e6496491664857311cb0c746d Mon Sep 17 00:00:00 2001 From: Eddio0141 Date: Wed, 11 Dec 2024 22:29:04 +0000 Subject: [PATCH 14/19] split now represented by range --- src/modules/tas_studio/editor/db.rs | 55 ++++--- src/modules/tas_studio/editor/mod.rs | 232 ++++++++++++++++++++------- 2 files changed, 204 insertions(+), 83 deletions(-) diff --git a/src/modules/tas_studio/editor/db.rs b/src/modules/tas_studio/editor/db.rs index 916bb5a0..9ceaa1bc 100644 --- a/src/modules/tas_studio/editor/db.rs +++ b/src/modules/tas_studio/editor/db.rs @@ -1,5 +1,7 @@ use std::ffi::CStr; +use std::fs; use std::num::NonZeroU32; +use std::ops::Range; use std::path::{Path, PathBuf}; use std::{collections::HashSet, fmt}; @@ -47,9 +49,11 @@ impl fmt::Debug for Branch { #[derive(Debug, Clone, PartialEq, Eq)] pub struct SplitInfo { - // this is 1 index right after the split marker - // it is guaranteed to have a framebulk somewhere after this - pub start_idx: usize, + // must guarantee to have framebulk before and after this + /// range of split marker + /// - starts from 1 line after previous framebulk + /// - ends before next framebulk + pub split_range: Range, // for the sake of easier searching, the last framebulk index is stored pub bulk_idx: usize, // TODO: could probably get away with &str @@ -99,6 +103,10 @@ impl SplitInfo { } } + // for split marker range + // TODO: check for more unrelated things + let mut split_start_idx = line_idx + 1; + let mut splits = Vec::new(); let mut used_save_names = HashSet::new(); @@ -112,7 +120,7 @@ impl SplitInfo { const SPLIT_MARKER: &str = "bxt-rs-split"; let name; - let start_idx; + let split_range; let split_type; match line { @@ -125,19 +133,21 @@ impl SplitInfo { } name = save_name.as_str(); - start_idx = line_idx + 1; + split_range = line_idx..line_idx + 1; split_type = SplitType::Save; } Line::Comment(comment) => { let comment = comment.trim(); if !comment.starts_with(SPLIT_MARKER) { + split_start_idx = line_idx + 1; continue; } - let comment = &comment[SPLIT_MARKER.len()..]; + let comment = &comment[SPLIT_MARKER.len()..].trim(); - if !comment.is_empty() && !comment.chars().next().unwrap().is_whitespace() { + if comment.is_empty() { + split_start_idx = line_idx + 1; continue; } @@ -148,10 +158,9 @@ impl SplitInfo { if Self::no_framebulks_left(&mut lines) { break; } - SplitType::Reset } else { - lines.reset_peek(); + lines.reset_peek(); // used peek to check reset if Self::no_framebulks_left(&mut lines) { break; } @@ -159,27 +168,25 @@ impl SplitInfo { SplitType::Comment }; - start_idx = line_idx + 1; - let comment = comment.trim_start(); - if comment.is_empty() { - continue; - } - name = comment; + split_range = split_start_idx..line_idx + 1; + name = comment.trim_start(); } Line::FrameBulk(_) => { bulk_idx += 1; + split_start_idx = line_idx + 1; continue; } _ => continue, } if used_save_names.contains(name) { + split_start_idx = line_idx + 1; continue; } used_save_names.insert(name); splits.push(SplitInfo { - start_idx, + split_range, name: name.to_owned(), split_type, bulk_idx, @@ -225,7 +232,7 @@ impl SplitInfo { pub fn split_hltas(&self, hltas: &HLTAS) -> HLTAS { let properties = hltas.properties.clone(); - let lines = hltas.lines[self.start_idx..].to_owned(); + let lines = hltas.lines[self.split_range.end..].to_owned(); let last_fb = hltas.lines[self.bulk_idx].frame_bulk().unwrap(); @@ -275,6 +282,12 @@ impl SplitInfo { pub fn validate(&mut self, game_dir: &Path) { self.ready = self.save_path(game_dir).is_file(); } + + #[cfg(not(test))] + pub fn invalidate(&mut self, game_dir: &Path) -> std::io::Result<()> { + self.ready = false; + fs::remove_file(self.save_path(game_dir)) + } } #[derive(Debug, Clone)] @@ -812,28 +825,28 @@ mod tests { let splits = SplitInfo::split_lines(script.lines.iter()); let expected = vec![ SplitInfo { - start_idx: 2, + split_range: 1..2, bulk_idx: 0, name: "name".to_string(), split_type: SplitType::Comment, ready: false, }, SplitInfo { - start_idx: 9, + split_range: 8..9, bulk_idx: 5, name: "name2".to_string(), split_type: SplitType::Save, ready: false, }, SplitInfo { - start_idx: 21, + split_range: 19..21, bulk_idx: 14, name: "name3".to_string(), split_type: SplitType::Reset, ready: false, }, SplitInfo { - start_idx: 28, + split_range: 27..28, bulk_idx: 20, name: "name4".to_string(), split_type: SplitType::Comment, diff --git a/src/modules/tas_studio/editor/mod.rs b/src/modules/tas_studio/editor/mod.rs index ead69d93..07b7a87d 100644 --- a/src/modules/tas_studio/editor/mod.rs +++ b/src/modules/tas_studio/editor/mod.rs @@ -1,7 +1,6 @@ use std::cmp::min; use std::ffi::CStr; use std::fmt::Write; -use std::fs; use std::iter::{self, zip}; use std::num::NonZeroU32; use std::ops::ControlFlow; @@ -10,7 +9,7 @@ use std::time::Instant; use bxt_ipc_types::Frame; use bxt_strafe::{Hull, Trace}; -use color_eyre::eyre::{self, ensure}; +use color_eyre::eyre::{self, ensure, Context}; use db::SplitInfo; use glam::{IVec2, Vec2, Vec3}; use hltas::types::{ @@ -3323,62 +3322,65 @@ impl Editor { let to_str = String::from_utf8(buffer) .expect("Line serialization should never produce invalid UTF-8"); - // if modified before or on split idx, it is invalid - // simply find the first split that is covered - // TODO: test - let splits = self.split_markers(); - let last_line_idx = first_line_idx + count - 1; - let invalid_split_idx = - splits.partition_point(|s| s.start_idx < first_line_idx && s.start_idx > last_line_idx); - - // offsets are applied for after the last_line_idx range - let offset_split_idx = - splits[invalid_split_idx..].partition_point(|s| s.start_idx < last_line_idx); - if offset_split_idx < splits.len() { - let split_offset_cnt = isize::try_from(to.len()) - .expect("Length of the replacement line is too huge to be isize") - - isize::try_from(count) - .expect("Number of lines to replace is too huge to be isize"); - // same thing as above, but for framebulks - let fb_to_cnt = to - .iter() - .filter(|l| matches!(l, Line::FrameBulk(_))) - .count(); - let fb_count_cnt = from_lines - .iter() - .filter(|l| matches!(l, Line::FrameBulk(_))) - .count(); - let bulk_offset_cnt = isize::try_from(fb_to_cnt) - .expect("Index of framebulk is too large for calculating as isize") - - isize::try_from(fb_count_cnt) - .expect("Index of framebulk is too large for calculating as isize"); - - // apply offsets - let splits = self.split_markers_mut(); - for split in splits[offset_split_idx..].iter_mut() { - split - .start_idx - .checked_add_signed(split_offset_cnt) - .expect("Split marker index is out of range"); - split - .bulk_idx - .checked_add_signed(bulk_offset_cnt) - .expect("Split marker framebulk index is out of range"); + // remove split covered by `count` + self.split_markers_mut().retain(|s| { + s.split_range.end <= first_line_idx || s.split_range.start > first_line_idx + count - 1 + }); + + // generate splits in `to` + + // if the first split in `to` is at index 0, and previous framebulk exists before `to` lines in the final script, the split marker generation wouldn't pick this up + // to get around this issue, we just include the lines before `to` up to the framebulk + let mut prev_script_start_idx = 0usize; + let lines = &self.script().lines; + for (i, line) in lines[..first_line_idx].iter().enumerate().rev() { + if matches!(line, Line::FrameBulk(_)) { + prev_script_start_idx = i; + break; } } + let to_for_splits = lines[prev_script_start_idx..first_line_idx] + .iter() + .chain(to) + .chain(&lines[first_line_idx + count..]); + + let mut to_splits = SplitInfo::split_lines_with_stop(to_for_splits, count); + let offset_from = if !to_splits.is_empty() { + for split in to_splits.iter_mut() { + // offset to make up for the missing count + split.bulk_idx += prev_script_start_idx; + split.split_range.start += prev_script_start_idx; + split.split_range.end += prev_script_start_idx; + } + let splits = self.split_markers_mut(); + let insert_idx = splits + .iter() + .position(|s| to_splits[0].split_range.start > s.split_range.start) + .map(|i| i + 1) + .unwrap_or(splits.len()); + let offset_from = insert_idx + to_splits.len(); + for (i, split) in to_splits.into_iter().enumerate() { + splits.insert(insert_idx + i, split); + } + offset_from + } else { + let splits = self.split_markers(); + splits + .iter() + .position(|s| s.split_range.end >= first_line_idx) + .map(|i| i + 1) + .unwrap_or(splits.len()) + }; - // handle `to` lines split info - // TODO: test - let to_splits = SplitInfo::split_lines_with_stop( - to.iter() - .chain(self.script().lines[offset_split_idx..].iter()), - to.len(), - ); - let splits = self.split_markers_mut(); - for (i, split) in to_splits.into_iter().enumerate() { - let mut split = split; - split.start_idx += first_line_idx; - splits.insert(invalid_split_idx + i, split); + // offset after `to` splits + let offset = isize::try_from(to.len()) + .expect("length of `to` cannot be represented as isize") + - isize::try_from(count).expect("`count` cannot be represented as isize"); + for split in self.split_markers_mut()[offset_from..].iter_mut() { + // shouldn't panic + split.bulk_idx = split.bulk_idx.checked_add_signed(offset).unwrap(); + split.split_range.start = split.split_range.start.checked_add_signed(offset).unwrap(); + split.split_range.end = split.split_range.end.checked_add_signed(offset).unwrap(); } let op = Operation::ReplaceMultiple { @@ -3545,6 +3547,23 @@ impl Editor { // don't validate by checking save file, its a rewrite branch.splits = SplitInfo::split_lines(new_script.lines.iter()); + #[cfg(not(test))] + { + // TODO: is this fine to do + let game_dir = Path::new( + unsafe { CStr::from_ptr(engine::com_gamedir.get(MainThreadMarker::new()).cast()) } + .to_str() + .unwrap(), + ); + + // removes all corresponding saves so a game reload won't use wrong saves + for split in branch.splits.iter_mut() { + split + .invalidate(game_dir) + .wrap_err("Failed to delete save file linked to split")?; + } + } + let mut buffer = Vec::new(); script .to_writer(&mut buffer) @@ -4673,15 +4692,15 @@ impl Editor { ); for split in splits[invalid_split_idx..].iter_mut() { - split.ready = false; + #[cfg(test)] + { + split.ready = false; + } + // TODO: propagate error, print outside. #[cfg(not(test))] - { - let save_path = split.save_path(game_dir); - // TODO: propagate error, print outside. - if let Err(err) = fs::remove_file(save_path) { - error!("error receiving request from server: {err:?}"); - } + if let Err(err) = split.invalidate(game_dir) { + error!("error receiving request from server: {err:?}"); } } } @@ -5291,6 +5310,95 @@ mod tests { ); } + #[test] + fn rewrite_split_check() { + let script = HLTAS::from_str( + "version 1\nframes\n\ + ----------|------|------|0.004|0|-|6\n\ + ----------|------|------|0.004|10|-|6\n\ + // bxt-rs-split name\n\ + ----------|------|------|0.004|20|-|6\n\ + ----------|------|------|0.004|30|-|6", + ) + .unwrap(); + let mut editor = Editor::create_in_memory(&script).unwrap(); + + let new_script = HLTAS::from_str( + "version 1\nframes\n\ + ----------|------|------|0.004|0|-|6\n\ + ----------|------|------|0.004|10|-|6\n\ + // bxt-rs-split name\n\ + ----------|------|------|0.004|30|-|6", + ) + .unwrap(); + editor.rewrite(new_script).unwrap(); + assert_eq!( + &[SplitInfo { + split_range: 2..3, + bulk_idx: 1, + name: "name".to_string(), + split_type: db::SplitType::Comment, + ready: false + }], + editor.split_markers() + ); + + let new_script = HLTAS::from_str( + "version 1\nframes\n\ + ----------|------|------|0.004|0|-|6\n\ + // bxt-rs-split name\n\ + ----------|------|------|0.004|30|-|6", + ) + .unwrap(); + editor.rewrite(new_script).unwrap(); + assert_eq!( + &[SplitInfo { + split_range: 1..2, + bulk_idx: 0, + name: "name".to_string(), + split_type: db::SplitType::Comment, + ready: false + }], + editor.split_markers() + ); + + let script = HLTAS::from_str( + "version 1\nframes\n\ + ----------|------|------|0.004|10|-|6\n\ + // bxt-rs-split name\n\ + ----------|------|------|0.004|20|-|6", + ) + .unwrap(); + let mut editor = Editor::create_in_memory(&script).unwrap(); + + let new_script = HLTAS::from_str( + "version 1\nframes\n\ + ----------|------|------|0.004|20|-|6", + ) + .unwrap(); + editor.rewrite(new_script).unwrap(); + assert!(editor.split_markers().is_empty()); + + let new_script = HLTAS::from_str( + "version 1\nframes\n\ + ----------|------|------|0.004|20|-|6\n\ + // bxt-rs-split name\n\ + ----------|------|------|0.004|10|-|6", + ) + .unwrap(); + editor.rewrite(new_script).unwrap(); + assert_eq!( + vec![SplitInfo { + split_range: 1..2, + bulk_idx: 0, + name: "name".to_owned(), + split_type: db::SplitType::Comment, + ready: false + }], + editor.split_markers() + ); + } + proptest! { #![proptest_config(ProptestConfig { cases: if std::env::var_os("RUN_SLOW_TESTS").is_none() { From 301af0f4caab3d27d0a82f582102f6284ea1018f Mon Sep 17 00:00:00 2001 From: Eddio0141 Date: Fri, 13 Dec 2024 20:12:37 +0000 Subject: [PATCH 15/19] fixed broken split logic fixed broken split logic --- src/modules/tas_studio/editor/db.rs | 18 +++++++++--- src/modules/tas_studio/editor/mod.rs | 43 ++++++++++++++++++++++++---- 2 files changed, 51 insertions(+), 10 deletions(-) diff --git a/src/modules/tas_studio/editor/db.rs b/src/modules/tas_studio/editor/db.rs index 9ceaa1bc..0e4273df 100644 --- a/src/modules/tas_studio/editor/db.rs +++ b/src/modules/tas_studio/editor/db.rs @@ -128,7 +128,7 @@ impl SplitInfo { // TODO: handle setting shared rng, and what property lines do i bring over? // TODO: handle completely invalid back to back splits Line::Save(save_name) => { - if Self::no_framebulks_left(&mut lines) { + if Self::no_framebulks_left(&mut lines, line_idx + 1, stop_idx) { break; } @@ -155,13 +155,13 @@ impl SplitInfo { split_type = if matches!(lines.peek(), Some(Line::Reset { .. })) { lines.next(); // consume reset line_idx += 1; - if Self::no_framebulks_left(&mut lines) { + if Self::no_framebulks_left(&mut lines, line_idx, stop_idx) { break; } SplitType::Reset } else { lines.reset_peek(); // used peek to check reset - if Self::no_framebulks_left(&mut lines) { + if Self::no_framebulks_left(&mut lines, line_idx, stop_idx) { break; } @@ -197,11 +197,21 @@ impl SplitInfo { splits } - fn no_framebulks_left<'a, T: Iterator>(lines: &mut MultiPeek) -> bool { + fn no_framebulks_left<'a, T: Iterator>( + lines: &mut MultiPeek, + mut line_idx: usize, + stop_idx: usize, + ) -> bool { while let Some(line) = lines.peek() { + if line_idx >= stop_idx { + return true; + } + if matches!(line, Line::FrameBulk(_)) { return false; } + + line_idx += 1; } true } diff --git a/src/modules/tas_studio/editor/mod.rs b/src/modules/tas_studio/editor/mod.rs index 07b7a87d..faf8af5f 100644 --- a/src/modules/tas_studio/editor/mod.rs +++ b/src/modules/tas_studio/editor/mod.rs @@ -3323,9 +3323,18 @@ impl Editor { .expect("Line serialization should never produce invalid UTF-8"); // remove split covered by `count` - self.split_markers_mut().retain(|s| { - s.split_range.end <= first_line_idx || s.split_range.start > first_line_idx + count - 1 - }); + if count == 0 { + // inserts lines, check if `to` inserts into split range + self.split_markers_mut().retain(|s| { + s.split_range.start > first_line_idx || s.split_range.end <= first_line_idx + }); + } else { + self.split_markers_mut().retain(|s| { + s.split_range.start >= first_line_idx + count + || (s.split_range.start < first_line_idx + && s.split_range.end <= first_line_idx + count) + }); + } // generate splits in `to` @@ -3344,7 +3353,10 @@ impl Editor { .chain(to) .chain(&lines[first_line_idx + count..]); - let mut to_splits = SplitInfo::split_lines_with_stop(to_for_splits, count); + let mut to_splits = SplitInfo::split_lines_with_stop( + to_for_splits, + to.len() + (first_line_idx - prev_script_start_idx), + ); let offset_from = if !to_splits.is_empty() { for split in to_splits.iter_mut() { // offset to make up for the missing count @@ -3367,8 +3379,7 @@ impl Editor { let splits = self.split_markers(); splits .iter() - .position(|s| s.split_range.end >= first_line_idx) - .map(|i| i + 1) + .position(|s| s.split_range.start >= first_line_idx) .unwrap_or(splits.len()) }; @@ -5397,6 +5408,26 @@ mod tests { }], editor.split_markers() ); + + let new_script = HLTAS::from_str( + "version 1\nframes\n\ + ----------|------|------|0.004|20|-|6\n\ + seed 0\n\ + // bxt-rs-split name\n\ + ----------|------|------|0.004|10|-|6", + ) + .unwrap(); + editor.rewrite(new_script).unwrap(); + assert_eq!( + vec![SplitInfo { + split_range: 1..3, + bulk_idx: 0, + name: "name".to_owned(), + split_type: db::SplitType::Comment, + ready: false + }], + editor.split_markers() + ); } proptest! { From 6dd13a5426681feedf96f5422f5e2732e46abe74 Mon Sep 17 00:00:00 2001 From: Eddio0141 Date: Tue, 24 Dec 2024 18:01:33 +0000 Subject: [PATCH 16/19] fixed broken split logic --- src/modules/tas_studio/editor/mod.rs | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/src/modules/tas_studio/editor/mod.rs b/src/modules/tas_studio/editor/mod.rs index faf8af5f..b776c26b 100644 --- a/src/modules/tas_studio/editor/mod.rs +++ b/src/modules/tas_studio/editor/mod.rs @@ -3353,10 +3353,21 @@ impl Editor { .chain(to) .chain(&lines[first_line_idx + count..]); - let mut to_splits = SplitInfo::split_lines_with_stop( - to_for_splits, - to.len() + (first_line_idx - prev_script_start_idx), - ); + let mut to_splits_stop = lines[first_line_idx + count..].iter(); + let to_splits_stop = if first_line_idx < lines.len() + && matches!(&lines[first_line_idx], Line::FrameBulk(_)) + && to.len().saturating_sub(count) == 0 + { + // doesn't need to regenerate split, go before fb + to_splits_stop + .take_while(|t| !matches!(t, Line::FrameBulk(_))) + .count() + } else { + to_splits_stop + .take_while_inclusive(|t| !matches!(t, Line::FrameBulk(_))) + .count() + } + to.len(); + let mut to_splits = SplitInfo::split_lines_with_stop(to_for_splits, to_splits_stop); let offset_from = if !to_splits.is_empty() { for split in to_splits.iter_mut() { // offset to make up for the missing count From 7446438867bafb41d51977bc8394ed2d97b7afcb Mon Sep 17 00:00:00 2001 From: Eddio0141 Date: Mon, 30 Dec 2024 22:46:27 +0000 Subject: [PATCH 17/19] shared / non-shared rng handling --- src/modules/tas_studio/editor/db.rs | 49 ++++++++++++++++++++++------ src/modules/tas_studio/editor/mod.rs | 16 ++++++--- 2 files changed, 51 insertions(+), 14 deletions(-) diff --git a/src/modules/tas_studio/editor/db.rs b/src/modules/tas_studio/editor/db.rs index 0e4273df..3f8547f4 100644 --- a/src/modules/tas_studio/editor/db.rs +++ b/src/modules/tas_studio/editor/db.rs @@ -9,11 +9,11 @@ use bincode::Options; use color_eyre::eyre::{self, ensure, eyre}; use hltas::types::FrameBulk; use hltas::{types::Line, HLTAS}; -use itertools::{Itertools, MultiPeek}; +use itertools::{Either, Itertools, MultiPeek}; use rusqlite::{params, Connection, OpenFlags, OptionalExtension}; use serde::{Deserialize, Serialize}; -use crate::hooks::engine; +use crate::hooks::engine::{self, RngState}; use crate::utils::MainThreadMarker; use super::operation::Operation; @@ -61,8 +61,15 @@ pub struct SplitInfo { pub split_type: SplitType, // ready as in, there's a save created, and lines before and including start_idx is still unchanged pub ready: bool, + // TODO: timing on grabbing rng values + // TODO: how do i get shared rng from engine? + // TODO: i most likely need to add this information being sent from a sim client, how can this be done in a clean way + pub shared_rng: Option, + pub non_shared_rng: Option>, // TODO: what if the sim client is using no_refresh, would post restart rng state be "valid" } +const SPLIT_LOAD_FRAMES: u32 = 15; + #[derive(Debug, Clone, PartialEq, Eq)] pub enum SplitType { Comment, @@ -77,7 +84,6 @@ impl SplitInfo { Self::split_lines_with_stop(lines, usize::MAX) } - // TODO: test stop_idx #[must_use] pub fn split_lines_with_stop<'a, T: Iterator>( lines: T, @@ -106,6 +112,7 @@ impl SplitInfo { // for split marker range // TODO: check for more unrelated things let mut split_start_idx = line_idx + 1; + let mut prev_seed_set = None; let mut splits = Vec::new(); let mut used_save_names = HashSet::new(); @@ -122,10 +129,10 @@ impl SplitInfo { let name; let split_range; let split_type; + let non_shared_rng; match line { // TODO: save name;load name console - // TODO: handle setting shared rng, and what property lines do i bring over? // TODO: handle completely invalid back to back splits Line::Save(save_name) => { if Self::no_framebulks_left(&mut lines, line_idx + 1, stop_idx) { @@ -133,8 +140,9 @@ impl SplitInfo { } name = save_name.as_str(); - split_range = line_idx..line_idx + 1; + split_range = split_start_idx..line_idx + 1; split_type = SplitType::Save; + non_shared_rng = None; } Line::Comment(comment) => { let comment = comment.trim(); @@ -152,12 +160,13 @@ impl SplitInfo { } // linked to reset? - split_type = if matches!(lines.peek(), Some(Line::Reset { .. })) { + split_type = if let Some(Line::Reset { non_shared_seed }) = lines.peek() { lines.next(); // consume reset line_idx += 1; if Self::no_framebulks_left(&mut lines, line_idx, stop_idx) { break; } + non_shared_rng = Some(Either::Left(*non_shared_seed)); SplitType::Reset } else { lines.reset_peek(); // used peek to check reset @@ -165,6 +174,7 @@ impl SplitInfo { break; } + non_shared_rng = None; SplitType::Comment }; @@ -174,6 +184,11 @@ impl SplitInfo { Line::FrameBulk(_) => { bulk_idx += 1; split_start_idx = line_idx + 1; + prev_seed_set = None; + continue; + } + Line::SharedSeed(seed) => { + prev_seed_set = Some(*seed); continue; } _ => continue, @@ -185,12 +200,16 @@ impl SplitInfo { } used_save_names.insert(name); + let shared_rng = prev_seed_set.map(|seed| seed.wrapping_sub(SPLIT_LOAD_FRAMES - 1)); + splits.push(SplitInfo { split_range, name: name.to_owned(), split_type, bulk_idx, ready: false, + non_shared_rng, + shared_rng, }); } @@ -259,7 +278,7 @@ impl SplitInfo { action_keys: Default::default(), frame_time: last_fb.frame_time.to_owned(), pitch: Default::default(), - frame_count: NonZeroU32::try_from(14).unwrap(), + frame_count: NonZeroU32::try_from(SPLIT_LOAD_FRAMES - 1).unwrap(), console_command: Default::default(), }; hltas.lines.insert(0, Line::FrameBulk(padding)); @@ -791,6 +810,7 @@ fn update_branch(conn: &Connection, branch: &Branch) -> eyre::Result<()> { #[cfg(test)] mod tests { use hltas::HLTAS; + use itertools::Either; use crate::modules::tas_studio::editor::db::{SplitInfo, SplitType}; @@ -806,6 +826,7 @@ mod tests { ----------|------|------|0.003|-|-|10\n\ ----------|------|------|0.003|-|-|10\n\ ----------|------|------|0.003|-|-|10\n\ + seed 123 save name2 ----------|------|------|0.004|-|-|10\n\ ----------|------|------|0.004|-|-|10\n\ @@ -840,27 +861,35 @@ mod tests { name: "name".to_string(), split_type: SplitType::Comment, ready: false, + non_shared_rng: None, + shared_rng: None, }, SplitInfo { - split_range: 8..9, + split_range: 8..10, bulk_idx: 5, name: "name2".to_string(), split_type: SplitType::Save, ready: false, + non_shared_rng: None, + shared_rng: Some(123 - 14), }, SplitInfo { - split_range: 19..21, + split_range: 20..22, bulk_idx: 14, name: "name3".to_string(), split_type: SplitType::Reset, ready: false, + non_shared_rng: Some(Either::Left(1)), + shared_rng: None, }, SplitInfo { - split_range: 27..28, + split_range: 28..29, bulk_idx: 20, name: "name4".to_string(), split_type: SplitType::Comment, ready: false, + non_shared_rng: None, + shared_rng: None, }, ]; diff --git a/src/modules/tas_studio/editor/mod.rs b/src/modules/tas_studio/editor/mod.rs index b776c26b..e4caec70 100644 --- a/src/modules/tas_studio/editor/mod.rs +++ b/src/modules/tas_studio/editor/mod.rs @@ -5360,7 +5360,9 @@ mod tests { bulk_idx: 1, name: "name".to_string(), split_type: db::SplitType::Comment, - ready: false + ready: false, + non_shared_rng: None, + shared_rng: None, }], editor.split_markers() ); @@ -5379,7 +5381,9 @@ mod tests { bulk_idx: 0, name: "name".to_string(), split_type: db::SplitType::Comment, - ready: false + ready: false, + non_shared_rng: None, + shared_rng: None, }], editor.split_markers() ); @@ -5415,7 +5419,9 @@ mod tests { bulk_idx: 0, name: "name".to_owned(), split_type: db::SplitType::Comment, - ready: false + ready: false, + non_shared_rng: None, + shared_rng: None, }], editor.split_markers() ); @@ -5435,7 +5441,9 @@ mod tests { bulk_idx: 0, name: "name".to_owned(), split_type: db::SplitType::Comment, - ready: false + ready: false, + non_shared_rng: None, + shared_rng: Some(u32::wrapping_neg(14)), }], editor.split_markers() ); From 21e81d5ce2f8cd5f4e85c9a7a04b655a1c9bd8d3 Mon Sep 17 00:00:00 2001 From: Eddio0141 Date: Mon, 30 Dec 2024 23:08:53 +0000 Subject: [PATCH 18/19] apply rng values --- src/modules/tas_studio/editor/db.rs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/modules/tas_studio/editor/db.rs b/src/modules/tas_studio/editor/db.rs index 3f8547f4..a42c7e99 100644 --- a/src/modules/tas_studio/editor/db.rs +++ b/src/modules/tas_studio/editor/db.rs @@ -14,6 +14,8 @@ use rusqlite::{params, Connection, OpenFlags, OptionalExtension}; use serde::{Deserialize, Serialize}; use crate::hooks::engine::{self, RngState}; +use crate::modules::rng_set::RngSet; +use crate::modules::Module; use crate::utils::MainThreadMarker; use super::operation::Operation; @@ -270,6 +272,10 @@ impl SplitInfo { // TODO: if reset or no autopause with manual save load, do not enable autopause hltas.properties.load_command = Some(format!("bxt_autopause 1;_bxt_load \"{}\"", self.name)); + hltas.properties.seeds = Some(hltas::types::Seeds { + shared: self.shared_rng.unwrap(), // TODO: is this applied? I recall it being able to + non_shared: 0, // not used, it is applied below if reset is supposed to be used + }); // TODO: could the loading frames be figured out let padding = FrameBulk { @@ -285,6 +291,10 @@ impl SplitInfo { if matches!(self.split_type, SplitType::Reset) { // TODO: for reset, its same as doing save load without autopause, is this correct // TODO: this exists because I don't know how to generate load time frames + + // this should not fail if split state is tracked correctly, as splitting hltas is done if its valid + let non_shared_rng = self.non_shared_rng.unwrap().unwrap_right(); + let padding = FrameBulk { auto_actions: Default::default(), movement_keys: Default::default(), @@ -292,7 +302,7 @@ impl SplitInfo { frame_time: last_fb.frame_time.to_owned(), pitch: Default::default(), frame_count: NonZeroU32::MIN, - console_command: Some("unpause".to_owned()), + console_command: Some(format!("unpause;{} {non_shared_rng}", RngSet.name())), }; hltas.lines.insert(1, Line::FrameBulk(padding)); } else { From 25952fd3cdb259087c69d0d0805f30c4eba21943 Mon Sep 17 00:00:00 2001 From: Eddio0141 Date: Sat, 4 Jan 2025 21:19:38 +0000 Subject: [PATCH 19/19] send rng values and apply to split --- src/hooks/engine.rs | 3 ++- src/hooks/server.rs | 6 ++++++ src/modules/tas_studio/editor/db.rs | 3 --- src/modules/tas_studio/editor/mod.rs | 19 +++++++++++++++++-- src/modules/tas_studio/mod.rs | 7 ++++++- src/modules/tas_studio/remote.rs | 3 +++ 6 files changed, 34 insertions(+), 7 deletions(-) diff --git a/src/hooks/engine.rs b/src/hooks/engine.rs index fa987980..ac03f7a8 100644 --- a/src/hooks/engine.rs +++ b/src/hooks/engine.rs @@ -11,6 +11,7 @@ use std::str::FromStr; use bxt_macros::pattern; use bxt_patterns::Patterns; +use serde::{Deserialize, Serialize}; use crate::ffi::com_model::{mleaf_s, model_s}; use crate::ffi::command::cmd_function_s; @@ -1322,7 +1323,7 @@ impl SCREENINFO { } } -#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize, Serialize)] pub struct RngState { pub idum: c_int, pub iy: c_int, diff --git a/src/hooks/server.rs b/src/hooks/server.rs index ac095d36..3af210f5 100644 --- a/src/hooks/server.rs +++ b/src/hooks/server.rs @@ -2,6 +2,7 @@ #![allow(non_snake_case, non_upper_case_globals)] +use std::borrow::BorrowMut; use std::os::raw::*; use std::ptr::NonNull; @@ -70,10 +71,15 @@ pub unsafe extern "C" fn my_CmdStart( tas_recording::on_cmd_start(marker, *cmd, random_seed); tas_optimizer::on_cmd_start(marker); + *RANDOM_SEED.borrow_mut(marker) = Some(random_seed); + CmdStart.get(marker)(player, cmd, random_seed); }) } +// TODO: directly grab from server, see comment https://github.com/YaLTeR/bxt-rs/pull/110#discussion_r1899846171 +pub static RANDOM_SEED: MainThreadRefCell> = MainThreadRefCell::new(None); + pub unsafe extern "C" fn my_PM_Move(ppmove: *mut playermove_s, server: c_int) { abort_on_panic(move || { let marker = MainThreadMarker::new(); diff --git a/src/modules/tas_studio/editor/db.rs b/src/modules/tas_studio/editor/db.rs index a42c7e99..da2ca79a 100644 --- a/src/modules/tas_studio/editor/db.rs +++ b/src/modules/tas_studio/editor/db.rs @@ -63,9 +63,6 @@ pub struct SplitInfo { pub split_type: SplitType, // ready as in, there's a save created, and lines before and including start_idx is still unchanged pub ready: bool, - // TODO: timing on grabbing rng values - // TODO: how do i get shared rng from engine? - // TODO: i most likely need to add this information being sent from a sim client, how can this be done in a clean way pub shared_rng: Option, pub non_shared_rng: Option>, // TODO: what if the sim client is using no_refresh, would post restart rng state be "valid" } diff --git a/src/modules/tas_studio/editor/mod.rs b/src/modules/tas_studio/editor/mod.rs index e4caec70..27566168 100644 --- a/src/modules/tas_studio/editor/mod.rs +++ b/src/modules/tas_studio/editor/mod.rs @@ -17,7 +17,7 @@ use hltas::types::{ VectorialStrafingConstraints, }; use hltas::HLTAS; -use itertools::Itertools; +use itertools::{Either, Itertools}; use thiserror::Error; use self::db::{Action, ActionKind, Branch, Db}; @@ -30,8 +30,9 @@ use self::utils::{ }; use super::remote::{AccurateFrame, PlayRequest}; use super::MainThreadMarker; -use crate::hooks::engine; +use crate::hooks::engine::{self, rng_state}; use crate::hooks::sdl::MouseState; +use crate::hooks::server::RANDOM_SEED; use crate::modules::tas_optimizer::simulator::Simulator; use crate::modules::tas_studio::editor::utils::MaxAccelOffsetValues; use crate::modules::triangle_drawing::triangle_api::{Primitive, RenderMode}; @@ -3828,6 +3829,7 @@ impl Editor { return None; } + // TODO: test let split_frame_idx = self.split_hltas().map(|split| split.1).unwrap_or_default(); let actual_frame_idx = frame.frame_idx + split_frame_idx; // TODO: make this nicer somehow maybe? @@ -3880,12 +3882,23 @@ impl Editor { if actual_frame_idx + 1 > branch.first_predicted_frame { branch.first_predicted_frame = actual_frame_idx + 1; + // TODO: test let splits = &mut branch.branch.splits; let split_valid_to = splits.partition_point(|s| s.bulk_idx >= branch.first_predicted_frame); for split in splits[..split_valid_to].iter_mut() { split.ready = true; } + + // just in case + // TODO: test + if split_valid_to <= splits.len() { + let split = &mut splits[split_valid_to - 1]; + + // TODO: test if rng is accurate + split.shared_rng = Some(frame.random_seed); + split.non_shared_rng = Some(Either::Right(frame.rng_state)); + } } if branch.frames.len() == actual_frame_idx { @@ -3916,6 +3929,7 @@ impl Editor { .sum::(); if actual_frame_idx + 1 == frame_count { + // TODO: test unsafe { self.update_split_hltas(MainThreadMarker::new()); } @@ -3985,6 +3999,7 @@ impl Editor { } branch.auto_smoothing.script = Some(smoothed_script.clone()); + // TODO: test if let Some(split) = &branch.branch.split { branch.branch.split = Some((smoothed_script.clone(), split.1)); } diff --git a/src/modules/tas_studio/mod.rs b/src/modules/tas_studio/mod.rs index 312f8a9e..1885e18a 100644 --- a/src/modules/tas_studio/mod.rs +++ b/src/modules/tas_studio/mod.rs @@ -36,7 +36,8 @@ use crate::ffi::cvar::cvar_s; use crate::ffi::usercmd::usercmd_s; use crate::handler; use crate::hooks::bxt::{OnTasPlaybackFrameData, BXT_IS_TAS_EDITOR_ACTIVE}; -use crate::hooks::engine::con_print; +use crate::hooks::engine::{con_print, rng_state}; +use crate::hooks::server::RANDOM_SEED; use crate::hooks::{bxt, client, engine, sdl}; use crate::modules::tas_studio::editor::{CameraViewAdjustmentMode, MaxAccelYawOffsetMode}; use crate::utils::*; @@ -1851,6 +1852,10 @@ pub unsafe fn on_tas_playback_frame( generation, branch_idx, is_smoothed, + random_seed: RANDOM_SEED + .borrow(marker) + .expect("failed to obtain random seed"), + rng_state: rng_state(marker).expect("failed to obtain rng state"), }; match &mut *state { diff --git a/src/modules/tas_studio/remote.rs b/src/modules/tas_studio/remote.rs index 3c05c323..edb10f3f 100644 --- a/src/modules/tas_studio/remote.rs +++ b/src/modules/tas_studio/remote.rs @@ -14,6 +14,7 @@ use ipc_channel::ipc::{IpcOneShotServer, IpcReceiver, IpcSender}; use once_cell::sync::Lazy; use serde::{Deserialize, Serialize}; +use crate::hooks::engine::RngState; use crate::hooks::{bxt, engine}; use crate::modules::remote_forbid; use crate::utils::{MainThreadCell, MainThreadMarker, PointerTrait}; @@ -43,6 +44,8 @@ pub struct AccurateFrame { pub branch_idx: usize, /// Whether we're playing through the smoothed version of the script. pub is_smoothed: bool, + pub random_seed: u32, + pub rng_state: RngState, } impl fmt::Debug for AccurateFrame {