Sign in Sign up
kretrod/lodestone Public
Branches
master
1460 lines (1359 loc) · 52.1 KB Raw
//! Drawing the menu inside the game's own OpenGL context.
//!
//! The game is mid-frame when our hook runs: its shader program, sampler
//! objects, blend mode, scissor box and pixel-transfer settings are all set up
//! for whatever it was drawing. Two things follow from that:
//!
//!   * we must neutralise the state egui assumes is default — a sampler object
//!     left bound on unit 0 renders the whole menu flat black, and a bound
//!     pixel-unpack buffer turns every font-atlas upload into garbage;
//!   * we must put all of it back before returning, or the game's next frame
//!     renders wrong.

use std::ffi::CString;
use std::sync::Arc;
use std::time::Instant;

use egui::{Align2, Color32, FontId, Rounding, Stroke};
use glow::HasContext;

use crate::state::{AimMode, AuraTarget, BindTarget, FlyMode, ModuleId, Shared, SpeedMode};

/// Monocraft — a Minecraft-styled typeface, SIL Open Font License. Embedded so
/// the client is one file, with a file beside the DLL taking precedence if you
/// want to swap the face without a rebuild.
const FONT: &[u8] = include_bytes!("../assets/Monocraft.ttf");

// Minecraft's own palette. The game is drawn in these, the font is a Minecraft
// face, and the menu sits on top of both — anything softer or more fashionable
// reads as something bolted on from outside.
const BG: Color32 = Color32::from_rgba_premultiplied(12, 12, 14, 232);
const HEADER: Color32 = Color32::from_rgb(28, 28, 32);
const ROW_ON: Color32 = Color32::from_rgba_premultiplied(30, 46, 30, 230);
const HOVER: Color32 = Color32::from_rgba_premultiplied(34, 34, 40, 235);
const LINE: Color32 = Color32::from_rgb(64, 64, 70);
const ACCENT: Color32 = Color32::from_rgb(85, 255, 85); // §a
const AQUA: Color32 = Color32::from_rgb(85, 255, 255); // §b
const TEXT: Color32 = Color32::from_rgb(255, 255, 255);
const DIM: Color32 = Color32::from_rgb(170, 170, 170); // §7
const DARK: Color32 = Color32::from_rgb(85, 85, 85); // §8
const WARN: Color32 = Color32::from_rgb(255, 170, 0); // §6
const OFF: Color32 = Color32::from_rgb(40, 40, 46);

/// Minecraft draws every string twice: once offset down-right in a quarter
/// shade, then the glyph over it. Text without that shadow is the single
/// clearest sign of something that was not drawn by the game.
fn shadowed(
    painter: &egui::Painter,
    pos: egui::Pos2,
    align: Align2,
    text: impl ToString,
    size: f32,
    colour: Color32,
) {
    let text = text.to_string();
    let shadow = Color32::from_rgba_premultiplied(
        colour.r() / 4,
        colour.g() / 4,
        colour.b() / 4,
        colour.a(),
    );
    painter.text(
        pos + egui::vec2(1.0, 1.0),
        align,
        &text,
        FontId::proportional(size),
        shadow,
    );
    painter.text(pos, align, &text, FontId::proportional(size), colour);
}

#[derive(Clone, Copy, PartialEq, Eq)]
enum Tab {
    Combat,
    Movement,
    World,
    Esp,
    Visuals,
    Misc,
}

impl Tab {
    const ALL: [Tab; 6] = [
        Tab::Combat,
        Tab::Movement,
        Tab::World,
        Tab::Esp,
        Tab::Visuals,
        Tab::Misc,
    ];
    fn label(self) -> &'static str {
        match self {
            Tab::Combat => "COMBAT",
            Tab::Movement => "MOVEMENT",
            Tab::World => "WORLD",
            Tab::Esp => "ESP",
            Tab::Visuals => "VISUALS",
            Tab::Misc => "MISC",
        }
    }
}

pub struct Overlay {
    ctx: egui::Context,
    painter: egui_glow::Painter,
    gl: Arc<glow::Context>,
    start: Instant,
    tab: Tab,
    frame_times: [f32; 60],
    frame_i: usize,
    last_frame: Instant,
    logged_state: bool,
}

impl Overlay {
    pub fn new() -> Result<Self, String> {
        // SAFETY: the game's GL context is current on this thread — we are
        // inside its swap-buffers call.
        let gl = unsafe {
            glow::Context::from_loader_function(|name| gl_proc(name) as *const std::ffi::c_void)
        };
        let gl = Arc::new(gl);
        // SAFETY: as above.
        unsafe {
            crate::log(&format!(
                "gl version {:?} renderer {:?}",
                gl.get_parameter_string(glow::VERSION),
                gl.get_parameter_string(glow::RENDERER)
            ));
        }
        let painter = egui_glow::Painter::new(gl.clone(), "", None, false)
            .map_err(|e| format!("egui painter: {e}"))?;

        let ctx = egui::Context::default();
        ctx.set_fonts(fonts());
        ctx.set_style(style());
        Ok(Self {
            ctx,
            painter,
            gl,
            start: Instant::now(),
            tab: Tab::Combat,
            frame_times: [0.0; 60],
            frame_i: 0,
            last_frame: Instant::now(),
            logged_state: false,
        })
    }

    pub fn fps(&self) -> f32 {
        let sum: f32 = self.frame_times.iter().sum();
        let n = self.frame_times.iter().filter(|t| **t > 0.0).count();
        if n == 0 || sum <= 0.0 {
            0.0
        } else {
            n as f32 / sum
        }
    }

    pub fn draw(&mut self, width: i32, height: i32, shared: &mut Shared) {
        let dt = self.last_frame.elapsed().as_secs_f32();
        self.last_frame = Instant::now();
        self.frame_times[self.frame_i] = dt;
        self.frame_i = (self.frame_i + 1) % self.frame_times.len();

        // A pixel face is happiest on whole pixels, so the scale steps in
        // quarters rather than sliding continuously.
        let scale = ((shared.cfg.ui_scale * 4.0).round() / 4.0).clamp(0.5, 3.0);
        shared.scale = scale;

        // Tell egui the density *before* it lays anything out. Without this it
        // rasterises the font atlas at 1x and the painter magnifies those
        // bitmaps to fit, which is exactly what a blurry scaled menu is. Set
        // here, the glyphs are re-rendered at the size they will be drawn.
        if (self.ctx.pixels_per_point() - scale).abs() > f32::EPSILON {
            self.ctx.set_pixels_per_point(scale);
        }

        let size = egui::vec2(width as f32 / scale, height as f32 / scale);

        let raw = egui::RawInput {
            screen_rect: Some(egui::Rect::from_min_size(egui::Pos2::ZERO, size)),
            time: Some(self.start.elapsed().as_secs_f64()),
            events: std::mem::take(&mut shared.events),
            focused: true,
            ..Default::default()
        };

        let open = shared.menu_open;
        // egui::Context is a handle; cloning it frees `self` for the closure.
        let ctx = self.ctx.clone();
        let out = ctx.run(raw, |ctx| {
            if open {
                self.menu(ctx, shared);
            }
            self.esp_overlay(ctx, shared);
            self.hud(ctx, shared, open);
        });

        let prims = ctx.tessellate(out.shapes, out.pixels_per_point);
        // SAFETY: we are on the render thread with the game's context current.
        unsafe {
            let saved = GlState::save(&self.gl);
            if !self.logged_state {
                self.logged_state = true;
                saved.log_once();
            }
            saved.neutralise(&self.gl);
            self.painter.paint_and_update_textures(
                [width as u32, height as u32],
                out.pixels_per_point,
                &prims,
                &out.textures_delta,
            );
            saved.restore(&self.gl);
        }
    }

    // ---- the menu ----------------------------------------------------------
    //
    // Five independent windows rather than one panel: each section is its own
    // thing, dragged and collapsed on its own, so you can keep Combat and ESP
    // open side by side and shove the rest out of the way. egui remembers
    // where you put each one and whether it is folded up.

    fn menu(&mut self, ctx: &egui::Context, shared: &mut Shared) {
        let screen = ctx.screen_rect();
        let width = 216.0;
        let gap = 8.0;
        let per_row = (((screen.width() - 24.0) / (width + gap)).floor() as usize).clamp(1, 6);

        for (i, tab) in Tab::ALL.into_iter().enumerate() {
            let col = i % per_row;
            let row = i / per_row;
            let pos = [
                12.0 + col as f32 * (width + gap),
                12.0 + row as f32 * (screen.height() * 0.5),
            ];
            // Use the screen, not a number picked in advance: a window grows to
            // its content and only starts scrolling when it runs out of room
            // below where it sits.
            let max_height = (screen.height() - pos[1] - 24.0).max(160.0);
            egui::Window::new(tab.label())
                .id(egui::Id::new(("lodestone-section", i)))
                .default_pos(pos)
                // Width fixed, height free. fixed_size pins *both*, and a
                // height of zero leaves egui to clamp it to a minimum — which
                // is why these were stuck short no matter how the rows grew.
                .default_width(width)
                .resizable(false)
                .collapsible(true)
                .frame(
                    egui::Frame::none()
                        .fill(BG)
                        .stroke(Stroke::new(1.0, LINE))
                        .rounding(Rounding::ZERO)
                        .inner_margin(egui::Margin {
                            left: 0.0,
                            right: 0.0,
                            top: 0.0,
                            bottom: 2.0,
                        }),
                )
                .show(ctx, |ui| {
                    ui.set_width(width);
                    ui.spacing_mut().item_spacing = egui::vec2(0.0, 0.0);
                    egui::ScrollArea::vertical()
                        .id_salt(("lodestone-scroll", i))
                        // Use the screen, not a number picked in advance: a
                        // window grows to its content and only starts
                        // scrolling when it runs out of room below itself.
                        .max_height(max_height)
                        .auto_shrink([false, true])
                        .show(ui, |ui| match tab {
                            Tab::Combat => combat(ui, shared),
                            Tab::Movement => movement(ui, shared),
                            Tab::World => world(ui, shared),
                            Tab::Esp => esp(ui, shared),
                            Tab::Visuals => visuals(ui, shared),
                            Tab::Misc => misc(ui, shared),
                        });
                });
        }
    }

    // ---- world overlay -----------------------------------------------------

    /// Draw the scanned entities into the world, projecting each one with the
    /// same camera the game is rendering from.
    fn esp_overlay(&self, ctx: &egui::Context, shared: &Shared) {
        let cfg = &shared.cfg.esp;
        let g = &shared.game;
        let nothing_to_draw =
            g.targets.is_empty() && g.blocks.is_empty() && g.base_hits.is_empty();
        if nothing_to_draw {
            return;
        }
        let layer = egui::LayerId::new(egui::Order::Background, egui::Id::new("lodestone-esp"));
        let painter = ctx.layer_painter(layer);
        let screen = ctx.screen_rect();
        let view = View::new(g.camera, g.camera_yaw, g.camera_pitch, g.fov, screen.size());

        // Blocks first, so entities draw over them. Capped: selecting coal at a
        // wide radius can find tens of thousands, and every one is eight
        // projected corners and a rectangle.
        const MAX_BLOCK_BOXES: usize = 3000;
        if !g.blocks.is_empty() {
            for (x, y, z, kind) in g.blocks.iter().take(MAX_BLOCK_BOXES) {
                let min = (*x as f64, *y as f64, *z as f64);
                let max = (min.0 + 1.0, min.1 + 1.0, min.2 + 1.0);
                let Some(rect) = view.project_box(min, max) else {
                    continue;
                };
                if !screen.intersects(rect) {
                    continue;
                }
                let crate::state::BlockKind::Ore(group) = kind;
                painter.rect_stroke(rect, Rounding::ZERO, Stroke::new(1.0, ore_colour(*group)));
            }
        }

        // Block entities: containers, and the things that mark out a base.
        const MAX_HIT_BOXES: usize = 400;
        for hit in g.base_hits.iter().take(MAX_HIT_BOXES) {
            if hit.distance > cfg.distance.max(64.0) && !hit.is_base {
                continue;
            }
            let min = (hit.x as f64, hit.y as f64, hit.z as f64);
            let max = (min.0 + 1.0, min.1 + 1.0, min.2 + 1.0);
            let Some(rect) = view.project_box(min, max) else {
                continue;
            };
            if !screen.intersects(rect) {
                continue;
            }
            let colour = if hit.is_base {
                Color32::from_rgb(236, 130, 220)
            } else {
                Color32::from_rgb(232, 186, 104)
            };
            painter.rect_stroke(rect, Rounding::ZERO, Stroke::new(1.0, colour));
            if hit.is_base || rect.height() > 18.0 {
                painter.text(
                    rect.center_top() + egui::vec2(0.0, -11.0),
                    Align2::CENTER_TOP,
                    format!("{} [{:.0}m]", hit.label, hit.distance),
                    FontId::proportional(9.5),
                    colour,
                );
            }
        }

        const MAX_ENTITY_BOXES: usize = 192;
        for t in g.targets.iter().take(MAX_ENTITY_BOXES) {
            let Some(rect) = view.project_box(t.min, t.max) else {
                continue;
            };
            if !screen.intersects(rect) {
                continue;
            }
            let colour = kind_colour(t.kind);

            if cfg.boxes {
                painter.rect_stroke(rect, Rounding::ZERO, Stroke::new(1.0, colour));
                // A darker outline keeps the box readable against bright terrain.
                painter.rect_stroke(
                    rect.expand(1.0),
                    Rounding::ZERO,
                    Stroke::new(1.0, Color32::from_black_alpha(120)),
                );
            }
            if cfg.tracers {
                painter.line_segment(
                    [egui::pos2(screen.center().x, screen.bottom()), rect.center_bottom()],
                    Stroke::new(1.0, colour.linear_multiply(0.8)),
                );
            }
            if cfg.health_bars && t.max_health > 0.0 {
                let frac = (t.health / t.max_health).clamp(0.0, 1.0);
                let bar = egui::Rect::from_min_max(
                    egui::pos2(rect.left() - 5.0, rect.top()),
                    egui::pos2(rect.left() - 2.0, rect.bottom()),
                );
                painter.rect_filled(bar, Rounding::ZERO, Color32::from_black_alpha(140));
                let filled = egui::Rect::from_min_max(
                    egui::pos2(bar.left(), bar.bottom() - bar.height() * frac),
                    bar.max,
                );
                painter.rect_filled(
                    filled,
                    Rounding::ZERO,
                    Color32::from_rgb(
                        (255.0 * (1.0 - frac)) as u8,
                        (220.0 * frac) as u8,
                        80,
                    ),
                );
            }
            if cfg.nametags && !t.name.is_empty() {
                let label = format!("{} [{:.0}m]", t.name, t.distance);
                let pos = egui::pos2(rect.center().x, rect.top() - 12.0);
                painter.text(
                    pos + egui::vec2(1.0, 1.0),
                    Align2::CENTER_TOP,
                    &label,
                    FontId::proportional(10.0),
                    Color32::from_black_alpha(160),
                );
                painter.text(pos, Align2::CENTER_TOP, &label, FontId::proportional(10.0), colour);
            }
        }
    }

    // ---- always-on HUD -----------------------------------------------------

    fn hud(&self, ctx: &egui::Context, shared: &Shared, menu_open: bool) {
        let cfg = &shared.cfg;
        let g = &shared.game;
        let layer = egui::LayerId::new(egui::Order::Background, egui::Id::new("lodestone-hud"));
        let painter = ctx.layer_painter(layer);
        let screen = ctx.screen_rect();

        if cfg.visuals.watermark {
            shadowed(&painter, egui::pos2(4.0, 4.0), Align2::LEFT_TOP, "Lodestone", 19.0, ACCENT);
            if !menu_open {
                shadowed(
                    &painter,
                    egui::pos2(4.0, 24.0),
                    Align2::LEFT_TOP,
                    "[insert]",
                    11.0,
                    DARK,
                );
            }
        }

        if cfg.visuals.hud_coords && g.in_world {
            shadowed(
                &painter,
                egui::pos2(4.0, screen.height() - 14.0),
                Align2::LEFT_TOP,
                format!("{:.0} {:.0} {:.0}", g.pos.0, g.pos.1, g.pos.2),
                12.0,
                DIM,
            );
        }

        if cfg.visuals.hud_modules {
            let mut y = 4.0;
            for name in active_modules(cfg) {
                shadowed(
                    &painter,
                    egui::pos2(screen.width() - 4.0, y),
                    Align2::RIGHT_TOP,
                    name,
                    13.0,
                    ACCENT,
                );
                y += 15.0;
            }
        }
    }
}

/// One colour per X-Ray group, in the order they are listed.
fn ore_colour(group: usize) -> Color32 {
    const PALETTE: &[Color32] = &[
        Color32::from_rgb(108, 222, 226), // diamond
        Color32::from_rgb(176, 120, 220), // ancient debris
        Color32::from_rgb(104, 222, 132), // emerald
        Color32::from_rgb(240, 208, 96),  // gold
        Color32::from_rgb(214, 178, 150), // iron
        Color32::from_rgb(238, 90, 90),   // redstone
        Color32::from_rgb(96, 132, 232),  // lapis
        Color32::from_rgb(226, 142, 88),  // copper
        Color32::from_rgb(130, 136, 146), // coal
        Color32::from_rgb(228, 224, 214), // quartz
        Color32::from_rgb(196, 108, 232), // spawner
        Color32::from_rgb(236, 108, 196), // portal
        Color32::from_rgb(232, 186, 104), // chest
    ];
    PALETTE.get(group).copied().unwrap_or(ACCENT)
}

fn kind_colour(kind: crate::mc::TargetKind) -> Color32 {
    use crate::mc::TargetKind::*;
    match kind {
        Player => Color32::from_rgb(240, 96, 110),
        Mob => Color32::from_rgb(236, 160, 84),
        Animal => Color32::from_rgb(126, 216, 140),
        Item => Color32::from_rgb(120, 190, 236),
        Other => DIM,
    }
}

/// World-to-screen for one frame's camera.
///
/// Minecraft's yaw is zero looking south (+Z) and increases clockwise, so the
/// forward vector is (-sin yaw · cos pitch, -sin pitch, cos yaw · cos pitch).
/// Right is forward × world-up, and the projection is an ordinary pinhole with
/// the game's vertical field of view.
struct View {
    camera: (f64, f64, f64),
    right: (f64, f64, f64),
    up: (f64, f64, f64),
    forward: (f64, f64, f64),
    half: egui::Vec2,
    tan_half_fov: f64,
    aspect: f64,
}

impl View {
    fn new(
        camera: (f64, f64, f64),
        yaw: f32,
        pitch: f32,
        fov: f32,
        size: egui::Vec2,
    ) -> Self {
        let (sy, cy) = (yaw as f64).to_radians().sin_cos();
        let (sp, cp) = (pitch as f64).to_radians().sin_cos();
        let forward = (-sy * cp, -sp, cy * cp);
        // forward × (0,1,0)
        let right = (-forward.2, 0.0, forward.0);
        let rl = (right.0 * right.0 + right.2 * right.2).sqrt().max(1e-9);
        let right = (right.0 / rl, 0.0, right.2 / rl);
        // up = right × forward
        let up = (
            right.1 * forward.2 - right.2 * forward.1,
            right.2 * forward.0 - right.0 * forward.2,
            right.0 * forward.1 - right.1 * forward.0,
        );
        Self {
            camera,
            right,
            up,
            forward,
            half: size / 2.0,
            tan_half_fov: ((fov.max(1.0) as f64) / 2.0).to_radians().tan(),
            aspect: (size.x / size.y.max(1.0)) as f64,
        }
    }

    fn project(&self, p: (f64, f64, f64)) -> Option<egui::Pos2> {
        let d = (
            p.0 - self.camera.0,
            p.1 - self.camera.1,
            p.2 - self.camera.2,
        );
        let z = dot(d, self.forward);
        // Anything at or behind the eye has no meaningful screen position.
        if z <= 0.05 {
            return None;
        }
        let x = dot(d, self.right);
        let y = dot(d, self.up);
        Some(egui::pos2(
            self.half.x * (1.0 + (x / (z * self.tan_half_fov * self.aspect)) as f32),
            self.half.y * (1.0 - (y / (z * self.tan_half_fov)) as f32),
        ))
    }

    /// The screen rectangle covering all eight corners of a world-space box.
    fn project_box(&self, min: (f64, f64, f64), max: (f64, f64, f64)) -> Option<egui::Rect> {
        let mut rect: Option<egui::Rect> = None;
        for i in 0..8 {
            let corner = (
                if i & 1 == 0 { min.0 } else { max.0 },
                if i & 2 == 0 { min.1 } else { max.1 },
                if i & 4 == 0 { min.2 } else { max.2 },
            );
            let p = self.project(corner)?;
            rect = Some(match rect {
                None => egui::Rect::from_min_max(p, p),
                Some(r) => r.union(egui::Rect::from_min_max(p, p)),
            });
        }
        rect
    }
}

fn dot(a: (f64, f64, f64), b: (f64, f64, f64)) -> f64 {
    a.0 * b.0 + a.1 * b.1 + a.2 * b.2
}

fn active_modules(cfg: &crate::state::Config) -> Vec<String> {
    let m = &cfg.movement;
    let cb = &cfg.combat;
    let v = &cfg.visuals;
    let e = &cfg.esp;
    let mut out: Vec<String> = Vec::new();
    if m.fly {
        out.push(format!("Fly [{}]", m.fly_mode.label()));
    }
    if m.speed {
        out.push(format!("Speed [{}]", m.speed_mode.label()));
    }
    for (on, name) in [
        (m.freecam, "Freecam"),
        (m.no_fall, "No Fall"),
        (m.jetpack, "Jetpack"),
        (m.sprint, "Auto Sprint"),
        (m.noclip, "Noclip"),
        (m.step, "Step"),
        (m.jump_power, "High Jump"),
        (m.jesus, "Jesus"),
        (m.spider, "Spider"),
        (m.bhop, "Bhop"),
        (m.blink, "Blink"),
        (e.base_finder, "Base Finder"),
        (cb.criticals, "Criticals"),
        (cb.auto_dodge, "Auto Dodge"),
        (cb.auto_totem, "Auto Totem"),
        (cb.auto_shield, "Auto Shield"),
        (cfg.building.air_place, "Air Place"),
        (cfg.building.auto_build, "Auto Build"),
        (cfg.misc.auto_respawn, "Auto Respawn"),
        (cb.kill_aura, "Kill Aura"),
        (cb.aimbot, "Aimbot"),
        (cb.trigger_bot, "Trigger Bot"),
        (cb.reach, "Reach"),
        (cb.auto_clicker, "Auto Clicker"),
        (cb.anti_knockback, "Anti KB"),
        (e.players || e.mobs || e.animals || e.items || e.containers, "ESP"),
        (e.xray, "X-Ray"),
        (v.fullbright, "Fullbright"),
        (v.no_fog, "No Fog"),
        (v.no_hurt_cam, "No Hurt Cam"),
        (v.no_culling, "No Culling"),
        (v.view_distance, "Extend View"),
        (cfg.misc.hide_from_capture, "Hidden"),
    ] {
        if on {
            out.push(name.to_string());
        }
    }
    out
}

// ---- chrome ----------------------------------------------------------------





// ---- widgets ---------------------------------------------------------------

/// A module row.
///
/// No switch graphic: an enabled module is simply lit — white text on a dark
/// green row with an accent edge — and a disabled one is grey. That is how
/// every client of this kind has looked for fifteen years, and it reads faster
/// than a row of toggles because the eye picks out the lit ones at a glance.
fn module(ui: &mut egui::Ui, shared: &mut Shared, id: ModuleId) -> bool {
    let on = id.get(&shared.cfg);
    let target = BindTarget::Module(id);
    let listening = shared.binding == Some(target);
    let bind = shared.bind_for(target);

    let (rect, response) =
        ui.allocate_exact_size(egui::vec2(ui.available_width(), 19.0), egui::Sense::click());

    // Left click toggles, right click starts listening for a key.
    if response.clicked() {
        id.toggle(&mut shared.cfg);
    }
    if response.secondary_clicked() {
        shared.binding = Some(target);
    }

    let painter = ui.painter();
    if on {
        painter.rect_filled(rect, Rounding::ZERO, ROW_ON);
        painter.rect_filled(
            egui::Rect::from_min_size(rect.left_top(), egui::vec2(2.0, rect.height())),
            Rounding::ZERO,
            ACCENT,
        );
    } else if response.hovered() {
        painter.rect_filled(rect, Rounding::ZERO, HOVER);
    }
    shadowed(
        painter,
        egui::pos2(rect.left() + 7.0, rect.center().y),
        Align2::LEFT_CENTER,
        id.label(),
        13.0,
        if on { TEXT } else { DIM },
    );

    let key = if listening {
        Some("...".to_string())
    } else {
        bind.map(key_name)
    };
    if let Some(key) = key {
        shadowed(
            painter,
            egui::pos2(rect.right() - 4.0, rect.center().y),
            Align2::RIGHT_CENTER,
            format!("[{key}]"),
            11.0,
            if listening { ACCENT } else { DARK },
        );
    }
    response.clicked()
}

/// Short label for a virtual-key code.
pub fn key_name(vk: u32) -> String {
    match vk {
        0x08 => "BKSP".into(),
        0x09 => "TAB".into(),
        0x0D => "ENTER".into(),
        0x10 => "SHIFT".into(),
        0x11 => "CTRL".into(),
        0x12 => "ALT".into(),
        0x14 => "CAPS".into(),
        0x20 => "SPACE".into(),
        0x21 => "PGUP".into(),
        0x22 => "PGDN".into(),
        0x23 => "END".into(),
        0x24 => "HOME".into(),
        0x25 => "LEFT".into(),
        0x26 => "UP".into(),
        0x27 => "RIGHT".into(),
        0x28 => "DOWN".into(),
        0x2D => "INS".into(),
        0x2E => "DEL".into(),
        0x30..=0x39 => ((b'0' + (vk - 0x30) as u8) as char).to_string(),
        0x41..=0x5A => ((b'A' + (vk - 0x41) as u8) as char).to_string(),
        0x60..=0x69 => format!("NUM{}", vk - 0x60),
        0x70..=0x7B => format!("F{}", vk - 0x70 + 1),
        other => format!("{other:#04x}"),
    }
}

/// A setting: a filled bar with its name and value written across it. Drag it.
fn slider(ui: &mut egui::Ui, v: &mut f32, range: std::ops::RangeInclusive<f32>, label: &str) {
    let (rect, response) = ui.allocate_exact_size(
        egui::vec2(ui.available_width(), 17.0),
        egui::Sense::click_and_drag(),
    );
    let inner = rect.shrink2(egui::vec2(8.0, 2.0));
    let (lo, hi) = (*range.start(), *range.end());

    if response.dragged() || response.clicked() {
        if let Some(p) = response.interact_pointer_pos() {
            let t = ((p.x - inner.left()) / inner.width()).clamp(0.0, 1.0);
            *v = lo + (hi - lo) * t;
        }
    }

    let t = ((*v - lo) / (hi - lo)).clamp(0.0, 1.0);
    let painter = ui.painter();
    painter.rect_filled(inner, Rounding::ZERO, OFF);
    painter.rect_filled(
        egui::Rect::from_min_size(inner.min, egui::vec2(inner.width() * t, inner.height())),
        Rounding::ZERO,
        Color32::from_rgb(38, 92, 38),
    );
    painter.rect_stroke(inner, Rounding::ZERO, Stroke::new(1.0, LINE));
    shadowed(
        painter,
        egui::pos2(inner.left() + 3.0, inner.center().y),
        Align2::LEFT_CENTER,
        label,
        11.0,
        DIM,
    );
    let decimals = if hi - lo > 20.0 { 0 } else { 2 };
    shadowed(
        painter,
        egui::pos2(inner.right() - 3.0, inner.center().y),
        Align2::RIGHT_CENTER,
        format!("{:.*}", decimals, v),
        11.0,
        TEXT,
    );
}

/// A sub-option, indented under its module.
fn check(ui: &mut egui::Ui, v: &mut bool, label: &str) {
    let (rect, response) =
        ui.allocate_exact_size(egui::vec2(ui.available_width(), 17.0), egui::Sense::click());
    if response.clicked() {
        *v = !*v;
    }
    let painter = ui.painter();
    if response.hovered() {
        painter.rect_filled(rect, Rounding::ZERO, HOVER);
    }
    shadowed(
        painter,
        egui::pos2(rect.left() + 14.0, rect.center().y),
        Align2::LEFT_CENTER,
        format!("{} {}", if *v { "x" } else { "-" }, label),
        12.0,
        if *v { AQUA } else { DARK },
    );
}

/// A mode picker, drawn as one row of names with the active one lit.
fn modes<T: Copy + PartialEq>(
    ui: &mut egui::Ui,
    value: &mut T,
    all: impl IntoIterator<Item = T>,
    label: impl Fn(T) -> &'static str,
) {
    let options: Vec<T> = all.into_iter().collect();
    let (rect, _) =
        ui.allocate_exact_size(egui::vec2(ui.available_width(), 17.0), egui::Sense::hover());
    let inner = rect.shrink2(egui::vec2(8.0, 1.0));
    let each = inner.width() / options.len().max(1) as f32;
    for (i, m) in options.into_iter().enumerate() {
        let cell = egui::Rect::from_min_size(
            egui::pos2(inner.left() + each * i as f32, inner.top()),
            egui::vec2(each, inner.height()),
        );
        let response = ui.interact(cell, ui.id().with(("mode", i, label(m))), egui::Sense::click());
        if response.clicked() {
            *value = m;
        }
        let selected = *value == m;
        let painter = ui.painter();
        painter.rect_filled(
            cell,
            Rounding::ZERO,
            if selected {
                Color32::from_rgb(38, 92, 38)
            } else if response.hovered() {
                HOVER
            } else {
                OFF
            },
        );
        painter.rect_stroke(cell, Rounding::ZERO, Stroke::new(1.0, LINE));
        shadowed(
            painter,
            cell.center(),
            Align2::CENTER_CENTER,
            label(m),
            10.0,
            if selected { TEXT } else { DARK },
        );
    }
}

/// A section heading inside a window.
fn heading(ui: &mut egui::Ui, text: &str) {
    let (rect, _) =
        ui.allocate_exact_size(egui::vec2(ui.available_width(), 18.0), egui::Sense::hover());
    let painter = ui.painter();
    painter.rect_filled(rect, Rounding::ZERO, HEADER);
    shadowed(
        painter,
        egui::pos2(rect.left() + 5.0, rect.center().y),
        Align2::LEFT_CENTER,
        text,
        11.0,
        DIM,
    );
}

/// A plain button row.
fn button(ui: &mut egui::Ui, text: &str, colour: Color32) -> bool {
    let (rect, response) =
        ui.allocate_exact_size(egui::vec2(ui.available_width(), 19.0), egui::Sense::click());
    let painter = ui.painter();
    painter.rect_filled(rect, Rounding::ZERO, if response.hovered() { HOVER } else { OFF });
    painter.rect_stroke(rect, Rounding::ZERO, Stroke::new(1.0, LINE));
    shadowed(painter, rect.center(), Align2::CENTER_CENTER, text, 12.0, colour);
    response.clicked()
}

fn combat(ui: &mut egui::Ui, shared: &mut Shared) {
    module(ui, shared, ModuleId::KillAura);
    if shared.cfg.combat.kill_aura {
        let c = &mut shared.cfg.combat;
        modes(ui, &mut c.aura_target, AuraTarget::ALL, |m| m.label());
        slider(ui, &mut c.aura_range, 2.0..=6.0, "range");
        if !c.aura_cooldown {
            slider(ui, &mut c.aura_cps, 1.0..=20.0, "cps");
        }
        check(ui, &mut c.aura_cooldown, "cooldown");
        check(ui, &mut c.aura_rotate, "rotate");
        check(ui, &mut c.aura_through_walls, "walls");
        check(ui, &mut c.aura_players, "players");
        check(ui, &mut c.aura_mobs, "mobs");
        check(ui, &mut c.aura_animals, "animals");
    }

    module(ui, shared, ModuleId::Aimbot);
    if shared.cfg.combat.aimbot {
        let c = &mut shared.cfg.combat;
        modes(ui, &mut c.aim_mode, AimMode::ALL, |m| m.label());
        slider(ui, &mut c.aim_fov, 5.0..=180.0, "fov");
        slider(ui, &mut c.aim_speed, 0.05..=1.0, "smooth");
    }

    module(ui, shared, ModuleId::TriggerBot);
    if shared.cfg.combat.trigger_bot {
        slider(ui, &mut shared.cfg.combat.trigger_delay, 0.0..=0.5, "delay");
    }

    module(ui, shared, ModuleId::AutoDodge);
    if shared.cfg.combat.auto_dodge {
        let c = &mut shared.cfg.combat;
        slider(ui, &mut c.dodge_range, 3.0..=16.0, "range");
        slider(ui, &mut c.dodge_speed, 0.1..=0.5, "speed");
        check(ui, &mut c.dodge_arrows, "arrows");
        check(ui, &mut c.dodge_cliffs, "ledges");
    }

    module(ui, shared, ModuleId::Reach);
    if shared.cfg.combat.reach {
        slider(ui, &mut shared.cfg.combat.reach_distance, 3.0..=6.0, "blocks");
    }

    module(ui, shared, ModuleId::AutoClicker);
    if shared.cfg.combat.auto_clicker {
        let c = &mut shared.cfg.combat;
        slider(ui, &mut c.click_cps, 1.0..=20.0, "cps");
        slider(ui, &mut c.click_jitter, 0.0..=1.0, "jitter");
    }

    module(ui, shared, ModuleId::Criticals);

    module(ui, shared, ModuleId::AutoTotem);
    if shared.cfg.combat.auto_totem {
        slider(ui, &mut shared.cfg.combat.totem_health, 2.0..=19.0, "health");
    }
    module(ui, shared, ModuleId::AutoShield);

    module(ui, shared, ModuleId::AntiKnockback);
    if shared.cfg.combat.anti_knockback {
        let c = &mut shared.cfg.combat;
        slider(ui, &mut c.kb_horizontal, 0.0..=1.0, "horizontal");
        slider(ui, &mut c.kb_vertical, 0.0..=1.0, "vertical");
    }
}

fn movement(ui: &mut egui::Ui, shared: &mut Shared) {
    module(ui, shared, ModuleId::Fly);
    if shared.cfg.movement.fly {
        let c = &mut shared.cfg.movement;
        modes(ui, &mut c.fly_mode, FlyMode::ALL, |m| m.label());
        match c.fly_mode {
            FlyMode::Teleport => {
                slider(ui, &mut c.fly_step, 0.5..=8.0, "step");
                slider(ui, &mut c.fly_step_interval, 0.05..=1.0, "delay");
            }
            FlyMode::Creative => slider(ui, &mut c.fly_speed, 0.01..=0.6, "speed"),
            _ => slider(ui, &mut c.fly_speed, 0.05..=2.0, "speed"),
        }
        check(ui, &mut c.fly_anti_kick, "anti-kick");
        if c.fly_anti_kick {
            slider(ui, &mut c.fly_dip_interval, 0.5..=4.0, "dip");
        }
    }

    module(ui, shared, ModuleId::Speed);
    if shared.cfg.movement.speed {
        let c = &mut shared.cfg.movement;
        modes(ui, &mut c.speed_mode, SpeedMode::ALL, |m| m.label());
        slider(ui, &mut c.speed_value, 0.05..=1.0, "amount");
    }

    module(ui, shared, ModuleId::Bhop);
    module(ui, shared, ModuleId::Sprint);
    module(ui, shared, ModuleId::NoFall);
    module(ui, shared, ModuleId::Noclip);
    module(ui, shared, ModuleId::Jesus);

    module(ui, shared, ModuleId::Spider);
    if shared.cfg.movement.spider {
        slider(ui, &mut shared.cfg.movement.spider_power, 0.1..=0.6, "grip");
    }

    module(ui, shared, ModuleId::Step);
    if shared.cfg.movement.step {
        slider(ui, &mut shared.cfg.movement.step_height, 0.6..=3.0, "height");
    }

    module(ui, shared, ModuleId::HighJump);
    if shared.cfg.movement.jump_power {
        slider(ui, &mut shared.cfg.movement.jump_multiplier, 1.0..=4.0, "power");
    }

    module(ui, shared, ModuleId::Jetpack);
    if shared.cfg.movement.jetpack {
        slider(ui, &mut shared.cfg.movement.jetpack_power, 0.1..=1.5, "power");
    }

    module(ui, shared, ModuleId::Freecam);
    if shared.cfg.movement.freecam {
        slider(ui, &mut shared.cfg.movement.freecam_speed, 0.1..=3.0, "speed");
    }

    module(ui, shared, ModuleId::Blink);
    heading(ui, "SERVER");
    check(ui, &mut shared.cfg.movement.speed_limit, "stay in limits");
}

fn world(ui: &mut egui::Ui, shared: &mut Shared) {
    module(ui, shared, ModuleId::AirPlace);
    module(ui, shared, ModuleId::AutoBuild);
    if shared.cfg.building.air_place || shared.cfg.building.auto_build {
        slider(ui, &mut shared.cfg.building.place_delay, 0.0..=0.5, "delay");
    }
}

fn esp(ui: &mut egui::Ui, shared: &mut Shared) {
    module(ui, shared, ModuleId::EspPlayers);
    module(ui, shared, ModuleId::EspMobs);
    module(ui, shared, ModuleId::EspAnimals);
    module(ui, shared, ModuleId::EspItems);
    module(ui, shared, ModuleId::EspContainers);
    module(ui, shared, ModuleId::BaseFinder);
    if shared.cfg.esp.base_finder {
        slider(ui, &mut shared.cfg.esp.base_chunk_radius, 2.0..=16.0, "chunks");
        let hits: Vec<_> = shared
            .game
            .base_hits
            .iter()
            .filter(|h| h.is_base)
            .take(6)
            .cloned()
            .collect();
        for h in hits {
            let (rect, _) = ui.allocate_exact_size(
                egui::vec2(ui.available_width(), 14.0),
                egui::Sense::hover(),
            );
            shadowed(
                ui.painter(),
                egui::pos2(rect.left() + 12.0, rect.center().y),
                Align2::LEFT_CENTER,
                format!("{} {} {} {}", h.label, h.x, h.y, h.z),
                10.0,
                Color32::from_rgb(255, 85, 255),
            );
        }
    }

    module(ui, shared, ModuleId::Xray);
    if shared.cfg.esp.xray {
        let selected = &mut shared.cfg.esp.xray_selected;
        selected.resize(crate::blocks::ORE_GROUPS.len(), false);
        for (i, (label, _)) in crate::blocks::ORE_GROUPS.iter().enumerate() {
            let (rect, response) = ui.allocate_exact_size(
                egui::vec2(ui.available_width(), 16.0),
                egui::Sense::click(),
            );
            if response.clicked() {
                selected[i] = !selected[i];
            }
            let on = selected[i];
            let painter = ui.painter();
            if response.hovered() {
                painter.rect_filled(rect, Rounding::ZERO, HOVER);
            }
            shadowed(
                painter,
                egui::pos2(rect.left() + 14.0, rect.center().y),
                Align2::LEFT_CENTER,
                format!("{} {}", if on { "x" } else { "-" }, label),
                12.0,
                if on { ore_colour(i) } else { DARK },
            );
        }
        slider(ui, &mut shared.cfg.esp.block_radius, 8.0..=48.0, "radius");
    }

    heading(ui, "DRAW");
    module(ui, shared, ModuleId::EspBoxes);
    module(ui, shared, ModuleId::EspTracers);
    module(ui, shared, ModuleId::EspNametags);
    module(ui, shared, ModuleId::EspHealth);
    slider(ui, &mut shared.cfg.esp.distance, 16.0..=256.0, "range");
}

fn visuals(ui: &mut egui::Ui, shared: &mut Shared) {
    module(ui, shared, ModuleId::Fullbright);
    module(ui, shared, ModuleId::NoCulling);
    module(ui, shared, ModuleId::ViewDistance);
    if shared.cfg.visuals.view_distance {
        slider(ui, &mut shared.cfg.visuals.view_distance_chunks, 8.0..=32.0, "chunks");
    }
    module(ui, shared, ModuleId::NoFog);
    module(ui, shared, ModuleId::NoWeather);
    module(ui, shared, ModuleId::NoHurtCam);
    module(ui, shared, ModuleId::NoBob);
    module(ui, shared, ModuleId::CustomFov);
    if shared.cfg.visuals.fov {
        slider(ui, &mut shared.cfg.visuals.fov_value, 30.0..=140.0, "fov");
    }
    heading(ui, "HUD");
    module(ui, shared, ModuleId::Watermark);
    module(ui, shared, ModuleId::HudCoords);
    module(ui, shared, ModuleId::HudModules);
}

fn misc(ui: &mut egui::Ui, shared: &mut Shared) {
    module(ui, shared, ModuleId::AutoRespawn);
    let changed = module(ui, shared, ModuleId::HideCapture);
    if changed {
        crate::capture::apply(shared.cfg.misc.hide_from_capture);
    }

    heading(ui, "BINDS");
    let listening = shared.binding == Some(BindTarget::Panic);
    let panic_key = shared
        .bind_for(BindTarget::Panic)
        .map(key_name)
        .unwrap_or_else(|| "-".into());
    if button(
        ui,
        &format!("PANIC  [{}]", if listening { "..." } else { &panic_key }),
        WARN,
    ) {
        shared.panic_off();
    }
    let (rect, response) =
        ui.allocate_exact_size(egui::vec2(ui.available_width(), 15.0), egui::Sense::click());
    if response.clicked() {
        shared.binding = Some(BindTarget::Panic);
    }
    shadowed(
        ui.painter(),
        egui::pos2(rect.left() + 7.0, rect.center().y),
        Align2::LEFT_CENTER,
        "right-click a module to bind",
        10.0,
        DARK,
    );

    heading(ui, "CONFIG");
    if button(ui, "SAVE", TEXT) {
        shared.status = match crate::config::save(shared) {
            Ok(_) => "saved".into(),
            Err(e) => e,
        };
    }
    if button(ui, "LOAD", TEXT) {
        shared.status = match crate::config::load(shared) {
            Ok(()) => "loaded".into(),
            Err(e) => e,
        };
    }
    slider(ui, &mut shared.cfg.ui_scale, 0.5..=3.0, "scale");

    heading(ui, "STATUS");
    let g = shared.game.clone();
    let world = if !g.in_world {
        "no world"
    } else if g.single_player {
        "singleplayer"
    } else {
        "server"
    };
    for (text, colour) in [
        (format!("{world}  {:.0}fps", g.fps), DIM),
        (format!("{:.0} {:.0} {:.0}", g.pos.0, g.pos.1, g.pos.2), DARK),
        (format!("{} entities  {} chunks", g.targets.len(), g.loaded_chunks), DARK),
        (shared.status.clone(), DARK),
    ] {
        let (rect, _) = ui.allocate_exact_size(
            egui::vec2(ui.available_width(), 14.0),
            egui::Sense::hover(),
        );
        shadowed(
            ui.painter(),
            egui::pos2(rect.left() + 7.0, rect.center().y),
            Align2::LEFT_CENTER,
            text,
            10.0,
            colour,
        );
    }

    if !shared.missing.is_empty() {
        heading(ui, "UNRESOLVED");
        for m in shared.missing.clone().iter().take(8) {
            let (rect, _) = ui.allocate_exact_size(
                egui::vec2(ui.available_width(), 13.0),
                egui::Sense::hover(),
            );
            shadowed(
                ui.painter(),
                egui::pos2(rect.left() + 7.0, rect.center().y),
                Align2::LEFT_CENTER,
                m,
                10.0,
                WARN,
            );
        }
    }

    heading(ui, "");
    if button(ui, "UNLOAD", WARN) {
        shared.eject = true;
    }
}

// ---- styling ---------------------------------------------------------------

fn fonts() -> egui::FontDefinitions {
    let mut f = egui::FontDefinitions::default();
    let external = crate::client_dir().join("Monocraft.ttf");
    let bytes = match std::fs::read(&external) {
        Ok(b) => {
            crate::log(&format!("font from {}: {} bytes", external.display(), b.len()));
            b
        }
        Err(_) => FONT.to_vec(),
    };
    f.font_data
        .insert("monocraft".to_owned(), egui::FontData::from_owned(bytes));
    // Use it for both families: the whole menu should read as one typeface.
    for family in [egui::FontFamily::Proportional, egui::FontFamily::Monospace] {
        f.families.entry(family).or_default().insert(0, "monocraft".to_owned());
    }
    f
}

fn style() -> egui::Style {
    let mut s = egui::Style::default();
    let mut v = egui::Visuals::dark();
    v.panel_fill = BG;
    v.window_fill = BG;
    v.window_rounding = Rounding::ZERO;
    v.window_shadow = egui::epaint::Shadow::NONE;
    v.window_stroke = Stroke::new(1.0, LINE);
    v.widgets.noninteractive.bg_fill = HEADER;
    v.widgets.noninteractive.fg_stroke = Stroke::new(1.0, ACCENT);
    v.widgets.inactive.bg_fill = OFF;
    v.widgets.inactive.weak_bg_fill = OFF;
    v.widgets.inactive.fg_stroke = Stroke::new(1.0, DIM);
    v.widgets.hovered.bg_fill = HOVER;
    v.widgets.hovered.fg_stroke = Stroke::new(1.0, TEXT);
    v.widgets.active.bg_fill = ACCENT;
    v.widgets.open.bg_fill = HEADER;
    v.selection.bg_fill = Color32::from_rgb(38, 92, 38);
    v.selection.stroke = Stroke::new(1.0, ACCENT);
    s.visuals = v;
    s.spacing.item_spacing = egui::vec2(0.0, 0.0);
    s.spacing.window_margin = egui::Margin::ZERO;
    s
}

// ---- GL plumbing -----------------------------------------------------------

/// Resolve a GL entry point: extensions come from wglGetProcAddress, core 1.1
/// functions only from the opengl32 export table.
fn gl_proc(name: &str) -> *const std::ffi::c_void {
    use windows_sys::Win32::System::LibraryLoader::{GetModuleHandleA, GetProcAddress};
    // SAFETY: opengl32 is loaded; both lookups are by NUL-terminated name and
    // the result is only ever used as a function pointer by glow.
    unsafe {
        let Ok(c) = CString::new(name) else {
            return std::ptr::null();
        };
        let opengl32 = GetModuleHandleA(c"opengl32.dll".as_ptr() as *const u8);
        if opengl32.is_null() {
            return std::ptr::null();
        }
        if let Some(wgl) = GetProcAddress(opengl32, c"wglGetProcAddress".as_ptr() as *const u8) {
            let wgl: unsafe extern "system" fn(*const u8) -> *const std::ffi::c_void =
                std::mem::transmute(wgl);
            let p = wgl(c.as_ptr() as *const u8);
            // wglGetProcAddress reports failure as any of these, not just null.
            let bad = p.is_null()
                || p as isize == 1
                || p as isize == 2
                || p as isize == 3
                || p as isize == -1;
            if !bad {
                return p;
            }
        }
        match GetProcAddress(opengl32, c.as_ptr() as *const u8) {
            Some(p) => p as *const std::ffi::c_void,
            None => std::ptr::null(),
        }
    }
}

/// The slice of GL state egui touches, saved so the game's frame is unharmed.
struct GlState {
    draw_framebuffer: i32,
    program: i32,
    vao: i32,
    array_buffer: i32,
    active_texture: i32,
    texture: i32,
    sampler0: i32,
    unpack_buffer: i32,
    unpack_alignment: i32,
    unpack_row_length: i32,
    unpack_skip_pixels: i32,
    unpack_skip_rows: i32,
    viewport: [i32; 4],
    scissor: [i32; 4],
    blend: bool,
    blend_src_rgb: i32,
    blend_dst_rgb: i32,
    blend_src_alpha: i32,
    blend_dst_alpha: i32,
    blend_eq_rgb: i32,
    blend_eq_alpha: i32,
    depth_test: bool,
    cull_face: bool,
    scissor_test: bool,
    stencil_test: bool,
    framebuffer_srgb: bool,
}

impl GlState {
    unsafe fn save(gl: &glow::Context) -> Self {
        let mut viewport = [0i32; 4];
        gl.get_parameter_i32_slice(glow::VIEWPORT, &mut viewport);
        let mut scissor = [0i32; 4];
        gl.get_parameter_i32_slice(glow::SCISSOR_BOX, &mut scissor);
        Self {
            draw_framebuffer: gl.get_parameter_i32(glow::DRAW_FRAMEBUFFER_BINDING),
            program: gl.get_parameter_i32(glow::CURRENT_PROGRAM),
            vao: gl.get_parameter_i32(glow::VERTEX_ARRAY_BINDING),
            array_buffer: gl.get_parameter_i32(glow::ARRAY_BUFFER_BINDING),
            active_texture: gl.get_parameter_i32(glow::ACTIVE_TEXTURE),
            texture: gl.get_parameter_i32(glow::TEXTURE_BINDING_2D),
            sampler0: gl.get_parameter_i32(glow::SAMPLER_BINDING),
            unpack_buffer: gl.get_parameter_i32(glow::PIXEL_UNPACK_BUFFER_BINDING),
            unpack_alignment: gl.get_parameter_i32(glow::UNPACK_ALIGNMENT),
            unpack_row_length: gl.get_parameter_i32(glow::UNPACK_ROW_LENGTH),
            unpack_skip_pixels: gl.get_parameter_i32(glow::UNPACK_SKIP_PIXELS),
            unpack_skip_rows: gl.get_parameter_i32(glow::UNPACK_SKIP_ROWS),
            viewport,
            scissor,
            blend: gl.is_enabled(glow::BLEND),
            blend_src_rgb: gl.get_parameter_i32(glow::BLEND_SRC_RGB),
            blend_dst_rgb: gl.get_parameter_i32(glow::BLEND_DST_RGB),
            blend_src_alpha: gl.get_parameter_i32(glow::BLEND_SRC_ALPHA),
            blend_dst_alpha: gl.get_parameter_i32(glow::BLEND_DST_ALPHA),
            blend_eq_rgb: gl.get_parameter_i32(glow::BLEND_EQUATION_RGB),
            blend_eq_alpha: gl.get_parameter_i32(glow::BLEND_EQUATION_ALPHA),
            depth_test: gl.is_enabled(glow::DEPTH_TEST),
            cull_face: gl.is_enabled(glow::CULL_FACE),
            scissor_test: gl.is_enabled(glow::SCISSOR_TEST),
            stencil_test: gl.is_enabled(glow::STENCIL_TEST),
            framebuffer_srgb: gl.is_enabled(glow::FRAMEBUFFER_SRGB),
        }
    }

    /// Put the context into the plain state egui assumes.
    unsafe fn neutralise(&self, gl: &glow::Context) {
        // The menu has to land on the framebuffer actually being presented.
        gl.bind_framebuffer(glow::DRAW_FRAMEBUFFER, None);
        gl.disable(glow::DEPTH_TEST);
        gl.disable(glow::CULL_FACE);
        gl.disable(glow::STENCIL_TEST);
        gl.disable(glow::SCISSOR_TEST);
        // egui writes plain sRGB values; a second hardware conversion washes
        // the whole menu out.
        gl.disable(glow::FRAMEBUFFER_SRGB);
        gl.color_mask(true, true, true, true);
        gl.polygon_mode(glow::FRONT_AND_BACK, glow::FILL);
        gl.active_texture(glow::TEXTURE0);
        // A sampler object bound here overrides the font texture's own
        // parameters, which renders the entire menu flat black.
        gl.bind_sampler(0, None);
        // With a pixel-unpack buffer bound, every texture upload reads from
        // that buffer instead of our pixels: the font atlas comes out as noise.
        gl.bind_buffer(glow::PIXEL_UNPACK_BUFFER, None);
        gl.pixel_store_i32(glow::UNPACK_ALIGNMENT, 4);
        gl.pixel_store_i32(glow::UNPACK_ROW_LENGTH, 0);
        gl.pixel_store_i32(glow::UNPACK_SKIP_PIXELS, 0);
        gl.pixel_store_i32(glow::UNPACK_SKIP_ROWS, 0);
        gl.use_program(None);
        gl.bind_vertex_array(None);
    }

    unsafe fn restore(&self, gl: &glow::Context) {
        gl.bind_framebuffer(glow::DRAW_FRAMEBUFFER, framebuffer(self.draw_framebuffer));
        gl.use_program(program(self.program));
        gl.bind_vertex_array(vertex_array(self.vao));
        gl.bind_buffer(glow::ARRAY_BUFFER, buffer(self.array_buffer));
        gl.bind_buffer(glow::PIXEL_UNPACK_BUFFER, buffer(self.unpack_buffer));
        gl.pixel_store_i32(glow::UNPACK_ALIGNMENT, self.unpack_alignment);
        gl.pixel_store_i32(glow::UNPACK_ROW_LENGTH, self.unpack_row_length);
        gl.pixel_store_i32(glow::UNPACK_SKIP_PIXELS, self.unpack_skip_pixels);
        gl.pixel_store_i32(glow::UNPACK_SKIP_ROWS, self.unpack_skip_rows);
        gl.active_texture(glow::TEXTURE0);
        gl.bind_sampler(0, sampler(self.sampler0));
        gl.bind_texture(glow::TEXTURE_2D, texture(self.texture));
        gl.active_texture(self.active_texture as u32);
        gl.viewport(self.viewport[0], self.viewport[1], self.viewport[2], self.viewport[3]);
        gl.scissor(self.scissor[0], self.scissor[1], self.scissor[2], self.scissor[3]);
        set_enabled(gl, glow::BLEND, self.blend);
        gl.blend_equation_separate(self.blend_eq_rgb as u32, self.blend_eq_alpha as u32);
        gl.blend_func_separate(
            self.blend_src_rgb as u32,
            self.blend_dst_rgb as u32,
            self.blend_src_alpha as u32,
            self.blend_dst_alpha as u32,
        );
        set_enabled(gl, glow::DEPTH_TEST, self.depth_test);
        set_enabled(gl, glow::CULL_FACE, self.cull_face);
        set_enabled(gl, glow::SCISSOR_TEST, self.scissor_test);
        set_enabled(gl, glow::STENCIL_TEST, self.stencil_test);
        set_enabled(gl, glow::FRAMEBUFFER_SRGB, self.framebuffer_srgb);
    }

    fn log_once(&self) {
        crate::log(&format!(
            "gl at swap: fbo={} program={} vao={} sampler0={} unpackBuf={} align={} rowLen={} \
             srgb={} blend={} depth={} cull={} scissor={} stencil={}",
            self.draw_framebuffer,
            self.program,
            self.vao,
            self.sampler0,
            self.unpack_buffer,
            self.unpack_alignment,
            self.unpack_row_length,
            self.framebuffer_srgb,
            self.blend,
            self.depth_test,
            self.cull_face,
            self.scissor_test,
            self.stencil_test
        ));
    }
}

// glow's handles are NonZero wrappers, so zero has to become None.
fn program(v: i32) -> Option<glow::Program> {
    (v != 0).then(|| unsafe { std::mem::transmute::<u32, glow::Program>(v as u32) })
}
fn vertex_array(v: i32) -> Option<glow::VertexArray> {
    (v != 0).then(|| unsafe { std::mem::transmute::<u32, glow::VertexArray>(v as u32) })
}
fn buffer(v: i32) -> Option<glow::Buffer> {
    (v != 0).then(|| unsafe { std::mem::transmute::<u32, glow::Buffer>(v as u32) })
}
fn texture(v: i32) -> Option<glow::Texture> {
    (v != 0).then(|| unsafe { std::mem::transmute::<u32, glow::Texture>(v as u32) })
}
fn sampler(v: i32) -> Option<glow::Sampler> {
    (v != 0).then(|| unsafe { std::mem::transmute::<u32, glow::Sampler>(v as u32) })
}
fn framebuffer(v: i32) -> Option<glow::Framebuffer> {
    (v != 0).then(|| unsafe { std::mem::transmute::<u32, glow::Framebuffer>(v as u32) })
}

unsafe fn set_enabled(gl: &glow::Context, cap: u32, on: bool) {
    if on {
        gl.enable(cap);
    } else {
        gl.disable(cap);
    }
}