1use mint::Point2;
2use ramer_douglas_peucker::rdp;
3use serde::{Deserialize, Serialize};
4use serde_json::{Error as SerdeError, Value};
5use thiserror::Error;
6
7#[derive(Debug, Serialize, Deserialize)]
11#[serde(deny_unknown_fields, rename_all = "camelCase")]
12pub struct FSPoint {
13 pub pos: i32,
14 pub at: i32,
15}
16
17#[derive(Debug, Serialize, Deserialize)]
20#[serde(deny_unknown_fields, rename_all = "camelCase")]
21pub struct SimulatorPresets {
22 pub name: String,
23 pub full_range: bool,
24 pub direction: i32,
25 pub rotation: f32,
26 pub length: f32,
27 pub width: f32,
28 pub offset: String,
29 pub color: String,
30}
31
32#[derive(Debug, Serialize, Deserialize)]
34#[serde(deny_unknown_fields, rename_all = "camelCase")]
35pub struct OFSMetadata {
36 bookmarks: Vec<i32>,
37 chapters: Vec<String>,
38 creator: String,
39 description: String,
40 duration: i32,
41 license: String,
42 notes: String,
43 performers: Vec<String>,
44 #[serde(rename = "script_url")]
45 script_url: String,
46 tags: Vec<String>,
47 title: String,
48 #[serde(rename = "type")]
49 ofs_type: String,
50 #[serde(rename = "video_url")]
51 video_url: String,
52}
53
54#[derive(Debug, Serialize, Deserialize)]
56#[serde(deny_unknown_fields, rename_all = "camelCase", default)]
57pub struct FScript {
58 pub version: String,
59 pub inverted: bool,
60 pub range: i32,
61 pub bookmark: i32,
62 pub last_position: i64,
63 pub graph_duration: i32,
64 pub speed_ratio: f32,
65 pub injection_speed: i32,
66 pub injection_bias: f32,
67 pub scripting_mode: i32,
68 pub simulator_presets: Vec<SimulatorPresets>,
69 pub active_simulator: i32,
70 pub reduction_tolerance: f32,
71 pub reduction_stretch: f32,
72 pub clips: Vec<Value>,
73 pub actions: Vec<FSPoint>,
74 pub raw_actions: Vec<FSPoint>,
75 pub metadata: OFSMetadata,
76}
77
78impl Default for FScript {
79 fn default() -> Self {
80 Self {
81 version: "".to_string(),
82 inverted: false,
83 range: -1,
84 bookmark: -1,
85 last_position: -1,
86 graph_duration: -1,
87 speed_ratio: -1.0,
88 injection_speed: -1,
89 injection_bias: -1.0,
90 scripting_mode: -1,
91 simulator_presets: Vec::new(),
92 active_simulator: -1,
93 reduction_tolerance: -1.0,
94 reduction_stretch: -1.0,
95 clips: Vec::new(),
96 actions: Vec::new(),
97 raw_actions: Vec::new(),
98 metadata: OFSMetadata {
99 bookmarks: Vec::new(),
100 chapters: Vec::new(),
101 creator: "".to_string(),
102 description: "".to_string(),
103 duration: -1,
104 license: "".to_string(),
105 notes: "".to_string(),
106 performers: Vec::new(),
107 script_url: "".to_string(),
108 tags: Vec::new(),
109 title: "".to_string(),
110 ofs_type: "".to_string(),
111 video_url: "".to_string(),
112 },
113 }
114 }
115}
116
117#[derive(Error, Debug)]
119pub enum FunscriptError {
120 #[error("file read error {0}")]
121 FileReadError(#[from] std::io::Error),
122 #[error("json error {0}")]
123 JsonError(#[from] SerdeError),
124 #[error("failed to {0} point at index {1}")]
125 PointError(String, usize),
126}
127
128pub fn load_funscript(path: &str) -> Result<FScript, FunscriptError> {
130 let file = std::fs::read_to_string(path)?;
131 let json = serde_json::from_str::<FScript>(&file)?;
132 Ok(json)
133}
134
135pub fn save_funscript(path: &str, script: &FScript) -> Result<(), FunscriptError> {
137 if !path.ends_with(".funscript") {
138 return Err(FunscriptError::FileReadError(std::io::Error::new(
139 std::io::ErrorKind::Other,
140 "invalid file extension",
141 )));
142 }
143
144 let json = serde_json::to_string_pretty(script)?;
145 std::fs::write(path, json)?;
146 Ok(())
147}
148
149pub fn get_pt(script: &mut FScript, idx: usize) -> Result<&mut FSPoint, FunscriptError> {
151 if idx >= script.actions.len() {
152 return Err(FunscriptError::PointError("get".to_string(), idx));
153 }
154 Ok(&mut script.actions[idx])
155}
156
157pub fn apply_rdp(script: &mut FScript, epsilon: f64) {
161 let mut points: Vec<Point2<i32>> = Vec::new();
162 for pt in &script.actions {
163 points.push(Point2 {
164 x: pt.at,
165 y: pt.pos,
166 });
167 }
168
169 let idxs = rdp(points.as_slice(), epsilon);
171 let mut reduced: Vec<Point2<i32>> = Vec::new();
172 for idx in idxs {
173 reduced.push(points[idx]);
174 }
175
176 script.actions.clear();
177 for pt in reduced {
178 script.actions.push(FSPoint {
179 at: pt.x,
180 pos: pt.y,
181 });
182 }
183}
184
185pub fn print_script(script: &FScript) {
187 println!("{}", serde_json::to_string_pretty(script).unwrap());
188}
189
190#[cfg(test)]
196mod tests {
197 use super::*;
198
199 #[test]
200 fn test_jfs_save_load_funscript() {
201 let path = "./test-scripts/joyfunscripter.funscript";
202 let save_path = "./test-scripts/out/joyfunscripter.funscript";
203
204 let mut s = load_funscript(path).unwrap();
205 assert!(s.last_position == 6388388382, "file has defaulted");
206 s.bookmark = 100000;
207 save_funscript(save_path, &s).unwrap();
208 let check = load_funscript(save_path).unwrap();
209 assert_eq!(check.bookmark, 100000);
210 }
211
212 #[test]
213 fn test_ofs_save_load_funscript() {
214 let path = "./test-scripts/openfunscripter.funscript";
215 let save_path = "./test-scripts/out/openfunscripter.funscript";
216
217 let mut s = load_funscript(path).unwrap();
218 assert!(s.metadata.duration == 2610, "file has defaulted");
219 s.bookmark = 100000;
220 save_funscript(save_path, &s).unwrap();
221 let check = load_funscript(save_path).unwrap();
222 assert_eq!(check.bookmark, 100000);
223 }
224
225 #[test]
226 fn test_get_set_pt() {
227 let path = "./test-scripts/openfunscripter.funscript";
228 let mut s = load_funscript(path).unwrap();
229 let pt = get_pt(&mut s, 0).unwrap();
230 assert_eq!(pt.at, 218703);
231 pt.at = 12345678;
232 assert_eq!(pt.at, 12345678);
233 }
234}