// Adapted from twgpu-tools // AGPL-3.0-only, Copyright 2025 Patiga use anyhow::Result; use image::{ImageFormat, RgbaImage}; use std::{collections::HashSet, io::BufReader, sync::Arc}; use twgpu::{ blit::Blit, sprites::{AtlasToken, SpriteTextures, TeeSprite}, }; use wgpu::{Device, Queue, Texture}; pub fn load_png(path: &str, version: twmap::Version) -> Result { let file = twstorage::read_file(path, version.into())?; let reader = image::io::Reader::with_format(BufReader::new(file), ImageFormat::Png); Ok(reader.decode()?.into_rgba8()) } pub fn init_sprite_textures( textures: &mut SpriteTextures, version: twmap::Version, device: &Device, queue: &Queue, ) -> Result<()> { println!("Loading sprite textures from filesystem"); let game = load_png("game.png", version)?; let particles = load_png("particles.png", version)?; let emoticons = load_png("emoticons.png", version)?; let extras = load_png("extras.png", version)?; textures.game_skin = textures.register_atlas_image(&game, device, queue); textures.particles = textures.register_atlas_image(&particles, device, queue); textures.emoticons = textures.register_atlas_image(&emoticons, device, queue); textures.extras = textures.register_atlas_image(&extras, device, queue); Ok(()) } type SkinResult = Result<(String, Vec), (String, String)>; pub struct SkinManager { known: HashSet, handles: Vec>, blit: Arc, device: Arc, queue: Arc, } fn load_skin(name: &str, blit: &Blit, device: &Device, queue: &Queue) -> Result> { // TODO check filename let path = format!("skins/{name}.png"); let path2 = format!("downloadedskins/{name}.png"); let image = match load_png(&path, twmap::Version::DDNet06) { Err(_) => load_png(&path2, twmap::Version::DDNet06)?, Ok(img) => img, }; let texture = blit.upload_mipmapped_atlas::(&image, device, queue); Ok(texture) } impl SkinManager { pub fn new( blit: Arc, textures: &mut SpriteTextures, device: Arc, queue: Arc, ) -> Self { let mut manager = Self { known: HashSet::new(), handles: Vec::new(), blit, device, queue, }; if let Some(default) = manager.load_skin("default", textures) { textures.default_skin = default; } if let Some(ninja) = manager.load_skin("x_ninja", textures) { textures.ninja_skin = ninja; } manager } pub fn load_skin( &mut self, name: &str, textures: &mut SpriteTextures, ) -> Option> { let texture = match load_skin(name, &self.blit, &self.device, &self.queue) { Ok(texture) => texture, Err(err) => { println!("Error loading skin '{name}': {err}"); return None; } }; self.known.insert(name.to_string()); let token = textures.register_skin_texture(texture, name.to_string(), &self.device); Some(token) } pub fn queue_load_skin(&mut self, name: String) { let device = self.device.clone(); let queue = self.queue.clone(); let blit = self.blit.clone(); let handle = std::thread::spawn(move || match load_skin(&name, &blit, &device, &queue) { Err(err) => Err((name, err.to_string())), Ok(texture) => Ok((name, texture)), }); self.handles.push(handle); } pub fn wait_for_queued(&mut self, textures: &mut SpriteTextures) { while let Some(handle) = self.handles.pop() { match handle.join().unwrap() { Ok((name, texture)) => { textures.register_skin_texture(texture, name, &self.device); } Err((name, err)) => println!("Error with skin '{name}': {err}"), } } } pub fn poll_queued(&mut self, textures: &mut SpriteTextures) { while !self.handles.is_empty() { if self.handles[0].is_finished() { match self.handles.remove(0).join().unwrap() { Ok((name, texture)) => { textures.register_skin_texture(texture, name, &self.device); } Err((name, err)) => println!("Error with skin '{name}': {err}"), } } else { return; } } } pub fn queue_snap_skins(&mut self, snap: &twsnap::Snap) { for player in snap.players.values() { if !self.known.contains(player.skin.as_str()) { println!("New skin: '{}'", player.skin); self.known.insert(player.skin.to_string()); self.queue_load_skin(player.skin.to_string()); } } } }