mirror of
https://github.com/italicsjenga/agb.git
synced 2024-12-23 16:21:33 +11:00
Merge remote-tracking branch 'origin/master' into affine
This commit is contained in:
commit
e63e830a9c
8
.vscode/agb.code-workspace
vendored
8
.vscode/agb.code-workspace
vendored
|
@ -32,6 +32,12 @@
|
|||
},
|
||||
{
|
||||
"path": "../book"
|
||||
},
|
||||
{
|
||||
"path": "../mgba-test-runner"
|
||||
},
|
||||
{
|
||||
"path": "../tools"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,15 +11,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
- Support for using windows on the GBA. Windows are used to selectively enable rendering of certain layers or effects.
|
||||
- Support for the blend mode of the GBA. Blending allows for alpha blending between layers and fading to black and white.
|
||||
- Added a new agb::sync module that contains GBA-specific synchronization primitives.
|
||||
- Added support for save files.
|
||||
- Added implementation of `HashMap.retain()`.
|
||||
|
||||
### Changes
|
||||
- Many of the places that originally disabled IRQs now use the `sync` module, reducing the chance of missed interrupts.
|
||||
- HashMap iterators now implement `size_hint` which should result in slightly better generation of code using those iterators.
|
||||
|
||||
### Fixed
|
||||
- Fixed the fast magnitude function in agb_fixnum. This is also used in fast_normalise. Previously only worked for positive (x, y).
|
||||
- Fixed formatting of fixed point numbers in the range (-1, 0), which previously appeared positive.
|
||||
|
||||
## Changed
|
||||
### Changed
|
||||
- `testing` is now a default feature, so you no longer need to add a separate `dev-dependencies` line for `agb` in order to enable unit tests for your project.
|
||||
|
||||
## [0.11.1] - 2022/08/02
|
||||
|
|
|
@ -2,6 +2,11 @@
|
|||
|
||||
## Rust for the Game Boy Advance
|
||||
|
||||
[![Docs](https://docs.rs/agb/badge.svg)](https://docs.rs/agb/latest/agb)
|
||||
[![Build](https://github.com/agbrs/agb/actions/workflows/build-and-test.yml/badge.svg?branch=master)](https://github.com/agbrs/agb/actions/workflows/build-and-test.yml)
|
||||
[![Licence](https://img.shields.io/crates/l/agb)](https://www.mozilla.org/en-US/MPL/2.0/)
|
||||
[![Crates.io](https://img.shields.io/crates/v/agb)](https://crates.io/crates/agb)
|
||||
|
||||
![AGB logo](.github/logo.png)
|
||||
|
||||
This is a library for making games on the Game Boy Advance using the Rust
|
||||
|
|
|
@ -7,6 +7,7 @@ fn main() {
|
|||
"src/sound/mixer/mixer.s",
|
||||
"src/agbabi/memset.s",
|
||||
"src/agbabi/memcpy.s",
|
||||
"src/save/asm_routines.s",
|
||||
];
|
||||
|
||||
println!("cargo:rerun-if-changed=gba.ld");
|
||||
|
|
|
@ -43,7 +43,7 @@ impl BumpAllocator {
|
|||
let resulting_ptr = ptr + amount_to_add;
|
||||
let new_current_ptr = resulting_ptr + layout.size();
|
||||
|
||||
if new_current_ptr as usize >= (self.start_end.borrow(cs).end)() {
|
||||
if new_current_ptr >= (self.start_end.borrow(cs).end)() {
|
||||
return None;
|
||||
}
|
||||
|
||||
|
|
|
@ -13,3 +13,19 @@
|
|||
.size \functionName,.-\functionName
|
||||
.endfunc
|
||||
.endm
|
||||
|
||||
.macro agb_thumb_func functionName:req
|
||||
.section .iwram.\functionName, "ax", %progbits
|
||||
.thumb
|
||||
.align 2
|
||||
.global \functionName
|
||||
.type \functionName, %function
|
||||
.func \functionName
|
||||
\functionName:
|
||||
.endm
|
||||
|
||||
.macro agb_thumb_end functionName:req
|
||||
.pool
|
||||
.size \functionName,.-\functionName
|
||||
.endfunc
|
||||
.endm
|
||||
|
|
|
@ -37,5 +37,8 @@ mod tests {
|
|||
display_logo(&mut map, &mut vram);
|
||||
|
||||
crate::test_runner::assert_image_output("gfx/test_logo.png");
|
||||
|
||||
map.clear(&mut vram);
|
||||
vram.gc();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -794,7 +794,7 @@ impl ObjectController {
|
|||
});
|
||||
|
||||
let loan = Loan {
|
||||
index: index as u8,
|
||||
index,
|
||||
phantom: PhantomData,
|
||||
};
|
||||
|
||||
|
@ -916,8 +916,8 @@ impl<'a> Object<'a> {
|
|||
/// [ObjectController::commit] is called.
|
||||
pub fn set_x(&mut self, x: u16) -> &mut Self {
|
||||
let object_inner = unsafe { self.object_inner() };
|
||||
object_inner.attrs.a1a.set_x(x.rem_euclid(1 << 9) as u16);
|
||||
object_inner.attrs.a1s.set_x(x.rem_euclid(1 << 9) as u16);
|
||||
object_inner.attrs.a1a.set_x(x.rem_euclid(1 << 9));
|
||||
object_inner.attrs.a1s.set_x(x.rem_euclid(1 << 9));
|
||||
self
|
||||
}
|
||||
|
||||
|
|
|
@ -24,3 +24,29 @@ pub(crate) unsafe fn dma_copy16(src: *const u16, dest: *mut u16, count: usize) {
|
|||
|
||||
DMA3_CONTROL.set(count as u32 | (1 << 31));
|
||||
}
|
||||
|
||||
pub(crate) fn dma3_exclusive<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
|
||||
})
|
||||
}
|
|
@ -188,7 +188,11 @@ impl<K, V, ALLOCATOR: ClonableAllocator> HashMap<K, V, ALLOCATOR> {
|
|||
|
||||
/// An iterator visiting all key-value pairs in an arbitrary order
|
||||
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
|
||||
|
@ -196,6 +200,14 @@ impl<K, V, ALLOCATOR: ClonableAllocator> HashMap<K, V, ALLOCATOR> {
|
|||
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
|
||||
#[must_use]
|
||||
pub fn is_empty(&self) -> bool {
|
||||
|
@ -332,6 +344,7 @@ where
|
|||
pub struct Iter<'a, K: 'a, V: 'a, ALLOCATOR: ClonableAllocator> {
|
||||
map: &'a HashMap<K, V, ALLOCATOR>,
|
||||
at: usize,
|
||||
num_found: usize,
|
||||
}
|
||||
|
||||
impl<'a, K, V, ALLOCATOR: ClonableAllocator> Iterator for Iter<'a, K, V, ALLOCATOR> {
|
||||
|
@ -347,10 +360,18 @@ impl<'a, K, V, ALLOCATOR: ClonableAllocator> Iterator for Iter<'a, K, V, ALLOCAT
|
|||
self.at += 1;
|
||||
|
||||
if node.has_value() {
|
||||
self.num_found += 1;
|
||||
return Some((node.key_ref().unwrap(), node.value_ref().unwrap()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn size_hint(&self) -> (usize, Option<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> {
|
||||
|
@ -358,7 +379,11 @@ impl<'a, K, V, ALLOCATOR: ClonableAllocator> IntoIterator for &'a HashMap<K, V,
|
|||
type IntoIter = Iter<'a, K, V, ALLOCATOR>;
|
||||
|
||||
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> {
|
||||
map: HashMap<K, V, ALLOCATOR>,
|
||||
at: usize,
|
||||
num_found: usize,
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
if let Some((k, v, _)) = maybe_kv {
|
||||
self.num_found += 1;
|
||||
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`]
|
||||
|
@ -399,7 +433,11 @@ impl<K, V, ALLOCATOR: ClonableAllocator> IntoIterator for HashMap<K, V, ALLOCATO
|
|||
type IntoIter = IterOwned<K, V, ALLOCATOR>;
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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 {
|
||||
let mut current_location = location;
|
||||
self.number_of_items -= 1;
|
||||
|
@ -1222,6 +1285,54 @@ mod test {
|
|||
drop_registry.assert_dropped_n_times(id1, 2);
|
||||
}
|
||||
|
||||
#[test_case]
|
||||
fn test_retain(_gba: &mut Gba) {
|
||||
let mut map = HashMap::new();
|
||||
|
||||
for i in 0..100 {
|
||||
map.insert(i, i);
|
||||
}
|
||||
|
||||
map.retain(|k, _| k % 2 == 0);
|
||||
|
||||
assert_eq!(map[&2], 2);
|
||||
assert_eq!(map.get(&3), None);
|
||||
|
||||
assert_eq!(map.iter().count(), 50); // force full iteration
|
||||
}
|
||||
|
||||
#[test_case]
|
||||
fn test_size_hint_iter(_gba: &mut Gba) {
|
||||
let mut map = HashMap::new();
|
||||
|
||||
for i in 0..100 {
|
||||
map.insert(i, i);
|
||||
}
|
||||
|
||||
let mut iter = map.iter();
|
||||
assert_eq!(iter.size_hint(), (100, Some(100)));
|
||||
|
||||
iter.next();
|
||||
|
||||
assert_eq!(iter.size_hint(), (99, Some(99)));
|
||||
}
|
||||
|
||||
#[test_case]
|
||||
fn test_size_hint_into_iter(_gba: &mut Gba) {
|
||||
let mut map = HashMap::new();
|
||||
|
||||
for i in 0..100 {
|
||||
map.insert(i, i);
|
||||
}
|
||||
|
||||
let mut iter = map.into_iter();
|
||||
assert_eq!(iter.size_hint(), (100, Some(100)));
|
||||
|
||||
iter.next();
|
||||
|
||||
assert_eq!(iter.size_hint(), (99, Some(99)));
|
||||
}
|
||||
|
||||
// Following test cases copied from the rust source
|
||||
// https://github.com/rust-lang/rust/blob/master/library/std/src/collections/hash/map/tests.rs
|
||||
mod rust_std_tests {
|
||||
|
|
|
@ -168,6 +168,7 @@ pub use agb_fixnum as fixnum;
|
|||
pub mod hash_map;
|
||||
/// Simple random number generator
|
||||
pub mod rng;
|
||||
pub mod save;
|
||||
mod single;
|
||||
/// Implements sound output.
|
||||
pub mod sound;
|
||||
|
@ -223,6 +224,8 @@ pub struct Gba {
|
|||
pub sound: sound::dmg::Sound,
|
||||
/// Manages access to the Game Boy Advance's direct sound mixer for playing raw wav files.
|
||||
pub mixer: sound::mixer::MixerController,
|
||||
/// Manages access to the Game Boy Advance cartridge's save chip.
|
||||
pub save: save::SaveManager,
|
||||
/// Manages access to the Game Boy Advance's 4 timers.
|
||||
pub timers: timer::TimerController,
|
||||
}
|
||||
|
@ -239,6 +242,7 @@ impl Gba {
|
|||
display: display::Display::new(),
|
||||
sound: sound::dmg::Sound::new(),
|
||||
mixer: sound::mixer::MixerController::new(),
|
||||
save: save::SaveManager::new(),
|
||||
timers: timer::TimerController::new(),
|
||||
}
|
||||
}
|
||||
|
|
49
agb/src/save/asm_routines.s
Normal file
49
agb/src/save/asm_routines.s
Normal 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
63
agb/src/save/asm_utils.rs
Normal 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
273
agb/src/save/eeprom.rs
Normal 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(§or[start..start + end_len]);
|
||||
buf = &mut buf[end_len..];
|
||||
offset += end_len;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Implements EEPROM verifies.
|
||||
fn verify(&self, mut offset: usize, mut buf: &[u8]) -> Result<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
472
agb/src/save/flash.rs
Normal 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, §or, timeout)
|
||||
}
|
||||
|
||||
/// Writes an entire 128b sector on Atmel devices, copying existing data in
|
||||
/// case of non-sector aligned writes.
|
||||
///
|
||||
/// This avoids allocating stack if there is no need to.
|
||||
fn write_atmel_sector(
|
||||
&self, offset: usize, buf: &[u8], start: usize, timeout: &mut Timeout,
|
||||
) -> Result<(), Error> {
|
||||
if start == 0 && buf.len() == 128 {
|
||||
self.write_atmel_sector_raw(offset, buf, timeout)
|
||||
} else {
|
||||
self.write_atmel_sector_safe(offset, buf, start, timeout)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The [`RawSaveAccess`] used for flash save media.
|
||||
pub struct FlashAccess;
|
||||
impl RawSaveAccess for FlashAccess {
|
||||
fn info(&self) -> Result<&'static MediaInfo, Error> {
|
||||
Ok(cached_chip_info()?.info)
|
||||
}
|
||||
|
||||
fn read(&self, offset: usize, buf: &mut [u8], _: &mut Timeout) -> Result<(), Error> {
|
||||
let chip = cached_chip_info()?;
|
||||
chip.check_len(offset, buf.len())?;
|
||||
|
||||
chip.read_buffer(offset, buf)
|
||||
}
|
||||
|
||||
fn verify(&self, offset: usize, buf: &[u8], _: &mut Timeout) -> Result<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
457
agb/src/save/mod.rs
Normal 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
57
agb/src/save/sram.rs
Normal 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
59
agb/src/save/utils.rs
Normal 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),
|
||||
}
|
||||
}
|
|
@ -284,7 +284,7 @@ mod test {
|
|||
// the actual main test loop
|
||||
let mut interrupt_seen = false;
|
||||
let mut no_interrupt_seen = false;
|
||||
for i in 0..100000 {
|
||||
for i in 0..250000 {
|
||||
// write to the static
|
||||
let new_value = [i; COUNT];
|
||||
value.write(new_value);
|
||||
|
|
105
agb/tests/save_test_common/mod.rs
Normal file
105
agb/tests/save_test_common/mod.rs
Normal 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");
|
||||
}
|
||||
}
|
16
agb/tests/test_save_eeprom_512b.rs
Normal file
16
agb/tests/test_save_eeprom_512b.rs
Normal 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 {}
|
||||
}
|
16
agb/tests/test_save_eeprom_8k.rs
Normal file
16
agb/tests/test_save_eeprom_8k.rs
Normal 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 {}
|
||||
}
|
16
agb/tests/test_save_flash_128k.rs
Normal file
16
agb/tests/test_save_flash_128k.rs
Normal 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 {}
|
||||
}
|
16
agb/tests/test_save_flash_64k.rs
Normal file
16
agb/tests/test_save_flash_64k.rs
Normal 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 {}
|
||||
}
|
16
agb/tests/test_save_sram.rs
Normal file
16
agb/tests/test_save_sram.rs
Normal 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 {}
|
||||
}
|
24
book/games/pong/Cargo.lock
generated
24
book/games/pong/Cargo.lock
generated
|
@ -217,9 +217,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.132"
|
||||
version = "0.2.134"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8371e4e5341c3a96db127eb2465ac681ced4c433e01dd0e938adbef26ba93ba5"
|
||||
checksum = "329c933548736bc49fd575ee68c89e8be4d260064184389a5b77517cddd99ffb"
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
|
@ -318,9 +318,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.14.0"
|
||||
version = "1.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2f7254b99e31cad77da24b08ebf628882739a608578bb1bcdfc1f9c21260d7c0"
|
||||
checksum = "e82dad04139b71a90c080c8463fe0dc7902db5192d939bd0950f074d014339e1"
|
||||
|
||||
[[package]]
|
||||
name = "png"
|
||||
|
@ -343,9 +343,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.43"
|
||||
version = "1.0.46"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0a2ca2c61bc9f3d74d2886294ab7b9853abd9c1ad903a3ac7815c58989bb7bab"
|
||||
checksum = "94e2ef8dbfc347b10c094890f778ee2e36ca9bb4262e86dc99cd217e35f3470b"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
@ -367,18 +367,18 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
|
|||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.144"
|
||||
version = "1.0.145"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0f747710de3dcd43b88c9168773254e809d8ddbdf9653b84e2554ab219f17860"
|
||||
checksum = "728eb6351430bccb993660dfffc5a72f91ccc1295abaa8ce19b27ebe4f75568b"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.144"
|
||||
version = "1.0.145"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "94ed3a816fb1d101812f83e789f888322c34e291f894f19590dc310963e87a00"
|
||||
checksum = "81fa1584d3d1bcacd84c277a0dfe21f5b0f6accf4a23d04d4c6d61f1af522b4c"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
@ -393,9 +393,9 @@ checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
|
|||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "1.0.99"
|
||||
version = "1.0.101"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "58dbef6ec655055e20b86b15a8cc6d439cca19b667537ac6a1369572d151ab13"
|
||||
checksum = "e90cde112c4b9690b8cbe810cba9ddd8bc1d7472e2cae317b69e9438c1cba7d2"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
|
24
examples/hyperspace-roll/Cargo.lock
generated
24
examples/hyperspace-roll/Cargo.lock
generated
|
@ -225,9 +225,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.132"
|
||||
version = "0.2.134"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8371e4e5341c3a96db127eb2465ac681ced4c433e01dd0e938adbef26ba93ba5"
|
||||
checksum = "329c933548736bc49fd575ee68c89e8be4d260064184389a5b77517cddd99ffb"
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
|
@ -326,9 +326,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.14.0"
|
||||
version = "1.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2f7254b99e31cad77da24b08ebf628882739a608578bb1bcdfc1f9c21260d7c0"
|
||||
checksum = "e82dad04139b71a90c080c8463fe0dc7902db5192d939bd0950f074d014339e1"
|
||||
|
||||
[[package]]
|
||||
name = "png"
|
||||
|
@ -344,9 +344,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.43"
|
||||
version = "1.0.46"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0a2ca2c61bc9f3d74d2886294ab7b9853abd9c1ad903a3ac7815c58989bb7bab"
|
||||
checksum = "94e2ef8dbfc347b10c094890f778ee2e36ca9bb4262e86dc99cd217e35f3470b"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
@ -368,18 +368,18 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
|
|||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.144"
|
||||
version = "1.0.145"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0f747710de3dcd43b88c9168773254e809d8ddbdf9653b84e2554ab219f17860"
|
||||
checksum = "728eb6351430bccb993660dfffc5a72f91ccc1295abaa8ce19b27ebe4f75568b"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.144"
|
||||
version = "1.0.145"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "94ed3a816fb1d101812f83e789f888322c34e291f894f19590dc310963e87a00"
|
||||
checksum = "81fa1584d3d1bcacd84c277a0dfe21f5b0f6accf4a23d04d4c6d61f1af522b4c"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
@ -394,9 +394,9 @@ checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
|
|||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "1.0.99"
|
||||
version = "1.0.101"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "58dbef6ec655055e20b86b15a8cc6d439cca19b667537ac6a1369572d151ab13"
|
||||
checksum = "e90cde112c4b9690b8cbe810cba9ddd8bc1d7472e2cae317b69e9438c1cba7d2"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
|
|
@ -207,7 +207,7 @@ impl RolledDice {
|
|||
let heal = *face_counts.entry(Face::Heal).or_default();
|
||||
if heal != 0 {
|
||||
actions.push(Action::PlayerHeal {
|
||||
amount: ((heal * (heal + 1)) / 2) as u32,
|
||||
amount: (heal * (heal + 1)) / 2,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -80,7 +80,7 @@ pub fn generate_upgrades(level: u32) -> Vec<Face> {
|
|||
.chain(core::iter::once(&next))
|
||||
.filter(|&x| *x == Face::Malfunction)
|
||||
.count();
|
||||
let maximum_number_of_malfunctions = if level < 5 { 0 } else { 1 };
|
||||
let maximum_number_of_malfunctions = (level >= 5).into();
|
||||
if upgrade_value(&upgrades, next) <= max_upgrade_value
|
||||
&& number_of_malfunctions <= maximum_number_of_malfunctions
|
||||
{
|
||||
|
|
|
@ -96,10 +96,10 @@ struct Agb<'a> {
|
|||
}
|
||||
|
||||
fn main(mut gba: agb::Gba) -> ! {
|
||||
save::init_save();
|
||||
save::init_save(&mut gba).expect("Could not initialize save game");
|
||||
|
||||
if save::load_high_score() > 1000 {
|
||||
save::save_high_score(0);
|
||||
save::save_high_score(&mut gba, 0).expect("Could not reset high score");
|
||||
}
|
||||
|
||||
let gfx = gba.display.object.get();
|
||||
|
@ -207,7 +207,8 @@ fn main(mut gba: agb::Gba) -> ! {
|
|||
agb.obj.commit();
|
||||
agb.sfx.customise();
|
||||
if save::load_high_score() < current_level {
|
||||
save::save_high_score(current_level);
|
||||
save::save_high_score(&mut gba, current_level)
|
||||
.expect("Could not save high score");
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
|
|
@ -1,44 +1,42 @@
|
|||
use agb::interrupt::free;
|
||||
use bare_metal::Mutex;
|
||||
use core::cell::RefCell;
|
||||
use agb::Gba;
|
||||
use agb::save::Error;
|
||||
use agb::sync::Static;
|
||||
|
||||
const RAM_ADDRESS: *mut u8 = 0x0E00_0000 as *mut u8;
|
||||
const HIGH_SCORE_ADDRESS_START: *mut u8 = RAM_ADDRESS.wrapping_offset(1);
|
||||
static HIGHSCORE: Static<u32> = Static::new(0);
|
||||
|
||||
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() {
|
||||
if (unsafe { RAM_ADDRESS.read_volatile() } == !0) {
|
||||
save_high_score(0);
|
||||
unsafe { RAM_ADDRESS.write_volatile(0) };
|
||||
}
|
||||
let mut access = gba.save.access()?;
|
||||
|
||||
let mut a = [0; 4];
|
||||
for (idx, a) in a.iter_mut().enumerate() {
|
||||
*a = unsafe { HIGH_SCORE_ADDRESS_START.add(idx).read_volatile() };
|
||||
}
|
||||
let mut buffer = [0; 1];
|
||||
access.read(0, &mut buffer)?;
|
||||
|
||||
let high_score = u32::from_le_bytes(a);
|
||||
if buffer[0] != 0 {
|
||||
access.prepare_write(0..1)?.write(0, &[0])?;
|
||||
core::mem::drop(access);
|
||||
save_high_score(gba, 0)?;
|
||||
} else {
|
||||
let mut buffer = [0; 4];
|
||||
access.read(1, &mut buffer)?;
|
||||
let high_score = u32::from_le_bytes(buffer);
|
||||
|
||||
free(|cs| {
|
||||
if high_score > 100 {
|
||||
HIGHSCORE.borrow(cs).replace(0);
|
||||
HIGHSCORE.write(0)
|
||||
} else {
|
||||
HIGHSCORE.borrow(cs).replace(high_score);
|
||||
HIGHSCORE.write(high_score)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn load_high_score() -> u32 {
|
||||
free(|cs| *HIGHSCORE.borrow(cs).borrow())
|
||||
HIGHSCORE.read()
|
||||
}
|
||||
|
||||
pub fn save_high_score(score: u32) {
|
||||
let a = score.to_le_bytes();
|
||||
|
||||
for (idx, &a) in a.iter().enumerate() {
|
||||
unsafe { HIGH_SCORE_ADDRESS_START.add(idx).write_volatile(a) };
|
||||
}
|
||||
|
||||
free(|cs| HIGHSCORE.borrow(cs).replace(score));
|
||||
pub fn save_high_score(gba: &mut Gba, score: u32) -> Result<(), Error> {
|
||||
gba.save.access()?.prepare_write(1..5)?.write(1, &score.to_le_bytes())?;
|
||||
HIGHSCORE.write(score);
|
||||
Ok(())
|
||||
}
|
||||
|
|
24
examples/the-hat-chooses-the-wizard/Cargo.lock
generated
24
examples/the-hat-chooses-the-wizard/Cargo.lock
generated
|
@ -223,9 +223,9 @@ checksum = "6c8af84674fe1f223a982c933a0ee1086ac4d4052aa0fb8060c12c6ad838e754"
|
|||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.132"
|
||||
version = "0.2.134"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8371e4e5341c3a96db127eb2465ac681ced4c433e01dd0e938adbef26ba93ba5"
|
||||
checksum = "329c933548736bc49fd575ee68c89e8be4d260064184389a5b77517cddd99ffb"
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
|
@ -324,9 +324,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.14.0"
|
||||
version = "1.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2f7254b99e31cad77da24b08ebf628882739a608578bb1bcdfc1f9c21260d7c0"
|
||||
checksum = "e82dad04139b71a90c080c8463fe0dc7902db5192d939bd0950f074d014339e1"
|
||||
|
||||
[[package]]
|
||||
name = "png"
|
||||
|
@ -342,9 +342,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.43"
|
||||
version = "1.0.46"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0a2ca2c61bc9f3d74d2886294ab7b9853abd9c1ad903a3ac7815c58989bb7bab"
|
||||
checksum = "94e2ef8dbfc347b10c094890f778ee2e36ca9bb4262e86dc99cd217e35f3470b"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
@ -372,18 +372,18 @@ checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09"
|
|||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.144"
|
||||
version = "1.0.145"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0f747710de3dcd43b88c9168773254e809d8ddbdf9653b84e2554ab219f17860"
|
||||
checksum = "728eb6351430bccb993660dfffc5a72f91ccc1295abaa8ce19b27ebe4f75568b"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.144"
|
||||
version = "1.0.145"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "94ed3a816fb1d101812f83e789f888322c34e291f894f19590dc310963e87a00"
|
||||
checksum = "81fa1584d3d1bcacd84c277a0dfe21f5b0f6accf4a23d04d4c6d61f1af522b4c"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
@ -409,9 +409,9 @@ checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
|
|||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "1.0.99"
|
||||
version = "1.0.101"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "58dbef6ec655055e20b86b15a8cc6d439cca19b667537ac6a1369572d151ab13"
|
||||
checksum = "e90cde112c4b9690b8cbe810cba9ddd8bc1d7472e2cae317b69e9438c1cba7d2"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
|
24
examples/the-purple-night/Cargo.lock
generated
24
examples/the-purple-night/Cargo.lock
generated
|
@ -241,9 +241,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.132"
|
||||
version = "0.2.134"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8371e4e5341c3a96db127eb2465ac681ced4c433e01dd0e938adbef26ba93ba5"
|
||||
checksum = "329c933548736bc49fd575ee68c89e8be4d260064184389a5b77517cddd99ffb"
|
||||
|
||||
[[package]]
|
||||
name = "libflate"
|
||||
|
@ -354,9 +354,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.14.0"
|
||||
version = "1.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2f7254b99e31cad77da24b08ebf628882739a608578bb1bcdfc1f9c21260d7c0"
|
||||
checksum = "e82dad04139b71a90c080c8463fe0dc7902db5192d939bd0950f074d014339e1"
|
||||
|
||||
[[package]]
|
||||
name = "png"
|
||||
|
@ -372,9 +372,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.43"
|
||||
version = "1.0.46"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0a2ca2c61bc9f3d74d2886294ab7b9853abd9c1ad903a3ac7815c58989bb7bab"
|
||||
checksum = "94e2ef8dbfc347b10c094890f778ee2e36ca9bb4262e86dc99cd217e35f3470b"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
@ -402,18 +402,18 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
|
|||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.144"
|
||||
version = "1.0.145"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0f747710de3dcd43b88c9168773254e809d8ddbdf9653b84e2554ab219f17860"
|
||||
checksum = "728eb6351430bccb993660dfffc5a72f91ccc1295abaa8ce19b27ebe4f75568b"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.144"
|
||||
version = "1.0.145"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "94ed3a816fb1d101812f83e789f888322c34e291f894f19590dc310963e87a00"
|
||||
checksum = "81fa1584d3d1bcacd84c277a0dfe21f5b0f6accf4a23d04d4c6d61f1af522b4c"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
@ -428,9 +428,9 @@ checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
|
|||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "1.0.99"
|
||||
version = "1.0.101"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "58dbef6ec655055e20b86b15a8cc6d439cca19b667537ac6a1369572d151ab13"
|
||||
checksum = "e90cde112c4b9690b8cbe810cba9ddd8bc1d7472e2cae317b69e9438c1cba7d2"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
|
|
@ -129,7 +129,7 @@ impl<'a> Level<'a> {
|
|||
let factor: Number = Number::new(1) / Number::new(8);
|
||||
let (x, y) = (v * factor).floor().get();
|
||||
|
||||
if (x < 0 || x > tilemap::WIDTH as i32) || (y < 0 || y > tilemap::HEIGHT as i32) {
|
||||
if !(0..=tilemap::WIDTH).contains(&x) || !(0..=tilemap::HEIGHT).contains(&y) {
|
||||
return Some(Rect::new((x * 8, y * 8).into(), (8, 8).into()));
|
||||
}
|
||||
let position = tilemap::WIDTH as usize * y as usize + x as usize;
|
||||
|
@ -1878,7 +1878,7 @@ enum MoveState {
|
|||
impl<'a> Game<'a> {
|
||||
fn has_just_reached_end(&self) -> bool {
|
||||
match self.boss {
|
||||
BossState::NotSpawned => self.offset.x.floor() + 248 >= tilemap::WIDTH as i32 * 8,
|
||||
BossState::NotSpawned => self.offset.x.floor() + 248 >= tilemap::WIDTH * 8,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
@ -1901,13 +1901,13 @@ impl<'a> Game<'a> {
|
|||
|
||||
if self.has_just_reached_end() {
|
||||
sfx.boss();
|
||||
self.offset.x = (tilemap::WIDTH as i32 * 8 - 248).into();
|
||||
self.offset.x = (tilemap::WIDTH * 8 - 248).into();
|
||||
self.move_state = MoveState::PinnedAtEnd;
|
||||
self.boss = BossState::Active(Boss::new(object_controller, self.offset))
|
||||
}
|
||||
}
|
||||
MoveState::PinnedAtEnd => {
|
||||
self.offset.x = (tilemap::WIDTH as i32 * 8 - 248).into();
|
||||
self.offset.x = (tilemap::WIDTH * 8 - 248).into();
|
||||
}
|
||||
MoveState::FollowingPlayer => {
|
||||
Game::update_sunrise(vram, self.sunrise_timer);
|
||||
|
@ -1917,8 +1917,8 @@ impl<'a> Game<'a> {
|
|||
let difference = self.player.entity.position.x - (self.offset.x + WIDTH / 2);
|
||||
|
||||
self.offset.x += difference / 8;
|
||||
if self.offset.x > (tilemap::WIDTH as i32 * 8 - 248).into() {
|
||||
self.offset.x = (tilemap::WIDTH as i32 * 8 - 248).into();
|
||||
if self.offset.x > (tilemap::WIDTH * 8 - 248).into() {
|
||||
self.offset.x = (tilemap::WIDTH * 8 - 248).into();
|
||||
} else if self.offset.x < 8.into() {
|
||||
self.offset.x = 8.into();
|
||||
self.move_state = MoveState::Ending;
|
||||
|
|
5
justfile
5
justfile
|
@ -68,8 +68,11 @@ update-linker-scripts:
|
|||
|
||||
publish: (_run-tool "publish")
|
||||
|
||||
release +args: (_run-tool "release" args)
|
||||
|
||||
_run-tool +tool:
|
||||
cargo run --manifest-path "{{justfile_directory() + "/tools/Cargo.toml"}}" -- {{tool}}
|
||||
(cd tools && cargo build)
|
||||
"$CARGO_TARGET_DIR/debug/tools" {{tool}}
|
||||
|
||||
_build-rom folder name:
|
||||
#!/usr/bin/env bash
|
||||
|
|
36
mgba-test-runner/Cargo.lock
generated
36
mgba-test-runner/Cargo.lock
generated
|
@ -107,9 +107,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
|||
|
||||
[[package]]
|
||||
name = "clang-sys"
|
||||
version = "1.3.3"
|
||||
version = "1.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a050e2153c5be08febd6734e29298e844fdb0fa21aeddd63b4eb7baa106c69b"
|
||||
checksum = "fa2e27ae6ab525c3d369ded447057bca5438d86dc3a68f6faafb8269ba82ebf3"
|
||||
dependencies = [
|
||||
"glob",
|
||||
"libc",
|
||||
|
@ -118,9 +118,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "3.2.21"
|
||||
version = "3.2.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1ed5341b2301a26ab80be5cbdced622e80ed808483c52e45e3310a877d3b37d7"
|
||||
checksum = "86447ad904c7fb335a790c9d7fe3d0d971dc523b8ccd1561a520de9a85302750"
|
||||
dependencies = [
|
||||
"atty",
|
||||
"bitflags",
|
||||
|
@ -163,9 +163,9 @@ checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797"
|
|||
|
||||
[[package]]
|
||||
name = "env_logger"
|
||||
version = "0.9.0"
|
||||
version = "0.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b2cf0344971ee6c64c31be0d530793fba457d322dfec2810c453d0ef228f9c3"
|
||||
checksum = "c90bf5f19754d10198ccb95b70664fc925bd1fc090a0fd9a6ebc54acc8cd6272"
|
||||
dependencies = [
|
||||
"atty",
|
||||
"humantime",
|
||||
|
@ -213,9 +213,9 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
|
|||
|
||||
[[package]]
|
||||
name = "image"
|
||||
version = "0.24.3"
|
||||
version = "0.24.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7e30ca2ecf7666107ff827a8e481de6a132a9b687ed3bb20bb1c144a36c00964"
|
||||
checksum = "bd8e4fb07cf672b1642304e731ef8a6a4c7891d67bb4fd4f5ce58cd6ed86803c"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"byteorder",
|
||||
|
@ -237,9 +237,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "jobserver"
|
||||
version = "0.1.24"
|
||||
version = "0.1.25"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "af25a77299a7f711a01975c35a6a424eb6862092cc2d6c72c4ed6cbc56dfc1fa"
|
||||
checksum = "068b1ee6743e4d11fb9c6a1e6064b3693a1b600e7f5f5988047d98b3dc9fb90b"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
@ -258,9 +258,9 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
|
|||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.132"
|
||||
version = "0.2.134"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8371e4e5341c3a96db127eb2465ac681ced4c433e01dd0e938adbef26ba93ba5"
|
||||
checksum = "329c933548736bc49fd575ee68c89e8be4d260064184389a5b77517cddd99ffb"
|
||||
|
||||
[[package]]
|
||||
name = "libloading"
|
||||
|
@ -355,9 +355,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.14.0"
|
||||
version = "1.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2f7254b99e31cad77da24b08ebf628882739a608578bb1bcdfc1f9c21260d7c0"
|
||||
checksum = "e82dad04139b71a90c080c8463fe0dc7902db5192d939bd0950f074d014339e1"
|
||||
|
||||
[[package]]
|
||||
name = "os_str_bytes"
|
||||
|
@ -385,9 +385,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.43"
|
||||
version = "1.0.46"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0a2ca2c61bc9f3d74d2886294ab7b9853abd9c1ad903a3ac7815c58989bb7bab"
|
||||
checksum = "94e2ef8dbfc347b10c094890f778ee2e36ca9bb4262e86dc99cd217e35f3470b"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
@ -447,9 +447,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "textwrap"
|
||||
version = "0.15.0"
|
||||
version = "0.15.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb"
|
||||
checksum = "949517c0cf1bf4ee812e2e07e08ab448e3ae0d23472aee8a06c985f0c8815b16"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
|
|
96
release.sh
96
release.sh
|
@ -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
234
tools/Cargo.lock
generated
|
@ -2,6 +2,15 @@
|
|||
# It is not intended for manual editing.
|
||||
version = 3
|
||||
|
||||
[[package]]
|
||||
name = "android_system_properties"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "atty"
|
||||
version = "0.2.14"
|
||||
|
@ -25,6 +34,12 @@ version = "1.3.2"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c1ad822118d20d2c234f427000d5acc36eabe1e29a348c89b63dd60b13f28e5d"
|
||||
|
||||
[[package]]
|
||||
name = "bytes"
|
||||
version = "1.2.1"
|
||||
|
@ -32,25 +47,44 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "ec8a7b6a70fde80372154c65702f00a0f56f3e1c36abbc6c440484be248856db"
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "3.2.21"
|
||||
name = "cfg-if"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1ed5341b2301a26ab80be5cbdced622e80ed808483c52e45e3310a877d3b37d7"
|
||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||
|
||||
[[package]]
|
||||
name = "chrono"
|
||||
version = "0.4.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bfd4d1b31faaa3a89d7934dbded3111da0d2ef28e3ebccdb4f0179f5929d1ef1"
|
||||
dependencies = [
|
||||
"iana-time-zone",
|
||||
"js-sys",
|
||||
"num-integer",
|
||||
"num-traits",
|
||||
"time",
|
||||
"wasm-bindgen",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0a1af219c3e254a8b4649d6ddaef886b2015089f35f2ac5e1db31410c0566ab8"
|
||||
dependencies = [
|
||||
"atty",
|
||||
"bitflags",
|
||||
"clap_lex",
|
||||
"indexmap",
|
||||
"strsim",
|
||||
"termcolor",
|
||||
"textwrap",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_lex"
|
||||
version = "0.2.4"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5"
|
||||
checksum = "0d4198f73e42b4936b35b5bb248d81d2b595ecb170da0bac7655c54eedfa8da8"
|
||||
dependencies = [
|
||||
"os_str_bytes",
|
||||
]
|
||||
|
@ -65,12 +99,24 @@ dependencies = [
|
|||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation-sys"
|
||||
version = "0.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc"
|
||||
|
||||
[[package]]
|
||||
name = "either"
|
||||
version = "1.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797"
|
||||
|
||||
[[package]]
|
||||
name = "glob"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574"
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.12.3"
|
||||
|
@ -86,6 +132,19 @@ dependencies = [
|
|||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iana-time-zone"
|
||||
version = "0.1.50"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fd911b35d940d2bd0bea0f9100068e5b97b51a1cbe13d13382f132e0365257a0"
|
||||
dependencies = [
|
||||
"android_system_properties",
|
||||
"core-foundation-sys",
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "1.9.1"
|
||||
|
@ -98,18 +157,36 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.10.4"
|
||||
version = "0.10.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d8bf247779e67a9082a4790b45e71ac7cfd1321331a5c856a74a9faebdab78d0"
|
||||
checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473"
|
||||
dependencies = [
|
||||
"either",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.132"
|
||||
name = "js-sys"
|
||||
version = "0.3.60"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8371e4e5341c3a96db127eb2465ac681ced4c433e01dd0e938adbef26ba93ba5"
|
||||
checksum = "49409df3e3bf0856b916e2ceaca09ee28e6871cf7d9ce97a692cacfdb2a25a47"
|
||||
dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.134"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "329c933548736bc49fd575ee68c89e8be4d260064184389a5b77517cddd99ffb"
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
|
@ -117,18 +194,72 @@ version = "2.5.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
|
||||
|
||||
[[package]]
|
||||
name = "num-integer"
|
||||
version = "0.1.45"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e82dad04139b71a90c080c8463fe0dc7902db5192d939bd0950f074d014339e1"
|
||||
|
||||
[[package]]
|
||||
name = "os_str_bytes"
|
||||
version = "6.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9ff7415e9ae3fff1225851df9e0d9e4e5479f947619774677a63572e55e80eff"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.46"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "94e2ef8dbfc347b10c094890f778ee2e36ca9bb4262e86dc99cd217e35f3470b"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "1.0.101"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e90cde112c4b9690b8cbe810cba9ddd8bc1d7472e2cae317b69e9438c1cba7d2"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "termcolor"
|
||||
version = "1.1.3"
|
||||
|
@ -139,10 +270,15 @@ dependencies = [
|
|||
]
|
||||
|
||||
[[package]]
|
||||
name = "textwrap"
|
||||
version = "0.15.0"
|
||||
name = "time"
|
||||
version = "0.1.44"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb"
|
||||
checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"wasi",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_edit"
|
||||
|
@ -159,10 +295,78 @@ dependencies = [
|
|||
name = "tools"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"clap",
|
||||
"glob",
|
||||
"toml_edit",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dcc811dc4066ac62f84f11307873c4850cb653bfa9b1719cee2bd2204a4bc5dd"
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.10.0+wasi-snapshot-preview1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f"
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.83"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eaf9f5aceeec8be17c128b2e93e031fb8a4d469bb9c4ae2d7dc1888b26887268"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"wasm-bindgen-macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-backend"
|
||||
version = "0.2.83"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4c8ffb332579b0557b52d268b91feab8df3615f265d5270fec2a8c95b17c1142"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"log",
|
||||
"once_cell",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro"
|
||||
version = "0.2.83"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "052be0f94026e6cbc75cdefc9bae13fd6052cdcaf532fa6c45e7ae33a1e6c810"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"wasm-bindgen-macro-support",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro-support"
|
||||
version = "0.2.83"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "07bc0c051dc5f23e307b13285f9d75df86bfdf816c5721e573dec1f9b8aa193c"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"wasm-bindgen-backend",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-shared"
|
||||
version = "0.2.83"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1c38c045535d93ec4f0b4defec448e4291638ee608530863b1e2ba115d4fff7f"
|
||||
|
||||
[[package]]
|
||||
name = "winapi"
|
||||
version = "0.3.9"
|
||||
|
|
|
@ -6,5 +6,7 @@ edition = "2021"
|
|||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
clap = "3.2"
|
||||
clap = "4"
|
||||
toml_edit = "0.14"
|
||||
glob = "0.3"
|
||||
chrono = "0.4"
|
||||
|
|
|
@ -2,16 +2,35 @@
|
|||
use clap::Command;
|
||||
|
||||
mod publish;
|
||||
mod release;
|
||||
mod utils;
|
||||
|
||||
fn main() {
|
||||
let matches = Command::new("Agb tools")
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
PublishError(publish::Error),
|
||||
ReleaseError(release::Error),
|
||||
}
|
||||
|
||||
fn cli() -> Command {
|
||||
Command::new("Agb tools")
|
||||
.subcommand_required(true)
|
||||
.arg_required_else_help(true)
|
||||
.subcommand(publish::command())
|
||||
.get_matches();
|
||||
.subcommand(release::command())
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let matches = cli().get_matches();
|
||||
|
||||
let result = match matches.subcommand() {
|
||||
Some(("publish", arg_matches)) => publish::publish(arg_matches),
|
||||
Some(("publish", arg_matches)) => {
|
||||
publish::publish(arg_matches).map_err(Error::PublishError)
|
||||
}
|
||||
|
||||
Some(("release", arg_matches)) => {
|
||||
release::release(arg_matches).map_err(Error::ReleaseError)
|
||||
}
|
||||
|
||||
_ => unreachable!("Exhausted list of subcommands and subcommand_required prevents `None`"),
|
||||
};
|
||||
|
||||
|
@ -19,3 +38,13 @@ fn main() {
|
|||
eprintln!("Error: {:?}", e);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn verify_cli() {
|
||||
cli().debug_assert();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
use clap::{Arg, ArgAction, ArgMatches};
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
use std::{env, thread};
|
||||
use toml_edit::Document;
|
||||
|
||||
use crate::utils::*;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
FindRootDirectory,
|
||||
|
@ -17,7 +19,7 @@ pub enum Error {
|
|||
CargoToml,
|
||||
}
|
||||
|
||||
pub fn command() -> clap::Command<'static> {
|
||||
pub fn command() -> clap::Command {
|
||||
clap::Command::new("publish")
|
||||
.about("Publishes agb and all subcrates")
|
||||
.arg(
|
||||
|
@ -31,7 +33,7 @@ pub fn command() -> clap::Command<'static> {
|
|||
pub fn publish(matches: &ArgMatches) -> Result<(), Error> {
|
||||
let dry_run = matches.get_one::<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 published_crates: HashSet<String> = HashSet::new();
|
||||
|
@ -60,11 +62,12 @@ pub fn publish(matches: &ArgMatches) -> Result<(), Error> {
|
|||
if *dry_run {
|
||||
println!("Would execute cargo publish for {publishable_crate}");
|
||||
} else {
|
||||
Command::new("cargo")
|
||||
assert!(Command::new("cargo")
|
||||
.arg("publish")
|
||||
.current_dir(&root_directory.join(publishable_crate))
|
||||
.spawn()
|
||||
.map_err(|_| Error::PublishCrate)?;
|
||||
.status()
|
||||
.map_err(|_| Error::PublishCrate)?
|
||||
.success());
|
||||
}
|
||||
|
||||
published_crates.insert(publishable_crate.to_string());
|
||||
|
@ -86,19 +89,6 @@ pub fn publish(matches: &ArgMatches) -> Result<(), Error> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn find_agb_root_directory() -> Result<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> {
|
||||
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 {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn verify_cli() {
|
||||
command().debug_assert();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn url_to_poll_should_return_correct_url() {
|
||||
let test_cases = [
|
||||
|
@ -215,16 +210,9 @@ mod test {
|
|||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_find_root_directory() -> Result<(), Error> {
|
||||
assert_ne!(find_agb_root_directory()?.to_string_lossy(), "");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_read_version() -> Result<(), Error> {
|
||||
let root_directory = find_agb_root_directory()?;
|
||||
let root_directory = crate::utils::find_agb_root_directory().unwrap();
|
||||
let my_version = read_cargo_toml_version(&root_directory.join("tools"))?;
|
||||
|
||||
assert_eq!(my_version, "0.1.0");
|
||||
|
@ -233,7 +221,7 @@ mod test {
|
|||
|
||||
#[test]
|
||||
fn should_detect_dependencies() -> Result<(), Error> {
|
||||
let root_directory = find_agb_root_directory()?;
|
||||
let root_directory = crate::utils::find_agb_root_directory().unwrap();
|
||||
let deps = get_agb_dependencies(&root_directory.join("agb"))?;
|
||||
|
||||
assert_eq!(
|
||||
|
|
305
tools/src/release.rs
Normal file
305
tools/src/release.rs
Normal 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
27
tools/src/utils.rs
Normal 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();
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue