Theme

alex/beta-minecraft-rust

Minecraft Beta 1.7.3 Rewrite

BRANCH main
main
SNAPSHOT Draw terrain GPU-driven from packed per-section buffers with GPU occlusion culling, add a render distance slider (2-128) with a quad/disk shape, cut chunk bookkeeping at high render distances, and fix a light underflow at y=0
DOWNLOAD
Download file

root / crates / client / src / screenshot / mod.rs

use std::fs::{File, OpenOptions};
use std::io::{self, BufWriter};
use std::path::Path;
use std::sync::mpsc::{self, Receiver, Sender, SyncSender};
use std::thread::JoinHandle;

use crate::assets::RgbaImage;

pub struct Screenshots {
    sender: Option<SyncSender<RgbaImage>>,
    messages: Sender<String>,
    results: Receiver<String>,
    worker: Option<JoinHandle<()>>,
}

impl Screenshots {
    pub fn new(game_dir: &Path) -> io::Result<Self> {
        let directory = game_dir.join("screenshots");
        let (sender, images) = mpsc::sync_channel::<RgbaImage>(2);
        let (messages, results) = mpsc::channel();
        let worker = std::thread::Builder::new()
            .name("screenshots".into())
            .spawn({
                let messages = messages.clone();
                move || {
                    for image in images {
                        let message = match save(&directory, &image) {
                            Ok(name) => format!("Saved screenshot as {name}"),
                            Err(error) => format!("Failed to save: {error}"),
                        };
                        tracing::info!("{message}");
                        let _ = messages.send(message);
                    }
                }
            })?;
        Ok(Self {
            sender: Some(sender),
            messages,
            results,
            worker: Some(worker),
        })
    }

    pub fn submit(&self, images: Vec<RgbaImage>) {
        for image in images {
            if self.sender.as_ref().unwrap().try_send(image).is_err() {
                let message = "Failed to save: screenshot queue is full";
                tracing::warn!("{message}");
                let _ = self.messages.send(message.into());
            }
        }
    }

    pub fn submit_on_shutdown(&self, images: Vec<RgbaImage>) {
        for image in images {
            if let Err(error) = self.sender.as_ref().unwrap().send(image) {
                tracing::error!("cannot finish screenshot: {error}");
            }
        }
    }

    /// The results of saving, and full queue errors.
    pub fn messages(&self) -> impl Iterator<Item = String> + '_ {
        self.results.try_iter()
    }
}

impl Drop for Screenshots {
    fn drop(&mut self) {
        self.sender.take();
        if let Some(worker) = self.worker.take() {
            let _ = worker.join();
        }
    }
}

/// Writes the image to the first free `<time>.png` or `<time>_<n>.png` and returns its file name.
fn save(directory: &Path, image: &RgbaImage) -> io::Result<String> {
    std::fs::create_dir_all(directory)?;
    let stamp = chrono::Local::now().format("%Y-%m-%d_%H.%M.%S");
    for suffix in 1_u64.. {
        let name = match suffix {
            1 => format!("{stamp}.png"),
            _ => format!("{stamp}_{suffix}.png"),
        };
        let path = directory.join(&name);
        let file = match OpenOptions::new().write(true).create_new(true).open(&path) {
            Ok(file) => file,
            Err(error) if error.kind() == io::ErrorKind::AlreadyExists => continue,
            Err(error) => return Err(error),
        };
        return write_png(file, image).map(|()| name).inspect_err(|_| {
            let _ = std::fs::remove_file(&path);
        });
    }
    unreachable!()
}

fn write_png(file: File, image: &RgbaImage) -> io::Result<()> {
    let mut encoder = png::Encoder::new(BufWriter::new(file), image.width, image.height);
    encoder.set_color(png::ColorType::Rgba);
    encoder.set_depth(png::BitDepth::Eight);
    let mut writer = encoder.write_header()?;
    writer.write_image_data(&image.pixels)?;
    Ok(writer.finish()?)
}