twgpu_tools/
lib.rs

1mod momentcap;
2
3pub mod dbg;
4mod download_texture;
5pub mod encoding;
6#[cfg(feature = "ffmpeg")]
7pub mod ff;
8pub mod sprite_renderer;
9pub mod tripod;
10
11pub use download_texture::*;
12
13use twgpu::{image, twgame, twmap, twsnap, vek, wgpu};
14
15use std::collections::HashSet;
16use std::error::Error;
17use std::io::{BufReader, Read};
18use std::path::Path;
19use std::str::FromStr;
20use std::sync::Arc;
21use std::{fs, mem, ops};
22
23use image::{ImageFormat, RgbaImage};
24use teehistorian::ThStream;
25use teehistorian_replayer::{teehistorian, twgame_core};
26use twgame::Map;
27use twgame_core::Snapper;
28use twgpu::blit::Blit;
29use twgpu::buffer::GpuVecBuffer;
30use twgpu::map::{GpuMapDataDyn, GpuMapDataStatic, GpuMapRender, GpuMapStatic};
31use twgpu::shared::{Clock, Rng};
32use twgpu::sprites::{
33    AtlasToken, Particles, SpriteRenderContext, SpriteTextures, SpriteVertex, TeeSprite,
34    TeeStorage, TextureToken,
35};
36use twgpu::textures::Samplers;
37use twgpu::GpuCamera;
38use twmap::{LoadMultiple, TwMap};
39use twsnap::compat::ddnet::{DemoChunk, DemoMapHash, DemoReader};
40use vek::Vec2;
41use wgpu::{BufferBinding, Device, Queue, Texture};
42
43pub type SpriteBuffer = GpuVecBuffer<[SpriteVertex; 4]>;
44pub type TextureBuffer = GpuVecBuffer<TextureToken>;
45
46pub fn parse_tuple<T: FromStr, const SEPARATOR: char>(
47    s: &str,
48) -> Result<(T, T), Box<dyn Error + Send + Sync + 'static>>
49where
50    T::Err: Error + Send + Sync + 'static,
51{
52    if s.matches(SEPARATOR).count() != 1 {
53        return Err(format!("Expected 2 values separated by '{SEPARATOR}'").into());
54    }
55    let mut values = s.split(SEPARATOR).map(|str| str.parse::<T>());
56    Ok((values.next().unwrap()?, values.next().unwrap()?))
57}
58
59pub fn gpu_map_from_path(
60    path: &Path,
61    map_static: &GpuMapStatic,
62    camera: &GpuCamera,
63    samplers: &Samplers,
64    device: &Device,
65    queue: &Queue,
66) -> Result<(twmap::TwMap, GpuMapDataStatic, GpuMapDataDyn, GpuMapRender), Box<dyn Error>> {
67    println!("Loading map");
68    let mut map = twmap::TwMap::parse_path(path)?;
69    map.embed_images_auto()?;
70    map.images.load()?;
71    map.groups
72        .load_conditionally(|layer| layer.kind() == twmap::LayerKind::Tiles)?;
73    println!("Uploading map data to GPU");
74    let map_static_data = GpuMapDataStatic::upload(&map, device, queue);
75    let map_dyn_data = GpuMapDataDyn::upload(&map, device);
76    let map_render = map_static.prepare_render(
77        &map,
78        &map_static_data,
79        &map_dyn_data,
80        camera,
81        samplers,
82        device,
83    );
84    Ok((map, map_static_data, map_dyn_data, map_render))
85}
86
87pub fn load_png(path: &str, version: twmap::Version) -> Result<RgbaImage, Box<dyn Error>> {
88    let file = twstorage::read_file(path, version.into())?;
89    let reader = image::io::Reader::with_format(BufReader::new(file), ImageFormat::Png);
90    Ok(reader.decode()?.into_rgba8())
91}
92
93pub fn init_sprite_textures(
94    textures: &mut SpriteTextures,
95    version: twmap::Version,
96    camera_buffer: BufferBinding,
97    device: &Device,
98    queue: &Queue,
99) -> Result<(), Box<dyn Error>> {
100    println!("Loading sprite textures from filesystem");
101    let game = load_png("game.png", version)?;
102    let particles = load_png("particles.png", version)?;
103    let emoticons = load_png("emoticons.png", version)?;
104    let extras = load_png("extras.png", version)?;
105    textures.game_skin = textures.register_atlas_image(&game, camera_buffer.clone(), device, queue);
106    textures.particles =
107        textures.register_atlas_image(&particles, camera_buffer.clone(), device, queue);
108    textures.emoticons =
109        textures.register_atlas_image(&emoticons, camera_buffer.clone(), device, queue);
110    textures.extras = textures.register_atlas_image(&extras, camera_buffer, device, queue);
111    Ok(())
112}
113
114type SkinResult = Result<(String, Vec<Texture>), (String, String)>;
115
116pub struct SkinManager {
117    known: HashSet<String>,
118    handles: Vec<std::thread::JoinHandle<SkinResult>>,
119    blit: Arc<Blit>,
120    device: Device,
121    queue: Queue,
122}
123
124fn load_skin(
125    name: &str,
126    blit: &Blit,
127    device: &Device,
128    queue: &Queue,
129) -> Result<Vec<Texture>, Box<dyn Error>> {
130    let options = sanitize_filename::OptionsForCheck {
131        windows: true,
132        truncate: true,
133    };
134    if !sanitize_filename::is_sanitized_with_options(name, options) {
135        return Err(format!("Skin name '{name}' is not sanitized").into());
136    }
137    let path = format!("skins/{name}.png");
138    let path2 = format!("downloadedskins/{name}.png");
139    let image = match load_png(&path, twmap::Version::DDNet06) {
140        Err(_) => load_png(&path2, twmap::Version::DDNet06)?,
141        Ok(img) => img,
142    };
143
144    let texture = blit.upload_mipmapped_atlas::<TeeSprite>(&image, device, queue);
145    Ok(texture)
146}
147
148impl SkinManager {
149    pub fn new(
150        blit: Arc<Blit>,
151        textures: &mut SpriteTextures,
152        camera_buffer: BufferBinding,
153        device: Device,
154        queue: Queue,
155    ) -> Self {
156        let mut manager = Self {
157            known: HashSet::new(),
158            handles: Vec::new(),
159            blit,
160            device,
161            queue,
162        };
163        if let Some(default) = manager.load_skin("default", textures, camera_buffer.clone()) {
164            textures.default_skin = default;
165        }
166        if let Some(ninja) = manager.load_skin("x_ninja", textures, camera_buffer) {
167            textures.ninja_skin = ninja;
168        }
169        manager
170    }
171
172    pub fn load_skin(
173        &mut self,
174        name: &str,
175        textures: &mut SpriteTextures,
176        camera_buffer: BufferBinding,
177    ) -> Option<AtlasToken<TeeSprite>> {
178        let texture = match load_skin(name, &self.blit, &self.device, &self.queue) {
179            Ok(texture) => texture,
180            Err(err) => {
181                println!("Error loading skin '{name}': {err}");
182                return None;
183            }
184        };
185        self.known.insert(name.to_string());
186        let token =
187            textures.register_skin_texture(texture, name.to_string(), camera_buffer, &self.device);
188        Some(token)
189    }
190
191    pub fn queue_load_skin(&mut self, name: String) {
192        let device = self.device.clone();
193        let queue = self.queue.clone();
194        let blit = self.blit.clone();
195        let handle = std::thread::spawn(move || match load_skin(&name, &blit, &device, &queue) {
196            Err(err) => Err((name, err.to_string())),
197            Ok(texture) => Ok((name, texture)),
198        });
199        self.handles.push(handle);
200    }
201
202    pub fn wait_for_queued(&mut self, textures: &mut SpriteTextures, camera_buffer: BufferBinding) {
203        while let Some(handle) = self.handles.pop() {
204            match handle.join().unwrap() {
205                Ok((name, texture)) => {
206                    textures.register_skin_texture(
207                        texture,
208                        name,
209                        camera_buffer.clone(),
210                        &self.device,
211                    );
212                }
213                Err((name, err)) => println!("Error with skin '{name}': {err}"),
214            }
215        }
216    }
217
218    pub fn poll_queued(&mut self, textures: &mut SpriteTextures, camera_buffer: BufferBinding) {
219        while !self.handles.is_empty() {
220            if self.handles[0].is_finished() {
221                match self.handles.remove(0).join().unwrap() {
222                    Ok((name, texture)) => {
223                        textures.register_skin_texture(
224                            texture,
225                            name,
226                            camera_buffer.clone(),
227                            &self.device,
228                        );
229                    }
230                    Err((name, err)) => println!("Error with skin '{name}': {err}"),
231                }
232            } else {
233                return;
234            }
235        }
236    }
237
238    pub fn queue_snap_skins(&mut self, snap: &twsnap::Snap) {
239        for player in snap.players.values() {
240            if !self.known.contains(player.skin.as_str()) {
241                println!("New skin: '{}'", player.skin);
242                self.known.insert(player.skin.to_string());
243                self.queue_load_skin(player.skin.to_string());
244            }
245        }
246    }
247}
248
249pub struct DemoProcessor {
250    demo: Box<dyn DemoLike>,
251    clock: Clock,
252    map: Arc<Map>,
253    from_snap: twsnap::Snap,
254    to_snap: twsnap::Snap,
255    tees: TeeStorage,
256    new_snap: bool,
257}
258
259fn maps(data: &[u8]) -> Result<(Arc<TwMap>, Arc<Map>), Box<dyn Error>> {
260    let mut twmap_map = TwMap::parse(data)?;
261    twmap_map.embed_images_auto()?;
262    twmap_map.load()?;
263    let twgame_map = twgame::Map::try_from(&mut twmap_map)?;
264    Ok((Arc::new(twmap_map), Arc::new(twgame_map)))
265}
266
267fn file_path_to_vec(path: &Path) -> Result<Vec<u8>, Box<dyn Error>> {
268    let mut buf = Vec::new();
269    let mut file = fs::File::open(path)?;
270    file.read_to_end(&mut buf)?;
271    Ok(buf)
272}
273
274fn retrieve_maps(name: &str, sha: Option<&str>) -> Result<(Arc<TwMap>, Arc<Map>), Box<dyn Error>> {
275    let version = twstorage::Version::DDNet06;
276    if let Some(sha) = sha {
277        let path = format!("downloadedmaps/{name}_{sha}.map");
278        if let Ok(mut file) = twstorage::read_file(&path, version) {
279            let mut data = Vec::new();
280            file.read_to_end(&mut data)?;
281            let map = maps(&data)?;
282            return Ok(map);
283        }
284    }
285    let path = format!("maps/{name}.map");
286    if let Ok(mut file) = twstorage::read_file(&path, version) {
287        let mut data = Vec::new();
288        file.read_to_end(&mut data)?;
289        let map = maps(&data)?;
290        return Ok(map);
291    }
292    Err("Map not embedded in demo and its file couldn't be located".into())
293}
294
295impl DemoProcessor {
296    pub fn new_by_extension(
297        path: &Path,
298        map: Option<&Path>,
299        pre: Option<(Arc<TwMap>, Arc<Map>)>,
300    ) -> Result<(Self, Arc<TwMap>, Arc<Map>), Box<dyn Error>> {
301        let file = fs::File::open(path)?;
302        let map = match pre {
303            Some(maps) => Some(maps),
304            None => match map {
305                None => None,
306                Some(path) => Some(maps(&file_path_to_vec(path)?)?),
307            },
308        };
309        let extension = path.extension().and_then(|os| os.to_str());
310        if extension == Some("teehistorian") {
311            let (demo, twmap, map) = TeehistorianDemo::new(file, map)?;
312            let demo = Self::new(Box::new(demo), map.clone())?;
313            Ok((demo, twmap, map))
314        } else if extension == Some("poses") {
315            let demo = MomentcapDemo::new(file)?;
316            let (twmap, map) = map.ok_or("Need explicit map file for momentcap file")?;
317            let demo = Self::new(Box::new(demo), map.clone())?;
318            Ok((demo, twmap, map))
319        } else {
320            // assume demo
321            if extension != Some("demo") {
322                println!("Unrecognized demo file extension, defaulting to normal demo");
323            }
324            let demo = DemoReader::new(file)?;
325            let (twmap, map) = match map {
326                Some(map) => map,
327                None => {
328                    if let Some(data) = demo.map_data() {
329                        maps(data)?
330                    } else {
331                        let map_name = demo.map_name();
332                        let map_sha = match demo.map_hash() {
333                            DemoMapHash::Crc(_) => None,
334                            DemoMapHash::Sha256(sha) => {
335                                Some(sha.map(|byte| format!("{byte:02x}")).join(""))
336                            }
337                        };
338                        retrieve_maps(map_name, map_sha.as_deref())?
339                    }
340                }
341            };
342            let demo = Self::new(Box::new(demo), map.clone())?;
343            Ok((demo, twmap, map))
344        }
345    }
346
347    pub fn new(mut demo: Box<dyn DemoLike>, map: Arc<Map>) -> Result<Self, Box<dyn Error>> {
348        let mut snap = twsnap::Snap::default();
349        let clock = loop {
350            match demo.next_demo_chunk(&mut snap)? {
351                None => break Clock::default(),
352                Some(DemoChunk::NetMsg) => continue,
353                Some(DemoChunk::Snapshot(tick)) => break Clock::new(tick.snap_tick()),
354            }
355        };
356        Ok(Self {
357            demo,
358            clock,
359            map,
360            from_snap: twsnap::Snap::default(),
361            to_snap: snap,
362            tees: TeeStorage::default(),
363            new_snap: true,
364        })
365    }
366
367    pub fn focused_player(&mut self) -> Option<u32> {
368        for (player_id, player) in self.from_snap.players.iter() {
369            if player.local {
370                return Some(player_id.0);
371            }
372        }
373        None
374    }
375
376    pub fn position_of_player(&mut self, id: u32) -> Option<Vec2<f32>> {
377        if let Some(tees) = self.tees.lerp_tee(
378            twsnap::SnapId(id),
379            &self.clock,
380            &self.from_snap,
381            &self.to_snap,
382            &self.map,
383        ) {
384            Some(self.clock.lerp_tee_vec(tees, |tee| tee.pos))
385        } else {
386            None
387        }
388    }
389
390    pub fn start_tick(&self) -> i32 {
391        self.clock.start_tick()
392    }
393
394    pub fn current_time(&self) -> f64 {
395        self.clock.current_time()
396    }
397
398    /// Returns `Ok(None)`, if the demo is over.
399    pub fn to_tick(
400        &mut self,
401        f_tick: f64,
402        safe_tick_threshold: u32,
403    ) -> Result<Option<bool>, Box<dyn Error>> {
404        if self.clock.update(f_tick) {
405            let new_tick = self.demo.snaps_for_tick(
406                &mut self.clock,
407                safe_tick_threshold,
408                &mut self.from_snap,
409                &mut self.to_snap,
410            )?;
411            if new_tick.is_some() {
412                self.new_snap = true;
413            }
414            Ok(new_tick.map(|_| true))
415        } else {
416            Ok(Some(false))
417        }
418    }
419
420    pub fn new_snap(&self) -> Option<&twsnap::Snap> {
421        self.new_snap.then_some(&self.to_snap)
422    }
423
424    pub fn render_ctx<'a>(
425        &'a mut self,
426        particles: &'a mut Particles,
427        textures: &'a SpriteTextures,
428        rng: &'a mut Rng,
429    ) -> SpriteRenderContext<'a> {
430        particles.update(&self.clock, &self.map, rng);
431        let mut ctx = SpriteRenderContext {
432            clock: &self.clock,
433            from_snap: &self.from_snap,
434            to_snap: &self.to_snap,
435            tees: &mut self.tees,
436            map: &self.map,
437            particles,
438            textures,
439            rng,
440        };
441        if self.new_snap {
442            ctx.process_events();
443            self.new_snap = false;
444        }
445        ctx
446    }
447}
448
449pub trait DemoLike {
450    fn next_demo_chunk(
451        &mut self,
452        snap: &mut twsnap::Snap,
453    ) -> Result<Option<DemoChunk>, Box<dyn Error>>;
454
455    /// This method skips to at least the given tick.
456    /// It might skip further than that, though typically not too far.
457    /// Returns Ok(None), if the demo is over
458    fn next_demo_chunk_after(
459        &mut self,
460        snap: &mut twsnap::Snap,
461        after: i32,
462    ) -> Result<Option<DemoChunk>, Box<dyn Error>> {
463        loop {
464            match self.next_demo_chunk(snap)? {
465                None => break Ok(None),
466                Some(DemoChunk::Snapshot(tick)) => {
467                    if tick.snap_tick() < after {
468                        continue;
469                    } else {
470                        break Ok(Some(DemoChunk::Snapshot(tick)));
471                    }
472                }
473                Some(_) => continue,
474            }
475        }
476    }
477
478    /// `safe_tick_threshold` is the amount of ticks, in which we expect at least 3 snapshots.
479    /// This method will try to skip any amount of ticks according to `safe_tick_threshold`.
480    /// And from there on out read each snap.
481    /// Returns an error, if we skipped too far.
482    fn snaps_for_tick(
483        &mut self,
484        clock: &mut Clock,
485        safe_tick_threshold: u32,
486        from: &mut twsnap::Snap,
487        to: &mut twsnap::Snap,
488    ) -> Result<Option<i32>, Box<dyn Error>> {
489        if !clock.update(clock.current_tick()) {
490            panic!("Already on appropriate tick!");
491        }
492        assert!(!clock.current_tick().is_nan());
493        assert!(clock.current_tick().is_sign_positive());
494        let diff = clock.current_tick() - clock.end_tick() as f64;
495        assert!(diff.is_sign_positive());
496        let mut skipped = false;
497        let mut snaps_after_skip = 0;
498        if diff as u32 > safe_tick_threshold {
499            skipped = true;
500            mem::swap(from, to);
501            let skip_to = clock.current_tick() as u32 - safe_tick_threshold;
502            match self.next_demo_chunk_after(to, skip_to.try_into().unwrap())? {
503                None => return Ok(None),
504                Some(DemoChunk::NetMsg) => {}
505                Some(DemoChunk::Snapshot(tick)) => {
506                    clock.next_tick(tick.snap_tick());
507                    snaps_after_skip += 1;
508                }
509            }
510        }
511        while clock.need_next_tick() {
512            match self.next_demo_chunk(from)? {
513                None => return Ok(None),
514                Some(DemoChunk::NetMsg) => continue,
515                Some(DemoChunk::Snapshot(tick)) => {
516                    mem::swap(from, to);
517                    snaps_after_skip += 1;
518                    clock.next_tick(tick.snap_tick());
519                }
520            }
521        }
522        if skipped && snaps_after_skip < 2 {
523            return Err(
524                "Too optimistic tick skip: too few snaps after skip. Adjust the skip_threshold!"
525                    .into(),
526            );
527        }
528        Ok(Some(clock.end_tick()))
529    }
530}
531
532impl DemoLike for DemoReader {
533    fn next_demo_chunk(
534        &mut self,
535        snap: &mut twsnap::Snap,
536    ) -> Result<Option<DemoChunk>, Box<dyn Error>> {
537        self.next_chunk(snap).map_err(Into::into)
538    }
539}
540
541pub struct TeehistorianDemo {
542    th: teehistorian::ThCompat<teehistorian::ThBufReader<fs::File>>,
543    replayer: teehistorian_replayer::ThReplayer,
544    world: twgame::DdnetReplayerWorld,
545}
546impl TeehistorianDemo {
547    pub fn new(
548        file: fs::File,
549        maps: Option<(Arc<TwMap>, Arc<twgame::Map>)>,
550    ) -> Result<(Self, Arc<TwMap>, Arc<Map>), Box<dyn Error>> {
551        let buf_read = teehistorian::ThBufReader::new(file);
552        let mut th = teehistorian::ThCompat::parse(buf_read)?;
553        let th_header = th.header()?;
554        let (twmap, map) = match maps {
555            Some(maps) => maps,
556            None => {
557                let th_header: serde_json::Value = serde_json::from_slice(th_header)?;
558                let map_name = th_header
559                    .get("map_name")
560                    .and_then(|val| val.as_str())
561                    .ok_or("teehistorian header with no map name")?;
562                let map_sha = th_header
563                    .get("map_sha256")
564                    .and_then(|val| val.as_str())
565                    .ok_or("teehistorian header with no map sha")?;
566                retrieve_maps(map_name, Some(map_sha))?
567            }
568        };
569
570        let mut world = twgame::DdnetReplayerWorld::new(map.clone(), true);
571        let replayer = teehistorian_replayer::ThReplayer::new(th_header, &mut world);
572        Ok((
573            Self {
574                th,
575                world,
576                replayer,
577            },
578            twmap,
579            map,
580        ))
581    }
582}
583
584impl DemoLike for TeehistorianDemo {
585    fn next_demo_chunk(
586        &mut self,
587        snap: &mut twsnap::Snap,
588    ) -> Result<Option<DemoChunk>, Box<dyn Error>> {
589        let last_tick = self.replayer.cur_time();
590        loop {
591            let chunk = match self.th.next_chunk() {
592                Ok(chunk) => Ok(Some(chunk)),
593                Err(teehistorian::Error::Eof) => Ok(None),
594                Err(err) => Err(err),
595            }?;
596            let teehistorian_ended = chunk.is_none();
597            self.replayer
598                .replay_next_chunk(&mut self.world, chunk, None);
599            let tick = self.replayer.cur_time();
600            if tick > last_tick {
601                snap.clear();
602                self.world.snap(snap);
603                break Ok(Some(DemoChunk::Snapshot(tick)));
604            }
605            if teehistorian_ended {
606                break Ok(None);
607            }
608        }
609    }
610}
611
612pub struct MomentcapDemo {
613    data: Vec<u8>,
614    ticks: ops::Range<i32>,
615}
616
617impl MomentcapDemo {
618    pub fn new(mut file: fs::File) -> Result<Self, Box<dyn Error>> {
619        let mut result = Self {
620            data: Vec::new(),
621            ticks: 49..10000 + 1,
622        };
623        file.read_to_end(&mut result.data)?;
624        result.poses()?;
625        Ok(result)
626    }
627    fn poses(&self) -> Result<&[momentcap::Pose], Box<dyn Error>> {
628        Ok(momentcap::read_poses(&self.data)?)
629    }
630}
631
632impl DemoLike for MomentcapDemo {
633    fn next_demo_chunk(
634        &mut self,
635        snap: &mut twsnap::Snap,
636    ) -> Result<Option<DemoChunk>, Box<dyn Error>> {
637        Ok(self.ticks.next().map(|tick| {
638            let tick = twsnap::time::Instant::from_snap_tick(tick);
639            momentcap::to_snap(self.poses().unwrap(), snap, tick);
640            DemoChunk::Snapshot(tick)
641        }))
642    }
643}