use std::collections::HashSet;
use std::error::Error;
use std::io::{BufReader, Read};
use std::mem;
use std::path::Path;
use std::rc::Rc;
use std::str::FromStr;
use std::sync::Arc;
use image::{ImageFormat, RgbaImage};
use twgame::Map;
use twgpu::blit::Blit;
use twgpu::map::{GpuMapData, GpuMapRender, GpuMapStatic};
use twgpu::shared::{Clock, Rng};
use twgpu::sprites::{
AtlasToken, ParticleData, SnapRendering, SpriteRenderContext, SpriteTextures, SpritesData,
TeeSprite, TeeStorage,
};
use twgpu::textures::Samplers;
use twgpu::GpuCamera;
use twmap::{LoadMultiple, TwMap};
use twsnap::compat::ddnet::{DemoChunk, DemoMapHash, DemoReader, ReadError};
use vek::Vec2;
use wgpu::{Device, Queue, Texture, TextureFormat, TextureView, TextureViewDescriptor};
pub fn parse_tuple<T: FromStr, const SEPARATOR: char>(
s: &str,
) -> Result<(T, T), Box<dyn Error + Send + Sync + 'static>>
where
T::Err: Error + Send + Sync + 'static,
{
if s.matches(SEPARATOR).count() != 1 {
return Err(format!("Expected 2 values separated by '{SEPARATOR}'").into());
}
let mut values = s.split(SEPARATOR).map(|str| str.parse::<T>());
Ok((values.next().unwrap()?, values.next().unwrap()?))
}
pub fn gpu_map_from_path(
path: &Path,
map_static: &GpuMapStatic,
camera: &GpuCamera,
samplers: &Samplers,
device: &Device,
queue: &Queue,
) -> Result<(twmap::TwMap, GpuMapData, GpuMapRender), Box<dyn Error>> {
println!("Loading map");
let mut map = twmap::TwMap::parse_path(path)?;
map.embed_images_auto()?;
map.images.load()?;
map.groups
.load_conditionally(|layer| layer.kind() == twmap::LayerKind::Tiles)?;
println!("Uploading map data to GPU");
let map_data = GpuMapData::upload(&map, device, queue);
let map_render = map_static.prepare_render(&map, &map_data, camera, samplers, device);
Ok((map, map_data, map_render))
}
pub struct DownloadTexture {
texture: wgpu::Texture,
download_buffer: wgpu::Buffer,
width: u32,
height: u32,
buffer_size: u64,
bytes_per_row: u32,
padded_bytes_per_row: u32,
}
impl DownloadTexture {
pub fn new(width: u32, height: u32, format: TextureFormat, device: &wgpu::Device) -> Self {
assert_ne!(width, 0);
assert_ne!(height, 0);
let texture = device.create_texture(&wgpu::TextureDescriptor {
label: Some("Render Target Texture"),
size: wgpu::Extent3d {
width,
height,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
view_formats: &[],
});
let bytes_per_pixel: u32 = format.block_copy_size(None).unwrap();
let bytes_per_row: u32 = width * bytes_per_pixel;
let align: u32 = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT;
let padding: u32 = (align - (bytes_per_row % align)) % align;
let padded_bytes_per_row: u32 = bytes_per_row + padding;
let buffer_size: u64 = u64::from(padded_bytes_per_row) * u64::from(height);
let download_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Image Download Buffer"),
size: buffer_size,
usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
Self {
texture,
download_buffer,
width,
height,
buffer_size,
bytes_per_row,
padded_bytes_per_row,
}
}
pub fn texture_view(&self) -> TextureView {
self.texture.create_view(&TextureViewDescriptor::default())
}
pub fn map<T: FnOnce() + Send + 'static>(
&self,
on_map: T,
device: &wgpu::Device,
queue: &wgpu::Queue,
) {
let mut command_encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("Download Texture Copy"),
});
command_encoder.copy_texture_to_buffer(
self.texture.as_image_copy(),
wgpu::ImageCopyBuffer {
buffer: &self.download_buffer,
layout: wgpu::ImageDataLayout {
offset: 0,
bytes_per_row: Some(self.padded_bytes_per_row),
rows_per_image: Some(self.height),
},
},
wgpu::Extent3d {
width: self.width,
height: self.height,
depth_or_array_layers: 1,
},
);
queue.submit(Some(command_encoder.finish()));
let buffer_slice = self.download_buffer.slice(..);
buffer_slice.map_async(wgpu::MapMode::Read, |e| {
e.unwrap();
on_map()
});
}
pub fn download_mapped<T: FnMut(&[u8])>(&self, row_closure: T) {
{
let downloaded = self.download_buffer.slice(..).get_mapped_range();
assert_eq!(downloaded.len(), self.buffer_size as usize);
downloaded
.chunks_exact(self.padded_bytes_per_row as usize)
.map(|padded_row| &padded_row[..self.bytes_per_row as usize])
.for_each(row_closure);
}
self.download_buffer.unmap();
}
pub fn download_rgba(&self, device: &wgpu::Device, queue: &wgpu::Queue) -> image::RgbaImage {
assert_eq!(self.texture.format(), TextureFormat::Rgba8Unorm);
self.map(|| {}, device, queue);
let mut image_buffer =
Vec::with_capacity(self.bytes_per_row as usize * self.height as usize);
device.poll(wgpu::Maintain::Wait);
self.download_mapped(|row| image_buffer.extend(row));
image::RgbaImage::from_vec(self.width, self.height, image_buffer).unwrap()
}
}
pub fn load_png(path: &str, version: twmap::Version) -> Result<RgbaImage, Box<dyn Error>> {
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<(), Box<dyn Error>> {
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(())
}
pub fn retrieve_maps(demo: &DemoReader) -> Result<(TwMap, Rc<Map>), Box<dyn Error>> {
let mut map = None;
match demo.map_data() {
Some(data) => map = Some(TwMap::parse(data)?),
None => {
let version = twstorage::Version::DDNet06;
if let DemoMapHash::Sha256(sha) = demo.map_hash() {
let sha = sha.map(|byte| format!("{byte:02x}")).join("");
let path = format!("downloadedmaps/{}_{}.map", demo.map_name(), sha);
if let Ok(mut file) = twstorage::read_file(&path, version) {
let mut data = Vec::new();
file.read_to_end(&mut data)?;
map = Some(TwMap::parse(&data)?);
}
}
let path = format!("maps/{}.map", demo.map_name());
if let Ok(mut file) = twstorage::read_file(&path, version) {
let mut data = Vec::new();
file.read_to_end(&mut data)?;
map = Some(TwMap::parse(&data)?);
}
}
}
let mut map = match map {
None => return Err("Map not embedded in demo and its file couldn't be located".into()),
Some(map) => map,
};
map.embed_images_auto()?;
map.load()?;
let twgame_map = Rc::new(twgame::Map::try_from(&mut map)?);
Ok((map, twgame_map))
}
type SkinResult = Result<(String, Vec<Texture>), (String, String)>;
pub struct SkinManager {
known: HashSet<String>,
handles: Vec<std::thread::JoinHandle<SkinResult>>,
blit: Arc<Blit>,
device: Arc<Device>,
queue: Arc<Queue>,
}
fn load_skin(
name: &str,
blit: &Blit,
device: &Device,
queue: &Queue,
) -> Result<Vec<Texture>, Box<dyn Error>> {
let options = sanitize_filename::OptionsForCheck {
windows: true,
truncate: true,
};
if !sanitize_filename::is_sanitized_with_options(name, options) {
return Err(format!("Skin name '{name}' is not sanitized").into());
}
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::<TeeSprite>(&image, device, queue);
Ok(texture)
}
impl SkinManager {
pub fn new(
blit: Arc<Blit>,
textures: &mut SpriteTextures,
device: Arc<Device>,
queue: Arc<Queue>,
) -> 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<AtlasToken<TeeSprite>> {
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());
}
}
}
}
pub struct DemoProcessor {
demo: DemoReader,
clock: Clock,
map: Rc<Map>,
from_snap: twsnap::Snap,
to_snap: twsnap::Snap,
tees: TeeStorage,
new_snap: bool,
}
impl DemoProcessor {
pub fn new(mut demo: DemoReader, map: Rc<Map>) -> Result<Self, ReadError> {
let mut snap = twsnap::Snap::default();
let clock = loop {
match demo.next_chunk(&mut snap)? {
None => break Clock::default(),
Some(DemoChunk::NetMsg) => continue,
Some(DemoChunk::Snapshot(tick)) => break Clock::new(tick),
}
};
Ok(Self {
demo,
clock,
map,
from_snap: twsnap::Snap::default(),
to_snap: snap,
tees: TeeStorage::default(),
new_snap: true,
})
}
pub fn camera_position(&mut self) -> Option<Vec2<f32>> {
for player in self.from_snap.players.values() {
if player.local {
if let Some(tees) = self.tees.lerp_tee(
player.uid,
&self.clock,
&self.from_snap,
&self.to_snap,
&self.map,
) {
return Some(self.clock.lerp_tee_vec(tees, |tee| tee.pos));
}
}
}
None
}
pub fn start_tick(&self) -> i32 {
self.clock.start_tick()
}
pub fn current_time(&self) -> f64 {
self.clock.current_time()
}
pub fn process_tick(&mut self, f_tick: f64) -> Result<bool, ReadError> {
let previous_time = self.clock.current_tick();
let need_new_snap = self.clock.update(f_tick);
if need_new_snap {
mem::swap(&mut self.from_snap, &mut self.to_snap);
loop {
match self.demo.next_chunk(&mut self.to_snap)? {
None => {
return {
mem::swap(&mut self.from_snap, &mut self.to_snap);
self.clock.update(previous_time);
Ok(false)
};
}
Some(DemoChunk::NetMsg) => continue,
Some(DemoChunk::Snapshot(tick)) => {
self.clock.next_tick(tick, f_tick);
break;
}
}
}
self.new_snap = true;
}
Ok(true)
}
pub fn new_snap(&self) -> Option<&twsnap::Snap> {
self.new_snap.then_some(&self.to_snap)
}
pub fn render(
&mut self,
sprites: &mut SpritesData,
particles: &mut ParticleData,
textures: &SpriteTextures,
rng: &mut Rng,
) {
particles.update(&self.clock, &self.map, rng);
let mut ctx = SpriteRenderContext {
clock: &self.clock,
from_snap: &self.from_snap,
to_snap: &self.to_snap,
tees: &mut self.tees,
map: &self.map,
sprites,
particles,
textures,
rng,
};
if self.new_snap {
ctx.process_events();
self.new_snap = false;
}
ctx.interpolate_render(&self.from_snap, &self.to_snap);
}
}