Add a no game (#427)

This adds a "no game" to replace the template.
* Inspired by how Love2D has a default game that says "No Game".
* This screen:
https://youtube.com/clip/Ugkx6atqwerxyyUSiVrFhmAh7pK2xNgjHxI9

- [x] Changelog updated
This commit is contained in:
Corwin 2023-05-11 20:08:28 +01:00 committed by GitHub
commit 84f22c0b30
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 340 additions and 48 deletions

View file

@ -6,6 +6,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
### Changed
- Changed the default template game
## [0.15.0] - 2023/04/25
### Added

View file

@ -239,9 +239,36 @@ impl ToTokens for ByteString<'_> {
}
}
#[proc_macro]
pub fn include_colours_inner(input: TokenStream) -> TokenStream {
let input_filename = parse_macro_input!(input as LitStr);
let input_filename = input_filename.value();
let root = std::env::var("CARGO_MANIFEST_DIR").expect("Failed to get cargo manifest dir");
let input_filename = Path::new(&root).join(input_filename);
let image = Image::load_from_file(Path::new(&input_filename));
let mut palette_data = Vec::with_capacity(image.width * image.height);
for y in 0..image.height {
for x in 0..image.width {
palette_data.push(image.colour(x, y).to_rgb15())
}
}
let filename = input_filename.to_string_lossy();
TokenStream::from(quote! {
{
const _: &[u8] = include_bytes!(#filename);
[#(#palette_data),*]
}
})
}
#[proc_macro]
pub fn include_aseprite_inner(input: TokenStream) -> TokenStream {
let parser = Punctuated::<LitStr, syn::Token![,]>::parse_separated_nonempty;
let parser = Punctuated::<LitStr, syn::Token![,]>::parse_terminated;
let parsed = match parser.parse(input) {
Ok(e) => e,
Err(e) => return e.to_compile_error().into(),
@ -432,8 +459,8 @@ fn palette_tile_data(
let mut tile_data = Vec::new();
for image in images {
add_image_to_tile_data(&mut tile_data, image, optimiser, 0)
for (image_idx, image) in images.iter().enumerate() {
add_image_to_tile_data(&mut tile_data, image, optimiser, image_idx)
}
let tile_data = collapse_to_4bpp(&tile_data);

View file

@ -138,17 +138,7 @@ impl Palette16Optimiser {
while !unsatisfied_palettes.is_empty() {
let palette = self.find_maximal_palette_for(&unsatisfied_palettes);
for test_palette in unsatisfied_palettes.clone() {
if test_palette.is_satisfied_by(&palette) {
unsatisfied_palettes.remove(&test_palette);
}
}
for (i, overall_palette) in self.palettes.iter().enumerate() {
if overall_palette.is_satisfied_by(&palette) {
assignments[i] = optimised_palettes.len();
}
}
unsatisfied_palettes.retain(|test_palette| !test_palette.is_satisfied_by(&palette));
optimised_palettes.push(palette);
@ -157,6 +147,13 @@ impl Palette16Optimiser {
}
}
for (i, overall_palette) in self.palettes.iter().enumerate() {
assignments[i] = optimised_palettes
.iter()
.position(|palette| overall_palette.is_satisfied_by(palette))
.unwrap();
}
Palette16OptimisationResults {
optimised_palettes,
assignments,

7
agb/examples/no_game.rs Normal file
View file

@ -0,0 +1,7 @@
#![no_std]
#![no_main]
#[agb::entry]
fn main(gba: agb::Gba) -> ! {
agb::no_game(gba);
}

BIN
agb/gfx/pastel.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 355 B

View file

@ -94,6 +94,7 @@ macro_rules! align_bytes {
#[macro_export]
macro_rules! include_aseprite {
($($aseprite_path: expr),*) => {{
#[allow(unused_imports)]
use $crate::display::object::{Size, Sprite, Tag, TagMap, Graphics};
use $crate::display::palette16::Palette16;
use $crate::align_bytes;
@ -367,4 +368,11 @@ impl Size {
Size::S32x64 => (32, 64),
}
}
#[must_use]
/// Returns the width and height of the size in pixels.
pub const fn to_tiles_width_height(self) -> (usize, usize) {
let wh = self.to_width_height();
(wh.0 / 8, wh.1 / 8)
}
}

View file

@ -1,6 +1,11 @@
use core::ptr::NonNull;
use core::{alloc::Allocator, ptr::NonNull};
use alloc::rc::{Rc, Weak};
use alloc::{
alloc::Global,
boxed::Box,
rc::{Rc, Weak},
vec::Vec,
};
use crate::{
agb_alloc::{block_allocator::BlockAllocator, bump_allocator::StartEnd},
@ -285,34 +290,61 @@ impl Default for SpriteLoader {
}
/// Sprite data that can be used to create sprites in vram.
pub struct DynamicSprite<'a> {
data: &'a [u8],
pub struct DynamicSprite<A: Allocator = Global> {
data: Box<[u8], A>,
size: Size,
}
impl DynamicSprite<'_> {
impl DynamicSprite {
#[must_use]
/// Creates a new dynamic sprite from underlying bytes. Note that despite
/// being an array of u8, this must be aligned to at least a 2 byte
/// boundary.
pub fn new(data: &[u8], size: Size) -> DynamicSprite {
let ptr = &data[0] as *const _ as usize;
if ptr % 2 != 0 {
panic!("data is not aligned to a 2 byte boundary");
}
if data.len() != size.number_of_tiles() * BYTES_PER_TILE_4BPP {
panic!(
"data is not of expected length, got {} expected {}",
data.len(),
size.number_of_tiles() * BYTES_PER_TILE_4BPP
);
}
/// Creates a new dynamic sprite.
pub fn new(size: Size) -> Self {
Self::new_in(size, Global)
}
}
impl<A: Allocator> DynamicSprite<A> {
#[must_use]
/// Creates a new dynamic sprite of a given size in a given allocator.
pub fn new_in(size: Size, allocator: A) -> Self {
let num_bytes = size.number_of_tiles() * BYTES_PER_TILE_4BPP;
let mut data = Vec::with_capacity_in(num_bytes, allocator);
data.resize(num_bytes, 0);
let data = data.into_boxed_slice();
DynamicSprite { data, size }
}
/// Set the pixel of a sprite to a given paletted pixel. Panics if the
/// coordinate is out of range of the sprite or if the paletted pixel is
/// greater than 4 bits.
pub fn set_pixel(&mut self, x: usize, y: usize, paletted_pixel: usize) {
assert!(paletted_pixel < 0x10);
let (sprite_pixel_x, sprite_pixel_y) = self.size.to_width_height();
assert!(x < sprite_pixel_x, "x too big for sprite size");
assert!(y < sprite_pixel_y, "y too big for sprite size");
let (sprite_tile_x, _) = self.size.to_tiles_width_height();
let (adjust_tile_x, adjust_tile_y) = (x / 8, y / 8);
let tile_number_to_modify = adjust_tile_x + adjust_tile_y * sprite_tile_x;
let byte_to_modify_in_tile = x / 2 + y * 4;
let byte_to_modify = tile_number_to_modify * BYTES_PER_TILE_4BPP + byte_to_modify_in_tile;
let mut byte = self.data[byte_to_modify];
let parity = (x & 0b1) * 4;
byte = (byte & !(0b1111 << parity)) | ((paletted_pixel as u8) << parity);
self.data[byte_to_modify] = byte;
}
/// Tries to copy the sprite to vram to be used to set object sprites.
pub fn try_vram(&self, palette: PaletteVram) -> Result<SpriteVram, LoaderError> {
SpriteVram::new(self.data, self.size, palette)
SpriteVram::new(&self.data, self.size, palette)
}
#[must_use]

View file

@ -27,3 +27,12 @@ impl Palette16 {
Layout::new::<Self>()
}
}
#[macro_export]
macro_rules! include_palette {
($palette:literal) => {
$crate::include_colours_inner!($palette)
};
}
pub use include_palette;

View file

@ -104,6 +104,9 @@ pub use agb_image_converter::include_aseprite_inner;
#[doc(hidden)]
pub use agb_image_converter::include_font as include_font_inner;
#[doc(hidden)]
pub use agb_image_converter::include_colours_inner;
#[macro_export]
macro_rules! include_font {
($font_path: literal, $font_size: literal) => {{
@ -165,6 +168,11 @@ pub mod syscall;
/// Interactions with the internal timers
pub mod timer;
mod no_game;
/// Default game
pub use no_game::no_game;
pub(crate) mod arena;
pub use {agb_alloc::ExternalAllocator, agb_alloc::InternalAllocator};

213
agb/src/no_game.rs Normal file
View file

@ -0,0 +1,213 @@
//! The no game screen is what is displayed if there isn't a game made yet.
use agb_fixnum::{num, Num, Vector2D};
use alloc::vec::Vec;
use alloc::{boxed::Box, vec};
use crate::display::object::{DynamicSprite, PaletteVram, Size, SpriteVram};
use crate::display::palette16::Palette16;
use crate::{
display::{object::ObjectUnmanaged, HEIGHT, WIDTH},
include_palette,
interrupt::VBlank,
};
const PALETTE: &[u16] = &include_palette!("gfx/pastel.png");
fn letters() -> Vec<Vec<Vector2D<Num<i32, 8>>>> {
vec![
// N
vec![
(num!(0.), num!(0.)).into(),
(num!(1.), num!(1.)).into(),
(num!(2.), num!(2.)).into(),
(num!(3.), num!(3.)).into(),
(num!(0.), num!(1.)).into(),
(num!(0.), num!(2.)).into(),
(num!(0.), num!(3.)).into(),
(num!(3.), num!(0.)).into(),
(num!(3.), num!(1.)).into(),
(num!(3.), num!(2.)).into(),
(num!(3.), num!(3.)).into(),
],
// O
vec![
(num!(0.), num!(0.)).into(),
(num!(0.), num!(1.)).into(),
(num!(0.), num!(2.)).into(),
(num!(0.), num!(3.)).into(),
(num!(1.), num!(3.)).into(),
(num!(2.), num!(3.)).into(),
(num!(3.), num!(3.)).into(),
(num!(3.), num!(2.)).into(),
(num!(3.), num!(1.)).into(),
(num!(3.), num!(0.)).into(),
(num!(2.), num!(0.)).into(),
(num!(1.), num!(0.)).into(),
],
// G
vec![
(num!(3.), num!(0.)).into(),
(num!(2.), num!(0.)).into(),
(num!(1.), num!(0.)).into(),
(num!(0.), num!(0.)).into(),
(num!(0.), num!(1.)).into(),
(num!(0.), num!(2.)).into(),
(num!(0.), num!(3.)).into(),
(num!(1.), num!(3.)).into(),
(num!(2.), num!(3.)).into(),
(num!(3.), num!(3.)).into(),
(num!(3.), num!(2.25)).into(),
(num!(3.), num!(1.5)).into(),
(num!(2.), num!(1.5)).into(),
],
// A
vec![
(num!(0.), num!(0.)).into(),
(num!(0.), num!(1.)).into(),
(num!(0.), num!(2.)).into(),
(num!(0.), num!(3.)).into(),
(num!(3.), num!(3.)).into(),
(num!(3.), num!(2.)).into(),
(num!(3.), num!(1.)).into(),
(num!(3.), num!(0.)).into(),
(num!(2.), num!(0.)).into(),
(num!(1.), num!(0.)).into(),
(num!(1.), num!(1.5)).into(),
(num!(2.), num!(1.5)).into(),
],
// M
vec![
(num!(0.), num!(0.)).into(),
(num!(0.), num!(1.)).into(),
(num!(0.), num!(2.)).into(),
(num!(0.), num!(3.)).into(),
(num!(3.), num!(3.)).into(),
(num!(3.), num!(2.)).into(),
(num!(3.), num!(1.)).into(),
(num!(3.), num!(0.)).into(),
(num!(1.5), num!(1.5)).into(),
(num!(0.75), num!(0.75)).into(),
(num!(2.25), num!(0.75)).into(),
],
// E
vec![
(num!(0.), num!(0.)).into(),
(num!(0.), num!(1.)).into(),
(num!(0.), num!(2.)).into(),
(num!(0.), num!(3.)).into(),
(num!(1.), num!(3.)).into(),
(num!(2.), num!(3.)).into(),
(num!(3.), num!(3.)).into(),
(num!(3.), num!(0.)).into(),
(num!(2.), num!(0.)).into(),
(num!(1.), num!(0.)).into(),
(num!(1.), num!(1.5)).into(),
(num!(2.), num!(1.5)).into(),
],
]
}
fn generate_sprites() -> Box<[SpriteVram]> {
let mut sprites = Vec::new();
// generate palettes
let palettes: Vec<PaletteVram> = PALETTE
.chunks(15)
.map(|x| {
core::iter::once(0)
.chain(x.iter().copied())
.chain(core::iter::repeat(0))
.take(16)
.collect::<Vec<_>>()
})
.map(|palette| {
let palette = Palette16::new(palette.try_into().unwrap());
PaletteVram::new(&palette).unwrap()
})
.collect();
// generate sprites
let mut sprite = DynamicSprite::new(Size::S8x8);
for (palette, colour) in (0..PALETTE.len()).map(|x| (x / 15, x % 15)) {
for y in 0..8 {
for x in 0..8 {
sprite.set_pixel(x, y, colour + 1);
}
}
sprites.push(sprite.to_vram(palettes[palette].clone()));
}
sprites.into_boxed_slice()
}
pub fn no_game(mut gba: crate::Gba) -> ! {
let (mut oam, _) = gba.display.object.get_unmanaged();
let squares = generate_sprites();
let mut letter_positons = Vec::new();
let square_positions = {
let mut s = letters();
for letter in s.iter_mut() {
letter.sort_by_key(|a| a.magnitude_squared());
}
s
};
for (letter_idx, letter_parts) in square_positions.iter().enumerate() {
for part in letter_parts.iter() {
let position = part
.hadamard((8, 10).into())
.hadamard((num!(3.) / 2, num!(3.) / 2).into());
let letter_pos = Vector2D::new(
60 * (1 + letter_idx as i32 - ((letter_idx >= 2) as i32 * 3)),
70 * ((letter_idx >= 2) as i32),
);
letter_positons.push(position + letter_pos.change_base());
}
}
let bottom_right = letter_positons
.iter()
.copied()
.max_by_key(|x| x.manhattan_distance())
.unwrap();
let difference = (Vector2D::new(WIDTH - 8, HEIGHT - 8).change_base() - bottom_right) / 2;
for pos in letter_positons.iter_mut() {
*pos += difference;
}
let mut time: Num<i32, 8> = num!(0.);
let time_delta: Num<i32, 8> = num!(0.025);
let vblank = VBlank::get();
loop {
time += time_delta;
time %= 1;
let letters: Vec<ObjectUnmanaged> = letter_positons
.iter()
.enumerate()
.map(|(idx, position)| {
let time = time + Num::<i32, 8>::new(idx as i32) / 128;
(idx, *position + Vector2D::new(time.sin(), time.cos()) * 10)
})
.map(|(idx, pos)| {
let mut obj = ObjectUnmanaged::new(squares[idx % squares.len()].clone());
obj.show().set_position(pos.floor());
obj
})
.collect();
vblank.wait_for_vblank();
for (obj, slot) in letters.iter().zip(oam.iter()) {
slot.set(obj);
}
}
}

View file

@ -14,22 +14,10 @@
#![cfg_attr(test, reexport_test_harness_main = "test_main")]
#![cfg_attr(test, test_runner(agb::test_runner::test_runner))]
use agb::{display, syscall};
// The main function must take 1 arguments and never return. The agb::entry decorator
// ensures that everything is in order. `agb` will call this after setting up the stack
// and interrupt handlers correctly. It will also handle creating the `Gba` struct for you.
#[agb::entry]
fn main(mut gba: agb::Gba) -> ! {
let mut bitmap = gba.display.video.bitmap3();
for x in 0..display::WIDTH {
let y = syscall::sqrt(x << 6);
let y = (display::HEIGHT - y).clamp(0, display::HEIGHT - 1);
bitmap.draw_point(x, y, 0x001F);
}
loop {
syscall::halt();
}
agb::no_game(gba);
}