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)]
253mod checksum_field {
254 use crate::{ArrangementFile, HasChecksumField, OtToolsIoError};
255 #[test]
256 fn valid() -> Result<(), OtToolsIoError> {
257 let mut x = ArrangementFile::default();
258 x.checksum = x.calculate_checksum()?;
259 assert!(x.check_checksum()?);
260 Ok(())
261 }
262
263 #[test]
264 fn invalid() -> Result<(), OtToolsIoError> {
265 let mut x = ArrangementFile::default();
266 x.checksum = x.calculate_checksum()?;
267 x.checksum = u16::MAX;
269 assert!(!x.check_checksum()?);
270 Ok(())
271 }
272}
273
274impl HasHeaderField for ArrangementFile {
275 fn check_header(&self) -> Result<bool, OtToolsIoError> {
276 Ok(self.header == ARRANGEMENT_FILE_HEADER)
277 }
278}
279
280#[cfg(test)]
281mod header_field {
282 use crate::{ArrangementFile, HasHeaderField, OtToolsIoError};
283 #[test]
284 fn valid() -> Result<(), OtToolsIoError> {
285 assert!(ArrangementFile::default().check_header()?);
286 Ok(())
287 }
288
289 #[test]
290 fn invalid() -> Result<(), OtToolsIoError> {
291 let mut mutated = ArrangementFile::default();
292 mutated.header[0] = 0x00;
293 mutated.header[1] = 200;
294 mutated.header[2] = 126;
295 assert!(!mutated.check_header()?);
296 Ok(())
297 }
298}
299
300impl HasFileVersionField for ArrangementFile {
301 fn check_file_version(&self) -> Result<bool, OtToolsIoError> {
302 Ok(self.datatype_version == ARRANGEMENT_FILE_VERSION)
303 }
304}
305
306#[cfg(test)]
307mod file_version_field {
308 use crate::{ArrangementFile, HasFileVersionField, OtToolsIoError};
309 #[test]
310 fn valid() -> Result<(), OtToolsIoError> {
311 assert!(ArrangementFile::default().check_file_version()?);
312 Ok(())
313 }
314
315 #[test]
316 fn invalid() -> Result<(), OtToolsIoError> {
317 let x = ArrangementFile {
318 datatype_version: 0,
319 ..Default::default()
320 };
321 assert!(!x.check_file_version()?);
322 Ok(())
323 }
324}
325
326#[derive(Debug, Eq, PartialEq, Clone)]
330pub struct ArrangementBlock {
331 pub name: [u8; 15], pub unknown_1: [u8; 2],
336
337 pub n_rows: u8,
344
345 pub rows: Box<Array<ArrangeRow, 256>>,
347}
348
349impl Default for ArrangementBlock {
350 fn default() -> Self {
351 Self {
352 name: ARRANGEMENT_DEFAULT_NAME,
353 unknown_1: from_fn(|_| 0),
354 n_rows: 0,
355 rows: ArrangeRow::defaults(),
356 }
357 }
358}
359
360impl IsDefault for ArrangementBlock {
361 fn is_default(&self) -> bool {
362 let default = &Self::default();
363
364 default.unknown_1 == self.unknown_1
370 && default.n_rows == self.n_rows
371 && default.rows == self.rows
372 }
373}
374
375#[derive(Debug, PartialEq, Eq, Clone, ArrayDefaults, BoxedBigArrayDefaults)]
377pub enum ArrangeRow {
378 PatternRow {
380 pattern_id: u8,
382 repetitions: u8,
384 mute_mask: u8,
386 tempo_1: u8,
389 tempo_2: u8,
392 scene_a: u8,
394 scene_b: u8,
396 offset: u8,
398 length: u8,
402 midi_transpose: [u8; 8],
406 },
407 LoopOrJumpOrHaltRow {
414 loop_count: u8,
416 row_target: u8,
418 },
419 ReminderRow(String),
421 EmptyRow(),
423}
424
425impl Default for ArrangeRow {
426 fn default() -> Self {
427 Self::EmptyRow()
428 }
429}
430
431#[cfg(test)]
432mod checksum {
433 use crate::arrangements::ArrangementFile;
434 use crate::test_utils::get_arrange_dirpath;
435 use crate::{OctatrackFileIO, OtToolsIoError};
436 use std::path::Path;
437
438 fn arr_eq_helper(a: &ArrangementFile, b: &ArrangementFile) {
440 assert_eq!(a.header, b.header);
441 assert_eq!(a.datatype_version, b.datatype_version);
442 assert_eq!(a.unk1, b.unk1);
443 assert_eq!(a.unk2, b.unk2);
444 assert_eq!(a.arrangements_saved_state, b.arrangements_saved_state);
445 assert_eq!(a.arrangement_state_current, b.arrangement_state_current);
446 assert_eq!(a.arrangement_state_previous, b.arrangement_state_previous);
447 }
448
449 #[allow(dead_code)]
459 fn get_checksum_octarranger(bytes: &[u8]) -> Result<u16, OtToolsIoError> {
460 let bytes_no_header_no_chk = &bytes[16..bytes.len() - 2];
461
462 let mut chk: u16 = 0;
463 for byte_pair in &bytes_no_header_no_chk
464 .chunks(2)
465 .map(|x| x.to_vec())
466 .filter(|x| x.len() > 1)
467 .collect::<Vec<Vec<u8>>>()
468 {
469 let first_byte = byte_pair[0] as u16;
470 let second_byte = byte_pair[1] as u16;
471 chk = second_byte.wrapping_add(first_byte.wrapping_add(chk));
472 }
473
474 Ok(chk)
475 }
476
477 fn get_checksum_simple(bytes: &[u8]) -> Result<u16, OtToolsIoError> {
478 let bytes_no_header_no_chk = &bytes[16..bytes.len() - 2];
479
480 let mut prev;
481 let mut chk: u32 = 0;
482 for byte in bytes_no_header_no_chk {
483 prev = chk;
484 chk = chk.wrapping_add((*byte as u32).wrapping_add(0));
485 if byte != &0 {
486 println!("chk: {chk} diff: {}", chk - prev);
487 }
488 }
489 println!("CHK32: {chk}");
490 Ok((chk).wrapping_mul(1) as u16)
491 }
492
493 #[allow(dead_code)]
496 fn get_checksum_bank(bytes: &[u8]) -> Result<u16, OtToolsIoError> {
497 let bytes_no_header_no_chk = &bytes[16..bytes.len() - 2];
498 let default_bytes = &bincode::serialize(&ArrangementFile::default())?;
499 let def_important_bytes = &default_bytes[16..bytes.len() - 2];
500 let default_checksum: i32 = 1870;
501 let mut byte_diffs: i32 = 0;
502 for (byte, def_byte) in bytes_no_header_no_chk.iter().zip(def_important_bytes) {
503 let byte_diff = (*byte as i32) - (*def_byte as i32);
504 if byte_diff != 0 {
505 byte_diffs += byte_diff;
506 }
507 }
508 let check = byte_diffs * 256 + default_checksum;
509 let modded = check.rem_euclid(65535);
510 Ok(modded as u16)
511 }
512
513 fn helper_test_chksum(fp: &Path) {
514 let valid = ArrangementFile::from_data_file(fp).unwrap();
515 let mut test = valid.clone();
516 test.checksum = 0;
517 arr_eq_helper(&test, &valid);
518
519 let bytes = bincode::serialize(&test).unwrap();
520 let r = get_checksum_simple(&bytes);
521 assert!(r.is_ok());
522 let res = r.unwrap();
523 let s_attr_chk: u32 = bytes[16..bytes.len() - 2]
524 .iter()
525 .map(|x| *x as u32)
526 .sum::<u32>()
527 .rem_euclid(u16::MAX as u32 + 1);
528
529 let non_zero_bytes = bytes.iter().filter(|b| b > &&0).count();
530 let non_zero_sum = bytes
531 .iter()
532 .cloned()
533 .filter(|b| b > &0)
534 .map(|x| x as u32)
535 .sum::<u32>();
536
537 println!(
538 "l: {} r: {} non_zero_bytes {} sum total: {} s-attr {} diff {} (or {})",
539 res,
540 valid.checksum,
541 non_zero_bytes,
542 non_zero_sum,
543 s_attr_chk,
544 res.wrapping_sub(valid.checksum),
545 valid.checksum.wrapping_sub(res)
546 );
547 println!(
548 "checksum bytes: {:?} target bytes: {:?}",
549 [(res >> 8) as u8, res as u8],
550 [(valid.checksum >> 8) as u8, valid.checksum as u8]
551 );
552 assert_eq!(res, valid.checksum);
553 }
554
555 #[test]
557 fn blank() {
558 helper_test_chksum(&get_arrange_dirpath().join("blank.work"));
559 }
560
561 #[test]
563 fn blank_diffname1() {
564 helper_test_chksum(&get_arrange_dirpath().join("blank-diffname1.work"));
565 }
566
567 #[test]
569 fn blank_diffname2() {
570 helper_test_chksum(&get_arrange_dirpath().join("blank-diffname2.work"));
571 }
572
573 #[test]
575 #[ignore]
576 fn one_rem_row_notext() {
577 helper_test_chksum(&get_arrange_dirpath().join("1-rem-blank-txt.work"));
578 }
579
580 #[test]
583 #[ignore]
584 fn one_rem_row_wtext() {
585 helper_test_chksum(&get_arrange_dirpath().join("1-rem-CHAIN-txt.work"));
586 }
587
588 #[test]
589 #[ignore]
590 fn two_rem_row_wtext() {
591 helper_test_chksum(&get_arrange_dirpath().join("2-rem-CHAIN-txt.work"));
592 }
593
594 #[test]
595 #[ignore]
596 fn four_patterns() {
597 helper_test_chksum(&get_arrange_dirpath().join("4-patterns.work"));
598 }
599
600 #[test]
601 #[ignore]
602 fn one_pattern() {
603 helper_test_chksum(&get_arrange_dirpath().join("1-pattern.work"));
604 }
605
606 #[test]
607 #[ignore]
608 fn one_halt() {
609 helper_test_chksum(&get_arrange_dirpath().join("1-halt.work"));
610 }
633
634 #[test]
635 #[ignore]
636 fn one_pattern_1_loop() {
637 helper_test_chksum(&get_arrange_dirpath().join("1-patt-1-loop.work"));
638 }
639
640 #[test]
641 #[ignore]
642 fn one_pattern_1_jump_1_loop() {
643 helper_test_chksum(&get_arrange_dirpath().join("1-patt-1-jump-1-loop.work"));
644 }
645
646 #[test]
647 #[ignore]
648 fn one_pattern_1_jump_1_loop_1_halt() {
649 helper_test_chksum(&get_arrange_dirpath().join("1-patt-1-jump-1-loop-1-halt.work"));
650 }
651
652 #[test]
653 #[ignore]
654 fn full_options() {
655 helper_test_chksum(&get_arrange_dirpath().join("full-options.work"));
656 }
657
658 #[test]
660 #[ignore]
661 fn full_options_no_rems() {
662 helper_test_chksum(&get_arrange_dirpath().join("full-options-no-rems.work"));
663 }
664
665 #[test]
666 fn no_saved_flag() {
667 helper_test_chksum(&get_arrange_dirpath().join("no-saved-flag.work"));
668 }
669
670 #[test]
671 fn with_saved_flag() {
672 helper_test_chksum(&get_arrange_dirpath().join("with-saved-flag.work"));
673 }
674
675 #[test]
676 fn blank_samename_saved() {
677 helper_test_chksum(&get_arrange_dirpath().join("blank-samename-saved-chktest.work"));
678 }
679
680 #[test]
681 fn blank_samename_unsaved() {
682 helper_test_chksum(&get_arrange_dirpath().join("blank-samename-unsaved-chktest.work"));
683 }
684}