diff --git a/src/hooks/engine.rs b/src/hooks/engine.rs index fa98798..ac03f7a 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 ac095d3..3af210f 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 66d52cd..da2ca79 100644 --- a/src/modules/tas_studio/editor/db.rs +++ b/src/modules/tas_studio/editor/db.rs @@ -1,12 +1,23 @@ -use std::fmt; -use std::path::Path; +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}; use bincode::Options; use color_eyre::eyre::{self, ensure, eyre}; -use hltas::HLTAS; +use hltas::types::FrameBulk; +use hltas::{types::Line, HLTAS}; +use itertools::{Either, Itertools, MultiPeek}; 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; #[derive(Debug)] @@ -21,6 +32,9 @@ pub struct Branch { pub is_hidden: bool, 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, } @@ -35,6 +49,283 @@ impl fmt::Debug for Branch { } } +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SplitInfo { + // 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 + 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, + 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, + Reset, + Save, +} + +impl SplitInfo { + #[must_use] + #[inline(always)] + pub fn split_lines<'a, T: Iterator>(lines: T) -> Vec { + Self::split_lines_with_stop(lines, usize::MAX) + } + + #[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(); + } + + let mut line_idx = 0usize; + let mut bulk_idx = 0usize; + // skip till there's at least 1 framebulk + for line in lines.by_ref() { + if matches!(line, Line::FrameBulk(_)) { + break; + } + + line_idx += 1; + if line_idx >= stop_idx { + return Vec::new(); + } + } + + // 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(); + + while let Some(line) = lines.next() { + // this is correct, if FrameBulk is at index 0, we are searching from index 1 + line_idx += 1; + if line_idx >= stop_idx { + return splits; + } + + const SPLIT_MARKER: &str = "bxt-rs-split"; + + let name; + let split_range; + let split_type; + let non_shared_rng; + + match line { + // TODO: save name;load name console + // TODO: handle completely invalid back to back splits + Line::Save(save_name) => { + if Self::no_framebulks_left(&mut lines, line_idx + 1, stop_idx) { + break; + } + + name = save_name.as_str(); + split_range = split_start_idx..line_idx + 1; + split_type = SplitType::Save; + non_shared_rng = None; + } + 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()..].trim(); + + if comment.is_empty() { + split_start_idx = line_idx + 1; + continue; + } + + // linked to 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 + if Self::no_framebulks_left(&mut lines, line_idx, stop_idx) { + break; + } + + non_shared_rng = None; + SplitType::Comment + }; + + split_range = split_start_idx..line_idx + 1; + name = comment.trim_start(); + } + 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, + } + + if used_save_names.contains(name) { + split_start_idx = line_idx + 1; + continue; + } + 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, + }); + } + + splits + } + + 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 + } + + #[cfg(test)] + pub fn validate_all_by_saves(splits: &mut Vec, _marker: MainThreadMarker) { + for split in splits { + split.ready = false; + } + } + + #[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); + } + } + + pub fn save_path(&self, game_dir: &Path) -> PathBuf { + game_dir.join("SAVE").join(format!("{}.sav", self.name)) + } + + pub fn split_hltas(&self, hltas: &HLTAS) -> HLTAS { + let properties = hltas.properties.clone(); + let lines = hltas.lines[self.split_range.end..].to_owned(); + + 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)); + 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 { + 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::try_from(SPLIT_LOAD_FRAMES - 1).unwrap(), + console_command: Default::default(), + }; + hltas.lines.insert(0, Line::FrameBulk(padding)); + 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(), + action_keys: Default::default(), + frame_time: last_fb.frame_time.to_owned(), + pitch: Default::default(), + frame_count: NonZeroU32::MIN, + console_command: Some(format!("unpause;{} {non_shared_rng}", RngSet.name())), + }; + 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 + } + + 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)] pub struct GlobalSettings { pub current_branch_id: i64, @@ -190,12 +481,20 @@ impl Db { let script = HLTAS::from_str(&buffer) .map_err(|err| eyre!("invalid script value, cannot parse: {err:?}"))?; + 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, name, is_hidden, script, + splits, stop_frame, + split: None, }) } @@ -220,12 +519,20 @@ impl Db { let script = HLTAS::from_str(&buffer) .map_err(|err| eyre!("invalid script value, cannot parse: {err:?}"))?; + 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, name, is_hidden, script, stop_frame, + splits, + split: None, }) } stmt.finalize()?; @@ -506,3 +813,159 @@ fn update_branch(conn: &Connection, branch: &Branch) -> eyre::Result<()> { Ok(()) } + +#[cfg(test)] +mod tests { + use hltas::HLTAS; + use itertools::Either; + + use crate::modules::tas_studio::editor::db::{SplitInfo, SplitType}; + + #[test] + fn markers_from_hltas() { + 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\ + seed 123 + 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 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 + // bxt-rs-split name4 + ----------|------|------|0.007|-|-|10\n\ + // bxt-rs-split name4 + ----------|------|------|0.008|-|-|10\n", + ) + .unwrap(); + + let splits = SplitInfo::split_lines(script.lines.iter()); + let expected = vec![ + SplitInfo { + split_range: 1..2, + bulk_idx: 0, + name: "name".to_string(), + split_type: SplitType::Comment, + ready: false, + non_shared_rng: None, + shared_rng: None, + }, + SplitInfo { + 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: 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: 28..29, + bulk_idx: 20, + name: "name4".to_string(), + split_type: SplitType::Comment, + ready: false, + non_shared_rng: None, + shared_rng: None, + }, + ]; + + 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|90|-|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_autopause 1;_bxt_load \"name3\"\n\ + frames\n\ + ----------|------|------|0.005|-|-|14\n\ + ----------|------|------|0.005|-|-|1|unpause\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 bd7ff7e..2756616 100644 --- a/src/modules/tas_studio/editor/mod.rs +++ b/src/modules/tas_studio/editor/mod.rs @@ -1,4 +1,5 @@ -use std::cmp::{max, min}; +use std::cmp::min; +use std::ffi::CStr; use std::fmt::Write; use std::iter::{self, zip}; use std::num::NonZeroU32; @@ -8,14 +9,15 @@ 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::{ AutoMovement, Change, ChangeTarget, Line, StrafeDir, StrafeSettings, StrafeType, VectorialStrafingConstraints, }; use hltas::HLTAS; -use itertools::Itertools; +use itertools::{Either, Itertools}; use thiserror::Error; use self::db::{Action, ActionKind, Branch, Db}; @@ -27,7 +29,10 @@ use self::utils::{ FrameBulkExt, MaxAccelOffsetValuesMut, }; use super::remote::{AccurateFrame, PlayRequest}; +use super::MainThreadMarker; +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}; @@ -672,6 +677,13 @@ impl Editor { self.recompute_extra_camera_frame_data_if_needed(); self.generation = self.generation.wrapping_add(1); + + // 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) { @@ -3308,13 +3320,96 @@ 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"); + // remove split covered by `count` + 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` + + // 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_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 + 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.start >= first_line_idx) + .unwrap_or(splits.len()) + }; + + // 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 { first_line_idx, from, - to, + to: to_str, }; self.apply_operation(op) } @@ -3460,7 +3555,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 +3567,26 @@ impl Editor { return Ok(()); } + // 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) @@ -3713,8 +3829,11 @@ 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? - 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]; @@ -3735,7 +3854,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; } @@ -3747,35 +3866,55 @@ 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; } - branch.first_predicted_frame = max(frame.frame_idx + 1, branch.first_predicted_frame); + if actual_frame_idx + 1 > branch.first_predicted_frame { + branch.first_predicted_frame = actual_frame_idx + 1; - if branch.frames.len() == frame.frame_idx { + // 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 { 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); } } } @@ -3789,8 +3928,19 @@ 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 { + // TODO: test + 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 @@ -3849,6 +3999,11 @@ 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)); + } + return Some(PlayRequest { script: smoothed_script, generation: self.generation, @@ -4552,6 +4707,93 @@ 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, _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() { + #[cfg(test)] + { + split.ready = false; + } + + // TODO: propagate error, print outside. + #[cfg(not(test))] + if let Err(err) = split.invalidate(game_dir) { + 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 + 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) + } + + pub fn split_hltas(&self) -> Option<&(HLTAS, usize)> { + self.branch().branch.split.as_ref() + } } fn perpendicular(prev: Vec3, next: Vec3) -> Vec3 { @@ -5105,6 +5347,123 @@ 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, + non_shared_rng: None, + shared_rng: None, + }], + 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, + non_shared_rng: None, + shared_rng: None, + }], + 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, + non_shared_rng: None, + shared_rng: None, + }], + 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, + non_shared_rng: None, + shared_rng: Some(u32::wrapping_neg(14)), + }], + editor.split_markers() + ); + } + proptest! { #![proptest_config(ProptestConfig { cases: if std::env::var_os("RUN_SLOW_TESTS").is_none() { diff --git a/src/modules/tas_studio/mod.rs b/src/modules/tas_studio/mod.rs index 73fc6f6..1885e18 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 { @@ -2041,8 +2046,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, diff --git a/src/modules/tas_studio/remote.rs b/src/modules/tas_studio/remote.rs index 3c05c32..edb10f3 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 {