Merge remote-tracking branch 'origin/master' into affine

This commit is contained in:
Gwilym Kuiper 2022-10-08 17:05:00 +01:00
commit e63e830a9c
43 changed files with 2513 additions and 262 deletions

View file

@ -32,6 +32,12 @@
}, },
{ {
"path": "../book" "path": "../book"
},
{
"path": "../mgba-test-runner"
},
{
"path": "../tools"
} }
] ]
} }

View file

@ -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 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. - 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 a new agb::sync module that contains GBA-specific synchronization primitives.
- Added support for save files.
- Added implementation of `HashMap.retain()`.
### Changes ### Changes
- Many of the places that originally disabled IRQs now use the `sync` module, reducing the chance of missed interrupts. - 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
- Fixed the fast magnitude function in agb_fixnum. This is also used in fast_normalise. Previously only worked for positive (x, y). - 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. - 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. - `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 ## [0.11.1] - 2022/08/02

View file

@ -2,6 +2,11 @@
## Rust for the Game Boy Advance ## 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) ![AGB logo](.github/logo.png)
This is a library for making games on the Game Boy Advance using the Rust This is a library for making games on the Game Boy Advance using the Rust

View file

@ -7,6 +7,7 @@ fn main() {
"src/sound/mixer/mixer.s", "src/sound/mixer/mixer.s",
"src/agbabi/memset.s", "src/agbabi/memset.s",
"src/agbabi/memcpy.s", "src/agbabi/memcpy.s",
"src/save/asm_routines.s",
]; ];
println!("cargo:rerun-if-changed=gba.ld"); println!("cargo:rerun-if-changed=gba.ld");

View file

@ -43,7 +43,7 @@ impl BumpAllocator {
let resulting_ptr = ptr + amount_to_add; let resulting_ptr = ptr + amount_to_add;
let new_current_ptr = resulting_ptr + layout.size(); 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; return None;
} }

View file

@ -13,3 +13,19 @@
.size \functionName,.-\functionName .size \functionName,.-\functionName
.endfunc .endfunc
.endm .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

View file

@ -37,5 +37,8 @@ mod tests {
display_logo(&mut map, &mut vram); display_logo(&mut map, &mut vram);
crate::test_runner::assert_image_output("gfx/test_logo.png"); crate::test_runner::assert_image_output("gfx/test_logo.png");
map.clear(&mut vram);
vram.gc();
} }
} }

View file

@ -794,7 +794,7 @@ impl ObjectController {
}); });
let loan = Loan { let loan = Loan {
index: index as u8, index,
phantom: PhantomData, phantom: PhantomData,
}; };
@ -916,8 +916,8 @@ impl<'a> Object<'a> {
/// [ObjectController::commit] is called. /// [ObjectController::commit] is called.
pub fn set_x(&mut self, x: u16) -> &mut Self { pub fn set_x(&mut self, x: u16) -> &mut Self {
let object_inner = unsafe { self.object_inner() }; let object_inner = unsafe { self.object_inner() };
object_inner.attrs.a1a.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) as u16); object_inner.attrs.a1s.set_x(x.rem_euclid(1 << 9));
self self
} }

View file

@ -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)); DMA3_CONTROL.set(count as u32 | (1 << 31));
} }
pub(crate) fn dma3_exclusive<R>(f: impl FnOnce() -> R) -> R {
const DMA0_CTRL_HI: MemoryMapped<u16> = unsafe { MemoryMapped::new(dma_control_addr(0) + 2) };
const DMA1_CTRL_HI: MemoryMapped<u16> = unsafe { MemoryMapped::new(dma_control_addr(1) + 2) };
const DMA2_CTRL_HI: MemoryMapped<u16> = 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
})
}

View file

@ -188,7 +188,11 @@ impl<K, V, ALLOCATOR: ClonableAllocator> HashMap<K, V, ALLOCATOR> {
/// An iterator visiting all key-value pairs in an arbitrary order /// An iterator visiting all key-value pairs in an arbitrary order
pub fn iter(&self) -> impl Iterator<Item = (&'_ K, &'_ V)> { pub fn iter(&self) -> impl Iterator<Item = (&'_ K, &'_ V)> {
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 /// An iterator visiting all key-value pairs in an arbitrary order, with mutable references to the values
@ -196,6 +200,14 @@ impl<K, V, ALLOCATOR: ClonableAllocator> HashMap<K, V, ALLOCATOR> {
self.nodes.nodes.iter_mut().filter_map(Node::key_value_mut) self.nodes.nodes.iter_mut().filter_map(Node::key_value_mut)
} }
/// Retains only the elements specified by the predicate `f`.
pub fn retain<F>(&mut self, f: F)
where
F: FnMut(&K, &mut V) -> bool,
{
self.nodes.retain(f);
}
/// Returns `true` if the map contains no elements /// Returns `true` if the map contains no elements
#[must_use] #[must_use]
pub fn is_empty(&self) -> bool { pub fn is_empty(&self) -> bool {
@ -332,6 +344,7 @@ where
pub struct Iter<'a, K: 'a, V: 'a, ALLOCATOR: ClonableAllocator> { pub struct Iter<'a, K: 'a, V: 'a, ALLOCATOR: ClonableAllocator> {
map: &'a HashMap<K, V, ALLOCATOR>, map: &'a HashMap<K, V, ALLOCATOR>,
at: usize, at: usize,
num_found: usize,
} }
impl<'a, K, V, ALLOCATOR: ClonableAllocator> Iterator for Iter<'a, K, V, ALLOCATOR> { 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; self.at += 1;
if node.has_value() { if node.has_value() {
self.num_found += 1;
return Some((node.key_ref().unwrap(), node.value_ref().unwrap())); return Some((node.key_ref().unwrap(), node.value_ref().unwrap()));
} }
} }
} }
fn size_hint(&self) -> (usize, Option<usize>) {
(
self.map.len() - self.num_found,
Some(self.map.len() - self.num_found),
)
}
} }
impl<'a, K, V, ALLOCATOR: ClonableAllocator> IntoIterator for &'a HashMap<K, V, ALLOCATOR> { impl<'a, K, V, ALLOCATOR: ClonableAllocator> IntoIterator for &'a HashMap<K, V, ALLOCATOR> {
@ -358,7 +379,11 @@ impl<'a, K, V, ALLOCATOR: ClonableAllocator> IntoIterator for &'a HashMap<K, V,
type IntoIter = Iter<'a, K, V, ALLOCATOR>; type IntoIter = Iter<'a, K, V, ALLOCATOR>;
fn into_iter(self) -> Self::IntoIter { 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<K, V,
pub struct IterOwned<K, V, ALLOCATOR: Allocator = Global> { pub struct IterOwned<K, V, ALLOCATOR: Allocator = Global> {
map: HashMap<K, V, ALLOCATOR>, map: HashMap<K, V, ALLOCATOR>,
at: usize, at: usize,
num_found: usize,
} }
impl<K, V, ALLOCATOR: ClonableAllocator> Iterator for IterOwned<K, V, ALLOCATOR> { impl<K, V, ALLOCATOR: ClonableAllocator> Iterator for IterOwned<K, V, ALLOCATOR> {
@ -384,10 +410,18 @@ impl<K, V, ALLOCATOR: ClonableAllocator> Iterator for IterOwned<K, V, ALLOCATOR>
self.at += 1; self.at += 1;
if let Some((k, v, _)) = maybe_kv { if let Some((k, v, _)) = maybe_kv {
self.num_found += 1;
return Some((k, v)); return Some((k, v));
} }
} }
} }
fn size_hint(&self) -> (usize, Option<usize>) {
(
self.map.len() - self.num_found,
Some(self.map.len() - self.num_found),
)
}
} }
/// An iterator over entries of a [`HashMap`] /// An iterator over entries of a [`HashMap`]
@ -399,7 +433,11 @@ impl<K, V, ALLOCATOR: ClonableAllocator> IntoIterator for HashMap<K, V, ALLOCATO
type IntoIter = IterOwned<K, V, ALLOCATOR>; type IntoIter = IterOwned<K, V, ALLOCATOR>;
fn into_iter(self) -> Self::IntoIter { fn into_iter(self) -> Self::IntoIter {
IterOwned { map: self, at: 0 } IterOwned {
map: self,
at: 0,
num_found: 0,
}
} }
} }
@ -729,6 +767,31 @@ impl<K, V, ALLOCATOR: ClonableAllocator> NodeStorage<K, V, ALLOCATOR> {
inserted_location inserted_location
} }
fn retain<F>(&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 { fn remove_from_location(&mut self, location: usize) -> V {
let mut current_location = location; let mut current_location = location;
self.number_of_items -= 1; self.number_of_items -= 1;
@ -1222,6 +1285,54 @@ mod test {
drop_registry.assert_dropped_n_times(id1, 2); 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 // Following test cases copied from the rust source
// https://github.com/rust-lang/rust/blob/master/library/std/src/collections/hash/map/tests.rs // https://github.com/rust-lang/rust/blob/master/library/std/src/collections/hash/map/tests.rs
mod rust_std_tests { mod rust_std_tests {

View file

@ -168,6 +168,7 @@ pub use agb_fixnum as fixnum;
pub mod hash_map; pub mod hash_map;
/// Simple random number generator /// Simple random number generator
pub mod rng; pub mod rng;
pub mod save;
mod single; mod single;
/// Implements sound output. /// Implements sound output.
pub mod sound; pub mod sound;
@ -223,6 +224,8 @@ pub struct Gba {
pub sound: sound::dmg::Sound, pub sound: sound::dmg::Sound,
/// Manages access to the Game Boy Advance's direct sound mixer for playing raw wav files. /// Manages access to the Game Boy Advance's direct sound mixer for playing raw wav files.
pub mixer: sound::mixer::MixerController, 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. /// Manages access to the Game Boy Advance's 4 timers.
pub timers: timer::TimerController, pub timers: timer::TimerController,
} }
@ -239,6 +242,7 @@ impl Gba {
display: display::Display::new(), display: display::Display::new(),
sound: sound::dmg::Sound::new(), sound: sound::dmg::Sound::new(),
mixer: sound::mixer::MixerController::new(), mixer: sound::mixer::MixerController::new(),
save: save::SaveManager::new(),
timers: timer::TimerController::new(), timers: timer::TimerController::new(),
} }
} }

View file

@ -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

63
agb/src/save/asm_utils.rs Normal file
View file

@ -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 _)
}

273
agb/src/save/eeprom.rs Normal file
View file

@ -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<u16> = 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(&sector[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<bool, 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());
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<bool, Error> {
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<bool, Error> {
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)
}
}

472
agb/src/save/flash.rs Normal file
View file

@ -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<u8> = unsafe { MemoryMapped::new(0x0E000000) };
const FLASH_PORT_A: MemoryMapped<u8> = unsafe { MemoryMapped::new(0x0E005555) };
const FLASH_PORT_B: MemoryMapped<u8> = unsafe { MemoryMapped::new(0x0E002AAA) };
const FLASH_DATA: MemoryMapped1DArray<u8, 65536> = 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<u8> = 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<Self, Error> {
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<u16, Error> {
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<bool, 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());
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, &sector, 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<bool, Error> {
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(())
}
}
}

457
agb/src/save/mod.rs Normal file
View file

@ -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<bool, Error>;
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<Option<&'static dyn RawSaveAccess>> = 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<Timer>) -> Result<SaveData, Error> {
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<usize>) -> 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<bool, Error> {
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<usize>) -> Range<usize> {
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<usize>) -> Result<SavePreparedBlock, Error> {
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<usize>,
}
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>(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, Error> {
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, Error> {
SaveData::new(Some(timer))
}
}

57
agb/src/save/sram.rs Normal file
View file

@ -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<bool, Error> {
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(())
}
}

59
agb/src/save/utils.rs Normal file
View file

@ -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<Timer>,
}
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<Timer>) -> 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<RawMutexGuard<'static>, Error> {
static LOCK: RawMutex = RawMutex::new();
match LOCK.try_lock() {
Some(x) => Ok(x),
None => Err(Error::MediaInUse),
}
}

View file

@ -284,7 +284,7 @@ mod test {
// the actual main test loop // the actual main test loop
let mut interrupt_seen = false; let mut interrupt_seen = false;
let mut no_interrupt_seen = false; let mut no_interrupt_seen = false;
for i in 0..100000 { for i in 0..250000 {
// write to the static // write to the static
let new_value = [i; COUNT]; let new_value = [i; COUNT];
value.write(new_value); value.write(new_value);

View file

@ -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<MediaInfo> = 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");
}
}

View file

@ -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 {}
}

View file

@ -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 {}
}

View file

@ -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 {}
}

View file

@ -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 {}
}

View file

@ -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 {}
}

View file

@ -217,9 +217,9 @@ dependencies = [
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.132" version = "0.2.134"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8371e4e5341c3a96db127eb2465ac681ced4c433e01dd0e938adbef26ba93ba5" checksum = "329c933548736bc49fd575ee68c89e8be4d260064184389a5b77517cddd99ffb"
[[package]] [[package]]
name = "log" name = "log"
@ -318,9 +318,9 @@ dependencies = [
[[package]] [[package]]
name = "once_cell" name = "once_cell"
version = "1.14.0" version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f7254b99e31cad77da24b08ebf628882739a608578bb1bcdfc1f9c21260d7c0" checksum = "e82dad04139b71a90c080c8463fe0dc7902db5192d939bd0950f074d014339e1"
[[package]] [[package]]
name = "png" name = "png"
@ -343,9 +343,9 @@ dependencies = [
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.43" version = "1.0.46"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a2ca2c61bc9f3d74d2886294ab7b9853abd9c1ad903a3ac7815c58989bb7bab" checksum = "94e2ef8dbfc347b10c094890f778ee2e36ca9bb4262e86dc99cd217e35f3470b"
dependencies = [ dependencies = [
"unicode-ident", "unicode-ident",
] ]
@ -367,18 +367,18 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
[[package]] [[package]]
name = "serde" name = "serde"
version = "1.0.144" version = "1.0.145"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f747710de3dcd43b88c9168773254e809d8ddbdf9653b84e2554ab219f17860" checksum = "728eb6351430bccb993660dfffc5a72f91ccc1295abaa8ce19b27ebe4f75568b"
dependencies = [ dependencies = [
"serde_derive", "serde_derive",
] ]
[[package]] [[package]]
name = "serde_derive" name = "serde_derive"
version = "1.0.144" version = "1.0.145"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94ed3a816fb1d101812f83e789f888322c34e291f894f19590dc310963e87a00" checksum = "81fa1584d3d1bcacd84c277a0dfe21f5b0f6accf4a23d04d4c6d61f1af522b4c"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -393,9 +393,9 @@ checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
[[package]] [[package]]
name = "syn" name = "syn"
version = "1.0.99" version = "1.0.101"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "58dbef6ec655055e20b86b15a8cc6d439cca19b667537ac6a1369572d151ab13" checksum = "e90cde112c4b9690b8cbe810cba9ddd8bc1d7472e2cae317b69e9438c1cba7d2"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",

View file

@ -225,9 +225,9 @@ dependencies = [
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.132" version = "0.2.134"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8371e4e5341c3a96db127eb2465ac681ced4c433e01dd0e938adbef26ba93ba5" checksum = "329c933548736bc49fd575ee68c89e8be4d260064184389a5b77517cddd99ffb"
[[package]] [[package]]
name = "log" name = "log"
@ -326,9 +326,9 @@ dependencies = [
[[package]] [[package]]
name = "once_cell" name = "once_cell"
version = "1.14.0" version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f7254b99e31cad77da24b08ebf628882739a608578bb1bcdfc1f9c21260d7c0" checksum = "e82dad04139b71a90c080c8463fe0dc7902db5192d939bd0950f074d014339e1"
[[package]] [[package]]
name = "png" name = "png"
@ -344,9 +344,9 @@ dependencies = [
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.43" version = "1.0.46"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a2ca2c61bc9f3d74d2886294ab7b9853abd9c1ad903a3ac7815c58989bb7bab" checksum = "94e2ef8dbfc347b10c094890f778ee2e36ca9bb4262e86dc99cd217e35f3470b"
dependencies = [ dependencies = [
"unicode-ident", "unicode-ident",
] ]
@ -368,18 +368,18 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
[[package]] [[package]]
name = "serde" name = "serde"
version = "1.0.144" version = "1.0.145"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f747710de3dcd43b88c9168773254e809d8ddbdf9653b84e2554ab219f17860" checksum = "728eb6351430bccb993660dfffc5a72f91ccc1295abaa8ce19b27ebe4f75568b"
dependencies = [ dependencies = [
"serde_derive", "serde_derive",
] ]
[[package]] [[package]]
name = "serde_derive" name = "serde_derive"
version = "1.0.144" version = "1.0.145"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94ed3a816fb1d101812f83e789f888322c34e291f894f19590dc310963e87a00" checksum = "81fa1584d3d1bcacd84c277a0dfe21f5b0f6accf4a23d04d4c6d61f1af522b4c"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -394,9 +394,9 @@ checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
[[package]] [[package]]
name = "syn" name = "syn"
version = "1.0.99" version = "1.0.101"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "58dbef6ec655055e20b86b15a8cc6d439cca19b667537ac6a1369572d151ab13" checksum = "e90cde112c4b9690b8cbe810cba9ddd8bc1d7472e2cae317b69e9438c1cba7d2"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",

View file

@ -207,7 +207,7 @@ impl RolledDice {
let heal = *face_counts.entry(Face::Heal).or_default(); let heal = *face_counts.entry(Face::Heal).or_default();
if heal != 0 { if heal != 0 {
actions.push(Action::PlayerHeal { actions.push(Action::PlayerHeal {
amount: ((heal * (heal + 1)) / 2) as u32, amount: (heal * (heal + 1)) / 2,
}); });
} }

View file

@ -80,7 +80,7 @@ pub fn generate_upgrades(level: u32) -> Vec<Face> {
.chain(core::iter::once(&next)) .chain(core::iter::once(&next))
.filter(|&x| *x == Face::Malfunction) .filter(|&x| *x == Face::Malfunction)
.count(); .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 if upgrade_value(&upgrades, next) <= max_upgrade_value
&& number_of_malfunctions <= maximum_number_of_malfunctions && number_of_malfunctions <= maximum_number_of_malfunctions
{ {

View file

@ -96,10 +96,10 @@ struct Agb<'a> {
} }
fn main(mut gba: agb::Gba) -> ! { 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 { 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(); let gfx = gba.display.object.get();
@ -207,7 +207,8 @@ fn main(mut gba: agb::Gba) -> ! {
agb.obj.commit(); agb.obj.commit();
agb.sfx.customise(); agb.sfx.customise();
if save::load_high_score() < current_level { 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; break;
} }

View file

@ -1,44 +1,42 @@
use agb::interrupt::free; use agb::Gba;
use bare_metal::Mutex; use agb::save::Error;
use core::cell::RefCell; use agb::sync::Static;
const RAM_ADDRESS: *mut u8 = 0x0E00_0000 as *mut u8; static HIGHSCORE: Static<u32> = Static::new(0);
const HIGH_SCORE_ADDRESS_START: *mut u8 = RAM_ADDRESS.wrapping_offset(1);
static HIGHSCORE: Mutex<RefCell<u32>> = Mutex::new(RefCell::new(0)); pub fn init_save(gba: &mut Gba) -> Result<(), Error> {
gba.save.init_sram();
pub fn init_save() { let mut access = gba.save.access()?;
if (unsafe { RAM_ADDRESS.read_volatile() } == !0) {
save_high_score(0);
unsafe { RAM_ADDRESS.write_volatile(0) };
}
let mut a = [0; 4]; let mut buffer = [0; 1];
for (idx, a) in a.iter_mut().enumerate() { access.read(0, &mut buffer)?;
*a = unsafe { HIGH_SCORE_ADDRESS_START.add(idx).read_volatile() };
}
let high_score = u32::from_le_bytes(a); if buffer[0] != 0 {
access.prepare_write(0..1)?.write(0, &[0])?;
free(|cs| { core::mem::drop(access);
if high_score > 100 { save_high_score(gba, 0)?;
HIGHSCORE.borrow(cs).replace(0);
} else { } else {
HIGHSCORE.borrow(cs).replace(high_score); let mut buffer = [0; 4];
access.read(1, &mut buffer)?;
let high_score = u32::from_le_bytes(buffer);
if high_score > 100 {
HIGHSCORE.write(0)
} else {
HIGHSCORE.write(high_score)
} }
}); }
Ok(())
} }
pub fn load_high_score() -> u32 { pub fn load_high_score() -> u32 {
free(|cs| *HIGHSCORE.borrow(cs).borrow()) HIGHSCORE.read()
} }
pub fn save_high_score(score: u32) { pub fn save_high_score(gba: &mut Gba, score: u32) -> Result<(), Error> {
let a = score.to_le_bytes(); gba.save.access()?.prepare_write(1..5)?.write(1, &score.to_le_bytes())?;
HIGHSCORE.write(score);
for (idx, &a) in a.iter().enumerate() { Ok(())
unsafe { HIGH_SCORE_ADDRESS_START.add(idx).write_volatile(a) };
}
free(|cs| HIGHSCORE.borrow(cs).replace(score));
} }

View file

@ -223,9 +223,9 @@ checksum = "6c8af84674fe1f223a982c933a0ee1086ac4d4052aa0fb8060c12c6ad838e754"
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.132" version = "0.2.134"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8371e4e5341c3a96db127eb2465ac681ced4c433e01dd0e938adbef26ba93ba5" checksum = "329c933548736bc49fd575ee68c89e8be4d260064184389a5b77517cddd99ffb"
[[package]] [[package]]
name = "log" name = "log"
@ -324,9 +324,9 @@ dependencies = [
[[package]] [[package]]
name = "once_cell" name = "once_cell"
version = "1.14.0" version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f7254b99e31cad77da24b08ebf628882739a608578bb1bcdfc1f9c21260d7c0" checksum = "e82dad04139b71a90c080c8463fe0dc7902db5192d939bd0950f074d014339e1"
[[package]] [[package]]
name = "png" name = "png"
@ -342,9 +342,9 @@ dependencies = [
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.43" version = "1.0.46"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a2ca2c61bc9f3d74d2886294ab7b9853abd9c1ad903a3ac7815c58989bb7bab" checksum = "94e2ef8dbfc347b10c094890f778ee2e36ca9bb4262e86dc99cd217e35f3470b"
dependencies = [ dependencies = [
"unicode-ident", "unicode-ident",
] ]
@ -372,18 +372,18 @@ checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09"
[[package]] [[package]]
name = "serde" name = "serde"
version = "1.0.144" version = "1.0.145"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f747710de3dcd43b88c9168773254e809d8ddbdf9653b84e2554ab219f17860" checksum = "728eb6351430bccb993660dfffc5a72f91ccc1295abaa8ce19b27ebe4f75568b"
dependencies = [ dependencies = [
"serde_derive", "serde_derive",
] ]
[[package]] [[package]]
name = "serde_derive" name = "serde_derive"
version = "1.0.144" version = "1.0.145"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94ed3a816fb1d101812f83e789f888322c34e291f894f19590dc310963e87a00" checksum = "81fa1584d3d1bcacd84c277a0dfe21f5b0f6accf4a23d04d4c6d61f1af522b4c"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -409,9 +409,9 @@ checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
[[package]] [[package]]
name = "syn" name = "syn"
version = "1.0.99" version = "1.0.101"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "58dbef6ec655055e20b86b15a8cc6d439cca19b667537ac6a1369572d151ab13" checksum = "e90cde112c4b9690b8cbe810cba9ddd8bc1d7472e2cae317b69e9438c1cba7d2"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",

View file

@ -241,9 +241,9 @@ dependencies = [
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.132" version = "0.2.134"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8371e4e5341c3a96db127eb2465ac681ced4c433e01dd0e938adbef26ba93ba5" checksum = "329c933548736bc49fd575ee68c89e8be4d260064184389a5b77517cddd99ffb"
[[package]] [[package]]
name = "libflate" name = "libflate"
@ -354,9 +354,9 @@ dependencies = [
[[package]] [[package]]
name = "once_cell" name = "once_cell"
version = "1.14.0" version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f7254b99e31cad77da24b08ebf628882739a608578bb1bcdfc1f9c21260d7c0" checksum = "e82dad04139b71a90c080c8463fe0dc7902db5192d939bd0950f074d014339e1"
[[package]] [[package]]
name = "png" name = "png"
@ -372,9 +372,9 @@ dependencies = [
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.43" version = "1.0.46"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a2ca2c61bc9f3d74d2886294ab7b9853abd9c1ad903a3ac7815c58989bb7bab" checksum = "94e2ef8dbfc347b10c094890f778ee2e36ca9bb4262e86dc99cd217e35f3470b"
dependencies = [ dependencies = [
"unicode-ident", "unicode-ident",
] ]
@ -402,18 +402,18 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
[[package]] [[package]]
name = "serde" name = "serde"
version = "1.0.144" version = "1.0.145"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f747710de3dcd43b88c9168773254e809d8ddbdf9653b84e2554ab219f17860" checksum = "728eb6351430bccb993660dfffc5a72f91ccc1295abaa8ce19b27ebe4f75568b"
dependencies = [ dependencies = [
"serde_derive", "serde_derive",
] ]
[[package]] [[package]]
name = "serde_derive" name = "serde_derive"
version = "1.0.144" version = "1.0.145"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94ed3a816fb1d101812f83e789f888322c34e291f894f19590dc310963e87a00" checksum = "81fa1584d3d1bcacd84c277a0dfe21f5b0f6accf4a23d04d4c6d61f1af522b4c"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -428,9 +428,9 @@ checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
[[package]] [[package]]
name = "syn" name = "syn"
version = "1.0.99" version = "1.0.101"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "58dbef6ec655055e20b86b15a8cc6d439cca19b667537ac6a1369572d151ab13" checksum = "e90cde112c4b9690b8cbe810cba9ddd8bc1d7472e2cae317b69e9438c1cba7d2"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",

View file

@ -129,7 +129,7 @@ impl<'a> Level<'a> {
let factor: Number = Number::new(1) / Number::new(8); let factor: Number = Number::new(1) / Number::new(8);
let (x, y) = (v * factor).floor().get(); 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())); return Some(Rect::new((x * 8, y * 8).into(), (8, 8).into()));
} }
let position = tilemap::WIDTH as usize * y as usize + x as usize; let position = tilemap::WIDTH as usize * y as usize + x as usize;
@ -1878,7 +1878,7 @@ enum MoveState {
impl<'a> Game<'a> { impl<'a> Game<'a> {
fn has_just_reached_end(&self) -> bool { fn has_just_reached_end(&self) -> bool {
match self.boss { 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, _ => false,
} }
} }
@ -1901,13 +1901,13 @@ impl<'a> Game<'a> {
if self.has_just_reached_end() { if self.has_just_reached_end() {
sfx.boss(); 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.move_state = MoveState::PinnedAtEnd;
self.boss = BossState::Active(Boss::new(object_controller, self.offset)) self.boss = BossState::Active(Boss::new(object_controller, self.offset))
} }
} }
MoveState::PinnedAtEnd => { MoveState::PinnedAtEnd => {
self.offset.x = (tilemap::WIDTH as i32 * 8 - 248).into(); self.offset.x = (tilemap::WIDTH * 8 - 248).into();
} }
MoveState::FollowingPlayer => { MoveState::FollowingPlayer => {
Game::update_sunrise(vram, self.sunrise_timer); 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); let difference = self.player.entity.position.x - (self.offset.x + WIDTH / 2);
self.offset.x += difference / 8; self.offset.x += difference / 8;
if self.offset.x > (tilemap::WIDTH as i32 * 8 - 248).into() { if self.offset.x > (tilemap::WIDTH * 8 - 248).into() {
self.offset.x = (tilemap::WIDTH as i32 * 8 - 248).into(); self.offset.x = (tilemap::WIDTH * 8 - 248).into();
} else if self.offset.x < 8.into() { } else if self.offset.x < 8.into() {
self.offset.x = 8.into(); self.offset.x = 8.into();
self.move_state = MoveState::Ending; self.move_state = MoveState::Ending;

View file

@ -68,8 +68,11 @@ update-linker-scripts:
publish: (_run-tool "publish") publish: (_run-tool "publish")
release +args: (_run-tool "release" args)
_run-tool +tool: _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: _build-rom folder name:
#!/usr/bin/env bash #!/usr/bin/env bash

View file

@ -107,9 +107,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]] [[package]]
name = "clang-sys" name = "clang-sys"
version = "1.3.3" version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a050e2153c5be08febd6734e29298e844fdb0fa21aeddd63b4eb7baa106c69b" checksum = "fa2e27ae6ab525c3d369ded447057bca5438d86dc3a68f6faafb8269ba82ebf3"
dependencies = [ dependencies = [
"glob", "glob",
"libc", "libc",
@ -118,9 +118,9 @@ dependencies = [
[[package]] [[package]]
name = "clap" name = "clap"
version = "3.2.21" version = "3.2.22"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ed5341b2301a26ab80be5cbdced622e80ed808483c52e45e3310a877d3b37d7" checksum = "86447ad904c7fb335a790c9d7fe3d0d971dc523b8ccd1561a520de9a85302750"
dependencies = [ dependencies = [
"atty", "atty",
"bitflags", "bitflags",
@ -163,9 +163,9 @@ checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797"
[[package]] [[package]]
name = "env_logger" name = "env_logger"
version = "0.9.0" version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b2cf0344971ee6c64c31be0d530793fba457d322dfec2810c453d0ef228f9c3" checksum = "c90bf5f19754d10198ccb95b70664fc925bd1fc090a0fd9a6ebc54acc8cd6272"
dependencies = [ dependencies = [
"atty", "atty",
"humantime", "humantime",
@ -213,9 +213,9 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
[[package]] [[package]]
name = "image" name = "image"
version = "0.24.3" version = "0.24.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e30ca2ecf7666107ff827a8e481de6a132a9b687ed3bb20bb1c144a36c00964" checksum = "bd8e4fb07cf672b1642304e731ef8a6a4c7891d67bb4fd4f5ce58cd6ed86803c"
dependencies = [ dependencies = [
"bytemuck", "bytemuck",
"byteorder", "byteorder",
@ -237,9 +237,9 @@ dependencies = [
[[package]] [[package]]
name = "jobserver" name = "jobserver"
version = "0.1.24" version = "0.1.25"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af25a77299a7f711a01975c35a6a424eb6862092cc2d6c72c4ed6cbc56dfc1fa" checksum = "068b1ee6743e4d11fb9c6a1e6064b3693a1b600e7f5f5988047d98b3dc9fb90b"
dependencies = [ dependencies = [
"libc", "libc",
] ]
@ -258,9 +258,9 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.132" version = "0.2.134"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8371e4e5341c3a96db127eb2465ac681ced4c433e01dd0e938adbef26ba93ba5" checksum = "329c933548736bc49fd575ee68c89e8be4d260064184389a5b77517cddd99ffb"
[[package]] [[package]]
name = "libloading" name = "libloading"
@ -355,9 +355,9 @@ dependencies = [
[[package]] [[package]]
name = "once_cell" name = "once_cell"
version = "1.14.0" version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f7254b99e31cad77da24b08ebf628882739a608578bb1bcdfc1f9c21260d7c0" checksum = "e82dad04139b71a90c080c8463fe0dc7902db5192d939bd0950f074d014339e1"
[[package]] [[package]]
name = "os_str_bytes" name = "os_str_bytes"
@ -385,9 +385,9 @@ dependencies = [
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.43" version = "1.0.46"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a2ca2c61bc9f3d74d2886294ab7b9853abd9c1ad903a3ac7815c58989bb7bab" checksum = "94e2ef8dbfc347b10c094890f778ee2e36ca9bb4262e86dc99cd217e35f3470b"
dependencies = [ dependencies = [
"unicode-ident", "unicode-ident",
] ]
@ -447,9 +447,9 @@ dependencies = [
[[package]] [[package]]
name = "textwrap" name = "textwrap"
version = "0.15.0" version = "0.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb" checksum = "949517c0cf1bf4ee812e2e07e08ab448e3ae0d23472aee8a06c985f0c8815b16"
[[package]] [[package]]
name = "unicode-ident" name = "unicode-ident"

View file

@ -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 <version> [--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"

234
tools/Cargo.lock generated
View file

@ -2,6 +2,15 @@
# It is not intended for manual editing. # It is not intended for manual editing.
version = 3 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]] [[package]]
name = "atty" name = "atty"
version = "0.2.14" version = "0.2.14"
@ -25,6 +34,12 @@ version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bumpalo"
version = "3.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1ad822118d20d2c234f427000d5acc36eabe1e29a348c89b63dd60b13f28e5d"
[[package]] [[package]]
name = "bytes" name = "bytes"
version = "1.2.1" version = "1.2.1"
@ -32,25 +47,44 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec8a7b6a70fde80372154c65702f00a0f56f3e1c36abbc6c440484be248856db" checksum = "ec8a7b6a70fde80372154c65702f00a0f56f3e1c36abbc6c440484be248856db"
[[package]] [[package]]
name = "clap" name = "cfg-if"
version = "3.2.21" version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" 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 = [ dependencies = [
"atty", "atty",
"bitflags", "bitflags",
"clap_lex", "clap_lex",
"indexmap",
"strsim", "strsim",
"termcolor", "termcolor",
"textwrap",
] ]
[[package]] [[package]]
name = "clap_lex" name = "clap_lex"
version = "0.2.4" version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5" checksum = "0d4198f73e42b4936b35b5bb248d81d2b595ecb170da0bac7655c54eedfa8da8"
dependencies = [ dependencies = [
"os_str_bytes", "os_str_bytes",
] ]
@ -65,12 +99,24 @@ dependencies = [
"memchr", "memchr",
] ]
[[package]]
name = "core-foundation-sys"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc"
[[package]] [[package]]
name = "either" name = "either"
version = "1.8.0" version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797" checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797"
[[package]]
name = "glob"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574"
[[package]] [[package]]
name = "hashbrown" name = "hashbrown"
version = "0.12.3" version = "0.12.3"
@ -86,6 +132,19 @@ dependencies = [
"libc", "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]] [[package]]
name = "indexmap" name = "indexmap"
version = "1.9.1" version = "1.9.1"
@ -98,18 +157,36 @@ dependencies = [
[[package]] [[package]]
name = "itertools" name = "itertools"
version = "0.10.4" version = "0.10.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8bf247779e67a9082a4790b45e71ac7cfd1321331a5c856a74a9faebdab78d0" checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473"
dependencies = [ dependencies = [
"either", "either",
] ]
[[package]] [[package]]
name = "libc" name = "js-sys"
version = "0.2.132" version = "0.3.60"
source = "registry+https://github.com/rust-lang/crates.io-index" 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]] [[package]]
name = "memchr" name = "memchr"
@ -117,18 +194,72 @@ version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" 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]] [[package]]
name = "os_str_bytes" name = "os_str_bytes"
version = "6.3.0" version = "6.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ff7415e9ae3fff1225851df9e0d9e4e5479f947619774677a63572e55e80eff" 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]] [[package]]
name = "strsim" name = "strsim"
version = "0.10.0" version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" 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]] [[package]]
name = "termcolor" name = "termcolor"
version = "1.1.3" version = "1.1.3"
@ -139,10 +270,15 @@ dependencies = [
] ]
[[package]] [[package]]
name = "textwrap" name = "time"
version = "0.15.0" version = "0.1.44"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb" checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255"
dependencies = [
"libc",
"wasi",
"winapi",
]
[[package]] [[package]]
name = "toml_edit" name = "toml_edit"
@ -159,10 +295,78 @@ dependencies = [
name = "tools" name = "tools"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"chrono",
"clap", "clap",
"glob",
"toml_edit", "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]] [[package]]
name = "winapi" name = "winapi"
version = "0.3.9" version = "0.3.9"

View file

@ -6,5 +6,7 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
clap = "3.2" clap = "4"
toml_edit = "0.14" toml_edit = "0.14"
glob = "0.3"
chrono = "0.4"

View file

@ -2,16 +2,35 @@
use clap::Command; use clap::Command;
mod publish; mod publish;
mod release;
mod utils;
fn main() { #[derive(Debug)]
let matches = Command::new("Agb tools") pub enum Error {
PublishError(publish::Error),
ReleaseError(release::Error),
}
fn cli() -> Command {
Command::new("Agb tools")
.subcommand_required(true) .subcommand_required(true)
.arg_required_else_help(true) .arg_required_else_help(true)
.subcommand(publish::command()) .subcommand(publish::command())
.get_matches(); .subcommand(release::command())
}
fn main() {
let matches = cli().get_matches();
let result = match matches.subcommand() { 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`"), _ => unreachable!("Exhausted list of subcommands and subcommand_required prevents `None`"),
}; };
@ -19,3 +38,13 @@ fn main() {
eprintln!("Error: {:?}", e); eprintln!("Error: {:?}", e);
} }
} }
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn verify_cli() {
cli().debug_assert();
}
}

View file

@ -1,12 +1,14 @@
use clap::{Arg, ArgAction, ArgMatches}; use clap::{Arg, ArgAction, ArgMatches};
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
use std::fs; use std::fs;
use std::path::{Path, PathBuf}; use std::path::Path;
use std::process::Command; use std::process::Command;
use std::thread;
use std::time::Duration; use std::time::Duration;
use std::{env, thread};
use toml_edit::Document; use toml_edit::Document;
use crate::utils::*;
#[derive(Debug)] #[derive(Debug)]
pub enum Error { pub enum Error {
FindRootDirectory, FindRootDirectory,
@ -17,7 +19,7 @@ pub enum Error {
CargoToml, CargoToml,
} }
pub fn command() -> clap::Command<'static> { pub fn command() -> clap::Command {
clap::Command::new("publish") clap::Command::new("publish")
.about("Publishes agb and all subcrates") .about("Publishes agb and all subcrates")
.arg( .arg(
@ -31,7 +33,7 @@ pub fn command() -> clap::Command<'static> {
pub fn publish(matches: &ArgMatches) -> Result<(), Error> { pub fn publish(matches: &ArgMatches) -> Result<(), Error> {
let dry_run = matches.get_one::<bool>("Dry run").expect("defined by clap"); let dry_run = matches.get_one::<bool>("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<String> = HashSet::new(); let mut fully_published_crates: HashSet<String> = HashSet::new();
let mut published_crates: HashSet<String> = HashSet::new(); let mut published_crates: HashSet<String> = HashSet::new();
@ -60,11 +62,12 @@ pub fn publish(matches: &ArgMatches) -> Result<(), Error> {
if *dry_run { if *dry_run {
println!("Would execute cargo publish for {publishable_crate}"); println!("Would execute cargo publish for {publishable_crate}");
} else { } else {
Command::new("cargo") assert!(Command::new("cargo")
.arg("publish") .arg("publish")
.current_dir(&root_directory.join(publishable_crate)) .current_dir(&root_directory.join(publishable_crate))
.spawn() .status()
.map_err(|_| Error::PublishCrate)?; .map_err(|_| Error::PublishCrate)?
.success());
} }
published_crates.insert(publishable_crate.to_string()); published_crates.insert(publishable_crate.to_string());
@ -86,19 +89,6 @@ pub fn publish(matches: &ArgMatches) -> Result<(), Error> {
Ok(()) Ok(())
} }
fn find_agb_root_directory() -> Result<PathBuf, Error> {
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<bool, Error> { fn check_if_released(crate_to_publish: &str, expected_version: &str) -> Result<bool, Error> {
let url_to_poll = &get_url_to_poll(crate_to_publish); let url_to_poll = &get_url_to_poll(crate_to_publish);
@ -196,6 +186,11 @@ fn read_cargo_toml(folder: &Path) -> Result<Document, Error> {
mod test { mod test {
use super::*; use super::*;
#[test]
fn verify_cli() {
command().debug_assert();
}
#[test] #[test]
fn url_to_poll_should_return_correct_url() { fn url_to_poll_should_return_correct_url() {
let test_cases = [ 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] #[test]
fn should_read_version() -> Result<(), Error> { 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"))?; let my_version = read_cargo_toml_version(&root_directory.join("tools"))?;
assert_eq!(my_version, "0.1.0"); assert_eq!(my_version, "0.1.0");
@ -233,7 +221,7 @@ mod test {
#[test] #[test]
fn should_detect_dependencies() -> Result<(), Error> { 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"))?; let deps = get_agb_dependencies(&root_directory.join("agb"))?;
assert_eq!( assert_eq!(

305
tools/src/release.rs Normal file
View file

@ -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::<bool>("Dry run").expect("defined by clap");
let version = matches
.get_one::<Version>("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::<toml_edit::Document>()
.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::<toml_edit::Document>()
.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<String, Error> {
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<String, Error> {
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<Vec<std::path::PathBuf>, 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<Self, Self::Err> {
let version_array: Vec<_> = s
.split('.')
.map(|v| v.parse())
.collect::<Result<Vec<_>, _>>()
.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<Version, &'static str> {
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);
}
}

27
tools/src/utils.rs Normal file
View file

@ -0,0 +1,27 @@
use std::{env, path::PathBuf};
#[derive(Debug)]
pub struct FindRootDirectoryError;
pub fn find_agb_root_directory() -> Result<PathBuf, FindRootDirectoryError> {
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();
}
}