diff --git a/.vscode/agb.code-workspace b/.vscode/agb.code-workspace index 95eb27cb..f7b0fd40 100644 --- a/.vscode/agb.code-workspace +++ b/.vscode/agb.code-workspace @@ -32,6 +32,12 @@ }, { "path": "../book" + }, + { + "path": "../mgba-test-runner" + }, + { + "path": "../tools" } ] -} \ No newline at end of file +} diff --git a/CHANGELOG.md b/CHANGELOG.md index 20c9d4bb..4db58884 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,15 +11,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Support for using windows on the GBA. Windows are used to selectively enable rendering of certain layers or effects. - Support for the blend mode of the GBA. Blending allows for alpha blending between layers and fading to black and white. - Added a new agb::sync module that contains GBA-specific synchronization primitives. +- Added support for save files. +- Added implementation of `HashMap.retain()`. ### Changes - Many of the places that originally disabled IRQs now use the `sync` module, reducing the chance of missed interrupts. +- HashMap iterators now implement `size_hint` which should result in slightly better generation of code using those iterators. ### Fixed - Fixed the fast magnitude function in agb_fixnum. This is also used in fast_normalise. Previously only worked for positive (x, y). - Fixed formatting of fixed point numbers in the range (-1, 0), which previously appeared positive. -## Changed +### Changed - `testing` is now a default feature, so you no longer need to add a separate `dev-dependencies` line for `agb` in order to enable unit tests for your project. ## [0.11.1] - 2022/08/02 diff --git a/README.md b/README.md index df80be43..6da2f92a 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,11 @@ ## Rust for the Game Boy Advance +[![Docs](https://docs.rs/agb/badge.svg)](https://docs.rs/agb/latest/agb) +[![Build](https://github.com/agbrs/agb/actions/workflows/build-and-test.yml/badge.svg?branch=master)](https://github.com/agbrs/agb/actions/workflows/build-and-test.yml) +[![Licence](https://img.shields.io/crates/l/agb)](https://www.mozilla.org/en-US/MPL/2.0/) +[![Crates.io](https://img.shields.io/crates/v/agb)](https://crates.io/crates/agb) + ![AGB logo](.github/logo.png) This is a library for making games on the Game Boy Advance using the Rust diff --git a/agb/build.rs b/agb/build.rs index 2bb2f4bb..d4245b4a 100644 --- a/agb/build.rs +++ b/agb/build.rs @@ -7,6 +7,7 @@ fn main() { "src/sound/mixer/mixer.s", "src/agbabi/memset.s", "src/agbabi/memcpy.s", + "src/save/asm_routines.s", ]; println!("cargo:rerun-if-changed=gba.ld"); diff --git a/agb/src/agb_alloc/bump_allocator.rs b/agb/src/agb_alloc/bump_allocator.rs index 717ba45e..0ea27d2d 100644 --- a/agb/src/agb_alloc/bump_allocator.rs +++ b/agb/src/agb_alloc/bump_allocator.rs @@ -43,7 +43,7 @@ impl BumpAllocator { let resulting_ptr = ptr + amount_to_add; let new_current_ptr = resulting_ptr + layout.size(); - if new_current_ptr as usize >= (self.start_end.borrow(cs).end)() { + if new_current_ptr >= (self.start_end.borrow(cs).end)() { return None; } diff --git a/agb/src/asm_include.s b/agb/src/asm_include.s index 5508e4fe..1682792c 100644 --- a/agb/src/asm_include.s +++ b/agb/src/asm_include.s @@ -13,3 +13,19 @@ .size \functionName,.-\functionName .endfunc .endm + +.macro agb_thumb_func functionName:req +.section .iwram.\functionName, "ax", %progbits +.thumb +.align 2 +.global \functionName +.type \functionName, %function +.func \functionName +\functionName: +.endm + +.macro agb_thumb_end functionName:req +.pool +.size \functionName,.-\functionName +.endfunc +.endm diff --git a/agb/src/display/example_logo.rs b/agb/src/display/example_logo.rs index b21df025..fa4397d8 100644 --- a/agb/src/display/example_logo.rs +++ b/agb/src/display/example_logo.rs @@ -37,5 +37,8 @@ mod tests { display_logo(&mut map, &mut vram); crate::test_runner::assert_image_output("gfx/test_logo.png"); + + map.clear(&mut vram); + vram.gc(); } } diff --git a/agb/src/display/object.rs b/agb/src/display/object.rs index 4d76b9a5..fba6bd04 100644 --- a/agb/src/display/object.rs +++ b/agb/src/display/object.rs @@ -794,7 +794,7 @@ impl ObjectController { }); let loan = Loan { - index: index as u8, + index, phantom: PhantomData, }; @@ -916,8 +916,8 @@ impl<'a> Object<'a> { /// [ObjectController::commit] is called. pub fn set_x(&mut self, x: u16) -> &mut Self { let object_inner = unsafe { self.object_inner() }; - object_inner.attrs.a1a.set_x(x.rem_euclid(1 << 9) as u16); - object_inner.attrs.a1s.set_x(x.rem_euclid(1 << 9) as u16); + object_inner.attrs.a1a.set_x(x.rem_euclid(1 << 9)); + object_inner.attrs.a1s.set_x(x.rem_euclid(1 << 9)); self } diff --git a/agb/src/dma.rs b/agb/src/dma.rs index 71651cfc..1bf1b0bd 100644 --- a/agb/src/dma.rs +++ b/agb/src/dma.rs @@ -24,3 +24,29 @@ pub(crate) unsafe fn dma_copy16(src: *const u16, dest: *mut u16, count: usize) { DMA3_CONTROL.set(count as u32 | (1 << 31)); } + +pub(crate) fn dma3_exclusive(f: impl FnOnce() -> R) -> R { + const DMA0_CTRL_HI: MemoryMapped = unsafe { MemoryMapped::new(dma_control_addr(0) + 2) }; + const DMA1_CTRL_HI: MemoryMapped = unsafe { MemoryMapped::new(dma_control_addr(1) + 2) }; + const DMA2_CTRL_HI: MemoryMapped = unsafe { MemoryMapped::new(dma_control_addr(2) + 2) }; + + crate::interrupt::free(|_| { + let dma0_ctl = DMA0_CTRL_HI.get(); + let dma1_ctl = DMA1_CTRL_HI.get(); + let dma2_ctl = DMA2_CTRL_HI.get(); + DMA0_CTRL_HI.set(dma0_ctl & !(1 << 15)); + DMA1_CTRL_HI.set(dma1_ctl & !(1 << 15)); + DMA2_CTRL_HI.set(dma2_ctl & !(1 << 15)); + + // Executes the body of the function with DMAs and IRQs disabled. + let ret = f(); + + // Continues higher priority DMAs if they were enabled before. + DMA0_CTRL_HI.set(dma0_ctl); + DMA1_CTRL_HI.set(dma1_ctl); + DMA2_CTRL_HI.set(dma2_ctl); + + // returns the return value + ret + }) +} \ No newline at end of file diff --git a/agb/src/hash_map.rs b/agb/src/hash_map.rs index 45939093..d537b4cf 100644 --- a/agb/src/hash_map.rs +++ b/agb/src/hash_map.rs @@ -188,7 +188,11 @@ impl HashMap { /// An iterator visiting all key-value pairs in an arbitrary order pub fn iter(&self) -> impl Iterator { - self.nodes.nodes.iter().filter_map(Node::key_value_ref) + Iter { + map: self, + at: 0, + num_found: 0, + } } /// An iterator visiting all key-value pairs in an arbitrary order, with mutable references to the values @@ -196,6 +200,14 @@ impl HashMap { self.nodes.nodes.iter_mut().filter_map(Node::key_value_mut) } + /// Retains only the elements specified by the predicate `f`. + pub fn retain(&mut self, f: F) + where + F: FnMut(&K, &mut V) -> bool, + { + self.nodes.retain(f); + } + /// Returns `true` if the map contains no elements #[must_use] pub fn is_empty(&self) -> bool { @@ -332,6 +344,7 @@ where pub struct Iter<'a, K: 'a, V: 'a, ALLOCATOR: ClonableAllocator> { map: &'a HashMap, at: usize, + num_found: usize, } impl<'a, K, V, ALLOCATOR: ClonableAllocator> Iterator for Iter<'a, K, V, ALLOCATOR> { @@ -347,10 +360,18 @@ impl<'a, K, V, ALLOCATOR: ClonableAllocator> Iterator for Iter<'a, K, V, ALLOCAT self.at += 1; if node.has_value() { + self.num_found += 1; return Some((node.key_ref().unwrap(), node.value_ref().unwrap())); } } } + + fn size_hint(&self) -> (usize, Option) { + ( + self.map.len() - self.num_found, + Some(self.map.len() - self.num_found), + ) + } } impl<'a, K, V, ALLOCATOR: ClonableAllocator> IntoIterator for &'a HashMap { @@ -358,7 +379,11 @@ impl<'a, K, V, ALLOCATOR: ClonableAllocator> IntoIterator for &'a HashMap; fn into_iter(self) -> Self::IntoIter { - Iter { map: self, at: 0 } + Iter { + map: self, + at: 0, + num_found: 0, + } } } @@ -369,6 +394,7 @@ impl<'a, K, V, ALLOCATOR: ClonableAllocator> IntoIterator for &'a HashMap { map: HashMap, at: usize, + num_found: usize, } impl Iterator for IterOwned { @@ -384,10 +410,18 @@ impl Iterator for IterOwned self.at += 1; if let Some((k, v, _)) = maybe_kv { + self.num_found += 1; return Some((k, v)); } } } + + fn size_hint(&self) -> (usize, Option) { + ( + self.map.len() - self.num_found, + Some(self.map.len() - self.num_found), + ) + } } /// An iterator over entries of a [`HashMap`] @@ -399,7 +433,11 @@ impl IntoIterator for HashMap; fn into_iter(self) -> Self::IntoIter { - IterOwned { map: self, at: 0 } + IterOwned { + map: self, + at: 0, + num_found: 0, + } } } @@ -729,6 +767,31 @@ impl NodeStorage { inserted_location } + fn retain(&mut self, mut f: F) + where + F: FnMut(&K, &mut V) -> bool, + { + let num_nodes = self.nodes.len(); + let mut i = 0; + + while i < num_nodes { + let node = &mut self.nodes[i]; + + if let Some((k, v)) = node.key_value_mut() { + if !f(k, v) { + self.remove_from_location(i); + + // Need to continue before adding 1 to i because remove from location could + // put the element which was next into the ith location in the nodes array, + // so we need to check if that one needs removing too. + continue; + } + } + + i += 1; + } + } + fn remove_from_location(&mut self, location: usize) -> V { let mut current_location = location; self.number_of_items -= 1; @@ -1222,6 +1285,54 @@ mod test { drop_registry.assert_dropped_n_times(id1, 2); } + #[test_case] + fn test_retain(_gba: &mut Gba) { + let mut map = HashMap::new(); + + for i in 0..100 { + map.insert(i, i); + } + + map.retain(|k, _| k % 2 == 0); + + assert_eq!(map[&2], 2); + assert_eq!(map.get(&3), None); + + assert_eq!(map.iter().count(), 50); // force full iteration + } + + #[test_case] + fn test_size_hint_iter(_gba: &mut Gba) { + let mut map = HashMap::new(); + + for i in 0..100 { + map.insert(i, i); + } + + let mut iter = map.iter(); + assert_eq!(iter.size_hint(), (100, Some(100))); + + iter.next(); + + assert_eq!(iter.size_hint(), (99, Some(99))); + } + + #[test_case] + fn test_size_hint_into_iter(_gba: &mut Gba) { + let mut map = HashMap::new(); + + for i in 0..100 { + map.insert(i, i); + } + + let mut iter = map.into_iter(); + assert_eq!(iter.size_hint(), (100, Some(100))); + + iter.next(); + + assert_eq!(iter.size_hint(), (99, Some(99))); + } + // Following test cases copied from the rust source // https://github.com/rust-lang/rust/blob/master/library/std/src/collections/hash/map/tests.rs mod rust_std_tests { diff --git a/agb/src/lib.rs b/agb/src/lib.rs index 8614c49f..d8f221b1 100644 --- a/agb/src/lib.rs +++ b/agb/src/lib.rs @@ -168,6 +168,7 @@ pub use agb_fixnum as fixnum; pub mod hash_map; /// Simple random number generator pub mod rng; +pub mod save; mod single; /// Implements sound output. pub mod sound; @@ -223,6 +224,8 @@ pub struct Gba { pub sound: sound::dmg::Sound, /// Manages access to the Game Boy Advance's direct sound mixer for playing raw wav files. pub mixer: sound::mixer::MixerController, + /// Manages access to the Game Boy Advance cartridge's save chip. + pub save: save::SaveManager, /// Manages access to the Game Boy Advance's 4 timers. pub timers: timer::TimerController, } @@ -239,6 +242,7 @@ impl Gba { display: display::Display::new(), sound: sound::dmg::Sound::new(), mixer: sound::mixer::MixerController::new(), + save: save::SaveManager::new(), timers: timer::TimerController::new(), } } diff --git a/agb/src/save/asm_routines.s b/agb/src/save/asm_routines.s new file mode 100644 index 00000000..f257a524 --- /dev/null +++ b/agb/src/save/asm_routines.s @@ -0,0 +1,49 @@ +.include "src/asm_include.s" + +@ +@ char WramReadByte(const char* offset); +@ +@ A routine that reads a byte from a given memory offset. +@ +agb_thumb_func agb_rs__WramReadByte + ldrb r0, [r0] + bx lr +agb_thumb_end agb_rs__WramReadByte + +@ +@ bool WramVerifyBuf(const char* buf1, const char* buf2, int count); +@ +@ A routine that compares two memory offsets. +@ +agb_thumb_func agb_rs__WramVerifyBuf + push {r4-r5, lr} + movs r5, r0 @ set up r5 to be r0, so we can use it immediately for the return result + movs r0, #0 @ set up r0 so the default return result is false + + @ At this point, buf1 is actually in r5, so r0 can be used as a status return +1: ldrb r3, [r5,r2] + ldrb r4, [r1,r2] + cmp r3, r4 + bne 0f + sub r2, #1 + bpl 1b + + @ Returns from the function successfully + movs r0, #1 +0: @ Jumps to here return the function unsuccessfully, because r0 contains 0 at this point + pop {r4-r5, pc} +agb_thumb_end agb_rs__WramVerifyBuf + + +@ +@ void WramXferBuf(const char* source, char* dest, int count); +@ +@ A routine that copies one buffer into another. +@ +agb_thumb_func agb_rs__WramXferBuf +0: sub r2, #1 + ldrb r3, [r0,r2] + strb r3, [r1,r2] + bne 0b + bx lr +agb_thumb_end agb_rs__WramXferBuf diff --git a/agb/src/save/asm_utils.rs b/agb/src/save/asm_utils.rs new file mode 100644 index 00000000..577d99f0 --- /dev/null +++ b/agb/src/save/asm_utils.rs @@ -0,0 +1,63 @@ +//! A module containing low-level assembly functions that can be loaded into +//! WRAM. Both flash media and battery-backed SRAM require reads to be +//! performed via code in WRAM and cannot be accessed by DMA. + +extern "C" { + fn agb_rs__WramXferBuf(src: *const u8, dst: *mut u8, count: usize); + fn agb_rs__WramReadByte(src: *const u8) -> u8; + fn agb_rs__WramVerifyBuf(buf1: *const u8, buf2: *const u8, count: usize) -> bool; +} + +/// Copies data from a given memory address into a buffer. +/// +/// This should be used to access any data found in flash or battery-backed +/// SRAM, as you must read those one byte at a time and from code stored +/// in WRAM. +/// +/// This uses raw addresses into the memory space. Use with care. +#[inline(always)] +pub unsafe fn read_raw_buf(dst: &mut [u8], src: usize) { + if !dst.is_empty() { + agb_rs__WramXferBuf(src as _, dst.as_mut_ptr(), dst.len()); + } +} + +/// Copies data from a buffer into a given memory address. +/// +/// This is not strictly needed to write into save media, but reuses the +/// optimized loop used in `read_raw_buf`, and will often be faster. +/// +/// This uses raw addresses into the memory space. Use with care. +#[inline(always)] +pub unsafe fn write_raw_buf(dst: usize, src: &[u8]) { + if !src.is_empty() { + agb_rs__WramXferBuf(src.as_ptr(), dst as _, src.len()); + } +} + +/// Verifies that the data in a buffer matches that in a given memory address. +/// +/// This should be used to access any data found in flash or battery-backed +/// SRAM, as you must read those one byte at a time and from code stored +/// in WRAM. +/// +/// This uses raw addresses into the memory space. Use with care. +#[inline(always)] +pub unsafe fn verify_raw_buf(buf1: &[u8], buf2: usize) -> bool { + if !buf1.is_empty() { + agb_rs__WramVerifyBuf(buf1.as_ptr(), buf2 as _, buf1.len() - 1) + } else { + true + } +} + +/// Reads a byte from a given memory address. +/// +/// This should be used to access any data found in flash or battery-backed +/// SRAM, as you must read those from code found in WRAM. +/// +/// This uses raw addresses into the memory space. Use with care. +#[inline(always)] +pub unsafe fn read_raw_byte(src: usize) -> u8 { + agb_rs__WramReadByte(src as _) +} diff --git a/agb/src/save/eeprom.rs b/agb/src/save/eeprom.rs new file mode 100644 index 00000000..d2f7258a --- /dev/null +++ b/agb/src/save/eeprom.rs @@ -0,0 +1,273 @@ +//! A module containing support for EEPROM. +//! +//! EEPROM requires using DMA to issue commands for both reading and writing. + +use crate::memory_mapped::MemoryMapped; +use crate::save::{Error, MediaInfo, MediaType, RawSaveAccess}; +use crate::save::utils::Timeout; +use core::cmp; + +const PORT: MemoryMapped = unsafe { MemoryMapped::new(0x0DFFFF00) }; +const SECTOR_SHIFT: usize = 3; +const SECTOR_LEN: usize = 1 << SECTOR_SHIFT; +const SECTOR_MASK: usize = SECTOR_LEN - 1; + +/// Sends a DMA command to EEPROM. +fn dma_send(source: &[u32], ct: usize) { + crate::dma::dma3_exclusive(|| unsafe { + core::sync::atomic::compiler_fence(core::sync::atomic::Ordering::SeqCst); + crate::dma::dma_copy16(source.as_ptr() as *mut u16, 0x0DFFFF00 as *mut u16, ct); + }); +} + +/// Receives a DMA packet from EEPROM. +fn dma_receive(source: &mut [u32], ct: usize) { + crate::dma::dma3_exclusive(|| unsafe { + crate::dma::dma_copy16(0x0DFFFF00 as *mut u16, source.as_ptr() as *mut u16, ct); + core::sync::atomic::compiler_fence(core::sync::atomic::Ordering::SeqCst); + }); +} + +/// Union type to help build/receive commands. +struct BufferData { + idx: usize, + data: BufferContents, +} +#[repr(align(4))] +union BufferContents { + uninit: (), + bits: [u16; 82], + words: [u32; 41], +} +impl BufferData { + fn new() -> Self { + BufferData { idx: 0, data: BufferContents { uninit: () } } + } + + /// Writes a bit to the output buffer. + fn write_bit(&mut self, val: u8) { + unsafe { + self.data.bits[self.idx] = val as u16; + self.idx += 1; + } + } + + /// Writes a number to the output buffer + fn write_num(&mut self, count: usize, num: u32) { + for i in 0..count { + self.write_bit(((num >> (count - 1 - i)) & 1) as u8); + } + } + + /// Reads a number from the input buffer. + fn read_num(&mut self, off: usize, count: usize) -> u32 { + let mut accum = 0; + unsafe { + for i in 0..count { + accum <<= 1; + accum |= self.data.bits[off + i] as u32; + } + } + accum + } + + /// Receives a number of words into the input buffer. + fn receive(&mut self, count: usize) { + unsafe { + dma_receive(&mut self.data.words, count); + } + } + + /// Submits the current buffer via DMA. + fn submit(&self) { + unsafe { + dma_send(&self.data.words, self.idx); + } + } +} + +/// The properties of a given EEPROM type. +struct EepromProperties { + addr_bits: usize, + byte_len: usize, +} +impl EepromProperties { + /// Reads a block from the save media. + #[allow(clippy::needless_range_loop)] + fn read_sector(&self, word: usize) -> [u8; 8] { + // Set address command. The command is two one bits, followed by the + // address, followed by a zero bit. + // + // 512B Command: [1 1|n n n n n n|0] + // 8KiB Command: [1 1|n n n n n n n n n n n n n n|0] + let mut buf = BufferData::new(); + buf.write_bit(1); + buf.write_bit(1); + buf.write_num(self.addr_bits, word as u32); + buf.write_bit(0); + buf.submit(); + + // Receive the buffer data. The EEPROM sends 3 irrelevant bits followed + // by 64 data bits. + buf.receive(68); + let mut out = [0; 8]; + for i in 0..8 { + out[i] = buf.read_num(4 + i * 8, 8) as u8; + } + out + } + + /// Writes a sector directly. + #[allow(clippy::needless_range_loop)] + fn write_sector_raw( + &self, word: usize, block: &[u8], timeout: &mut Timeout, + ) -> Result<(), Error> { + // Write sector command. The command is a one bit, followed by a + // zero bit, followed by the address, followed by 64 bits of data. + // + // 512B Command: [1 0|n n n n n n|v v v v ...] + // 8KiB Command: [1 0|n n n n n n n n n n n n n n|v v v v ...] + let mut buf = BufferData::new(); + buf.write_bit(1); + buf.write_bit(0); + buf.write_num(self.addr_bits, word as u32); + for i in 0..8 { + buf.write_num(8, block[i] as u32); + } + buf.write_bit(0); + buf.submit(); + + // Wait for the sector to be written for 10 milliseconds. + timeout.start(); + while PORT.get() & 1 != 1 { + if timeout.check_timeout_met(10) { + return Err(Error::OperationTimedOut); + } + } + Ok(()) + } + + /// Writes a sector to the EEPROM, keeping any current contents outside the + /// buffer's range. + fn write_sector_safe( + &self, word: usize, data: &[u8], start: usize, timeout: &mut Timeout, + ) -> Result<(), Error> { + let mut buf = self.read_sector(word); + buf[start..start + data.len()].copy_from_slice(data); + self.write_sector_raw(word, &buf, timeout) + } + + /// Writes a sector to the EEPROM. + fn write_sector( + &self, word: usize, data: &[u8], start: usize, timeout: &mut Timeout, + ) -> Result<(), Error> { + if data.len() == 8 && start == 0 { + self.write_sector_raw(word, data, timeout) + } else { + self.write_sector_safe(word, data, start, timeout) + } + } + + /// Checks whether an offset is in range. + fn check_offset(&self, offset: usize, len: usize) -> Result<(), Error> { + if offset.checked_add(len).is_none() && (offset + len) > self.byte_len { + Err(Error::OutOfBounds) + } else { + Ok(()) + } + } + + /// Implements EEPROM reads. + fn read(&self, mut offset: usize, mut buf: &mut [u8]) -> Result<(), Error> { + self.check_offset(offset, buf.len())?; + while !buf.is_empty() { + let start = offset & SECTOR_MASK; + let end_len = cmp::min(SECTOR_LEN - start, buf.len()); + let sector = self.read_sector(offset >> SECTOR_SHIFT); + buf[..end_len].copy_from_slice(§or[start..start + end_len]); + buf = &mut buf[end_len..]; + offset += end_len; + } + Ok(()) + } + + /// Implements EEPROM verifies. + fn verify(&self, mut offset: usize, mut buf: &[u8]) -> Result { + self.check_offset(offset, buf.len())?; + while !buf.is_empty() { + let start = offset & SECTOR_MASK; + let end_len = cmp::min(SECTOR_LEN - start, buf.len()); + if buf[..end_len] != self.read_sector(offset >> SECTOR_SHIFT) { + return Ok(false); + } + buf = &buf[end_len..]; + offset += end_len; + } + Ok(true) + } + + /// Implements EEPROM writes. + fn write(&self, mut offset: usize, mut buf: &[u8], timeout: &mut Timeout) -> Result<(), Error> { + self.check_offset(offset, buf.len())?; + while !buf.is_empty() { + let start = offset & SECTOR_MASK; + let end_len = cmp::min(SECTOR_LEN - start, buf.len()); + self.write_sector(offset >> SECTOR_SHIFT, &buf[..end_len], start, timeout)?; + buf = &buf[end_len..]; + offset += end_len; + } + Ok(()) + } +} +const PROPS_512B: EepromProperties = EepromProperties { addr_bits: 6, byte_len: 512 }; +const PROPS_8K: EepromProperties = EepromProperties { addr_bits: 14, byte_len: 8 * 1024 }; + +/// The [`RawSaveAccess`] used for 512 byte EEPROM. +pub struct Eeprom512B; +impl RawSaveAccess for Eeprom512B { + fn info(&self) -> Result<&'static MediaInfo, Error> { + Ok(&MediaInfo { + media_type: MediaType::Eeprom512B, + sector_shift: 3, + sector_count: 64, + uses_prepare_write: false, + }) + } + fn read(&self, offset: usize, buffer: &mut [u8], _: &mut Timeout) -> Result<(), Error> { + PROPS_512B.read(offset, buffer) + } + fn verify(&self, offset: usize, buffer: &[u8], _: &mut Timeout) -> Result { + PROPS_512B.verify(offset, buffer) + } + fn prepare_write(&self, _: usize, _: usize, _: &mut Timeout) -> Result<(), Error> { + Ok(()) + } + fn write(&self, offset: usize, buffer: &[u8], timeout: &mut Timeout) -> Result<(), Error> { + PROPS_512B.write(offset, buffer, timeout) + } +} + +/// The [`RawSaveAccess`] used for 8 KiB EEPROM. +pub struct Eeprom8K; +impl RawSaveAccess for Eeprom8K { + fn info(&self) -> Result<&'static MediaInfo, Error> { + Ok(&MediaInfo { + media_type: MediaType::Eeprom8K, + sector_shift: 3, + sector_count: 1024, + uses_prepare_write: false, + }) + } + fn read(&self, offset: usize, buffer: &mut [u8], _: &mut Timeout) -> Result<(), Error> { + PROPS_8K.read(offset, buffer) + } + fn verify(&self, offset: usize, buffer: &[u8], _: &mut Timeout) -> Result { + PROPS_8K.verify(offset, buffer) + } + fn prepare_write(&self, _: usize, _: usize, _: &mut Timeout) -> Result<(), Error> { + Ok(()) + } + fn write(&self, offset: usize, buffer: &[u8], timeout: &mut Timeout) -> Result<(), Error> { + PROPS_8K.write(offset, buffer, timeout) + } +} diff --git a/agb/src/save/flash.rs b/agb/src/save/flash.rs new file mode 100644 index 00000000..28b21e50 --- /dev/null +++ b/agb/src/save/flash.rs @@ -0,0 +1,472 @@ +//! Module for flash save media support. +//! +//! Flash may be read with ordinary read commands, but writing requires +//! sending structured commands to the flash chip. + +// TODO: Setup cartridge read timings for faster Flash access. + +use crate::memory_mapped::{MemoryMapped, MemoryMapped1DArray}; +use crate::save::{Error, MediaInfo, MediaType, RawSaveAccess}; +use crate::save::asm_utils::*; +use crate::sync::{InitOnce, Static}; +use core::cmp; +use crate::save::utils::Timeout; + +// Volatile address ports for flash +const FLASH_PORT_BANK: MemoryMapped = unsafe { MemoryMapped::new(0x0E000000) }; +const FLASH_PORT_A: MemoryMapped = unsafe { MemoryMapped::new(0x0E005555) }; +const FLASH_PORT_B: MemoryMapped = unsafe { MemoryMapped::new(0x0E002AAA) }; +const FLASH_DATA: MemoryMapped1DArray = unsafe { MemoryMapped1DArray::new(0x0E000000) }; + +// Various constants related to sector sizes +const BANK_SHIFT: usize = 16; // 64 KiB +const BANK_LEN: usize = 1 << BANK_SHIFT; +const BANK_MASK: usize = BANK_LEN - 1; + +// Constants relating to flash commands. +const CMD_SET_BANK: u8 = 0xB0; +const CMD_READ_CHIP_ID: u8 = 0x90; +const CMD_READ_CONTENTS: u8 = 0xF0; +const CMD_WRITE: u8 = 0xA0; +const CMD_ERASE_SECTOR_BEGIN: u8 = 0x80; +const CMD_ERASE_SECTOR_CONFIRM: u8 = 0x30; +const CMD_ERASE_SECTOR_ALL: u8 = 0x10; + +/// Starts a command to the flash chip. +fn start_flash_command() { + FLASH_PORT_A.set(0xAA); + FLASH_PORT_B.set(0x55); +} + +/// Helper function for issuing commands to the flash chip. +fn issue_flash_command(c2: u8) { + start_flash_command(); + FLASH_PORT_A.set(c2); +} + +/// A simple thing to avoid excessive bank switches +static CURRENT_BANK: Static = Static::new(!0); +fn set_bank(bank: u8) -> Result<(), Error> { + if bank == 0xFF { + Err(Error::OutOfBounds) + } else if bank != CURRENT_BANK.read() { + issue_flash_command(CMD_SET_BANK); + FLASH_PORT_BANK.set(bank); + CURRENT_BANK.write(bank); + Ok(()) + } else { + Ok(()) + } +} + +/// Identifies a particular f +/// lash chip in use by a Game Pak. +#[derive(Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Debug)] +#[repr(u8)] +pub enum FlashChipType { + /// 64KiB SST chip + Sst64K, + /// 64KiB Macronix chip + Macronix64K, + /// 64KiB Panasonic chip + Panasonic64K, + /// 64KiB Atmel chip + Atmel64K, + /// 128KiB Sanyo chip + Sanyo128K, + /// 128KiB Macronix chip + Macronix128K, + /// An unidentified chip + Unknown, +} +impl FlashChipType { + /// Returns the type of the flash chip currently in use. + pub fn detect() -> Result { + Ok(Self::from_id(detect_chip_id()?)) + } + + /// Determines the flash chip type from an ID. + pub fn from_id(id: u16) -> Self { + match id { + 0xD4BF => FlashChipType::Sst64K, + 0x1CC2 => FlashChipType::Macronix64K, + 0x1B32 => FlashChipType::Panasonic64K, + 0x3D1F => FlashChipType::Atmel64K, + 0x1362 => FlashChipType::Sanyo128K, + 0x09C2 => FlashChipType::Macronix128K, + _ => FlashChipType::Unknown, + } + } +} + +/// Determines the raw ID of the flash chip currently in use. +pub fn detect_chip_id() -> Result { + issue_flash_command(CMD_READ_CHIP_ID); + let high = unsafe { read_raw_byte(0x0E000001) }; + let low = unsafe { read_raw_byte(0x0E000000) }; + let id = (high as u16) << 8 | low as u16; + issue_flash_command(CMD_READ_CONTENTS); + Ok(id) +} + +/// Information relating to a particular flash chip that could be found in a +/// Game Pak. +#[allow(dead_code)] +struct ChipInfo { + /// The wait state required to read from the chip. + read_wait: u8, + /// The wait state required to write to the chip. + write_wait: u8, + + /// The timeout in milliseconds for writes to this chip. + write_timeout: u16, + /// The timeout in milliseconds for erasing a sector in this chip. + erase_sector_timeout: u16, + /// The timeout in milliseconds for erasing the entire chip. + erase_chip_timeout: u16, + + /// The number of 64KiB banks in this chip. + bank_count: u8, + /// Whether this is an Atmel chip, which has 128 byte sectors instead of 4K. + uses_atmel_api: bool, + /// Whether this is an Macronix chip, which requires an additional command + /// to cancel the current action after a timeout. + requires_cancel_command: bool, + + /// The [`MediaInfo`] to return for this chip type. + info: &'static MediaInfo, +} + +// Media info for the various chipsets. +static INFO_64K: MediaInfo = MediaInfo { + media_type: MediaType::Flash64K, + sector_shift: 12, // 4 KiB + sector_count: 16, // 4 KiB * 16 = 64 KiB + uses_prepare_write: true, +}; +static INFO_64K_ATMEL: MediaInfo = MediaInfo { + media_type: MediaType::Flash64K, + sector_shift: 7, // 128 bytes + sector_count: 512, // 128 bytes * 512 = 64 KiB + uses_prepare_write: false, +}; +static INFO_128K: MediaInfo = MediaInfo { + media_type: MediaType::Flash128K, + sector_shift: 12, + sector_count: 32, // 4 KiB * 32 = 128 KiB + uses_prepare_write: true, +}; + +// Chip info for the various chipsets. +static CHIP_INFO_SST_64K: ChipInfo = ChipInfo { + read_wait: 2, // 2 cycles + write_wait: 1, // 3 cycles + write_timeout: 10, + erase_sector_timeout: 40, + erase_chip_timeout: 200, + bank_count: 1, + uses_atmel_api: false, + requires_cancel_command: false, + info: &INFO_64K, +}; +static CHIP_INFO_MACRONIX_64K: ChipInfo = ChipInfo { + read_wait: 1, // 3 cycles + write_wait: 3, // 8 cycles + write_timeout: 10, + erase_sector_timeout: 2000, + erase_chip_timeout: 2000, + bank_count: 1, + uses_atmel_api: false, + requires_cancel_command: true, + info: &INFO_64K, +}; +static CHIP_INFO_PANASONIC_64K: ChipInfo = ChipInfo { + read_wait: 2, // 2 cycles + write_wait: 0, // 4 cycles + write_timeout: 10, + erase_sector_timeout: 500, + erase_chip_timeout: 500, + bank_count: 1, + uses_atmel_api: false, + requires_cancel_command: false, + info: &INFO_64K, +}; +static CHIP_INFO_ATMEL_64K: ChipInfo = ChipInfo { + read_wait: 3, // 8 cycles + write_wait: 3, // 8 cycles + write_timeout: 40, + erase_sector_timeout: 40, + erase_chip_timeout: 40, + bank_count: 1, + uses_atmel_api: true, + requires_cancel_command: false, + info: &INFO_64K_ATMEL, +}; +static CHIP_INFO_GENERIC_64K: ChipInfo = ChipInfo { + read_wait: 3, // 8 cycles + write_wait: 3, // 8 cycles + write_timeout: 40, + erase_sector_timeout: 2000, + erase_chip_timeout: 2000, + bank_count: 1, + uses_atmel_api: false, + requires_cancel_command: true, + info: &INFO_128K, +}; +static CHIP_INFO_GENERIC_128K: ChipInfo = ChipInfo { + read_wait: 1, // 3 cycles + write_wait: 3, // 8 cycles + write_timeout: 10, + erase_sector_timeout: 2000, + erase_chip_timeout: 2000, + bank_count: 2, + uses_atmel_api: false, + requires_cancel_command: false, + info: &INFO_128K, +}; + +impl FlashChipType { + /// Returns the internal info for this chip. + fn chip_info(self) -> &'static ChipInfo { + match self { + FlashChipType::Sst64K => &CHIP_INFO_SST_64K, + FlashChipType::Macronix64K => &CHIP_INFO_MACRONIX_64K, + FlashChipType::Panasonic64K => &CHIP_INFO_PANASONIC_64K, + FlashChipType::Atmel64K => &CHIP_INFO_ATMEL_64K, + FlashChipType::Sanyo128K => &CHIP_INFO_GENERIC_128K, + FlashChipType::Macronix128K => &CHIP_INFO_GENERIC_128K, + FlashChipType::Unknown => &CHIP_INFO_GENERIC_64K, + } + } +} +static CHIP_INFO: InitOnce<&'static ChipInfo> = InitOnce::new(); +fn cached_chip_info() -> Result<&'static ChipInfo, Error> { + CHIP_INFO + .try_get(|| -> Result<_, Error> { Ok(FlashChipType::detect()?.chip_info()) }) + .map(Clone::clone) +} + +/// Actual implementation of the ChipInfo functions. +impl ChipInfo { + /// Returns the total length of this chip. + fn total_len(&self) -> usize { + self.info.sector_count << self.info.sector_shift + } + + // Checks whether a byte offset is in bounds. + fn check_len(&self, offset: usize, len: usize) -> Result<(), Error> { + if offset.checked_add(len).is_some() && offset + len <= self.total_len() { + Ok(()) + } else { + Err(Error::OutOfBounds) + } + } + + // Checks whether a sector offset is in bounds. + fn check_sector_len(&self, offset: usize, len: usize) -> Result<(), Error> { + if offset.checked_add(len).is_some() && offset + len <= self.info.sector_count { + Ok(()) + } else { + Err(Error::OutOfBounds) + } + } + + /// Sets the currently active bank. + fn set_bank(&self, bank: usize) -> Result<(), Error> { + if bank >= self.bank_count as usize { + Err(Error::OutOfBounds) + } else if self.bank_count > 1 { + set_bank(bank as u8) + } else { + Ok(()) + } + } + + /// Reads a buffer from save media into memory. + fn read_buffer(&self, mut offset: usize, mut buf: &mut [u8]) -> Result<(), Error> { + while !buf.is_empty() { + self.set_bank(offset >> BANK_SHIFT)?; + let start = offset & BANK_MASK; + let end_len = cmp::min(BANK_LEN - start, buf.len()); + unsafe { + read_raw_buf(&mut buf[..end_len], 0x0E000000 + start); + } + buf = &mut buf[end_len..]; + offset += end_len; + } + Ok(()) + } + + /// Verifies that a buffer was properly stored into save media. + fn verify_buffer(&self, mut offset: usize, mut buf: &[u8]) -> Result { + while !buf.is_empty() { + self.set_bank(offset >> BANK_SHIFT)?; + let start = offset & BANK_MASK; + let end_len = cmp::min(BANK_LEN - start, buf.len()); + if !unsafe { verify_raw_buf(&buf[..end_len], 0x0E000000 + start) } { + return Ok(false); + } + buf = &buf[end_len..]; + offset += end_len; + } + Ok(true) + } + + /// Waits for a timeout, or an operation to complete. + fn wait_for_timeout( + &self, offset: usize, val: u8, ms: u16, timeout: &mut Timeout, + ) -> Result<(), Error> { + timeout.start(); + let offset = 0x0E000000 + offset; + + while unsafe { read_raw_byte(offset) != val } { + if timeout.check_timeout_met(ms) { + if self.requires_cancel_command { + FLASH_PORT_A.set(0xF0); + } + return Err(Error::OperationTimedOut); + } + } + Ok(()) + } + + /// Erases a sector to flash. + fn erase_sector(&self, sector: usize, timeout: &mut Timeout) -> Result<(), Error> { + let offset = sector << self.info.sector_shift; + self.set_bank(offset >> BANK_SHIFT)?; + issue_flash_command(CMD_ERASE_SECTOR_BEGIN); + start_flash_command(); + FLASH_DATA.set(offset & BANK_MASK, CMD_ERASE_SECTOR_CONFIRM); + self.wait_for_timeout(offset & BANK_MASK, 0xFF, self.erase_sector_timeout, timeout) + } + + /// Erases the entire chip. + fn erase_chip(&self, timeout: &mut Timeout) -> Result<(), Error> { + issue_flash_command(CMD_ERASE_SECTOR_BEGIN); + issue_flash_command(CMD_ERASE_SECTOR_ALL); + self.wait_for_timeout(0, 0xFF, 3000, timeout) + } + + /// Writes a byte to the save media. + fn write_byte(&self, offset: usize, byte: u8, timeout: &mut Timeout) -> Result<(), Error> { + issue_flash_command(CMD_WRITE); + FLASH_DATA.set(offset, byte); + self.wait_for_timeout(offset, byte, self.write_timeout, timeout) + } + + /// Writes an entire buffer to the save media. + #[allow(clippy::needless_range_loop)] + fn write_buffer(&self, offset: usize, buf: &[u8], timeout: &mut Timeout) -> Result<(), Error> { + self.set_bank(offset >> BANK_SHIFT)?; + for i in 0..buf.len() { + let byte_off = offset + i; + if (byte_off & BANK_MASK) == 0 { + self.set_bank(byte_off >> BANK_SHIFT)?; + } + self.write_byte(byte_off & BANK_MASK, buf[i], timeout)?; + } + Ok(()) + } + + /// Erases and writes an entire 128b sector on Atmel devices. + #[allow(clippy::needless_range_loop)] + fn write_atmel_sector_raw( + &self, offset: usize, buf: &[u8], timeout: &mut Timeout, + ) -> Result<(), Error> { + crate::interrupt::free(|_| { + issue_flash_command(CMD_WRITE); + for i in 0..128 { + FLASH_DATA.set(offset + i, buf[i]); + } + self.wait_for_timeout(offset + 127, buf[127], self.erase_sector_timeout, timeout) + })?; + Ok(()) + } + + /// Writes an entire 128b sector on Atmel devices, copying existing data in + /// case of non-sector aligned writes. + #[inline(never)] // avoid allocating the 128 byte buffer for no reason. + fn write_atmel_sector_safe( + &self, offset: usize, buf: &[u8], start: usize, timeout: &mut Timeout, + ) -> Result<(), Error> { + let mut sector = [0u8; 128]; + self.read_buffer(offset, &mut sector[0..start])?; + sector[start..start + buf.len()].copy_from_slice(buf); + self.read_buffer(offset + start + buf.len(), &mut sector[start + buf.len()..128])?; + self.write_atmel_sector_raw(offset, §or, timeout) + } + + /// Writes an entire 128b sector on Atmel devices, copying existing data in + /// case of non-sector aligned writes. + /// + /// This avoids allocating stack if there is no need to. + fn write_atmel_sector( + &self, offset: usize, buf: &[u8], start: usize, timeout: &mut Timeout, + ) -> Result<(), Error> { + if start == 0 && buf.len() == 128 { + self.write_atmel_sector_raw(offset, buf, timeout) + } else { + self.write_atmel_sector_safe(offset, buf, start, timeout) + } + } +} + +/// The [`RawSaveAccess`] used for flash save media. +pub struct FlashAccess; +impl RawSaveAccess for FlashAccess { + fn info(&self) -> Result<&'static MediaInfo, Error> { + Ok(cached_chip_info()?.info) + } + + fn read(&self, offset: usize, buf: &mut [u8], _: &mut Timeout) -> Result<(), Error> { + let chip = cached_chip_info()?; + chip.check_len(offset, buf.len())?; + + chip.read_buffer(offset, buf) + } + + fn verify(&self, offset: usize, buf: &[u8], _: &mut Timeout) -> Result { + let chip = cached_chip_info()?; + chip.check_len(offset, buf.len())?; + + chip.verify_buffer(offset, buf) + } + + fn prepare_write( + &self, sector: usize, count: usize, timeout: &mut Timeout, + ) -> Result<(), Error> { + let chip = cached_chip_info()?; + chip.check_sector_len(sector, count)?; + + if chip.uses_atmel_api { + Ok(()) + } else if count == chip.info.sector_count { + chip.erase_chip(timeout) + } else { + for i in sector..sector + count { + chip.erase_sector(i, timeout)?; + } + Ok(()) + } + } + + fn write(&self, mut offset: usize, mut buf: &[u8], timeout: &mut Timeout) -> Result<(), Error> { + let chip = cached_chip_info()?; + chip.check_len(offset, buf.len())?; + + if chip.uses_atmel_api { + while !buf.is_empty() { + let start = offset & 127; + let end_len = cmp::min(128 - start, buf.len()); + chip.write_atmel_sector(offset & !127, &buf[..end_len], start, timeout)?; + buf = &buf[end_len..]; + offset += end_len; + } + Ok(()) + } else { + // Write the bytes one by one. + chip.write_buffer(offset, buf, timeout)?; + Ok(()) + } + } +} diff --git a/agb/src/save/mod.rs b/agb/src/save/mod.rs new file mode 100644 index 00000000..485cc832 --- /dev/null +++ b/agb/src/save/mod.rs @@ -0,0 +1,457 @@ +//! Module for reading and writing to save media. +//! +//! ## Save media types +//! +//! There are, broadly speaking, three different kinds of save media that can be +//! found in official Game Carts: +//! +//! * Battery-Backed SRAM: The simplest kind of save media, which can be accessed +//! like normal memory. You can have SRAM up to 32KiB, and while there exist a +//! few variants this does not matter much for a game developer. +//! * EEPROM: A kind of save media based on very cheap chips and slow chips. +//! These are accessed using a serial interface based on reading/writing bit +//! streams into IO registers. This memory comes in 8KiB and 512 byte versions, +//! which unfortunately cannot be distinguished at runtime. +//! * Flash: A kind of save media based on flash memory. Flash memory can be read +//! like ordinary memory, but writing requires sending commands using multiple +//! IO register spread across the address space. This memory comes in 64KiB +//! and 128KiB variants, which can thankfully be distinguished using a chip ID. +//! +//! As these various types of save media cannot be easily distinguished at +//! runtime, the kind of media in use should be set manually. +//! +//! ## Setting save media type +//! +//! To use save media in your game, you must set which type to use. This is done +//! by calling one of the following functions at startup: +//! +//! * For 32 KiB battery-backed SRAM, call [`init_sram`]. +//! * For 64 KiB flash memory, call [`init_flash_64k`]. +//! * For 128 KiB flash memory, call [`init_flash_128k`]. +//! * For 512 byte EEPROM, call [`init_eeprom_512b`]. +//! * For 8 KiB EEPROM, call [`init_eeprom_8k`]. +//! +//! [`init_sram`]: SaveManager::init_sram +//! [`init_flash_64k`]: SaveManager::init_flash_64k +//! [`init_flash_128k`]: SaveManager::init_flash_128k +//! [`init_eeprom_512b`]: SaveManager::init_eeprom_512b +//! [`init_eeprom_8k`]: SaveManager::init_eeprom_8k +//! +//! ## Using save media +//! +//! To access save media, use the [`SaveData::new`] method to create a new +//! [`SaveData`] object. Its methods are used to read or write save media. +//! +//! Reading data from the savegame is simple. Use [`read`] to copy data from an +//! offset in the savegame into a buffer in memory. +//! +//! Writing to save media requires you to prepare the area for writing by calling +//! the [`prepare_write`] method to return a [`SavePreparedBlock`], which contains +//! the actual [`write`] method. +//! +//! The `prepare_write` method leaves everything in a sector that overlaps the +//! range passed to it in an implementation defined state. On some devices it may +//! do nothing, and on others, it may clear the entire range to `0xFF`. +//! +//! Because writes can only be prepared on a per-sector basis, a clear on a range +//! of `4000..5000` on a device with 4096 byte sectors will actually clear a range +//! of `0..8192`. Use [`sector_size`] to find the sector size, or [`align_range`] +//! to directly calculate the range of memory that will be affected by the clear. +//! +//! [`read`]: SaveData::read +//! [`prepare_write`]: SaveData::prepare_write +//! [`write`]: SavePreparedBlock::write +//! [`sector_size`]: SaveAccess::sector_size +//! [`align_range`]: SaveAccess::align_range +//! +//! ## Performance and Other Details +//! +//! The performance characteristics of the media types are as follows: +//! +//! * SRAM is simply a form of battery backed memory, and has no particular +//! performance characteristics. Reads and writes at any alignment are +//! efficient. Furthermore, no timer is needed for accesses to this type of +//! media. `prepare_write` does not immediately erase any data. +//! * Non-Atmel flash chips have a sector size of 4096 bytes. Reads and writes +//! to any alignment are efficient, however, `prepare_write` will erase all +//! data in an entire sector before writing. +//! * Atmel flash chips have a sector size of 128 bytes. Reads to any alignment +//! are efficient, however, unaligned writes are extremely slow. +//! `prepare_write` does not immediately erase any data. +//! * EEPROM has a sector size of 8 bytes. Unaligned reads and writes are +//! slower than aligned writes, however, this is easily mitigated by the +//! small sector size. + +use core::ops::Range; +use crate::save::utils::Timeout; +use crate::sync::{Mutex, RawMutexGuard}; +use crate::timer::Timer; + +mod asm_utils; +mod eeprom; +mod flash; +mod sram; +mod utils; + +/// A list of save media types. +#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Debug)] +#[non_exhaustive] +pub enum MediaType { + /// 32KiB Battery-Backed SRAM or FRAM + Sram32K, + /// 8KiB EEPROM + Eeprom8K, + /// 512B EEPROM + Eeprom512B, + /// 64KiB flash chip + Flash64K, + /// 128KiB flash chip + Flash128K, +} + +/// The type used for errors encountered while reading or writing save media. +#[derive(Clone, Debug)] +#[non_exhaustive] +pub enum Error { + /// There is no save media attached to this game cart. + NoMedia, + /// Failed to write the data to save media. + WriteError, + /// An operation on save media timed out. + OperationTimedOut, + /// An attempt was made to access save media at an invalid offset. + OutOfBounds, + /// The media is already in use. + /// + /// This can generally only happen in an IRQ that happens during an ongoing + /// save media operation. + MediaInUse, + /// This command cannot be used with the save media in use. + IncompatibleCommand, +} + +/// Information about the save media used. +#[derive(Clone, Debug)] +#[non_exhaustive] +pub struct MediaInfo { + /// The type of save media installed. + pub media_type: MediaType, + /// The power-of-two size of each sector. Zero represents a sector size of + /// 0, implying sectors are not in use. + /// + /// (For example, 512 byte sectors would return 9 here.) + pub sector_shift: usize, + /// The size of the save media, in sectors. + pub sector_count: usize, + /// Whether the save media type requires media be prepared before writing. + pub uses_prepare_write: bool, +} +impl MediaInfo { + /// Returns the sector size of the save media. It is generally optimal to + /// write data in blocks that are aligned to the sector size. + #[must_use] + pub fn sector_size(&self) -> usize { + 1 << self.sector_shift + } + + /// Returns the total length of this save media. + #[must_use] + #[allow(clippy::len_without_is_empty)] // is_empty() would always be false + pub fn len(&self) -> usize { + self.sector_count << self.sector_shift + } +} + +/// A trait allowing low-level saving and writing to save media. +trait RawSaveAccess: Sync { + fn info(&self) -> Result<&'static MediaInfo, Error>; + fn read(&self, offset: usize, buffer: &mut [u8], timeout: &mut Timeout) -> Result<(), Error>; + fn verify(&self, offset: usize, buffer: &[u8], timeout: &mut Timeout) -> Result; + fn prepare_write(&self, sector: usize, count: usize, timeout: &mut Timeout) -> Result<(), Error>; + fn write(&self, offset: usize, buffer: &[u8], timeout: &mut Timeout) -> Result<(), Error>; +} + +static CURRENT_SAVE_ACCESS: Mutex> = Mutex::new(None); + +fn set_save_implementation(access_impl: &'static dyn RawSaveAccess) { + let mut access = CURRENT_SAVE_ACCESS.lock(); + assert!(access.is_none(), "Cannot initialize the savegame engine more than once."); + *access = Some(access_impl); +} + +fn get_save_implementation() -> Option<&'static dyn RawSaveAccess> { + *CURRENT_SAVE_ACCESS.lock() +} + +/// Allows reading and writing of save media. +pub struct SaveData { + _lock: RawMutexGuard<'static>, + access: &'static dyn RawSaveAccess, + info: &'static MediaInfo, + timeout: utils::Timeout, +} +impl SaveData { + /// Creates a new save accessor around the current save implementaiton. + fn new(timer: Option) -> Result { + match get_save_implementation() { + Some(access) => Ok(SaveData { + _lock: utils::lock_media_access()?, + access, + info: access.info()?, + timeout: utils::Timeout::new(timer), + }), + None => Err(Error::NoMedia), + } + } + + /// Returns the media info underlying this accessor. + #[must_use] + pub fn media_info(&self) -> &'static MediaInfo { + self.info + } + + /// Returns the save media type being used. + #[must_use] + pub fn media_type(&self) -> MediaType { + self.info.media_type + } + + /// Returns the sector size of the save media. It is generally optimal to + /// write data in blocks that are aligned to the sector size. + #[must_use] + pub fn sector_size(&self) -> usize { + self.info.sector_size() + } + + /// Returns the total length of this save media. + #[must_use] + #[allow(clippy::len_without_is_empty)] // is_empty() would always be false + pub fn len(&self) -> usize { + self.info.len() + } + + fn check_bounds(&self, range: Range) -> Result<(), Error> { + if range.start >= self.len() || range.end > self.len() { + Err(Error::OutOfBounds) + } else { + Ok(()) + } + } + fn check_bounds_len(&self, offset: usize, len: usize) -> Result<(), Error> { + self.check_bounds(offset..(offset + len)) + } + + /// Copies data from the save media to a buffer. + /// + /// If an error is returned, the contents of the buffer are unpredictable. + pub fn read(&mut self, offset: usize, buffer: &mut [u8]) -> Result<(), Error> { + self.check_bounds_len(offset, buffer.len())?; + self.access.read(offset, buffer, &mut self.timeout) + } + + /// Verifies that a given block of memory matches the save media. + pub fn verify(&mut self, offset: usize, buffer: &[u8]) -> Result { + self.check_bounds_len(offset, buffer.len())?; + self.access.verify(offset, buffer, &mut self.timeout) + } + + /// Returns a range that contains all sectors the input range overlaps. + /// + /// This can be used to calculate which blocks would be erased by a call + /// to [`prepare_write`](`SaveAccess::prepare_write`) + #[must_use] + pub fn align_range(&self, range: Range) -> Range { + let shift = self.info.sector_shift; + let mask = (1 << shift) - 1; + (range.start & !mask)..((range.end + mask) & !mask) + } + + /// Prepares a given span of offsets for writing. + /// + /// This will erase any data in any sector overlapping the input range. To + /// calculate which offset ranges would be affected, use the + /// [`align_range`](`SaveAccess::align_range`) function. + pub fn prepare_write(&mut self, range: Range) -> Result { + self.check_bounds(range.clone())?; + if self.info.uses_prepare_write { + let range = self.align_range(range.clone()); + let shift = self.info.sector_shift; + self.access.prepare_write( + range.start >> shift, range.len() >> shift, &mut self.timeout, + )?; + } + Ok(SavePreparedBlock { + parent: self, + range + }) + } +} + +/// A block of save memory that has been prepared for writing. +pub struct SavePreparedBlock<'a> { + parent: &'a mut SaveData, + range: Range, +} +impl<'a> SavePreparedBlock<'a> { + /// Writes a given buffer into the save media. + /// + /// Multiple overlapping writes to the same memory range without a separate + /// call to `prepare_write` will leave the save data in an unpredictable + /// state. If an error is returned, the contents of the save media is + /// unpredictable. + pub fn write(&mut self, offset: usize, buffer: &[u8]) -> Result<(), Error> { + if buffer.is_empty() { + Ok(()) + } else if !self.range.contains(&offset) || + !self.range.contains(&(offset + buffer.len() - 1)) { + Err(Error::OutOfBounds) + } else { + self.parent.access.write(offset, buffer, &mut self.parent.timeout) + } + } + + /// Writes and validates a given buffer into the save media. + /// + /// This function will verify that the write has completed successfully, and + /// return an error if it has not done so. + /// + /// Multiple overlapping writes to the same memory range without a separate + /// call to `prepare_write` will leave the save data in an unpredictable + /// state. If an error is returned, the contents of the save media is + /// unpredictable. + pub fn write_and_verify(&mut self, offset: usize, buffer: &[u8]) -> Result<(), Error> { + self.write(offset, buffer)?; + if !self.parent.verify(offset, buffer)? { + Err(Error::WriteError) + } else { + Ok(()) + } + } +} + +mod marker { + #[repr(align(4))] + struct Align(T); + + static EEPROM: Align<[u8; 12]> = Align(*b"EEPROM_Vnnn\0"); + static SRAM: Align<[u8; 12]> = Align(*b"SRAM_Vnnn\0\0\0"); + static FLASH512K: Align<[u8; 16]> = Align(*b"FLASH512_Vnnn\0\0\0"); + static FLASH1M: Align<[u8; 16]> = Align(*b"FLASH1M_Vnnn\0\0\0\0"); + + #[inline(always)] + pub fn emit_eeprom_marker() { + crate::sync::memory_read_hint(&EEPROM); + } + #[inline(always)] + pub fn emit_sram_marker() { + crate::sync::memory_read_hint(&SRAM); + } + #[inline(always)] + pub fn emit_flash_512k_marker() { + crate::sync::memory_read_hint(&FLASH512K); + } + #[inline(always)] + pub fn emit_flash_1m_marker() { + crate::sync::memory_read_hint(&FLASH1M); + } +} + +/// Allows access to the cartridge's save data. +#[non_exhaustive] +pub struct SaveManager {} +impl SaveManager { + pub(crate) const fn new() -> Self { + SaveManager {} + } + + /// Declares that the ROM uses battery backed SRAM/FRAM. + /// + /// Battery Backed SRAM is generally very fast, but limited in size compared + /// to flash chips. + /// + /// This creates a marker in the ROM that allows emulators to understand what + /// save type the Game Pak uses, and configures the save manager to use the + /// given save type. + /// + /// Only one `init_*` function may be called in the lifetime of the program. + pub fn init_sram(&mut self) { + marker::emit_sram_marker(); + set_save_implementation(&sram::BatteryBackedAccess); + } + + /// Declares that the ROM uses 64KiB flash memory. + /// + /// Flash save media is generally very slow to write to and relatively fast + /// to read from. It is the only real option if you need larger save data. + /// + /// This creates a marker in the ROM that allows emulators to understand what + /// save type the Game Pak uses, and configures the save manager to use the + /// given save type. + /// + /// Only one `init_*` function may be called in the lifetime of the program. + pub fn init_flash_64k(&mut self) { + marker::emit_flash_512k_marker(); + set_save_implementation(&flash::FlashAccess); + } + + /// Declares that the ROM uses 128KiB flash memory. + /// + /// Flash save media is generally very slow to write to and relatively fast + /// to read from. It is the only real option if you need larger save data. + /// + /// This creates a marker in the ROM that allows emulators to understand what + /// save type the Game Pak uses, and configures the save manager to use the + /// given save type. + /// + /// Only one `init_*` function may be called in the lifetime of the program. + pub fn init_flash_128k(&mut self) { + marker::emit_flash_1m_marker(); + set_save_implementation(&flash::FlashAccess); + } + + /// Declares that the ROM uses 512 bytes EEPROM memory. + /// + /// EEPROM is generally pretty slow and also very small. It's mainly used in + /// Game Paks because it's cheap. + /// + /// This creates a marker in the ROM that allows emulators to understand what + /// save type the Game Pak uses, and configures the save manager to use the + /// given save type. + /// + /// Only one `init_*` function may be called in the lifetime of the program. + pub fn init_eeprom_512b(&mut self) { + marker::emit_eeprom_marker(); + set_save_implementation(&eeprom::Eeprom512B); + } + + /// Declares that the ROM uses 8 KiB EEPROM memory. + /// + /// EEPROM is generally pretty slow and also very small. It's mainly used in + /// Game Paks because it's cheap. + /// + /// This creates a marker in the ROM that allows emulators to understand what + /// save type the Game Pak uses, and configures the save manager to use the + /// given save type. + /// + /// Only one `init_*` function may be called in the lifetime of the program. + pub fn init_eeprom_8k(&mut self) { + marker::emit_eeprom_marker(); + set_save_implementation(&eeprom::Eeprom8K); + } + + /// Creates a new accessor to the save data. + /// + /// You must have initialized the save manager beforehand to use a specific + /// type of media before calling this method. + pub fn access(&mut self) -> Result { + SaveData::new(None) + } + + /// Creates a new accessor to the save data that uses the given timer for timeouts. + /// + /// You must have initialized the save manager beforehand to use a specific + /// type of media before calling this method. + pub fn access_with_timer(&mut self, timer: Timer) -> Result { + SaveData::new(Some(timer)) + } +} \ No newline at end of file diff --git a/agb/src/save/sram.rs b/agb/src/save/sram.rs new file mode 100644 index 00000000..614b751e --- /dev/null +++ b/agb/src/save/sram.rs @@ -0,0 +1,57 @@ +//! Module for battery backed SRAM save media support. +//! +//! SRAM acts as ordinary memory mapped into the memory space, and as such +//! is accessed using normal memory read/write commands. + +use crate::save::{Error, MediaInfo, MediaType, RawSaveAccess}; +use crate::save::asm_utils::*; +use crate::save::utils::Timeout; + +const SRAM_SIZE: usize = 32 * 1024; // 32 KiB + +/// Checks whether an offset is contained within the bounds of the SRAM. +fn check_bounds(offset: usize, len: usize) -> Result<(), Error> { + if offset.checked_add(len).is_none() || offset + len > SRAM_SIZE { + return Err(Error::OutOfBounds); + } + Ok(()) +} + +/// The [`RawSaveAccess`] used for battery backed SRAM. +pub struct BatteryBackedAccess; +impl RawSaveAccess for BatteryBackedAccess { + fn info(&self) -> Result<&'static MediaInfo, Error> { + Ok(&MediaInfo { + media_type: MediaType::Sram32K, + sector_shift: 0, + sector_count: SRAM_SIZE, + uses_prepare_write: false, + }) + } + + fn read(&self, offset: usize, buffer: &mut [u8], _: &mut Timeout) -> Result<(), Error> { + check_bounds(offset, buffer.len())?; + unsafe { + read_raw_buf(buffer, 0x0E000000 + offset); + } + Ok(()) + } + + fn verify(&self, offset: usize, buffer: &[u8], _: &mut Timeout) -> Result { + check_bounds(offset, buffer.len())?; + let val = unsafe { verify_raw_buf(buffer, 0x0E000000 + offset) }; + Ok(val) + } + + fn prepare_write(&self, _: usize, _: usize, _: &mut Timeout) -> Result<(), Error> { + Ok(()) + } + + fn write(&self, offset: usize, buffer: &[u8], _: &mut Timeout) -> Result<(), Error> { + check_bounds(offset, buffer.len())?; + unsafe { + write_raw_buf(0x0E000000 + offset, buffer); + } + Ok(()) + } +} diff --git a/agb/src/save/utils.rs b/agb/src/save/utils.rs new file mode 100644 index 00000000..b94cb3fe --- /dev/null +++ b/agb/src/save/utils.rs @@ -0,0 +1,59 @@ +//! A package containing useful utilities for writing save accessors. + +use super::Error; +use crate::sync::{RawMutex, RawMutexGuard}; +use crate::timer::{Timer, Divider}; + +/// A timeout type used to prevent hardware errors in save media from hanging +/// the game. +pub struct Timeout { + timer: Option, +} +impl Timeout { + /// Creates a new timeout from the timer passed to [`set_timer_for_timeout`]. + /// + /// ## Errors + /// + /// If another timeout has already been created. + #[inline(never)] + pub fn new(timer: Option) -> Self { + Timeout { timer } + } + + /// Starts this timeout. + pub fn start(&mut self) { + if let Some(timer) = &mut self.timer { + timer.set_enabled(false); + timer.set_divider(Divider::Divider1024); + timer.set_interrupt(false); + timer.set_overflow_amount(0xFFFF); + timer.set_cascade(false); + timer.set_enabled(true); + } + } + + /// Returns whether a number of milliseconds has passed since the last call + /// to [`Timeout::start()`]. + pub fn check_timeout_met(&self, check_ms: u16) -> bool { + if let Some(timer) = &self.timer { + check_ms * 17 < timer.value() + } else { + false + } + } +} +impl Drop for Timeout { + fn drop(&mut self) { + if let Some(timer) = &mut self.timer { + timer.set_enabled(false); + } + } +} + +pub fn lock_media_access() -> Result, Error> { + static LOCK: RawMutex = RawMutex::new(); + match LOCK.try_lock() { + Some(x) => Ok(x), + None => Err(Error::MediaInUse), + } +} diff --git a/agb/src/sync/statics.rs b/agb/src/sync/statics.rs index 3970589f..62ec9d1d 100644 --- a/agb/src/sync/statics.rs +++ b/agb/src/sync/statics.rs @@ -284,7 +284,7 @@ mod test { // the actual main test loop let mut interrupt_seen = false; let mut no_interrupt_seen = false; - for i in 0..100000 { + for i in 0..250000 { // write to the static let new_value = [i; COUNT]; value.write(new_value); diff --git a/agb/tests/save_test_common/mod.rs b/agb/tests/save_test_common/mod.rs new file mode 100644 index 00000000..d3f823c2 --- /dev/null +++ b/agb/tests/save_test_common/mod.rs @@ -0,0 +1,105 @@ +use core::cmp; +use agb::save::{Error, MediaInfo}; +use agb::sync::InitOnce; + +fn init_sram(gba: &mut agb::Gba) -> &'static MediaInfo { + static ONCE: InitOnce = InitOnce::new(); + ONCE.get(|| { + crate::save_setup(gba); + gba.save.access().unwrap().media_info().clone() + }) +} + +#[derive(Clone)] +struct Rng(u32); +impl Rng { + fn iter(&mut self) { + self.0 = self.0.wrapping_mul(2891336453).wrapping_add(100001); + } + fn next_u8(&mut self) -> u8 { + self.iter(); + (self.0 >> 22) as u8 ^ self.0 as u8 + } + fn next_under(&mut self, under: u32) -> u32 { + self.iter(); + let scale = 31 - under.leading_zeros(); + ((self.0 >> scale) ^ self.0) % under + } +} + +const MAX_BLOCK_SIZE: usize = 4 * 1024; + +#[allow(clippy::needless_range_loop)] +fn do_test( + gba: &mut agb::Gba, seed: Rng, offset: usize, len: usize, block_size: usize, +) -> Result<(), Error> { + let mut buffer = [0; MAX_BLOCK_SIZE]; + + let timers = gba.timers.timers(); + let mut access = gba.save.access_with_timer(timers.timer2)?; + + // writes data to the save media + let mut prepared = access.prepare_write(offset..offset + len)?; + let mut rng = seed.clone(); + let mut current = offset; + let end = offset + len; + while current != end { + let cur_len = cmp::min(end - current, block_size); + for i in 0..cur_len { + buffer[i] = rng.next_u8(); + } + prepared.write(current, &buffer[..cur_len])?; + current += cur_len; + } + + // validates the save media + rng = seed; + current = offset; + while current != end { + let cur_len = cmp::min(end - current, block_size); + access.read(current, &mut buffer[..cur_len])?; + for i in 0..cur_len { + let cur_byte = rng.next_u8(); + assert_eq!( + buffer[i], cur_byte, + "Read does not match earlier write: {} != {} @ 0x{:05x}", + buffer[i], cur_byte, current + i, + ); + } + current += cur_len; + } + + Ok(()) +} + +#[test_case] +fn test_4k_blocks(gba: &mut agb::Gba) { + let info = init_sram(gba); + + if info.len() >= (1 << 12) { + do_test(gba, Rng(2000), 0, info.len(), 4 * 1024).expect("Test encountered error"); + } +} + +#[test_case] +fn test_512b_blocks(gba: &mut agb::Gba) { + let info = init_sram(gba); + do_test(gba, Rng(1000), 0, info.len(), 512).expect("Test encountered error"); +} + +#[test_case] +fn test_partial_writes(gba: &mut agb::Gba) { + let info = init_sram(gba); + + // test with random segments now. + let mut rng = Rng(12345); + for i in 0..8 { + let rand_length = rng.next_under((info.len() >> 1) as u32) as usize + 50; + let rand_offset = rng.next_under(info.len() as u32 - rand_length as u32) as usize; + let block_size = cmp::min(rand_length >> 2, MAX_BLOCK_SIZE - 100); + let block_size = rng.next_under(block_size as u32) as usize + 50; + + do_test(gba, Rng(i * 10000), rand_offset, rand_length, block_size) + .expect("Test encountered error"); + } +} \ No newline at end of file diff --git a/agb/tests/test_save_eeprom_512b.rs b/agb/tests/test_save_eeprom_512b.rs new file mode 100644 index 00000000..19c3f6c9 --- /dev/null +++ b/agb/tests/test_save_eeprom_512b.rs @@ -0,0 +1,16 @@ +#![no_std] +#![no_main] +#![feature(custom_test_frameworks)] +#![reexport_test_harness_main = "test_main"] +#![test_runner(agb::test_runner::test_runner)] + +mod save_test_common; + +fn save_setup(gba: &mut agb::Gba) { + gba.save.init_eeprom_512b(); +} + +#[agb::entry] +fn entry(_gba: agb::Gba) -> ! { + loop {} +} diff --git a/agb/tests/test_save_eeprom_8k.rs b/agb/tests/test_save_eeprom_8k.rs new file mode 100644 index 00000000..95677321 --- /dev/null +++ b/agb/tests/test_save_eeprom_8k.rs @@ -0,0 +1,16 @@ +#![no_std] +#![no_main] +#![feature(custom_test_frameworks)] +#![reexport_test_harness_main = "test_main"] +#![test_runner(agb::test_runner::test_runner)] + +mod save_test_common; + +fn save_setup(gba: &mut agb::Gba) { + gba.save.init_eeprom_8k(); +} + +#[agb::entry] +fn entry(_gba: agb::Gba) -> ! { + loop {} +} diff --git a/agb/tests/test_save_flash_128k.rs b/agb/tests/test_save_flash_128k.rs new file mode 100644 index 00000000..7256ddfb --- /dev/null +++ b/agb/tests/test_save_flash_128k.rs @@ -0,0 +1,16 @@ +#![no_std] +#![no_main] +#![feature(custom_test_frameworks)] +#![reexport_test_harness_main = "test_main"] +#![test_runner(agb::test_runner::test_runner)] + +mod save_test_common; + +fn save_setup(gba: &mut agb::Gba) { + gba.save.init_flash_128k(); +} + +#[agb::entry] +fn entry(_gba: agb::Gba) -> ! { + loop {} +} diff --git a/agb/tests/test_save_flash_64k.rs b/agb/tests/test_save_flash_64k.rs new file mode 100644 index 00000000..6c179ae3 --- /dev/null +++ b/agb/tests/test_save_flash_64k.rs @@ -0,0 +1,16 @@ +#![no_std] +#![no_main] +#![feature(custom_test_frameworks)] +#![reexport_test_harness_main = "test_main"] +#![test_runner(agb::test_runner::test_runner)] + +mod save_test_common; + +fn save_setup(gba: &mut agb::Gba) { + gba.save.init_flash_64k(); +} + +#[agb::entry] +fn entry(_gba: agb::Gba) -> ! { + loop {} +} diff --git a/agb/tests/test_save_sram.rs b/agb/tests/test_save_sram.rs new file mode 100644 index 00000000..f3348eda --- /dev/null +++ b/agb/tests/test_save_sram.rs @@ -0,0 +1,16 @@ +#![no_std] +#![no_main] +#![feature(custom_test_frameworks)] +#![reexport_test_harness_main = "test_main"] +#![test_runner(agb::test_runner::test_runner)] + +mod save_test_common; + +fn save_setup(gba: &mut agb::Gba) { + gba.save.init_sram(); +} + +#[agb::entry] +fn entry(_gba: agb::Gba) -> ! { + loop {} +} diff --git a/book/games/pong/Cargo.lock b/book/games/pong/Cargo.lock index 2f28485a..9d847e4d 100644 --- a/book/games/pong/Cargo.lock +++ b/book/games/pong/Cargo.lock @@ -217,9 +217,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.132" +version = "0.2.134" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8371e4e5341c3a96db127eb2465ac681ced4c433e01dd0e938adbef26ba93ba5" +checksum = "329c933548736bc49fd575ee68c89e8be4d260064184389a5b77517cddd99ffb" [[package]] name = "log" @@ -318,9 +318,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.14.0" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f7254b99e31cad77da24b08ebf628882739a608578bb1bcdfc1f9c21260d7c0" +checksum = "e82dad04139b71a90c080c8463fe0dc7902db5192d939bd0950f074d014339e1" [[package]] name = "png" @@ -343,9 +343,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.43" +version = "1.0.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a2ca2c61bc9f3d74d2886294ab7b9853abd9c1ad903a3ac7815c58989bb7bab" +checksum = "94e2ef8dbfc347b10c094890f778ee2e36ca9bb4262e86dc99cd217e35f3470b" dependencies = [ "unicode-ident", ] @@ -367,18 +367,18 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] name = "serde" -version = "1.0.144" +version = "1.0.145" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f747710de3dcd43b88c9168773254e809d8ddbdf9653b84e2554ab219f17860" +checksum = "728eb6351430bccb993660dfffc5a72f91ccc1295abaa8ce19b27ebe4f75568b" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.144" +version = "1.0.145" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94ed3a816fb1d101812f83e789f888322c34e291f894f19590dc310963e87a00" +checksum = "81fa1584d3d1bcacd84c277a0dfe21f5b0f6accf4a23d04d4c6d61f1af522b4c" dependencies = [ "proc-macro2", "quote", @@ -393,9 +393,9 @@ checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" [[package]] name = "syn" -version = "1.0.99" +version = "1.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58dbef6ec655055e20b86b15a8cc6d439cca19b667537ac6a1369572d151ab13" +checksum = "e90cde112c4b9690b8cbe810cba9ddd8bc1d7472e2cae317b69e9438c1cba7d2" dependencies = [ "proc-macro2", "quote", diff --git a/examples/hyperspace-roll/Cargo.lock b/examples/hyperspace-roll/Cargo.lock index 99c59380..e9a74349 100644 --- a/examples/hyperspace-roll/Cargo.lock +++ b/examples/hyperspace-roll/Cargo.lock @@ -225,9 +225,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.132" +version = "0.2.134" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8371e4e5341c3a96db127eb2465ac681ced4c433e01dd0e938adbef26ba93ba5" +checksum = "329c933548736bc49fd575ee68c89e8be4d260064184389a5b77517cddd99ffb" [[package]] name = "log" @@ -326,9 +326,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.14.0" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f7254b99e31cad77da24b08ebf628882739a608578bb1bcdfc1f9c21260d7c0" +checksum = "e82dad04139b71a90c080c8463fe0dc7902db5192d939bd0950f074d014339e1" [[package]] name = "png" @@ -344,9 +344,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.43" +version = "1.0.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a2ca2c61bc9f3d74d2886294ab7b9853abd9c1ad903a3ac7815c58989bb7bab" +checksum = "94e2ef8dbfc347b10c094890f778ee2e36ca9bb4262e86dc99cd217e35f3470b" dependencies = [ "unicode-ident", ] @@ -368,18 +368,18 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] name = "serde" -version = "1.0.144" +version = "1.0.145" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f747710de3dcd43b88c9168773254e809d8ddbdf9653b84e2554ab219f17860" +checksum = "728eb6351430bccb993660dfffc5a72f91ccc1295abaa8ce19b27ebe4f75568b" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.144" +version = "1.0.145" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94ed3a816fb1d101812f83e789f888322c34e291f894f19590dc310963e87a00" +checksum = "81fa1584d3d1bcacd84c277a0dfe21f5b0f6accf4a23d04d4c6d61f1af522b4c" dependencies = [ "proc-macro2", "quote", @@ -394,9 +394,9 @@ checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" [[package]] name = "syn" -version = "1.0.99" +version = "1.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58dbef6ec655055e20b86b15a8cc6d439cca19b667537ac6a1369572d151ab13" +checksum = "e90cde112c4b9690b8cbe810cba9ddd8bc1d7472e2cae317b69e9438c1cba7d2" dependencies = [ "proc-macro2", "quote", diff --git a/examples/hyperspace-roll/src/battle.rs b/examples/hyperspace-roll/src/battle.rs index b0354242..04f76fdd 100644 --- a/examples/hyperspace-roll/src/battle.rs +++ b/examples/hyperspace-roll/src/battle.rs @@ -207,7 +207,7 @@ impl RolledDice { let heal = *face_counts.entry(Face::Heal).or_default(); if heal != 0 { actions.push(Action::PlayerHeal { - amount: ((heal * (heal + 1)) / 2) as u32, + amount: (heal * (heal + 1)) / 2, }); } diff --git a/examples/hyperspace-roll/src/level_generation.rs b/examples/hyperspace-roll/src/level_generation.rs index be1a87af..04c45b5f 100644 --- a/examples/hyperspace-roll/src/level_generation.rs +++ b/examples/hyperspace-roll/src/level_generation.rs @@ -80,7 +80,7 @@ pub fn generate_upgrades(level: u32) -> Vec { .chain(core::iter::once(&next)) .filter(|&x| *x == Face::Malfunction) .count(); - let maximum_number_of_malfunctions = if level < 5 { 0 } else { 1 }; + let maximum_number_of_malfunctions = (level >= 5).into(); if upgrade_value(&upgrades, next) <= max_upgrade_value && number_of_malfunctions <= maximum_number_of_malfunctions { diff --git a/examples/hyperspace-roll/src/main.rs b/examples/hyperspace-roll/src/main.rs index 49e43b73..9e1f4823 100644 --- a/examples/hyperspace-roll/src/main.rs +++ b/examples/hyperspace-roll/src/main.rs @@ -96,10 +96,10 @@ struct Agb<'a> { } fn main(mut gba: agb::Gba) -> ! { - save::init_save(); + save::init_save(&mut gba).expect("Could not initialize save game"); if save::load_high_score() > 1000 { - save::save_high_score(0); + save::save_high_score(&mut gba, 0).expect("Could not reset high score"); } let gfx = gba.display.object.get(); @@ -207,7 +207,8 @@ fn main(mut gba: agb::Gba) -> ! { agb.obj.commit(); agb.sfx.customise(); if save::load_high_score() < current_level { - save::save_high_score(current_level); + save::save_high_score(&mut gba, current_level) + .expect("Could not save high score"); } break; } diff --git a/examples/hyperspace-roll/src/save.rs b/examples/hyperspace-roll/src/save.rs index e5df03e8..0db839de 100644 --- a/examples/hyperspace-roll/src/save.rs +++ b/examples/hyperspace-roll/src/save.rs @@ -1,44 +1,42 @@ -use agb::interrupt::free; -use bare_metal::Mutex; -use core::cell::RefCell; +use agb::Gba; +use agb::save::Error; +use agb::sync::Static; -const RAM_ADDRESS: *mut u8 = 0x0E00_0000 as *mut u8; -const HIGH_SCORE_ADDRESS_START: *mut u8 = RAM_ADDRESS.wrapping_offset(1); +static HIGHSCORE: Static = Static::new(0); -static HIGHSCORE: Mutex> = Mutex::new(RefCell::new(0)); +pub fn init_save(gba: &mut Gba) -> Result<(), Error> { + gba.save.init_sram(); -pub fn init_save() { - if (unsafe { RAM_ADDRESS.read_volatile() } == !0) { - save_high_score(0); - unsafe { RAM_ADDRESS.write_volatile(0) }; - } + let mut access = gba.save.access()?; - let mut a = [0; 4]; - for (idx, a) in a.iter_mut().enumerate() { - *a = unsafe { HIGH_SCORE_ADDRESS_START.add(idx).read_volatile() }; - } + let mut buffer = [0; 1]; + access.read(0, &mut buffer)?; - let high_score = u32::from_le_bytes(a); + if buffer[0] != 0 { + access.prepare_write(0..1)?.write(0, &[0])?; + core::mem::drop(access); + save_high_score(gba, 0)?; + } else { + let mut buffer = [0; 4]; + access.read(1, &mut buffer)?; + let high_score = u32::from_le_bytes(buffer); - free(|cs| { if high_score > 100 { - HIGHSCORE.borrow(cs).replace(0); + HIGHSCORE.write(0) } else { - HIGHSCORE.borrow(cs).replace(high_score); + HIGHSCORE.write(high_score) } - }); + } + + Ok(()) } pub fn load_high_score() -> u32 { - free(|cs| *HIGHSCORE.borrow(cs).borrow()) + HIGHSCORE.read() } -pub fn save_high_score(score: u32) { - let a = score.to_le_bytes(); - - for (idx, &a) in a.iter().enumerate() { - unsafe { HIGH_SCORE_ADDRESS_START.add(idx).write_volatile(a) }; - } - - free(|cs| HIGHSCORE.borrow(cs).replace(score)); +pub fn save_high_score(gba: &mut Gba, score: u32) -> Result<(), Error> { + gba.save.access()?.prepare_write(1..5)?.write(1, &score.to_le_bytes())?; + HIGHSCORE.write(score); + Ok(()) } diff --git a/examples/the-hat-chooses-the-wizard/Cargo.lock b/examples/the-hat-chooses-the-wizard/Cargo.lock index 19c728cf..3ffac11c 100644 --- a/examples/the-hat-chooses-the-wizard/Cargo.lock +++ b/examples/the-hat-chooses-the-wizard/Cargo.lock @@ -223,9 +223,9 @@ checksum = "6c8af84674fe1f223a982c933a0ee1086ac4d4052aa0fb8060c12c6ad838e754" [[package]] name = "libc" -version = "0.2.132" +version = "0.2.134" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8371e4e5341c3a96db127eb2465ac681ced4c433e01dd0e938adbef26ba93ba5" +checksum = "329c933548736bc49fd575ee68c89e8be4d260064184389a5b77517cddd99ffb" [[package]] name = "log" @@ -324,9 +324,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.14.0" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f7254b99e31cad77da24b08ebf628882739a608578bb1bcdfc1f9c21260d7c0" +checksum = "e82dad04139b71a90c080c8463fe0dc7902db5192d939bd0950f074d014339e1" [[package]] name = "png" @@ -342,9 +342,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.43" +version = "1.0.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a2ca2c61bc9f3d74d2886294ab7b9853abd9c1ad903a3ac7815c58989bb7bab" +checksum = "94e2ef8dbfc347b10c094890f778ee2e36ca9bb4262e86dc99cd217e35f3470b" dependencies = [ "unicode-ident", ] @@ -372,18 +372,18 @@ checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09" [[package]] name = "serde" -version = "1.0.144" +version = "1.0.145" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f747710de3dcd43b88c9168773254e809d8ddbdf9653b84e2554ab219f17860" +checksum = "728eb6351430bccb993660dfffc5a72f91ccc1295abaa8ce19b27ebe4f75568b" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.144" +version = "1.0.145" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94ed3a816fb1d101812f83e789f888322c34e291f894f19590dc310963e87a00" +checksum = "81fa1584d3d1bcacd84c277a0dfe21f5b0f6accf4a23d04d4c6d61f1af522b4c" dependencies = [ "proc-macro2", "quote", @@ -409,9 +409,9 @@ checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" [[package]] name = "syn" -version = "1.0.99" +version = "1.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58dbef6ec655055e20b86b15a8cc6d439cca19b667537ac6a1369572d151ab13" +checksum = "e90cde112c4b9690b8cbe810cba9ddd8bc1d7472e2cae317b69e9438c1cba7d2" dependencies = [ "proc-macro2", "quote", diff --git a/examples/the-purple-night/Cargo.lock b/examples/the-purple-night/Cargo.lock index d42b4a43..8ade9367 100644 --- a/examples/the-purple-night/Cargo.lock +++ b/examples/the-purple-night/Cargo.lock @@ -241,9 +241,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.132" +version = "0.2.134" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8371e4e5341c3a96db127eb2465ac681ced4c433e01dd0e938adbef26ba93ba5" +checksum = "329c933548736bc49fd575ee68c89e8be4d260064184389a5b77517cddd99ffb" [[package]] name = "libflate" @@ -354,9 +354,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.14.0" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f7254b99e31cad77da24b08ebf628882739a608578bb1bcdfc1f9c21260d7c0" +checksum = "e82dad04139b71a90c080c8463fe0dc7902db5192d939bd0950f074d014339e1" [[package]] name = "png" @@ -372,9 +372,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.43" +version = "1.0.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a2ca2c61bc9f3d74d2886294ab7b9853abd9c1ad903a3ac7815c58989bb7bab" +checksum = "94e2ef8dbfc347b10c094890f778ee2e36ca9bb4262e86dc99cd217e35f3470b" dependencies = [ "unicode-ident", ] @@ -402,18 +402,18 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] name = "serde" -version = "1.0.144" +version = "1.0.145" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f747710de3dcd43b88c9168773254e809d8ddbdf9653b84e2554ab219f17860" +checksum = "728eb6351430bccb993660dfffc5a72f91ccc1295abaa8ce19b27ebe4f75568b" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.144" +version = "1.0.145" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94ed3a816fb1d101812f83e789f888322c34e291f894f19590dc310963e87a00" +checksum = "81fa1584d3d1bcacd84c277a0dfe21f5b0f6accf4a23d04d4c6d61f1af522b4c" dependencies = [ "proc-macro2", "quote", @@ -428,9 +428,9 @@ checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" [[package]] name = "syn" -version = "1.0.99" +version = "1.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58dbef6ec655055e20b86b15a8cc6d439cca19b667537ac6a1369572d151ab13" +checksum = "e90cde112c4b9690b8cbe810cba9ddd8bc1d7472e2cae317b69e9438c1cba7d2" dependencies = [ "proc-macro2", "quote", diff --git a/examples/the-purple-night/src/main.rs b/examples/the-purple-night/src/main.rs index 07fc59cc..54c9a6f6 100644 --- a/examples/the-purple-night/src/main.rs +++ b/examples/the-purple-night/src/main.rs @@ -129,7 +129,7 @@ impl<'a> Level<'a> { let factor: Number = Number::new(1) / Number::new(8); let (x, y) = (v * factor).floor().get(); - if (x < 0 || x > tilemap::WIDTH as i32) || (y < 0 || y > tilemap::HEIGHT as i32) { + if !(0..=tilemap::WIDTH).contains(&x) || !(0..=tilemap::HEIGHT).contains(&y) { return Some(Rect::new((x * 8, y * 8).into(), (8, 8).into())); } let position = tilemap::WIDTH as usize * y as usize + x as usize; @@ -1878,7 +1878,7 @@ enum MoveState { impl<'a> Game<'a> { fn has_just_reached_end(&self) -> bool { match self.boss { - BossState::NotSpawned => self.offset.x.floor() + 248 >= tilemap::WIDTH as i32 * 8, + BossState::NotSpawned => self.offset.x.floor() + 248 >= tilemap::WIDTH * 8, _ => false, } } @@ -1901,13 +1901,13 @@ impl<'a> Game<'a> { if self.has_just_reached_end() { sfx.boss(); - self.offset.x = (tilemap::WIDTH as i32 * 8 - 248).into(); + self.offset.x = (tilemap::WIDTH * 8 - 248).into(); self.move_state = MoveState::PinnedAtEnd; self.boss = BossState::Active(Boss::new(object_controller, self.offset)) } } MoveState::PinnedAtEnd => { - self.offset.x = (tilemap::WIDTH as i32 * 8 - 248).into(); + self.offset.x = (tilemap::WIDTH * 8 - 248).into(); } MoveState::FollowingPlayer => { Game::update_sunrise(vram, self.sunrise_timer); @@ -1917,8 +1917,8 @@ impl<'a> Game<'a> { let difference = self.player.entity.position.x - (self.offset.x + WIDTH / 2); self.offset.x += difference / 8; - if self.offset.x > (tilemap::WIDTH as i32 * 8 - 248).into() { - self.offset.x = (tilemap::WIDTH as i32 * 8 - 248).into(); + if self.offset.x > (tilemap::WIDTH * 8 - 248).into() { + self.offset.x = (tilemap::WIDTH * 8 - 248).into(); } else if self.offset.x < 8.into() { self.offset.x = 8.into(); self.move_state = MoveState::Ending; diff --git a/justfile b/justfile index 23837d70..201ced58 100644 --- a/justfile +++ b/justfile @@ -68,8 +68,11 @@ update-linker-scripts: publish: (_run-tool "publish") +release +args: (_run-tool "release" args) + _run-tool +tool: - cargo run --manifest-path "{{justfile_directory() + "/tools/Cargo.toml"}}" -- {{tool}} + (cd tools && cargo build) + "$CARGO_TARGET_DIR/debug/tools" {{tool}} _build-rom folder name: #!/usr/bin/env bash diff --git a/mgba-test-runner/Cargo.lock b/mgba-test-runner/Cargo.lock index cdd404be..a9dcbf58 100644 --- a/mgba-test-runner/Cargo.lock +++ b/mgba-test-runner/Cargo.lock @@ -107,9 +107,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "clang-sys" -version = "1.3.3" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a050e2153c5be08febd6734e29298e844fdb0fa21aeddd63b4eb7baa106c69b" +checksum = "fa2e27ae6ab525c3d369ded447057bca5438d86dc3a68f6faafb8269ba82ebf3" dependencies = [ "glob", "libc", @@ -118,9 +118,9 @@ dependencies = [ [[package]] name = "clap" -version = "3.2.21" +version = "3.2.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ed5341b2301a26ab80be5cbdced622e80ed808483c52e45e3310a877d3b37d7" +checksum = "86447ad904c7fb335a790c9d7fe3d0d971dc523b8ccd1561a520de9a85302750" dependencies = [ "atty", "bitflags", @@ -163,9 +163,9 @@ checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797" [[package]] name = "env_logger" -version = "0.9.0" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b2cf0344971ee6c64c31be0d530793fba457d322dfec2810c453d0ef228f9c3" +checksum = "c90bf5f19754d10198ccb95b70664fc925bd1fc090a0fd9a6ebc54acc8cd6272" dependencies = [ "atty", "humantime", @@ -213,9 +213,9 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "image" -version = "0.24.3" +version = "0.24.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e30ca2ecf7666107ff827a8e481de6a132a9b687ed3bb20bb1c144a36c00964" +checksum = "bd8e4fb07cf672b1642304e731ef8a6a4c7891d67bb4fd4f5ce58cd6ed86803c" dependencies = [ "bytemuck", "byteorder", @@ -237,9 +237,9 @@ dependencies = [ [[package]] name = "jobserver" -version = "0.1.24" +version = "0.1.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af25a77299a7f711a01975c35a6a424eb6862092cc2d6c72c4ed6cbc56dfc1fa" +checksum = "068b1ee6743e4d11fb9c6a1e6064b3693a1b600e7f5f5988047d98b3dc9fb90b" dependencies = [ "libc", ] @@ -258,9 +258,9 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "libc" -version = "0.2.132" +version = "0.2.134" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8371e4e5341c3a96db127eb2465ac681ced4c433e01dd0e938adbef26ba93ba5" +checksum = "329c933548736bc49fd575ee68c89e8be4d260064184389a5b77517cddd99ffb" [[package]] name = "libloading" @@ -355,9 +355,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.14.0" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f7254b99e31cad77da24b08ebf628882739a608578bb1bcdfc1f9c21260d7c0" +checksum = "e82dad04139b71a90c080c8463fe0dc7902db5192d939bd0950f074d014339e1" [[package]] name = "os_str_bytes" @@ -385,9 +385,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.43" +version = "1.0.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a2ca2c61bc9f3d74d2886294ab7b9853abd9c1ad903a3ac7815c58989bb7bab" +checksum = "94e2ef8dbfc347b10c094890f778ee2e36ca9bb4262e86dc99cd217e35f3470b" dependencies = [ "unicode-ident", ] @@ -447,9 +447,9 @@ dependencies = [ [[package]] name = "textwrap" -version = "0.15.0" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb" +checksum = "949517c0cf1bf4ee812e2e07e08ab448e3ae0d23472aee8a06c985f0c8815b16" [[package]] name = "unicode-ident" diff --git a/release.sh b/release.sh deleted file mode 100755 index a454e429..00000000 --- a/release.sh +++ /dev/null @@ -1,96 +0,0 @@ -#!/usr/bin/env bash - -# Fail if any command fails -set -e -set -x - -VERSION=$1 -NO_COMMIT=$2 - -# Sanity check that we actually have a version -if [ "$VERSION" = "" ]; then - echo "Usage $0 [--no-commit]" - exit 1 -fi - -# Check the format of version -if echo "$VERSION" | grep -q -Ev "^[0-9]+\.[0-9]+\.[0-9]+$"; then - echo "Version must be of the form x.y.z, got $VERSION" - exit 1 -fi - -# Check if no commit option is valid -if [ ! "$NO_COMMIT" = "" ] && [ ! "$NO_COMMIT" = "--no-commit" ]; then - echo "Must pass either no last argument or --no-commit" - exit 1 -fi - -function maybe_git() { - if [ "$NO_COMMIT" = "--no-commit" ]; then - echo "Would run: git $*" - else - git "$@" - fi -} - -# Check that no out-standing changes in git -if [ "$NO_COMMIT" = "" ] && [ -n "$(git status --porcelain)" ]; then - echo "Uncommitted changes, please commit first" - exit 1 -fi - -# Check that we are in the master branch, but only if actually committing -if [ ! "$NO_COMMIT" = "--no-commit" ] && [ "$(git symbolic-ref --short HEAD)" != "master" ]; then - echo "You must be in the master branch before releasing" - exit 1 -fi - -TAGNAME="v$VERSION" - -for PROJECT_TOML_FILE in agb/Cargo.toml agb-*/Cargo.toml; do - DIRECTORY=$(dirname "$PROJECT_TOML_FILE") - - # Update the version in Cargo.toml - sed -i -e "s/^version = \".*\"/version = \"$VERSION\"/" "$DIRECTORY/Cargo.toml" - - # Also update the lock file - (cd "$DIRECTORY" && cargo update) - - if [ "$DIRECTORY" = "agb" ]; then - # also update the agb version in the template and the examples - sed -i -e "s/^agb = \".*\"/agb = \"$VERSION\"/" template/Cargo.toml - - for EXAMPLE_TOML_FILE in examples/*/Cargo.toml book/games/*/Cargo.toml template/Cargo.toml; do - EXAMPLE_DIR=$(dirname "$EXAMPLE_TOML_FILE") - sed -E -i -e "/agb =/ s/version = \"[^\"]+\"/version = \"$VERSION\"/" "$EXAMPLE_DIR/Cargo.toml" - done - for EXAMPLE_TOML_FILE in examples/*/Cargo.toml book/games/*/Cargo.toml; do - EXAMPLE_DIR=$(dirname "$EXAMPLE_TOML_FILE") - (cd "$EXAMPLE_DIR" && cargo update) - done - else - PROJECT_NAME_WITH_UNDERSCORES=$(echo -n "$DIRECTORY" | tr - _) - - for CARGO_TOML_FILE in agb-*/Cargo.toml agb/Cargo.toml examples/*/Cargo.toml book/games/*/Cargo.toml; do - sed -i -E -e "s/($PROJECT_NAME_WITH_UNDERSCORES = .*version = \")[^\"]+(\".*)/\1$VERSION\2/" "$CARGO_TOML_FILE" - (cd "$(dirname "$CARGO_TOML_FILE")" && cargo generate-lockfile) - done - fi -done - -# Sanity check to make sure the build works -just ci - -for EXAMPLE_TOML_FILE in examples/*/Cargo.toml book/games/*/Cargo.toml; do - EXAMPLE_DIR=$(dirname "$EXAMPLE_TOML_FILE") - (cd "$EXAMPLE_DIR" && cargo check --release) -done - -# Commit the Cargo.toml changes -maybe_git commit -am "Release v$VERSION" - -# Tag the version -maybe_git tag -a "$TAGNAME" -m "v$VERSION" - -echo "Done! Push with" -echo "git push --atomic origin master $TAGNAME" diff --git a/tools/Cargo.lock b/tools/Cargo.lock index b175a490..871dc847 100644 --- a/tools/Cargo.lock +++ b/tools/Cargo.lock @@ -2,6 +2,15 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "atty" version = "0.2.14" @@ -25,6 +34,12 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "bumpalo" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1ad822118d20d2c234f427000d5acc36eabe1e29a348c89b63dd60b13f28e5d" + [[package]] name = "bytes" version = "1.2.1" @@ -32,25 +47,44 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec8a7b6a70fde80372154c65702f00a0f56f3e1c36abbc6c440484be248856db" [[package]] -name = "clap" -version = "3.2.21" +name = "cfg-if" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ed5341b2301a26ab80be5cbdced622e80ed808483c52e45e3310a877d3b37d7" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfd4d1b31faaa3a89d7934dbded3111da0d2ef28e3ebccdb4f0179f5929d1ef1" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-integer", + "num-traits", + "time", + "wasm-bindgen", + "winapi", +] + +[[package]] +name = "clap" +version = "4.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a1af219c3e254a8b4649d6ddaef886b2015089f35f2ac5e1db31410c0566ab8" dependencies = [ "atty", "bitflags", "clap_lex", - "indexmap", "strsim", "termcolor", - "textwrap", ] [[package]] name = "clap_lex" -version = "0.2.4" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5" +checksum = "0d4198f73e42b4936b35b5bb248d81d2b595ecb170da0bac7655c54eedfa8da8" dependencies = [ "os_str_bytes", ] @@ -65,12 +99,24 @@ dependencies = [ "memchr", ] +[[package]] +name = "core-foundation-sys" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" + [[package]] name = "either" version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797" +[[package]] +name = "glob" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" + [[package]] name = "hashbrown" version = "0.12.3" @@ -86,6 +132,19 @@ dependencies = [ "libc", ] +[[package]] +name = "iana-time-zone" +version = "0.1.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd911b35d940d2bd0bea0f9100068e5b97b51a1cbe13d13382f132e0365257a0" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "js-sys", + "wasm-bindgen", + "winapi", +] + [[package]] name = "indexmap" version = "1.9.1" @@ -98,18 +157,36 @@ dependencies = [ [[package]] name = "itertools" -version = "0.10.4" +version = "0.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8bf247779e67a9082a4790b45e71ac7cfd1321331a5c856a74a9faebdab78d0" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" dependencies = [ "either", ] [[package]] -name = "libc" -version = "0.2.132" +name = "js-sys" +version = "0.3.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8371e4e5341c3a96db127eb2465ac681ced4c433e01dd0e938adbef26ba93ba5" +checksum = "49409df3e3bf0856b916e2ceaca09ee28e6871cf7d9ce97a692cacfdb2a25a47" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "libc" +version = "0.2.134" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "329c933548736bc49fd575ee68c89e8be4d260064184389a5b77517cddd99ffb" + +[[package]] +name = "log" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" +dependencies = [ + "cfg-if", +] [[package]] name = "memchr" @@ -117,18 +194,72 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" +[[package]] +name = "num-integer" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e82dad04139b71a90c080c8463fe0dc7902db5192d939bd0950f074d014339e1" + [[package]] name = "os_str_bytes" version = "6.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ff7415e9ae3fff1225851df9e0d9e4e5479f947619774677a63572e55e80eff" +[[package]] +name = "proc-macro2" +version = "1.0.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94e2ef8dbfc347b10c094890f778ee2e36ca9bb4262e86dc99cd217e35f3470b" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179" +dependencies = [ + "proc-macro2", +] + [[package]] name = "strsim" version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" +[[package]] +name = "syn" +version = "1.0.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e90cde112c4b9690b8cbe810cba9ddd8bc1d7472e2cae317b69e9438c1cba7d2" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "termcolor" version = "1.1.3" @@ -139,10 +270,15 @@ dependencies = [ ] [[package]] -name = "textwrap" -version = "0.15.0" +name = "time" +version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb" +checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255" +dependencies = [ + "libc", + "wasi", + "winapi", +] [[package]] name = "toml_edit" @@ -159,10 +295,78 @@ dependencies = [ name = "tools" version = "0.1.0" dependencies = [ + "chrono", "clap", + "glob", "toml_edit", ] +[[package]] +name = "unicode-ident" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcc811dc4066ac62f84f11307873c4850cb653bfa9b1719cee2bd2204a4bc5dd" + +[[package]] +name = "wasi" +version = "0.10.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" + +[[package]] +name = "wasm-bindgen" +version = "0.2.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaf9f5aceeec8be17c128b2e93e031fb8a4d469bb9c4ae2d7dc1888b26887268" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c8ffb332579b0557b52d268b91feab8df3615f265d5270fec2a8c95b17c1142" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "052be0f94026e6cbc75cdefc9bae13fd6052cdcaf532fa6c45e7ae33a1e6c810" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bc0c051dc5f23e307b13285f9d75df86bfdf816c5721e573dec1f9b8aa193c" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c38c045535d93ec4f0b4defec448e4291638ee608530863b1e2ba115d4fff7f" + [[package]] name = "winapi" version = "0.3.9" diff --git a/tools/Cargo.toml b/tools/Cargo.toml index 25c3cb11..b0777d6c 100644 --- a/tools/Cargo.toml +++ b/tools/Cargo.toml @@ -6,5 +6,7 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -clap = "3.2" +clap = "4" toml_edit = "0.14" +glob = "0.3" +chrono = "0.4" diff --git a/tools/src/main.rs b/tools/src/main.rs index 09fce409..79118229 100644 --- a/tools/src/main.rs +++ b/tools/src/main.rs @@ -2,16 +2,35 @@ use clap::Command; mod publish; +mod release; +mod utils; -fn main() { - let matches = Command::new("Agb tools") +#[derive(Debug)] +pub enum Error { + PublishError(publish::Error), + ReleaseError(release::Error), +} + +fn cli() -> Command { + Command::new("Agb tools") .subcommand_required(true) .arg_required_else_help(true) .subcommand(publish::command()) - .get_matches(); + .subcommand(release::command()) +} + +fn main() { + let matches = cli().get_matches(); let result = match matches.subcommand() { - Some(("publish", arg_matches)) => publish::publish(arg_matches), + Some(("publish", arg_matches)) => { + publish::publish(arg_matches).map_err(Error::PublishError) + } + + Some(("release", arg_matches)) => { + release::release(arg_matches).map_err(Error::ReleaseError) + } + _ => unreachable!("Exhausted list of subcommands and subcommand_required prevents `None`"), }; @@ -19,3 +38,13 @@ fn main() { eprintln!("Error: {:?}", e); } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn verify_cli() { + cli().debug_assert(); + } +} diff --git a/tools/src/publish.rs b/tools/src/publish.rs index 9542f7e6..da637e1e 100644 --- a/tools/src/publish.rs +++ b/tools/src/publish.rs @@ -1,12 +1,14 @@ use clap::{Arg, ArgAction, ArgMatches}; use std::collections::{HashMap, HashSet}; use std::fs; -use std::path::{Path, PathBuf}; +use std::path::Path; use std::process::Command; +use std::thread; use std::time::Duration; -use std::{env, thread}; use toml_edit::Document; +use crate::utils::*; + #[derive(Debug)] pub enum Error { FindRootDirectory, @@ -17,7 +19,7 @@ pub enum Error { CargoToml, } -pub fn command() -> clap::Command<'static> { +pub fn command() -> clap::Command { clap::Command::new("publish") .about("Publishes agb and all subcrates") .arg( @@ -31,7 +33,7 @@ pub fn command() -> clap::Command<'static> { pub fn publish(matches: &ArgMatches) -> Result<(), Error> { let dry_run = matches.get_one::("Dry run").expect("defined by clap"); - let root_directory = find_agb_root_directory()?; + let root_directory = find_agb_root_directory().map_err(|_| Error::FindRootDirectory)?; let mut fully_published_crates: HashSet = HashSet::new(); let mut published_crates: HashSet = HashSet::new(); @@ -60,11 +62,12 @@ pub fn publish(matches: &ArgMatches) -> Result<(), Error> { if *dry_run { println!("Would execute cargo publish for {publishable_crate}"); } else { - Command::new("cargo") + assert!(Command::new("cargo") .arg("publish") .current_dir(&root_directory.join(publishable_crate)) - .spawn() - .map_err(|_| Error::PublishCrate)?; + .status() + .map_err(|_| Error::PublishCrate)? + .success()); } published_crates.insert(publishable_crate.to_string()); @@ -86,19 +89,6 @@ pub fn publish(matches: &ArgMatches) -> Result<(), Error> { Ok(()) } -fn find_agb_root_directory() -> Result { - let mut current_path = env::current_dir().map_err(|_| Error::FindRootDirectory)?; - - while !current_path.clone().join("justfile").exists() { - current_path = current_path - .parent() - .ok_or(Error::FindRootDirectory)? - .to_owned(); - } - - Ok(current_path) -} - fn check_if_released(crate_to_publish: &str, expected_version: &str) -> Result { let url_to_poll = &get_url_to_poll(crate_to_publish); @@ -196,6 +186,11 @@ fn read_cargo_toml(folder: &Path) -> Result { mod test { use super::*; + #[test] + fn verify_cli() { + command().debug_assert(); + } + #[test] fn url_to_poll_should_return_correct_url() { let test_cases = [ @@ -215,16 +210,9 @@ mod test { } } - #[test] - fn should_find_root_directory() -> Result<(), Error> { - assert_ne!(find_agb_root_directory()?.to_string_lossy(), ""); - - Ok(()) - } - #[test] fn should_read_version() -> Result<(), Error> { - let root_directory = find_agb_root_directory()?; + let root_directory = crate::utils::find_agb_root_directory().unwrap(); let my_version = read_cargo_toml_version(&root_directory.join("tools"))?; assert_eq!(my_version, "0.1.0"); @@ -233,7 +221,7 @@ mod test { #[test] fn should_detect_dependencies() -> Result<(), Error> { - let root_directory = find_agb_root_directory()?; + let root_directory = crate::utils::find_agb_root_directory().unwrap(); let deps = get_agb_dependencies(&root_directory.join("agb"))?; assert_eq!( diff --git a/tools/src/release.rs b/tools/src/release.rs new file mode 100644 index 00000000..30ae3d2e --- /dev/null +++ b/tools/src/release.rs @@ -0,0 +1,305 @@ +use std::{path::Path, process::Command}; + +use crate::utils::find_agb_root_directory; + +pub fn command() -> clap::Command { + clap::Command::new("release") + .about("Prepares and commits the changes required to release agb") + .arg( + clap::Arg::new("version") + .required(true) + .help("New version to release") + .value_parser(version_parser), + ) + .arg( + clap::Arg::new("Dry run") + .long("dry-run") + .help("Don't do anything with git (but does everything else)") + .action(clap::ArgAction::SetTrue), + ) +} + +pub fn release(matches: &clap::ArgMatches) -> Result<(), Error> { + let dry_run = matches.get_one::("Dry run").expect("defined by clap"); + let version = matches + .get_one::("version") + .expect("defined by clap"); + + let root_directory = find_agb_root_directory().map_err(|_| Error::FindRootDirectory)?; + + // if not dry run, check that there are no out-standing changes in git + if !dry_run && !execute_git_command(&root_directory, &["status", "--porcelain"])?.is_empty() { + println!("Uncommitted changes, please commit first"); + return Ok(()); + } + + // Check that we are in the master branch + if !dry_run + && execute_git_command(&root_directory, &["symbolic-ref", "--short", "HEAD"])? != "master" + { + println!("You must be on the master branch before releasing"); + return Ok(()); + } + + let project_toml_files = glob_many(&root_directory, &["agb-*/Cargo.toml"])?; + let agb_cargo_toml = root_directory.join("agb/Cargo.toml"); + + update_to_version(&root_directory, &agb_cargo_toml, version)?; + + for toml_file in &project_toml_files { + update_to_version(&root_directory, toml_file, version)?; + } + + assert!(Command::new("just") + .arg("ci") + .current_dir(&root_directory) + .status() + .map_err(|_| Error::JustCiFailed)? + .success()); + + let changelog_text = update_changelog(&root_directory, version)?; + + println!("Content of changelog:\n{changelog_text}"); + + if !dry_run { + execute_git_command( + &root_directory, + &["commit", "-am", &format!("Release v{version}")], + )?; + execute_git_command( + &root_directory, + &[ + "tag", + "-a", + &version.to_string(), + "-m", + &format!("#v{version}\n{changelog_text}"), + ], + )?; + } + + println!("Done! Push with"); + println!("git push --atomic origin master v{version}"); + + Ok(()) +} + +fn update_to_version( + root_directory: &Path, + toml_file: &Path, + new_version: &Version, +) -> Result<(), Error> { + let directory_name = toml_file.parent().unwrap().file_name().unwrap(); + let project_name = directory_name.to_string_lossy().replace('-', "_"); + + let toml_file_content = std::fs::read_to_string(toml_file).map_err(|_| Error::ReadTomlFile)?; + let mut cargo_toml = toml_file_content + .parse::() + .map_err(|_| Error::InvalidToml(toml_file.to_string_lossy().into_owned()))?; + + let new_version = format!("{new_version}"); + cargo_toml["package"]["version"] = toml_edit::value(&new_version); + + std::fs::write(toml_file, cargo_toml.to_string()).map_err(|_| Error::WriteTomlFile)?; + + for cargo_toml_file in glob_many( + root_directory, + &[ + "agb-*/Cargo.toml", + "agb/Cargo.toml", + "examples/*/Cargo.toml", + "book/games/*/Cargo.toml", + "template/Cargo.toml", + ], + )? { + let toml_file_content = + std::fs::read_to_string(&cargo_toml_file).map_err(|_| Error::ReadTomlFile)?; + let mut cargo_toml = toml_file_content + .parse::() + .map_err(|_| Error::InvalidToml(cargo_toml_file.to_string_lossy().into_owned()))?; + + if let Some(this_dep) = cargo_toml["dependencies"].get_mut(&project_name) { + match this_dep { + toml_edit::Item::Value(s @ toml_edit::Value::String(_)) => { + *s = new_version.clone().into() + } + toml_edit::Item::Value(toml_edit::Value::InlineTable(t)) => { + t["version"] = new_version.clone().into() + } + toml_edit::Item::None => continue, + _ => { + return Err(Error::InvalidToml(format!( + "{:?} while seaching dependencies in {}", + this_dep, + cargo_toml_file.to_string_lossy() + ))) + } + } + } + + std::fs::write(cargo_toml_file, cargo_toml.to_string()) + .map_err(|_| Error::WriteTomlFile)?; + } + + Ok(()) +} + +fn update_changelog(root_directory: &Path, new_version: &Version) -> Result { + use chrono::Datelike; + + let changelog_file = root_directory.join("CHANGELOG.md"); + let changelog_content = + std::fs::read_to_string(&changelog_file).map_err(|_| Error::FailedToReadChangelog)?; + + let today = chrono::Local::today(); + let formatted_date = format!( + "{:04}/{:02}/{:02}", + today.year(), + today.month(), + today.day() + ); + + const UNRELEASED_HEADER: &str = "## [Unreleased]"; + + let unreleased_bit_start = changelog_content + .find(UNRELEASED_HEADER) + .ok_or(Error::FailedToParseChangelog)? + + UNRELEASED_HEADER.len(); + let unreleased_bit_end = changelog_content[unreleased_bit_start..] + .find("\n## [") // the start of the next entry + .ok_or(Error::FailedToParseChangelog)? + + unreleased_bit_start; + + let change_content = changelog_content[unreleased_bit_start..unreleased_bit_end].to_owned(); + + let changelog_content = changelog_content.replacen( + UNRELEASED_HEADER, + &format!("{UNRELEASED_HEADER}\n\n## [{new_version}] - {formatted_date}"), + 1, + ); + + std::fs::write(&changelog_file, &changelog_content) + .map_err(|_| Error::FailedToWriteChangelog)?; + + Ok(change_content) +} + +fn execute_git_command(root_directory: &Path, args: &[&str]) -> Result { + let git_cmd = Command::new("git") + .args(args) + .current_dir(root_directory) + .output() + .map_err(|_| Error::Git("Failed to run command"))?; + + assert!(git_cmd.status.success()); + + String::from_utf8(git_cmd.stdout).map_err(|_| Error::Git("Output not utf-8")) +} + +fn glob_many(root_directory: &Path, globs: &[&str]) -> Result, Error> { + let mut result = vec![]; + + for g in globs.iter() { + for path in glob::glob(&root_directory.join(g).to_string_lossy()).expect("Invalid glob") { + result.push(path.map_err(|_| Error::Glob)?); + } + } + + Ok(result) +} + +#[derive(Debug)] +pub enum Error { + FindRootDirectory, + Git(&'static str), + Glob, + ReadTomlFile, + InvalidToml(String), + WriteTomlFile, + JustCiFailed, + CargoUpdateFailed, + FailedToReadChangelog, + FailedToWriteChangelog, + FailedToParseChangelog, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +struct Version { + major: u32, + minor: u32, + patch: u32, +} + +impl Version { + #[cfg(test)] + pub fn new(major: u32, minor: u32, patch: u32) -> Self { + Self { + major, + minor, + patch, + } + } +} + +impl std::fmt::Display for Version { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}.{}.{}", self.major, self.minor, self.patch) + } +} + +#[derive(Debug, PartialEq, Eq)] +struct ParseVersionError; + +impl std::str::FromStr for Version { + type Err = ParseVersionError; + + fn from_str(s: &str) -> Result { + let version_array: Vec<_> = s + .split('.') + .map(|v| v.parse()) + .collect::, _>>() + .map_err(|_| ParseVersionError)?; + + if version_array.len() > 3 || version_array.is_empty() { + return Err(ParseVersionError); + } + + Ok(Version { + major: version_array[0], + minor: *version_array.get(1).unwrap_or(&0), + patch: *version_array.get(2).unwrap_or(&0), + }) + } +} + +fn version_parser(maybe_version: &str) -> Result { + maybe_version + .parse() + .map_err(|_| "Failed to parse version, must be of the format x.y.z") +} + +#[cfg(test)] +mod tests { + use super::*; + use std::str::FromStr; + + #[test] + fn verify_cli() { + command().debug_assert(); + } + + #[test] + fn can_parse_versions() { + assert_eq!(Version::from_str("0.1.2").unwrap(), Version::new(0, 1, 2)); + assert_eq!(Version::from_str("0.1").unwrap(), Version::new(0, 1, 0)); + assert_eq!( + Version::from_str("33.23.4000").unwrap(), + Version::new(33, 23, 4000) + ); + + assert_eq!(Version::from_str("abc").unwrap_err(), ParseVersionError); + assert_eq!(Version::from_str("").unwrap_err(), ParseVersionError); + assert_eq!(Version::from_str("0.2.4.5").unwrap_err(), ParseVersionError); + assert_eq!(Version::from_str("0.2.4a").unwrap_err(), ParseVersionError); + } +} diff --git a/tools/src/utils.rs b/tools/src/utils.rs new file mode 100644 index 00000000..a9759165 --- /dev/null +++ b/tools/src/utils.rs @@ -0,0 +1,27 @@ +use std::{env, path::PathBuf}; + +#[derive(Debug)] +pub struct FindRootDirectoryError; + +pub fn find_agb_root_directory() -> Result { + let mut current_path = env::current_dir().map_err(|_| FindRootDirectoryError)?; + + while !current_path.clone().join("justfile").exists() { + current_path = current_path + .parent() + .ok_or(FindRootDirectoryError)? + .to_owned(); + } + + Ok(current_path) +} + +#[cfg(test)] +mod tests { + use super::find_agb_root_directory; + + #[test] + fn find_agb_root_directory_works() { + find_agb_root_directory().unwrap(); + } +}