Skip to content
Draft
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
300 changes: 298 additions & 2 deletions src/modules/tas_studio/editor/db.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
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};
use hltas::HLTAS;
use hltas::{types::Line, HLTAS};
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)]
Expand All @@ -21,6 +26,7 @@ pub struct Branch {
pub is_hidden: bool,

pub script: HLTAS,
pub splits: Vec<SplitInfo>,
pub stop_frame: u32,
}

Expand All @@ -35,6 +41,198 @@ 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,
// 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<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,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SplitType {
Comment,
Reset,
Save,
}

impl SplitInfo {
#[must_use]
pub fn split_lines<'a, T: Iterator<Item = &'a Line>>(lines: T) -> Vec<Self> {
Self::split_lines_with_stop(lines, usize::MAX)
}

// TODO: test stop_idx
#[must_use]
pub fn split_lines_with_stop<'a, T: Iterator<Item = &'a Line>>(
lines: T,
stop_idx: usize,
) -> Vec<Self> {
let mut lines = lines.into_iter().multipeek();

if lines.peek().is_none() {
return Vec::new();
}

let mut splits = Vec::new();

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;
}

line_idx += 1;
if line_idx >= stop_idx {
return splits;
}
}

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 start_idx;
let split_type;

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) {
break;
}
line_idx += 1;

name = Some(save_name.as_str());
start_idx = line_idx;
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;
}
line_idx += 1;

name = None;
start_idx = line_idx;
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;
}

// linked to reset?
split_type = if matches!(lines.peek(), Some(Line::Reset { .. })) {
lines.next(); // consume reset
if Self::no_framebulks_left(&mut lines) {
break;
}
line_idx += 1;

SplitType::Reset
} else {
if Self::no_framebulks_left(&mut lines) {
break;
}

SplitType::Comment
};
line_idx += 1;

start_idx = line_idx;
let comment = comment.trim_start();
if comment.is_empty() {
name = None;
} else {
name = Some(comment);
}
}
Line::FrameBulk(_) => {
bulk_idx += 1;
continue;
}
_ => 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,
split_type,
bulk_idx,
ready: false,
});
}

splits
}

fn no_framebulks_left<'a, T: Iterator<Item = &'a Line>>(lines: &mut MultiPeek<T>) -> bool {
while let Some(line) = lines.peek() {
if matches!(line, Line::FrameBulk(_)) {
return false;
}
}
true
}

pub fn validate_all_by_saves(splits: &mut Vec<SplitInfo>, 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)]
pub struct GlobalSettings {
pub current_branch_id: i64,
Expand Down Expand Up @@ -190,11 +388,18 @@ 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());
}
Comment on lines +485 to +488

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this fine?


Ok(Branch {
branch_id,
name,
is_hidden,
script,
splits,
stop_frame,
})
}
Expand All @@ -220,12 +425,19 @@ 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,
})
}
stmt.finalize()?;
Expand Down Expand Up @@ -506,3 +718,87 @@ 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_lines(script.lines.iter());
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,
},
];

assert_eq!(splits, expected);
}
}
Loading