2022-08-12 07:22:44 +10:00
|
|
|
use palette16::{Palette16OptimisationResults, Palette16Optimiser};
|
2022-08-12 07:47:52 +10:00
|
|
|
use palette256::Palette256;
|
2021-07-22 07:46:22 +10:00
|
|
|
use proc_macro::TokenStream;
|
2022-02-23 07:16:34 +11:00
|
|
|
use proc_macro2::Literal;
|
2023-04-14 06:53:13 +10:00
|
|
|
use syn::parse::{Parse, Parser};
|
2022-02-23 07:16:34 +11:00
|
|
|
use syn::{parse_macro_input, punctuated::Punctuated, LitStr};
|
2023-04-14 06:53:13 +10:00
|
|
|
use syn::{Expr, ExprLit, Lit, Token};
|
2021-04-20 08:01:28 +10:00
|
|
|
|
2022-08-12 07:22:44 +10:00
|
|
|
use std::collections::HashMap;
|
2022-02-23 07:24:32 +11:00
|
|
|
use std::path::PathBuf;
|
2022-02-23 07:16:34 +11:00
|
|
|
use std::{iter, path::Path, str};
|
2021-06-06 02:45:21 +10:00
|
|
|
|
2022-02-23 07:16:34 +11:00
|
|
|
use quote::{format_ident, quote, ToTokens};
|
2021-07-23 04:03:49 +10:00
|
|
|
|
2022-02-16 08:28:21 +11:00
|
|
|
mod aseprite;
|
2021-04-20 08:15:03 +10:00
|
|
|
mod colour;
|
2021-07-22 07:46:22 +10:00
|
|
|
mod config;
|
2023-08-29 23:07:19 +10:00
|
|
|
mod deduplicator;
|
2022-04-05 07:06:08 +10:00
|
|
|
mod font_loader;
|
2021-04-20 09:40:07 +10:00
|
|
|
mod image_loader;
|
|
|
|
mod palette16;
|
2022-08-12 07:47:52 +10:00
|
|
|
mod palette256;
|
2021-04-21 05:41:04 +10:00
|
|
|
mod rust_generator;
|
2021-04-20 09:40:07 +10:00
|
|
|
|
|
|
|
use image_loader::Image;
|
2021-04-20 08:15:03 +10:00
|
|
|
|
2021-07-22 07:07:09 +10:00
|
|
|
use colour::Colour;
|
2021-04-20 08:01:28 +10:00
|
|
|
|
2023-04-14 06:53:13 +10:00
|
|
|
#[derive(Debug, Clone, Copy)]
|
2022-08-12 06:16:26 +10:00
|
|
|
pub(crate) enum Colours {
|
|
|
|
Colours16,
|
|
|
|
Colours256,
|
|
|
|
}
|
|
|
|
|
2023-04-14 06:53:13 +10:00
|
|
|
struct BackgroundGfxOption {
|
|
|
|
module_name: String,
|
|
|
|
file_name: String,
|
|
|
|
colours: Colours,
|
2023-08-29 22:16:57 +10:00
|
|
|
deduplicate: bool,
|
2023-04-14 06:53:13 +10:00
|
|
|
}
|
|
|
|
|
|
|
|
impl config::Image for BackgroundGfxOption {
|
|
|
|
fn filename(&self) -> String {
|
2024-01-25 05:19:30 +11:00
|
|
|
self.file_name
|
|
|
|
.clone()
|
2024-01-25 06:17:04 +11:00
|
|
|
.replace(OUT_DIR_TOKEN, &get_out_dir(&self.file_name))
|
2023-04-14 06:53:13 +10:00
|
|
|
}
|
|
|
|
|
|
|
|
fn colours(&self) -> Colours {
|
|
|
|
self.colours
|
|
|
|
}
|
2023-08-29 22:18:14 +10:00
|
|
|
|
|
|
|
fn deduplicate(&self) -> bool {
|
|
|
|
self.deduplicate
|
|
|
|
}
|
2023-04-14 06:53:13 +10:00
|
|
|
}
|
|
|
|
|
|
|
|
impl Parse for BackgroundGfxOption {
|
|
|
|
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
|
|
|
|
let module_name: syn::Ident = input.parse()?;
|
|
|
|
let _: Token![=>] = input.parse()?;
|
|
|
|
|
2023-04-14 07:01:37 +10:00
|
|
|
let lookahead = input.lookahead1();
|
|
|
|
|
2023-04-14 06:53:13 +10:00
|
|
|
let colours = if lookahead.peek(syn::LitInt) {
|
|
|
|
let num_colours: syn::LitInt = input.parse()?;
|
|
|
|
|
|
|
|
match num_colours.base10_parse()? {
|
|
|
|
16 => Colours::Colours16,
|
|
|
|
256 => Colours::Colours256,
|
|
|
|
_ => {
|
|
|
|
return Err(syn::Error::new_spanned(
|
|
|
|
num_colours,
|
|
|
|
"Number of colours must be 16 or 256",
|
|
|
|
))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
Colours::Colours16
|
|
|
|
};
|
|
|
|
|
2023-08-29 22:16:57 +10:00
|
|
|
let lookahead = input.lookahead1();
|
|
|
|
|
|
|
|
let deduplicate = if lookahead.peek(syn::Ident) {
|
|
|
|
let deduplicate: syn::Ident = input.parse()?;
|
|
|
|
|
|
|
|
if deduplicate == "deduplicate" {
|
|
|
|
true
|
|
|
|
} else {
|
|
|
|
return Err(syn::Error::new_spanned(
|
|
|
|
deduplicate,
|
|
|
|
"Must either be the literal deduplicate or missing",
|
|
|
|
));
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
false
|
|
|
|
};
|
|
|
|
|
2023-04-14 06:53:13 +10:00
|
|
|
let file_name: syn::LitStr = input.parse()?;
|
|
|
|
|
|
|
|
Ok(Self {
|
|
|
|
module_name: module_name.to_string(),
|
|
|
|
file_name: file_name.value(),
|
|
|
|
colours,
|
2023-08-29 22:16:57 +10:00
|
|
|
deduplicate,
|
2023-04-14 06:53:13 +10:00
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
struct IncludeBackgroundGfxInput {
|
|
|
|
module_name: syn::Ident,
|
|
|
|
crate_prefix: String,
|
|
|
|
transparent_colour: Colour,
|
|
|
|
background_gfx_options: Vec<BackgroundGfxOption>,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Parse for IncludeBackgroundGfxInput {
|
|
|
|
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
|
|
|
|
let lookahead = input.lookahead1();
|
|
|
|
|
|
|
|
let crate_prefix: syn::Ident = if lookahead.peek(Token![crate]) {
|
|
|
|
let _: Token![crate] = input.parse()?;
|
|
|
|
let _: Token![,] = input.parse()?;
|
|
|
|
format_ident!("crate")
|
|
|
|
} else {
|
|
|
|
format_ident!("agb")
|
|
|
|
};
|
|
|
|
|
|
|
|
let module_name: syn::Ident = input.parse()?;
|
|
|
|
let _: Token![,] = input.parse()?;
|
|
|
|
|
2023-04-14 07:01:37 +10:00
|
|
|
let lookahead = input.lookahead1();
|
2023-04-14 06:53:13 +10:00
|
|
|
let transparent_colour: Colour = if lookahead.peek(syn::LitStr) {
|
|
|
|
let colour_str: syn::LitStr = input.parse()?;
|
|
|
|
let _: Token![,] = input.parse()?;
|
|
|
|
colour_str
|
|
|
|
.value()
|
|
|
|
.parse()
|
|
|
|
.map_err(|msg| syn::Error::new_spanned(colour_str, msg))?
|
|
|
|
} else {
|
|
|
|
Colour::from_rgb(255, 0, 255, 0)
|
|
|
|
};
|
|
|
|
|
|
|
|
let background_gfx_options =
|
|
|
|
input.parse_terminated(BackgroundGfxOption::parse, Token![,])?;
|
|
|
|
|
|
|
|
Ok(Self {
|
|
|
|
module_name,
|
|
|
|
crate_prefix: crate_prefix.to_string(),
|
|
|
|
transparent_colour,
|
|
|
|
background_gfx_options: background_gfx_options.into_iter().collect(),
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl config::Config for IncludeBackgroundGfxInput {
|
|
|
|
fn crate_prefix(&self) -> String {
|
|
|
|
self.crate_prefix.clone()
|
|
|
|
}
|
|
|
|
|
|
|
|
fn images(&self) -> HashMap<String, &dyn config::Image> {
|
|
|
|
self.background_gfx_options
|
|
|
|
.iter()
|
|
|
|
.map(|options| (options.module_name.clone(), options as &dyn config::Image))
|
|
|
|
.collect()
|
|
|
|
}
|
|
|
|
|
|
|
|
fn transparent_colour(&self) -> Option<Colour> {
|
|
|
|
Some(self.transparent_colour)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-02-03 23:40:59 +11:00
|
|
|
/// Including from the out directory is supported through the `$OUT_DIR` token.
|
|
|
|
///
|
|
|
|
/// ```rust,ignore
|
|
|
|
/// # #![no_std]
|
|
|
|
/// # #![no_main]
|
|
|
|
/// # use agb::include_background_gfx;
|
|
|
|
/// include_background_gfx!(generated_background, "000000", DATA => "$OUT_DIR/generated_background.aseprite");
|
|
|
|
/// ```
|
|
|
|
///
|
2023-04-14 06:53:13 +10:00
|
|
|
#[proc_macro]
|
|
|
|
pub fn include_background_gfx(input: TokenStream) -> TokenStream {
|
|
|
|
let config = Box::new(parse_macro_input!(input as IncludeBackgroundGfxInput));
|
|
|
|
|
|
|
|
let root = std::env::var("CARGO_MANIFEST_DIR").expect("Failed to get cargo manifest dir");
|
|
|
|
|
|
|
|
let module_name = config.module_name.clone();
|
|
|
|
include_gfx_from_config(config, module_name, Path::new(&root))
|
|
|
|
}
|
|
|
|
|
|
|
|
fn include_gfx_from_config(
|
|
|
|
config: Box<dyn config::Config>,
|
|
|
|
module_name: syn::Ident,
|
|
|
|
parent: &Path,
|
|
|
|
) -> TokenStream {
|
2021-07-23 04:03:49 +10:00
|
|
|
let images = config.images();
|
2022-08-12 07:22:44 +10:00
|
|
|
|
2022-10-09 02:45:58 +11:00
|
|
|
let mut optimiser = Palette16Optimiser::new(config.transparent_colour());
|
2022-08-12 07:22:44 +10:00
|
|
|
let mut assignment_offsets = HashMap::new();
|
|
|
|
let mut assignment_offset = 0;
|
|
|
|
|
2022-08-12 07:47:52 +10:00
|
|
|
let mut palette256 = Palette256::new();
|
|
|
|
|
2022-08-12 07:22:44 +10:00
|
|
|
for (name, settings) in images.iter() {
|
2023-01-07 05:26:03 +11:00
|
|
|
let image_filename = &parent.join(settings.filename());
|
2022-08-12 07:22:44 +10:00
|
|
|
let image = Image::load_from_file(image_filename);
|
|
|
|
|
2022-08-12 07:47:52 +10:00
|
|
|
match settings.colours() {
|
|
|
|
Colours::Colours16 => {
|
2023-04-14 07:18:13 +10:00
|
|
|
let tile_size = 8;
|
2022-08-12 07:47:52 +10:00
|
|
|
if image.width % tile_size != 0 || image.height % tile_size != 0 {
|
|
|
|
panic!("Image size not a multiple of tile size");
|
|
|
|
}
|
|
|
|
|
|
|
|
add_to_optimiser(
|
|
|
|
&mut optimiser,
|
|
|
|
&image,
|
|
|
|
tile_size,
|
2023-07-09 02:06:00 +10:00
|
|
|
tile_size,
|
2022-10-09 02:45:58 +11:00
|
|
|
config.transparent_colour(),
|
2022-08-12 07:47:52 +10:00
|
|
|
);
|
|
|
|
|
2023-04-14 07:18:13 +10:00
|
|
|
let num_tiles = image.width * image.height / 8usize.pow(2);
|
2022-08-12 07:47:52 +10:00
|
|
|
assignment_offsets.insert(name, assignment_offset);
|
|
|
|
assignment_offset += num_tiles;
|
|
|
|
}
|
|
|
|
Colours::Colours256 => {
|
|
|
|
palette256.add_image(&image);
|
|
|
|
}
|
2022-08-12 07:22:44 +10:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
let optimisation_results = optimiser.optimise_palettes();
|
2022-08-12 07:47:52 +10:00
|
|
|
let optimisation_results = palette256.extend_results(&optimisation_results);
|
2022-08-12 07:22:44 +10:00
|
|
|
|
|
|
|
let mut image_code = vec![];
|
|
|
|
|
|
|
|
for (image_name, &image) in images.iter() {
|
2022-08-12 08:48:57 +10:00
|
|
|
let assignment_offset = match image.colours() {
|
|
|
|
Colours::Colours16 => Some(assignment_offsets[image_name]),
|
|
|
|
_ => None,
|
|
|
|
};
|
|
|
|
|
2022-08-12 07:22:44 +10:00
|
|
|
image_code.push(convert_image(
|
|
|
|
image,
|
|
|
|
parent,
|
|
|
|
image_name,
|
|
|
|
&config.crate_prefix(),
|
|
|
|
&optimisation_results,
|
2022-08-12 08:48:57 +10:00
|
|
|
assignment_offset,
|
2022-08-12 07:22:44 +10:00
|
|
|
));
|
|
|
|
}
|
2021-07-22 07:07:09 +10:00
|
|
|
|
2022-10-09 09:02:54 +11:00
|
|
|
let palette_code =
|
|
|
|
rust_generator::generate_palette_code(&optimisation_results, &config.crate_prefix());
|
2021-07-22 07:07:09 +10:00
|
|
|
|
2021-07-23 04:03:49 +10:00
|
|
|
let module = quote! {
|
2022-01-03 04:54:44 +11:00
|
|
|
mod #module_name {
|
2022-10-09 09:02:54 +11:00
|
|
|
#palette_code
|
|
|
|
|
2021-07-23 04:03:49 +10:00
|
|
|
#(#image_code)*
|
|
|
|
}
|
|
|
|
};
|
2021-07-22 07:07:09 +10:00
|
|
|
|
2021-07-23 04:03:49 +10:00
|
|
|
TokenStream::from(module)
|
2021-04-20 09:40:07 +10:00
|
|
|
}
|
|
|
|
|
2022-02-23 07:16:34 +11:00
|
|
|
use quote::TokenStreamExt;
|
|
|
|
struct ByteString<'a>(&'a [u8]);
|
|
|
|
impl ToTokens for ByteString<'_> {
|
|
|
|
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
|
|
|
|
tokens.append(Literal::byte_string(self.0));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-05-07 07:42:03 +10:00
|
|
|
#[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),*]
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2022-02-16 08:33:48 +11:00
|
|
|
#[proc_macro]
|
|
|
|
pub fn include_aseprite_inner(input: TokenStream) -> TokenStream {
|
2024-01-25 06:17:04 +11:00
|
|
|
let out_dir_path = get_out_dir(&input.to_string());
|
2024-01-25 05:19:30 +11:00
|
|
|
|
2023-05-07 07:42:03 +10:00
|
|
|
let parser = Punctuated::<LitStr, syn::Token![,]>::parse_terminated;
|
2022-02-23 07:16:34 +11:00
|
|
|
let parsed = match parser.parse(input) {
|
|
|
|
Ok(e) => e,
|
|
|
|
Err(e) => return e.to_compile_error().into(),
|
|
|
|
};
|
2022-02-16 08:33:48 +11:00
|
|
|
|
2022-05-23 04:23:29 +10:00
|
|
|
let transparent_colour = Colour::from_rgb(255, 0, 255, 0);
|
|
|
|
|
|
|
|
let mut optimiser = palette16::Palette16Optimiser::new(Some(transparent_colour));
|
2022-02-23 07:16:34 +11:00
|
|
|
let mut images = Vec::new();
|
|
|
|
let mut tags = Vec::new();
|
2022-02-16 08:33:48 +11:00
|
|
|
|
2022-02-23 07:16:34 +11:00
|
|
|
let root = std::env::var("CARGO_MANIFEST_DIR").expect("Failed to get cargo manifest dir");
|
2022-02-16 08:33:48 +11:00
|
|
|
|
2022-02-23 07:24:32 +11:00
|
|
|
let filenames: Vec<PathBuf> = parsed
|
2022-02-23 07:16:34 +11:00
|
|
|
.iter()
|
|
|
|
.map(|s| s.value())
|
2024-01-25 06:17:04 +11:00
|
|
|
.map(|s| s.replace(OUT_DIR_TOKEN, &out_dir_path))
|
2022-02-23 07:24:32 +11:00
|
|
|
.map(|s| Path::new(&root).join(&*s))
|
2022-02-23 07:16:34 +11:00
|
|
|
.collect();
|
2022-02-16 08:33:48 +11:00
|
|
|
|
2022-02-23 07:16:34 +11:00
|
|
|
for filename in filenames.iter() {
|
2022-02-24 05:05:48 +11:00
|
|
|
let (frames, tag) = aseprite::generate_from_file(filename);
|
|
|
|
|
|
|
|
tags.push((tag, images.len()));
|
2022-02-16 08:33:48 +11:00
|
|
|
|
2022-02-24 05:05:48 +11:00
|
|
|
for frame in frames {
|
|
|
|
let width = frame.width();
|
2022-08-01 10:38:17 +10:00
|
|
|
let height = frame.height();
|
2022-08-01 21:08:22 +10:00
|
|
|
assert!(
|
|
|
|
valid_sprite_size(width, height),
|
2022-11-18 08:18:54 +11:00
|
|
|
"File {} contains sprites with size {}x{} which cannot be represented on the GameBoy Advance",
|
2022-08-01 21:08:22 +10:00
|
|
|
filename.display(),
|
|
|
|
width,
|
|
|
|
height
|
|
|
|
);
|
2022-02-16 08:33:48 +11:00
|
|
|
|
2022-02-24 05:05:48 +11:00
|
|
|
let image = Image::load_from_dyn_image(frame);
|
2023-07-09 02:06:00 +10:00
|
|
|
add_to_optimiser(
|
|
|
|
&mut optimiser,
|
|
|
|
&image,
|
|
|
|
width as usize,
|
|
|
|
height as usize,
|
|
|
|
Some(transparent_colour),
|
|
|
|
);
|
2022-02-24 05:05:48 +11:00
|
|
|
images.push(image);
|
|
|
|
}
|
2022-02-23 07:16:34 +11:00
|
|
|
}
|
2022-02-16 08:33:48 +11:00
|
|
|
|
2022-05-23 04:23:29 +10:00
|
|
|
let optimised_results = optimiser.optimise_palettes();
|
2022-02-16 08:33:48 +11:00
|
|
|
|
2022-08-07 18:49:32 +10:00
|
|
|
let (palette_data, tile_data, assignments) = palette_tile_data(&optimised_results, &images);
|
2022-02-16 08:33:48 +11:00
|
|
|
|
|
|
|
let palette_data = palette_data.iter().map(|colours| {
|
|
|
|
quote! {
|
|
|
|
Palette16::new([
|
|
|
|
#(#colours),*
|
|
|
|
])
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
let mut pre = 0;
|
2022-02-24 05:05:48 +11:00
|
|
|
let sprites = images
|
2022-02-16 08:33:48 +11:00
|
|
|
.iter()
|
|
|
|
.zip(assignments.iter())
|
|
|
|
.map(|(f, assignment)| {
|
|
|
|
let start: usize = pre;
|
2022-02-24 05:05:48 +11:00
|
|
|
let end: usize = pre + (f.width / 8) * (f.height / 8) * 32;
|
2022-02-23 07:16:34 +11:00
|
|
|
let data = ByteString(&tile_data[start..end]);
|
2022-02-16 08:33:48 +11:00
|
|
|
pre = end;
|
2022-02-24 05:05:48 +11:00
|
|
|
let width = f.width;
|
|
|
|
let height = f.height;
|
2022-02-16 08:33:48 +11:00
|
|
|
quote! {
|
2022-10-03 03:56:52 +11:00
|
|
|
unsafe {
|
|
|
|
Sprite::new(
|
|
|
|
&PALETTES[#assignment],
|
|
|
|
align_bytes!(u16, #data),
|
|
|
|
Size::from_width_height(#width, #height)
|
|
|
|
)
|
|
|
|
}
|
2022-02-16 08:33:48 +11:00
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2022-03-06 04:24:23 +11:00
|
|
|
let tags = tags.iter().flat_map(|(tag, num_images)| {
|
|
|
|
tag.iter().map(move |tag| {
|
|
|
|
let start = tag.from_frame() as usize + num_images;
|
|
|
|
let end = tag.to_frame() as usize + num_images;
|
|
|
|
let direction = tag.animation_direction() as usize;
|
|
|
|
|
|
|
|
let name = tag.name();
|
2023-02-08 07:32:43 +11:00
|
|
|
assert!(start <= end, "Tag {name} has start > end");
|
2022-03-06 04:24:23 +11:00
|
|
|
|
|
|
|
quote! {
|
|
|
|
(#name, Tag::new(SPRITES, #start, #end, #direction))
|
|
|
|
}
|
2022-02-23 07:16:34 +11:00
|
|
|
})
|
2022-03-06 04:24:23 +11:00
|
|
|
});
|
2022-02-16 08:33:48 +11:00
|
|
|
|
2022-02-23 07:16:34 +11:00
|
|
|
let include_paths = filenames.iter().map(|s| {
|
2022-02-23 07:24:32 +11:00
|
|
|
let s = s.as_os_str().to_string_lossy();
|
2022-02-16 08:33:48 +11:00
|
|
|
quote! {
|
2022-02-23 07:16:34 +11:00
|
|
|
const _: &[u8] = include_bytes!(#s);
|
2022-02-16 08:33:48 +11:00
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
let module = quote! {
|
2022-02-23 07:16:34 +11:00
|
|
|
#(#include_paths)*
|
2022-02-16 08:33:48 +11:00
|
|
|
|
|
|
|
|
|
|
|
const PALETTES: &[Palette16] = &[
|
|
|
|
#(#palette_data),*
|
|
|
|
];
|
|
|
|
|
|
|
|
pub const SPRITES: &[Sprite] = &[
|
|
|
|
#(#sprites),*
|
|
|
|
];
|
|
|
|
|
|
|
|
const TAGS: &TagMap = &TagMap::new(
|
2022-03-06 04:24:23 +11:00
|
|
|
&[
|
2022-02-16 08:33:48 +11:00
|
|
|
#(#tags),*
|
2022-03-06 04:24:23 +11:00
|
|
|
]
|
2022-02-16 08:33:48 +11:00
|
|
|
);
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
TokenStream::from(module)
|
|
|
|
}
|
|
|
|
|
2021-07-22 07:46:22 +10:00
|
|
|
fn convert_image(
|
|
|
|
settings: &dyn config::Image,
|
|
|
|
parent: &Path,
|
|
|
|
variable_name: &str,
|
|
|
|
crate_prefix: &str,
|
2022-08-12 07:22:44 +10:00
|
|
|
optimisation_results: &Palette16OptimisationResults,
|
2022-08-12 08:48:57 +10:00
|
|
|
assignment_offset: Option<usize>,
|
2021-07-23 04:04:30 +10:00
|
|
|
) -> proc_macro2::TokenStream {
|
2023-01-07 05:26:03 +11:00
|
|
|
let image_filename = &parent.join(settings.filename());
|
2021-07-22 07:07:09 +10:00
|
|
|
let image = Image::load_from_file(image_filename);
|
2023-08-29 23:07:19 +10:00
|
|
|
let deduplicate = settings.deduplicate();
|
2021-04-20 09:40:07 +10:00
|
|
|
|
2021-04-21 05:41:04 +10:00
|
|
|
rust_generator::generate_code(
|
2021-07-22 07:07:09 +10:00
|
|
|
variable_name,
|
2022-08-12 07:22:44 +10:00
|
|
|
optimisation_results,
|
2021-04-21 05:41:04 +10:00
|
|
|
&image,
|
2021-07-22 07:07:09 +10:00
|
|
|
&image_filename.to_string_lossy(),
|
|
|
|
crate_prefix.to_owned(),
|
2022-08-12 07:22:44 +10:00
|
|
|
assignment_offset,
|
2023-08-29 23:07:19 +10:00
|
|
|
deduplicate,
|
2021-07-27 08:27:16 +10:00
|
|
|
)
|
2021-04-20 08:01:28 +10:00
|
|
|
}
|
|
|
|
|
2022-02-23 07:16:34 +11:00
|
|
|
fn add_to_optimiser(
|
|
|
|
palette_optimiser: &mut palette16::Palette16Optimiser,
|
|
|
|
image: &Image,
|
2023-07-09 02:06:00 +10:00
|
|
|
tile_width: usize,
|
|
|
|
tile_height: usize,
|
2022-05-23 04:23:29 +10:00
|
|
|
transparent_colour: Option<Colour>,
|
2022-02-23 07:16:34 +11:00
|
|
|
) {
|
2023-07-09 02:06:00 +10:00
|
|
|
let tiles_x = image.width / tile_width;
|
|
|
|
let tiles_y = image.height / tile_height;
|
2021-04-20 09:40:07 +10:00
|
|
|
|
|
|
|
for y in 0..tiles_y {
|
|
|
|
for x in 0..tiles_x {
|
|
|
|
let mut palette = palette16::Palette16::new();
|
|
|
|
|
2023-07-09 02:06:00 +10:00
|
|
|
for j in 0..tile_height {
|
|
|
|
for i in 0..tile_width {
|
|
|
|
let colour = image.colour(x * tile_width + i, y * tile_height + j);
|
2021-04-20 09:40:07 +10:00
|
|
|
|
2022-05-23 04:23:29 +10:00
|
|
|
palette.add_colour(match (colour.is_transparent(), transparent_colour) {
|
|
|
|
(true, Some(transparent_colour)) => transparent_colour,
|
|
|
|
_ => colour,
|
|
|
|
});
|
2021-04-20 09:40:07 +10:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
palette_optimiser.add_palette(palette);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2022-02-16 08:33:48 +11:00
|
|
|
|
2022-08-07 18:49:32 +10:00
|
|
|
fn palette_tile_data(
|
2022-02-16 08:33:48 +11:00
|
|
|
optimiser: &Palette16OptimisationResults,
|
2022-02-23 07:16:34 +11:00
|
|
|
images: &[Image],
|
2022-02-16 08:33:48 +11:00
|
|
|
) -> (Vec<Vec<u16>>, Vec<u8>, Vec<usize>) {
|
|
|
|
let palette_data: Vec<Vec<u16>> = optimiser
|
|
|
|
.optimised_palettes
|
|
|
|
.iter()
|
|
|
|
.map(|palette| {
|
|
|
|
palette
|
|
|
|
.clone()
|
|
|
|
.into_iter()
|
|
|
|
.map(|colour| colour.to_rgb15())
|
|
|
|
.chain(iter::repeat(0))
|
|
|
|
.take(16)
|
|
|
|
.collect()
|
|
|
|
})
|
|
|
|
.collect();
|
|
|
|
|
2022-02-23 07:16:34 +11:00
|
|
|
let mut tile_data = Vec::new();
|
|
|
|
|
2023-05-07 05:31:43 +10:00
|
|
|
for (image_idx, image) in images.iter().enumerate() {
|
2023-08-29 23:39:41 +10:00
|
|
|
add_image_to_tile_data(
|
|
|
|
&mut tile_data,
|
|
|
|
image,
|
|
|
|
optimiser,
|
|
|
|
image_idx,
|
|
|
|
true,
|
|
|
|
&(0..images.len()).collect::<Vec<_>>(),
|
|
|
|
);
|
2022-02-16 08:33:48 +11:00
|
|
|
}
|
|
|
|
|
2022-08-07 18:49:32 +10:00
|
|
|
let tile_data = collapse_to_4bpp(&tile_data);
|
2022-02-16 08:33:48 +11:00
|
|
|
|
|
|
|
let assignments = optimiser.assignments.clone();
|
|
|
|
|
|
|
|
(palette_data, tile_data, assignments)
|
|
|
|
}
|
2022-02-24 05:14:42 +11:00
|
|
|
|
2022-08-07 18:49:32 +10:00
|
|
|
fn collapse_to_4bpp(tile_data: &[u8]) -> Vec<u8> {
|
|
|
|
tile_data
|
|
|
|
.chunks(2)
|
|
|
|
.map(|chunk| chunk[0] | (chunk[1] << 4))
|
|
|
|
.collect()
|
|
|
|
}
|
|
|
|
|
|
|
|
fn add_image_to_tile_data(
|
|
|
|
tile_data: &mut Vec<u8>,
|
|
|
|
image: &Image,
|
2022-08-12 07:22:44 +10:00
|
|
|
optimiser: &Palette16OptimisationResults,
|
|
|
|
assignment_offset: usize,
|
2023-07-09 02:06:00 +10:00
|
|
|
is_sprite: bool,
|
2023-08-29 23:39:41 +10:00
|
|
|
remap_index: &[usize],
|
2022-08-07 18:49:32 +10:00
|
|
|
) {
|
2023-04-14 07:18:13 +10:00
|
|
|
let tile_size = 8;
|
2022-08-07 18:49:32 +10:00
|
|
|
let tiles_x = image.width / tile_size;
|
|
|
|
let tiles_y = image.height / tile_size;
|
|
|
|
|
|
|
|
for y in 0..tiles_y {
|
|
|
|
for x in 0..tiles_x {
|
2023-07-09 02:06:00 +10:00
|
|
|
let assignment = if is_sprite {
|
|
|
|
assignment_offset
|
|
|
|
} else {
|
2023-08-29 23:39:41 +10:00
|
|
|
remap_index[y * tiles_x + x] + assignment_offset
|
2023-07-09 02:06:00 +10:00
|
|
|
};
|
|
|
|
|
|
|
|
let palette_index = optimiser.assignments[assignment];
|
2022-08-07 18:49:32 +10:00
|
|
|
let palette = &optimiser.optimised_palettes[palette_index];
|
|
|
|
|
|
|
|
for inner_y in 0..tile_size / 8 {
|
|
|
|
for inner_x in 0..tile_size / 8 {
|
|
|
|
for j in inner_y * 8..inner_y * 8 + 8 {
|
|
|
|
for i in inner_x * 8..inner_x * 8 + 8 {
|
|
|
|
let colour = image.colour(x * tile_size + i, y * tile_size + j);
|
2023-03-24 08:47:44 +11:00
|
|
|
tile_data.push(palette.colour_index(colour));
|
2022-02-16 08:33:48 +11:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2022-08-07 18:49:32 +10:00
|
|
|
}
|
2022-02-16 08:33:48 +11:00
|
|
|
|
2022-08-12 08:48:57 +10:00
|
|
|
fn add_image_256_to_tile_data(
|
|
|
|
tile_data: &mut Vec<u8>,
|
|
|
|
image: &Image,
|
|
|
|
optimiser: &Palette16OptimisationResults,
|
|
|
|
) {
|
2023-04-14 07:18:13 +10:00
|
|
|
let tile_size = 8;
|
2022-08-12 08:48:57 +10:00
|
|
|
let tiles_x = image.width / tile_size;
|
|
|
|
let tiles_y = image.height / tile_size;
|
2022-02-16 08:33:48 +11:00
|
|
|
|
2022-08-12 08:48:57 +10:00
|
|
|
let all_colours: Vec<_> = optimiser
|
|
|
|
.optimised_palettes
|
|
|
|
.iter()
|
|
|
|
.flat_map(|p| p.colours())
|
|
|
|
.collect();
|
2022-02-16 08:33:48 +11:00
|
|
|
|
2022-08-12 08:48:57 +10:00
|
|
|
for y in 0..tiles_y {
|
|
|
|
for x in 0..tiles_x {
|
|
|
|
for inner_y in 0..tile_size / 8 {
|
|
|
|
for inner_x in 0..tile_size / 8 {
|
|
|
|
for j in inner_y * 8..inner_y * 8 + 8 {
|
|
|
|
for i in inner_x * 8..inner_x * 8 + 8 {
|
|
|
|
let colour = image.colour(x * tile_size + i, y * tile_size + j);
|
|
|
|
tile_data.push(all_colours.iter().position(|c| **c == colour).unwrap() as u8);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2022-02-16 08:33:48 +11:00
|
|
|
}
|
2022-02-24 05:14:42 +11:00
|
|
|
|
2022-04-24 00:33:57 +10:00
|
|
|
fn flatten_group(expr: &Expr) -> &Expr {
|
|
|
|
match expr {
|
|
|
|
Expr::Group(group) => &group.expr,
|
|
|
|
_ => expr,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-04-05 07:06:08 +10:00
|
|
|
#[proc_macro]
|
|
|
|
pub fn include_font(input: TokenStream) -> TokenStream {
|
|
|
|
let parser = Punctuated::<Expr, syn::Token![,]>::parse_separated_nonempty;
|
|
|
|
let parsed = match parser.parse(input) {
|
|
|
|
Ok(e) => e,
|
|
|
|
Err(e) => return e.to_compile_error().into(),
|
|
|
|
};
|
|
|
|
|
|
|
|
let all_args: Vec<_> = parsed.into_iter().collect();
|
|
|
|
if all_args.len() != 2 {
|
|
|
|
panic!("Include_font requires 2 arguments, got {}", all_args.len());
|
|
|
|
}
|
|
|
|
|
2022-04-24 00:33:57 +10:00
|
|
|
let filename = match flatten_group(&all_args[0]) {
|
2022-04-05 07:06:08 +10:00
|
|
|
Expr::Lit(ExprLit {
|
|
|
|
lit: Lit::Str(str_lit),
|
|
|
|
..
|
|
|
|
}) => str_lit.value(),
|
|
|
|
_ => panic!("Expected literal string as first argument to include_font"),
|
|
|
|
};
|
|
|
|
|
2022-04-24 00:33:57 +10:00
|
|
|
let font_size = match flatten_group(&all_args[1]) {
|
2022-04-05 07:06:08 +10:00
|
|
|
Expr::Lit(ExprLit {
|
|
|
|
lit: Lit::Float(value),
|
|
|
|
..
|
|
|
|
}) => value.base10_parse::<f32>().expect("Invalid float literal"),
|
|
|
|
Expr::Lit(ExprLit {
|
|
|
|
lit: Lit::Int(value),
|
|
|
|
..
|
|
|
|
}) => value
|
|
|
|
.base10_parse::<i32>()
|
|
|
|
.expect("Invalid integer literal") as f32,
|
|
|
|
_ => panic!("Expected literal float or integer as second argument to include_font"),
|
|
|
|
};
|
|
|
|
|
|
|
|
let root = std::env::var("CARGO_MANIFEST_DIR").expect("Failed to get cargo manifest dir");
|
|
|
|
let path = Path::new(&root).join(&*filename);
|
|
|
|
|
|
|
|
let file_content = std::fs::read(&path).expect("Failed to read ttf file");
|
|
|
|
|
|
|
|
let rendered = font_loader::load_font(&file_content, font_size);
|
|
|
|
|
|
|
|
let include_path = path.to_string_lossy();
|
|
|
|
|
|
|
|
quote!({
|
|
|
|
let _ = include_bytes!(#include_path);
|
|
|
|
|
|
|
|
#rendered
|
|
|
|
})
|
|
|
|
.into()
|
|
|
|
}
|
|
|
|
|
2022-08-01 21:08:22 +10:00
|
|
|
fn valid_sprite_size(width: u32, height: u32) -> bool {
|
|
|
|
match (width, height) {
|
|
|
|
(8, 8) => true,
|
|
|
|
(16, 16) => true,
|
|
|
|
(32, 32) => true,
|
|
|
|
(64, 64) => true,
|
|
|
|
(16, 8) => true,
|
|
|
|
(32, 8) => true,
|
|
|
|
(32, 16) => true,
|
|
|
|
(64, 32) => true,
|
|
|
|
(8, 16) => true,
|
|
|
|
(8, 32) => true,
|
|
|
|
(16, 32) => true,
|
|
|
|
(32, 64) => true,
|
|
|
|
(_, _) => false,
|
|
|
|
}
|
|
|
|
}
|
2023-04-26 05:06:52 +10:00
|
|
|
|
2024-01-25 06:17:04 +11:00
|
|
|
const OUT_DIR_TOKEN: &str = "$OUT_DIR";
|
2024-01-25 05:19:30 +11:00
|
|
|
|
|
|
|
fn get_out_dir(raw_input: &str) -> String {
|
2024-01-25 06:17:04 +11:00
|
|
|
if raw_input.contains(OUT_DIR_TOKEN) {
|
2024-01-25 05:19:30 +11:00
|
|
|
std::env::var("OUT_DIR").expect("Failed to get OUT_DIR")
|
|
|
|
} else {
|
|
|
|
String::new()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-04-26 05:06:52 +10:00
|
|
|
#[cfg(test)]
|
|
|
|
mod tests {
|
|
|
|
use asefile::AnimationDirection;
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
// These directions defined in agb and have these values. This is important
|
|
|
|
// when outputting code for agb. If more animation directions are added then
|
|
|
|
// we will have to support them there.
|
|
|
|
fn directions_to_agb() {
|
|
|
|
assert_eq!(AnimationDirection::Forward as usize, 0);
|
|
|
|
assert_eq!(AnimationDirection::Reverse as usize, 1);
|
|
|
|
assert_eq!(AnimationDirection::PingPong as usize, 2);
|
|
|
|
}
|
|
|
|
}
|