hudhook 0.9.1

A graphics API hook with dear imgui render loop. Supports DirectX 9, 11, 12, and OpenGL 3.
use std::fs::File;
use std::io::Cursor;
use std::sync::Mutex;
use std::time::{Duration, Instant};

use hudhook::{ImguiRenderLoop, MessageFilter, RenderContext};
use image::imageops::FilterType;
use image::io::Reader as ImageReader;
use image::{DynamicImage, EncodableLayout, RgbaImage};
use imgui::{Condition, Context, Image, StyleColor, TextureId};
use tracing_subscriber::prelude::*;
use tracing_subscriber::{fmt, EnvFilter};

pub fn setup_tracing() {
    dotenv::dotenv().ok();

    let log_file = hudhook::util::get_dll_path()
        .map(|mut path| {
            path.set_extension("log");
            path
        })
        .and_then(|path| File::create(path).ok())
        .unwrap();

    tracing_subscriber::registry()
        .with(
            fmt::layer().event_format(
                fmt::format()
                    .with_level(true)
                    .with_thread_ids(true)
                    .with_file(true)
                    .with_line_number(true)
                    .with_thread_names(true),
            ),
        )
        .with(
            fmt::layer()
                .with_thread_ids(true)
                .with_file(true)
                .with_line_number(true)
                .with_thread_names(true)
                .with_writer(Mutex::new(log_file))
                .with_ansi(false)
                .boxed(),
        )
        .with(EnvFilter::from_default_env())
        .init();
}

const IMAGE_COUNT: usize = 16;

pub struct HookExample {
    frame_times: Vec<Duration>,
    first_time: Option<Instant>,
    last_time: Option<Instant>,
    dynamic_image: DynamicImage,
    image: [RgbaImage; IMAGE_COUNT],
    image_id: [Option<TextureId>; IMAGE_COUNT],
    image_pos: [[f32; 2]; IMAGE_COUNT],
    image_vel: [[f32; 2]; IMAGE_COUNT],
    last_upload_time: u64,
    main_window_movable: bool,
    startup: Instant,
}

impl HookExample {
    pub fn new() -> Self {
        println!("Initializing");
        hudhook::alloc_console().ok();

        let image0 = ImageReader::new(Cursor::new(include_bytes!("thingken.webp")))
            .with_guessed_format()
            .unwrap()
            .decode()
            .unwrap()
            .into_rgba8();

        let dynamic_image = DynamicImage::ImageRgba8(image0.clone());

        let image: [RgbaImage; IMAGE_COUNT] = (0..IMAGE_COUNT)
            .map(|i| {
                if i == 0 {
                    image0.clone()
                } else {
                    dynamic_image
                        .resize((i + 1) as u32 * 10, (i + 1) as u32 * 10, FilterType::Lanczos3)
                        .to_rgba8()
                }
            })
            .collect::<Vec<_>>()
            .try_into()
            .unwrap();

        let image_vel: [[f32; 2]; IMAGE_COUNT] = (0..IMAGE_COUNT)
            .map(|i| {
                let i = i + 1;
                if i % 2 == 0 {
                    [i as f32 * 10.0, i as f32 * 20.0]
                } else {
                    [i as f32 * 20.0, i as f32 * 10.0]
                }
            })
            .collect::<Vec<_>>()
            .try_into()
            .unwrap();

        HookExample {
            frame_times: Vec::new(),
            first_time: None,
            last_time: None,
            dynamic_image,
            image,
            image_id: [None; IMAGE_COUNT],
            image_pos: [[16.0, 16.0]; IMAGE_COUNT],
            image_vel,
            last_upload_time: 0,
            main_window_movable: true,
            startup: Instant::now(),
        }
    }
}

impl Default for HookExample {
    fn default() -> Self {
        Self::new()
    }
}

impl ImguiRenderLoop for HookExample {
    fn initialize<'a>(&'a mut self, _ctx: &mut Context, render_context: &'a mut dyn RenderContext) {
        for i in 0..IMAGE_COUNT {
            let image = &self.image[i];
            self.image_id[i] = render_context
                .load_texture(image.as_bytes(), image.width() as _, image.height() as _)
                .ok();
        }

        println!("{:?}", self.image_id);
    }

    fn before_render<'a>(
        &'a mut self,
        _ctx: &mut Context,
        render_context: &'a mut dyn RenderContext,
    ) {
        let elapsed = self.first_time.get_or_insert(Instant::now()).elapsed().as_secs();
        if elapsed != self.last_upload_time {
            self.last_upload_time = elapsed;

            self.dynamic_image.invert();
            if let Some(texture) = self.image_id[0] {
                render_context
                    .replace_texture(
                        texture,
                        self.dynamic_image.as_bytes(),
                        self.dynamic_image.width(),
                        self.dynamic_image.height(),
                    )
                    .unwrap();
            }
        }
    }

    fn render(&mut self, ui: &mut imgui::Ui) {
        let frame_time = if let Some(last_time) = self.last_time.as_mut() {
            let duration = last_time.elapsed();
            self.frame_times.push(duration);
            *last_time = Instant::now();
            duration.as_secs_f32()
        } else {
            self.last_time = Some(Instant::now());
            0.
        };

        let avg: Duration = if self.frame_times.is_empty() {
            Duration::from_nanos(0)
        } else {
            self.frame_times.iter().sum::<Duration>() / self.frame_times.len() as u32
        };

        let last = self.frame_times.last().copied().unwrap_or_else(|| Duration::from_nanos(0));

        ui.window("Hello world")
            .size([376.0, 568.0], Condition::FirstUseEver)
            .position([16.0, 16.0], Condition::FirstUseEver)
            .build(|| {
                ui.text("Hello world!");
                ui.text("こんにちは世界!");
                ui.text("This...is...imgui-rs!");
                for y in 0..16 {
                    for x in 0..16 {
                        let btn = y * 16 + x;
                        let _token = ui.push_style_color(
                            StyleColor::Text,
                            if ui.io().keys_down[btn as usize] {
                                [0., 1., 0., 1.]
                            } else {
                                [1., 1., 1., 1.]
                            },
                        );
                        ui.text(format!("{btn:02x}"));
                        ui.same_line();
                    }
                    ui.new_line();
                }
                ui.text(if ui.io().key_shift { "SHIFT" } else { "shift" });
                ui.text(if ui.io().key_ctrl { "CTRL" } else { "ctrl" });
                ui.text(if ui.io().key_alt { "ALT" } else { "alt" });
                ui.separator();
                let mouse_pos = ui.io().mouse_pos;
                ui.text(format!("Mouse Position: ({:.1},{:.1})", mouse_pos[0], mouse_pos[1]));
                ui.text(format!("Frame time: {:8.2}", last.as_secs_f64() * 1000.));
                ui.text(format!("Avg:        {:8.2}", avg.as_secs_f64() * 1000.));
                ui.text(format!("FPS:        {:8.2}", 1. / last.as_secs_f64()));
                ui.text(format!("Avg:        {:8.2}", 1. / avg.as_secs_f64()));
                ui.separator();
                ui.text(format!("Main window movable: {}", self.main_window_movable));
                if ui.button("Toggle movability") {
                    self.main_window_movable ^= true;
                }
            });

        ui.window("Image")
            .size([376.0, 568.0], Condition::FirstUseEver)
            .position([408.0, 16.0], Condition::FirstUseEver)
            .build(|| {
                let _token = ui.tab_bar("##tab_bar");

                if ui.tab_item("Animated").is_some() {
                    for i in 0..IMAGE_COUNT {
                        let pos = &mut self.image_pos[i];
                        let vel = &mut self.image_vel[i];
                        let image = &self.image[i];
                        let width = image.width() as f32;
                        let height = image.height() as f32;

                        let next_x = pos[0] + vel[0] * frame_time;
                        let next_y = pos[1] + vel[1] * frame_time;

                        if next_x < 16. || next_x > 376. - 16. - width {
                            vel[0] = -vel[0];
                        }
                        pos[0] = next_x.clamp(16., 376. - 16. - width);

                        if next_y < 16. || next_y > 568. - 16. - height {
                            vel[1] = -vel[1];
                        }
                        pos[1] = next_y.clamp(16., 568. - 16. - height);

                        ui.set_cursor_pos(*pos);

                        if let Some(tex_id) = self.image_id[i] {
                            Image::new(tex_id, [width, height]).build(ui);
                        }
                    }
                }

                if ui.tab_item("Image list").is_some() {
                    for i in 0..IMAGE_COUNT {
                        if let Some(tex_id) = self.image_id[i] {
                            let image = &self.image[i];
                            let width = image.width() as f32;
                            let height = image.height() as f32;
                            ui.text(format!("Image {i}"));
                            Image::new(tex_id, [width, height]).build(ui);
                        }
                    }
                }
            });

        if self.startup.elapsed() > Duration::from_millis(5000) {
            println!("Ejecting!");
            hudhook::eject();
        }
    }

    fn message_filter(&self, _io: &imgui::Io) -> MessageFilter {
        if self.main_window_movable {
            MessageFilter::empty()
        } else {
            MessageFilter::WindowControl
        }
    }
}