use std::collections::HashMap; use std::io::{BufReader, Read, Seek, SeekFrom}; use image::RgbaImage; use itertools::Itertools; use log::info; use crate::common; use crate::common::{Rect, FILE_TYPES}; use crate::engine_constants::EngineConstants; use crate::framework::backend::{BackendTexture, SpriteBatchCommand}; use crate::framework::context::Context; use crate::framework::error::{GameError, GameResult}; use crate::framework::filesystem; use crate::framework::graphics::{create_texture, FilterMode}; use crate::settings::Settings; use crate::shared_game_state::Season; pub static mut I_MAG: f32 = 1.0; pub static mut G_MAG: f32 = 1.0; pub trait SpriteBatch { fn width(&self) -> usize; fn height(&self) -> usize; fn dimensions(&self) -> (usize, usize); fn real_dimensions(&self) -> (usize, usize); fn scale(&self) -> (f32, f32); fn has_glow_layer(&self) -> bool; fn has_normal_layer(&self) -> bool; fn to_rect(&self) -> common::Rect; fn clear(&mut self); fn add(&mut self, x: f32, y: f32); fn add_rect(&mut self, x: f32, y: f32, rect: &common::Rect); fn add_rect_flip(&mut self, x: f32, y: f32, flip_x: bool, flip_y: bool, rect: &common::Rect); fn add_rect_tinted(&mut self, x: f32, y: f32, color: (u8, u8, u8, u8), rect: &common::Rect); fn add_rect_scaled(&mut self, x: f32, y: f32, scale_x: f32, scale_y: f32, rect: &common::Rect); fn add_rect_scaled_tinted( &mut self, x: f32, y: f32, color: (u8, u8, u8, u8), scale_x: f32, scale_y: f32, rect: &common::Rect, ); fn draw(&mut self, ctx: &mut Context) -> GameResult; fn draw_filtered(&mut self, _filter: FilterMode, _ctx: &mut Context) -> GameResult; } pub struct DummyBatch; impl SpriteBatch for DummyBatch { fn width(&self) -> usize { 1 } fn height(&self) -> usize { 1 } fn dimensions(&self) -> (usize, usize) { (1, 1) } fn real_dimensions(&self) -> (usize, usize) { (1, 1) } fn scale(&self) -> (f32, f32) { (1.0, 1.0) } fn has_glow_layer(&self) -> bool { false } fn has_normal_layer(&self) -> bool { false } fn to_rect(&self) -> Rect { Rect::new(0, 0, 1, 1) } fn clear(&mut self) {} fn add(&mut self, _x: f32, _y: f32) {} fn add_rect(&mut self, _x: f32, _y: f32, _rect: &Rect) {} fn add_rect_flip(&mut self, _x: f32, _y: f32, _flip_x: bool, _flip_y: bool, _rect: &Rect) {} fn add_rect_tinted(&mut self, _x: f32, _y: f32, _color: (u8, u8, u8, u8), _rect: &Rect) {} fn add_rect_scaled(&mut self, _x: f32, _y: f32, _scale_x: f32, _scale_y: f32, _rect: &Rect) {} fn add_rect_scaled_tinted( &mut self, _x: f32, _y: f32, _color: (u8, u8, u8, u8), _scale_x: f32, _scale_y: f32, _rect: &Rect, ) { } fn draw(&mut self, _ctx: &mut Context) -> GameResult { Ok(()) } fn draw_filtered(&mut self, _filter: FilterMode, _ctx: &mut Context) -> GameResult { Ok(()) } } pub struct SizedBatch { batch: Box, width: usize, height: usize, real_width: usize, real_height: usize, scale_x: f32, scale_y: f32, has_glow_layer: bool, has_normal_layer: bool, } impl SpriteBatch for SizedBatch { #[inline(always)] fn width(&self) -> usize { self.width } #[inline(always)] fn height(&self) -> usize { self.height } #[inline(always)] fn dimensions(&self) -> (usize, usize) { (self.width, self.height) } #[inline(always)] fn real_dimensions(&self) -> (usize, usize) { (self.real_width, self.real_height) } #[inline(always)] fn scale(&self) -> (f32, f32) { (self.scale_x, self.scale_y) } #[inline(always)] fn has_glow_layer(&self) -> bool { self.has_glow_layer } #[inline(always)] fn has_normal_layer(&self) -> bool { self.has_normal_layer } #[inline(always)] fn to_rect(&self) -> common::Rect { common::Rect::::new(0, 0, self.width, self.height) } #[inline(always)] fn clear(&mut self) { self.batch.clear(); } fn add(&mut self, x: f32, y: f32) { let mag = unsafe { I_MAG }; self.batch.add(SpriteBatchCommand::DrawRect( Rect { left: 0 as f32, top: 0 as f32, right: self.real_width as f32, bottom: self.real_height as f32 }, Rect { left: x * mag, top: y * mag, right: (x + self.width() as f32) * mag, bottom: (y + self.height() as f32) * mag, }, )); } #[inline(always)] fn add_rect(&mut self, x: f32, y: f32, rect: &common::Rect) { self.add_rect_scaled(x, y, 1.0, 1.0, rect) } fn add_rect_flip(&mut self, x: f32, y: f32, flip_x: bool, flip_y: bool, rect: &common::Rect) { if (rect.right - rect.left) == 0 || (rect.bottom - rect.top) == 0 { return; } let mag = unsafe { I_MAG }; self.batch.add(SpriteBatchCommand::DrawRectFlip( Rect { left: rect.left as f32 / self.scale_x, top: rect.top as f32 / self.scale_y, right: rect.right as f32 / self.scale_x, bottom: rect.bottom as f32 / self.scale_y, }, Rect { left: x * mag, top: y * mag, right: (x + rect.width() as f32) * mag, bottom: (y + rect.height() as f32) * mag, }, flip_x, flip_y, )); } #[inline(always)] fn add_rect_tinted(&mut self, x: f32, y: f32, color: (u8, u8, u8, u8), rect: &common::Rect) { self.add_rect_scaled_tinted(x, y, color, 1.0, 1.0, rect) } fn add_rect_scaled(&mut self, x: f32, y: f32, scale_x: f32, scale_y: f32, rect: &common::Rect) { if (rect.right.saturating_sub(rect.left)) == 0 || (rect.bottom.saturating_sub(rect.top)) == 0 { return; } let mag = unsafe { I_MAG }; self.batch.add(SpriteBatchCommand::DrawRect( Rect { left: rect.left as f32 / self.scale_x, top: rect.top as f32 / self.scale_y, right: rect.right as f32 / self.scale_x, bottom: rect.bottom as f32 / self.scale_y, }, Rect { left: x * mag, top: y * mag, right: (x + rect.width() as f32 * scale_x) * mag, bottom: (y + rect.height() as f32 * scale_y) * mag, }, )); } fn add_rect_scaled_tinted( &mut self, x: f32, y: f32, color: (u8, u8, u8, u8), scale_x: f32, scale_y: f32, rect: &common::Rect, ) { if (rect.right - rect.left) == 0 || (rect.bottom - rect.top) == 0 { return; } let mag = unsafe { I_MAG }; self.batch.add(SpriteBatchCommand::DrawRectTinted( Rect { left: rect.left as f32, top: rect.top as f32, right: rect.right as f32, bottom: rect.bottom as f32 }, Rect { left: x * mag, top: y * mag, right: (x + rect.width() as f32 * scale_x) * mag, bottom: (y + rect.height() as f32 * scale_y) * mag, }, color.into(), )); } #[inline(always)] fn draw(&mut self, ctx: &mut Context) -> GameResult { self.draw_filtered(FilterMode::Nearest, ctx) } fn draw_filtered(&mut self, _filter: FilterMode, _ctx: &mut Context) -> GameResult { //self.batch.set_filter(filter); self.batch.draw()?; self.batch.clear(); Ok(()) } } pub struct TextureSet { pub tex_map: HashMap>, pub paths: Vec, dummy_batch: Box, } impl TextureSet { pub fn new(base_path: &str) -> TextureSet { TextureSet { tex_map: HashMap::new(), paths: vec![base_path.to_string(), "".to_string()], dummy_batch: Box::new(DummyBatch), } } pub fn apply_seasonal_content(&mut self, season: Season, settings: &Settings) { if settings.original_textures { self.paths.insert(0, "/base/ogph/".to_string()) } else if settings.seasonal_textures { match season { Season::Halloween => self.paths.insert(0, "/Halloween/season/".to_string()), Season::Christmas => self.paths.insert(0, "/Christmas/season/".to_string()), _ => {} } } } fn make_transparent(rgba: &mut RgbaImage) { for (r, g, b, a) in rgba.iter_mut().tuples() { if *r == 0 && *g == 0 && *b == 0 { *a = 0; } } } fn load_image(&self, ctx: &mut Context, path: &str) -> GameResult> { let img = { let mut buf = [0u8; 8]; let mut reader = filesystem::open(ctx, path)?; reader.read_exact(&mut buf)?; reader.seek(SeekFrom::Start(0))?; let image = image::load(BufReader::new(reader), image::guess_format(&buf)?)?; let mut rgba = image.to_rgba8(); if image.color().channel_count() != 4 { TextureSet::make_transparent(&mut rgba); } rgba }; let (width, height) = img.dimensions(); create_texture(ctx, width as u16, height as u16, &img) } pub fn load_texture( &self, ctx: &mut Context, constants: &EngineConstants, name: &str, ) -> GameResult> { let path = self .paths .iter() .find_map(|s| { FILE_TYPES.iter().map(|ext| [s, name, ext].join("")).find(|path| filesystem::exists(ctx, path)) }) .ok_or_else(|| GameError::ResourceLoadError(format!("Texture {} does not exist.", name)))?; let has_glow_layer = self .paths .iter() .find_map(|s| { FILE_TYPES.iter().map(|ext| [s, name, ".glow", ext].join("")).find(|path| filesystem::exists(ctx, path)) }) .is_some(); info!("Loading texture: {}", path); let batch = self.load_image(ctx, &path)?; let size = batch.dimensions(); let orig_dimensions = constants.tex_sizes.get(name).unwrap_or_else(|| &size); let scale = orig_dimensions.0 as f32 / size.0 as f32; let width = (size.0 as f32 * scale) as usize; let height = (size.1 as f32 * scale) as usize; Ok(Box::new(SizedBatch { batch, width, height, scale_x: scale, scale_y: scale, real_width: size.0 as usize, real_height: size.1 as usize, has_glow_layer, has_normal_layer: false, })) } pub fn get_or_load_batch( &mut self, ctx: &mut Context, constants: &EngineConstants, name: &str, ) -> GameResult<&mut Box> { if ctx.headless { return Ok(&mut self.dummy_batch); } if !self.tex_map.contains_key(name) { let batch = self.load_texture(ctx, constants, name)?; self.tex_map.insert(name.to_owned(), batch); } Ok(self.tex_map.get_mut(name).unwrap()) } }