1mod deserialize;
12mod serialize;
13
14use crate::{
15 Defaults, HasChecksumField, HasFileVersionField, HasHeaderField, IsDefault, OctatrackFileIO,
16 OtToolsIoError,
17};
18use ot_tools_io_derive::{ArrayDefaults, BoxedBigArrayDefaults, IntegrityChecks};
19use serde::Serialize;
20use serde_big_array::Array;
21use std::array::from_fn;
22
23pub const ARRANGEMENT_FILE_HEADER: [u8; 21] = [
30 70, 79, 82, 77, 0, 0, 0, 0, 68, 80, 83, 49, 65, 82, 82, 65, 0, 0, 0, 0, 0,
31];
32
33pub const ARRANGEMENT_FILE_VERSION: u8 = 6;
35
36const ARRANGEMENT_DEFAULT_NAME: [u8; 15] =
41 [79, 67, 84, 65, 84, 79, 79, 76, 83, 45, 65, 82, 82, 32, 32];
42
43#[derive(Debug, Clone, Serialize, PartialEq, IntegrityChecks)]
46pub struct ArrangementFile {
47 pub header: [u8; 21],
49
50 pub datatype_version: u8,
54
55 pub unk1: [u8; 2],
60
61 pub arrangement_state_current: ArrangementBlock,
66
67 pub unk2: u8,
69
70 pub saved_flag: u8,
72
73 pub arrangement_state_previous: ArrangementBlock,
76
77 pub arrangements_saved_state: [u8; 8],
88
89 pub checksum: u16,
91}
92
93impl Default for ArrangementFile {
94 fn default() -> Self {
95 Self {
121 header: ARRANGEMENT_FILE_HEADER,
122 datatype_version: ARRANGEMENT_FILE_VERSION,
123 unk1: from_fn(|_| 0),
124 arrangement_state_current: ArrangementBlock::default(),
125 unk2: 0,
127 saved_flag: 0,
129 arrangement_state_previous: ArrangementBlock::default(),
130 arrangements_saved_state: from_fn(|_| 1),
136 checksum: 1973,
137 }
138 }
139}
140
141impl IsDefault for ArrangementFile {
142 fn is_default(&self) -> bool {
143 let default = &ArrangementFile::default();
144 self.arrangement_state_current.is_default()
147 && self.arrangement_state_previous.is_default()
148 && default.unk1 == self.unk1
149 && default.unk2 == self.unk2
150 }
151}
152
153#[cfg(test)]
154mod is_default {
155 use crate::arrangements::ArrangementFile;
156 use crate::test_utils::get_arrange_dirpath;
157 use crate::{IsDefault, OctatrackFileIO};
158
159 #[test]
160 fn true_not_modified_default() {
161 assert!(ArrangementFile::default().is_default())
162 }
163 #[test]
164 fn true_not_modified_file() {
165 let arr =
166 ArrangementFile::from_data_file(&get_arrange_dirpath().join("blank.work")).unwrap();
167 assert!(arr.is_default())
168 }
169 #[test]
170 fn false_modified_file() {
171 let arr = ArrangementFile::from_data_file(&get_arrange_dirpath().join("full-options.work"))
172 .unwrap();
173 assert!(!arr.is_default())
174 }
175}
176
177impl OctatrackFileIO for ArrangementFile {
178 fn encode(&self) -> Result<Vec<u8>, OtToolsIoError> {
179 let mut swapped = self.clone();
183 if cfg!(target_endian = "little") {
184 swapped.checksum = self.checksum.swap_bytes();
185 }
186 let bytes = bincode::serialize(&swapped)?;
187 Ok(bytes)
188 }
189}
190
191#[cfg(test)]
192mod decode {
193 use crate::{
194 read_bin_file, test_utils::get_arrange_dirpath, ArrangementFile, OctatrackFileIO,
195 OtToolsIoError,
196 };
197
198 const HACKED_ARR_NAME: [u8; 15] = [68, 73, 70, 70, 78, 65, 77, 69, 66, 76, 65, 78, 75, 49, 0];
199
200 #[test]
201 fn valid() -> Result<(), OtToolsIoError> {
202 let path = get_arrange_dirpath().join("blank.work");
203 let bytes = read_bin_file(&path)?;
204 let s = ArrangementFile::decode(&bytes)?;
205
206 let mut x = ArrangementFile::default();
208 x.arrangement_state_current.name = HACKED_ARR_NAME;
209 x.arrangement_state_previous.name = HACKED_ARR_NAME;
210 x.saved_flag = 1;
212
213 assert_eq!(s, x);
214 Ok(())
215 }
216}
217
218#[cfg(test)]
219mod encode {
220 const HACKED_ARR_NAME: [u8; 15] = [68, 73, 70, 70, 78, 65, 77, 69, 66, 76, 65, 78, 75, 49, 0];
221 use crate::{
222 read_bin_file, test_utils::get_arrange_dirpath, ArrangementFile, OctatrackFileIO,
223 OtToolsIoError,
224 };
225 #[test]
226 fn valid() -> Result<(), OtToolsIoError> {
227 let path = get_arrange_dirpath().join("blank.work");
228 let bytes = read_bin_file(&path)?;
229
230 let mut x = ArrangementFile::default();
232 x.arrangement_state_current.name = HACKED_ARR_NAME;
233 x.arrangement_state_previous.name = HACKED_ARR_NAME;
234 x.saved_flag = 1;
236
237 let b = x.encode()?;
238 assert_eq!(b, bytes);
239 Ok(())
240 }
241}
242
243impl HasChecksumField for ArrangementFile {
244 fn calculate_checksum(&self) -> Result<u16, OtToolsIoError> {
245 Ok(0)
246 }
247 fn check_checksum(&self) -> Result<bool, OtToolsIoError> {
248 Ok(self.checksum == self.calculate_checksum()?)
249 }
250}
251
252#[cfg(test)]
254mod checksum_field {
255 use crate::{ArrangementFile, HasChecksumField, OtToolsIoError};
256 #[test]
257 fn valid() -> Result<(), OtToolsIoError> {
258 let mut x = ArrangementFile::default();
259 x.checksum = x.calculate_checksum()?;
260 assert!(x.check_checksum()?);
261 Ok(())
262 }
263
264 #[test]
265 fn invalid() -> Result<(), OtToolsIoError> {
266 let x = ArrangementFile {
267 checksum: u16::MAX,
268 ..Default::default()
269 };
270 assert!(!x.check_checksum()?);
271 Ok(())
272 }
273
274 mod files {
275 use crate::arrangements::ArrangementFile;
276 use crate::test_utils::{get_arrange_dirpath, get_blank_proj_dirpath};
277 use crate::{OctatrackFileIO, OtToolsIoError};
278 use std::path::Path;
279
280 fn arr_eq_helper(a: &ArrangementFile, b: &ArrangementFile) {
282 assert_eq!(a.header, b.header);
283 assert_eq!(a.datatype_version, b.datatype_version);
284 assert_eq!(a.unk1, b.unk1);
285 assert_eq!(a.unk2, b.unk2);
286 assert_eq!(a.arrangements_saved_state, b.arrangements_saved_state);
287 assert_eq!(a.arrangement_state_current, b.arrangement_state_current);
288 assert_eq!(a.arrangement_state_previous, b.arrangement_state_previous);
289 }
290
291 #[allow(dead_code)]
301 fn get_checksum_octarranger(bytes: &[u8]) -> Result<u16, OtToolsIoError> {
302 let bytes_no_header_no_chk = &bytes[16..bytes.len() - 2];
303
304 let mut chk: u16 = 0;
305 for byte_pair in &bytes_no_header_no_chk
306 .chunks(2)
307 .map(|x| x.to_vec())
308 .filter(|x| x.len() > 1)
309 .collect::<Vec<Vec<u8>>>()
310 {
311 let first_byte = byte_pair[0] as u16;
312 let second_byte = byte_pair[1] as u16;
313 chk = second_byte.wrapping_add(first_byte.wrapping_add(chk));
314 }
315
316 Ok(chk)
317 }
318
319 fn get_checksum_simple(bytes: &[u8]) -> Result<u16, OtToolsIoError> {
320 let bytes_no_header_no_chk = &bytes[16..bytes.len() - 2];
321
322 let mut prev;
323 let mut chk: u32 = 0;
324 for byte in bytes_no_header_no_chk {
325 prev = chk;
326 chk = chk.wrapping_add((*byte as u32).wrapping_add(0));
327 if byte != &0 {
328 println!("chk: {chk} diff: {}", chk - prev);
329 }
330 }
331 println!("CHK32: {chk}");
332 Ok((chk).wrapping_mul(1) as u16)
333 }
334
335 #[allow(dead_code)]
338 fn get_checksum_bank(bytes: &[u8]) -> Result<u16, OtToolsIoError> {
339 let bytes_no_header_no_chk = &bytes[16..bytes.len() - 2];
340 let default_bytes = &bincode::serialize(&ArrangementFile::default())?;
341 let def_important_bytes = &default_bytes[16..bytes.len() - 2];
342 let default_checksum: i32 = 1870;
343 let mut byte_diffs: i32 = 0;
344 for (byte, def_byte) in bytes_no_header_no_chk.iter().zip(def_important_bytes) {
345 let byte_diff = (*byte as i32) - (*def_byte as i32);
346 if byte_diff != 0 {
347 byte_diffs += byte_diff;
348 }
349 }
350 let check = byte_diffs * 256 + default_checksum;
351 let modded = check.rem_euclid(65535);
352 Ok(modded as u16)
353 }
354
355 fn helper_test_chksum(fp: &Path) {
356 let valid = ArrangementFile::from_data_file(fp).unwrap();
357 let mut test = valid.clone();
358 test.checksum = 0;
359 arr_eq_helper(&test, &valid);
360
361 let bytes = bincode::serialize(&test).unwrap();
362 let r = get_checksum_simple(&bytes);
363 assert!(r.is_ok());
364 let res = r.unwrap();
365 let s_attr_chk: u32 = bytes[16..bytes.len() - 2]
366 .iter()
367 .map(|x| *x as u32)
368 .sum::<u32>()
369 .rem_euclid(u16::MAX as u32 + 1);
370
371 let non_zero_bytes = bytes.iter().filter(|b| b > &&0).count();
372 let non_zero_sum = bytes
373 .iter()
374 .cloned()
375 .filter(|b| b > &0)
376 .map(|x| x as u32)
377 .sum::<u32>();
378
379 println!(
380 "l: {} r: {} non_zero_bytes {} sum total: {} s-attr {} diff {} (or {})",
381 res,
382 valid.checksum,
383 non_zero_bytes,
384 non_zero_sum,
385 s_attr_chk,
386 res.wrapping_sub(valid.checksum),
387 valid.checksum.wrapping_sub(res)
388 );
389 println!(
390 "checksum bytes: {:?} target bytes: {:?}",
391 [(res >> 8) as u8, res as u8],
392 [(valid.checksum >> 8) as u8, valid.checksum as u8]
393 );
394 assert_eq!(res, valid.checksum);
395 }
396
397 #[test]
398 fn blank_project_file() {
399 helper_test_chksum(&get_blank_proj_dirpath().join("arr01.work"));
400 }
401
402 #[test]
404 fn blank() {
405 helper_test_chksum(&get_arrange_dirpath().join("blank.work"));
406 }
407
408 #[test]
410 fn blank_diffname1() {
411 helper_test_chksum(&get_arrange_dirpath().join("blank-diffname1.work"));
412 }
413
414 #[test]
416 fn blank_diffname2() {
417 helper_test_chksum(&get_arrange_dirpath().join("blank-diffname2.work"));
418 }
419
420 #[test]
422 #[ignore]
423 fn one_rem_row_notext() {
424 helper_test_chksum(&get_arrange_dirpath().join("1-rem-blank-txt.work"));
425 }
426
427 #[test]
430 #[ignore]
431 fn one_rem_row_wtext() {
432 helper_test_chksum(&get_arrange_dirpath().join("1-rem-CHAIN-txt.work"));
433 }
434
435 #[test]
436 #[ignore]
437 fn two_rem_row_wtext() {
438 helper_test_chksum(&get_arrange_dirpath().join("2-rem-CHAIN-txt.work"));
439 }
440
441 #[test]
442 #[ignore]
443 fn four_patterns() {
444 helper_test_chksum(&get_arrange_dirpath().join("4-patterns.work"));
445 }
446
447 #[test]
448 #[ignore]
449 fn one_pattern() {
450 helper_test_chksum(&get_arrange_dirpath().join("1-pattern.work"));
451 }
452
453 #[test]
454 #[ignore]
455 fn one_halt() {
456 helper_test_chksum(&get_arrange_dirpath().join("1-halt.work"));
457 }
480
481 #[test]
482 #[ignore]
483 fn one_pattern_1_loop() {
484 helper_test_chksum(&get_arrange_dirpath().join("1-patt-1-loop.work"));
485 }
486
487 #[test]
488 #[ignore]
489 fn one_pattern_1_jump_1_loop() {
490 helper_test_chksum(&get_arrange_dirpath().join("1-patt-1-jump-1-loop.work"));
491 }
492
493 #[test]
494 #[ignore]
495 fn one_pattern_1_jump_1_loop_1_halt() {
496 helper_test_chksum(&get_arrange_dirpath().join("1-patt-1-jump-1-loop-1-halt.work"));
497 }
498
499 #[test]
500 #[ignore]
501 fn full_options() {
502 helper_test_chksum(&get_arrange_dirpath().join("full-options.work"));
503 }
504
505 #[test]
507 #[ignore]
508 fn full_options_no_rems() {
509 helper_test_chksum(&get_arrange_dirpath().join("full-options-no-rems.work"));
510 }
511
512 #[test]
513 fn no_saved_flag() {
514 helper_test_chksum(&get_arrange_dirpath().join("no-saved-flag.work"));
515 }
516
517 #[test]
518 fn with_saved_flag() {
519 helper_test_chksum(&get_arrange_dirpath().join("with-saved-flag.work"));
520 }
521
522 #[test]
523 fn blank_samename_saved() {
524 helper_test_chksum(&get_arrange_dirpath().join("blank-samename-saved-chktest.work"));
525 }
526
527 #[test]
528 fn blank_samename_unsaved() {
529 helper_test_chksum(&get_arrange_dirpath().join("blank-samename-unsaved-chktest.work"));
530 }
531 }
532}
533
534impl HasHeaderField for ArrangementFile {
535 fn check_header(&self) -> Result<bool, OtToolsIoError> {
536 Ok(self.header == ARRANGEMENT_FILE_HEADER)
537 }
538}
539
540#[cfg(test)]
541mod header_field {
542 use crate::{ArrangementFile, HasHeaderField, OtToolsIoError};
543 #[test]
544 fn valid() -> Result<(), OtToolsIoError> {
545 assert!(ArrangementFile::default().check_header()?);
546 Ok(())
547 }
548
549 #[test]
550 fn invalid() -> Result<(), OtToolsIoError> {
551 let mut mutated = ArrangementFile::default();
552 mutated.header[0] = 0x00;
553 mutated.header[1] = 200;
554 mutated.header[2] = 126;
555 assert!(!mutated.check_header()?);
556 Ok(())
557 }
558}
559
560impl HasFileVersionField for ArrangementFile {
561 fn check_file_version(&self) -> Result<bool, OtToolsIoError> {
562 Ok(self.datatype_version == ARRANGEMENT_FILE_VERSION)
563 }
564}
565
566#[cfg(test)]
567mod file_version_field {
568 use crate::{ArrangementFile, HasFileVersionField, OtToolsIoError};
569 #[test]
570 fn valid() -> Result<(), OtToolsIoError> {
571 assert!(ArrangementFile::default().check_file_version()?);
572 Ok(())
573 }
574
575 #[test]
576 fn invalid() -> Result<(), OtToolsIoError> {
577 let x = ArrangementFile {
578 datatype_version: 0,
579 ..Default::default()
580 };
581 assert!(!x.check_file_version()?);
582 Ok(())
583 }
584}
585
586#[derive(Debug, Eq, PartialEq, Clone)]
590pub struct ArrangementBlock {
591 pub name: [u8; 15], pub unknown_1: [u8; 2],
596
597 pub n_rows: u8,
604
605 pub rows: Box<Array<ArrangeRow, 256>>,
607}
608
609impl Default for ArrangementBlock {
610 fn default() -> Self {
611 Self {
612 name: ARRANGEMENT_DEFAULT_NAME,
613 unknown_1: from_fn(|_| 0),
614 n_rows: 0,
615 rows: ArrangeRow::defaults(),
616 }
617 }
618}
619
620impl IsDefault for ArrangementBlock {
621 fn is_default(&self) -> bool {
622 let default = &Self::default();
623
624 default.unknown_1 == self.unknown_1
630 && default.n_rows == self.n_rows
631 && default.rows == self.rows
632 }
633}
634
635#[derive(Debug, PartialEq, Eq, Clone, ArrayDefaults, BoxedBigArrayDefaults)]
637pub enum ArrangeRow {
638 PatternRow {
640 pattern_id: u8,
642 repetitions: u8,
644 mute_mask: u8,
646 tempo_1: u8,
649 tempo_2: u8,
652 scene_a: u8,
654 scene_b: u8,
656 offset: u8,
658 length: u8,
662 midi_transpose: [u8; 8],
666 },
667 LoopOrJumpOrHaltRow {
674 loop_count: u8,
676 row_target: u8,
678 },
679 ReminderRow(String),
681 EmptyRow(),
683}
684
685impl Default for ArrangeRow {
686 fn default() -> Self {
687 Self::EmptyRow()
688 }
689}