Sign in Sign up
kretrod/lodestone Public
Branches
master
240 lines (223 loc) · 9.2 KB Raw
//! GLFW callback hook: turns GLFW input events into framework-neutral UiInput.
//!
//! Linux has no window procedure to subclass, which is how the Win32 side does
//! this. What it does have is GLFW's own callbacks, which the game installs once
//! when it creates its window. We put ourselves in front of each one and keep
//! the pointer we displaced, so the contract matches Windows exactly: while the
//! menu is open we swallow the event, and otherwise we chain straight through.
//!
//! Key codes are translated to Win32 virtual-key numbers on the way in. That
//! keeps one bind vocabulary across both platforms, so a config written on
//! Windows means the same thing here.

use std::ffi::c_void;
use std::sync::atomic::{AtomicBool, AtomicPtr, Ordering};

use crate::state::{self, UiInput};

// GLFW actions.
const RELEASE: i32 = 0;

type KeyCb = unsafe extern "C" fn(*mut c_void, i32, i32, i32, i32);
type MouseCb = unsafe extern "C" fn(*mut c_void, i32, i32, i32);
type CursorCb = unsafe extern "C" fn(*mut c_void, f64, f64);
type ScrollCb = unsafe extern "C" fn(*mut c_void, f64, f64);
type CharCb = unsafe extern "C" fn(*mut c_void, u32);

/// The callbacks we displaced, so every event the menu does not want still
/// reaches the game.
static PREV_KEY: AtomicPtr<c_void> = AtomicPtr::new(std::ptr::null_mut());
static PREV_MOUSE: AtomicPtr<c_void> = AtomicPtr::new(std::ptr::null_mut());
static PREV_CURSOR: AtomicPtr<c_void> = AtomicPtr::new(std::ptr::null_mut());
static PREV_SCROLL: AtomicPtr<c_void> = AtomicPtr::new(std::ptr::null_mut());
static PREV_CHAR: AtomicPtr<c_void> = AtomicPtr::new(std::ptr::null_mut());
static INSTALLED: AtomicBool = AtomicBool::new(false);
/// Right mouse button state, which is what `use`-held means for the cheats.
static RMB: AtomicBool = AtomicBool::new(false);

/// Whether the use button is held, tracked from the events we already see.
pub fn right_mouse_down() -> bool {
    RMB.load(Ordering::Relaxed)
}

/// Chain ourselves in front of the game's GLFW callbacks.
///
/// # Safety
/// `window` must be the GLFW window the game is rendering into.
pub unsafe fn install(window: *mut c_void) -> bool {
    if INSTALLED.load(Ordering::SeqCst) {
        return true;
    }
    let mut any = false;
    if let Some(p) = set_cb(window, b"glfwSetKeyCallback\0", key_cb as *mut c_void) {
        PREV_KEY.store(p, Ordering::SeqCst);
        any = true;
    }
    if let Some(p) = set_cb(window, b"glfwSetMouseButtonCallback\0", mouse_cb as *mut c_void) {
        PREV_MOUSE.store(p, Ordering::SeqCst);
        any = true;
    }
    if let Some(p) = set_cb(window, b"glfwSetCursorPosCallback\0", cursor_cb as *mut c_void) {
        PREV_CURSOR.store(p, Ordering::SeqCst);
        any = true;
    }
    if let Some(p) = set_cb(window, b"glfwSetScrollCallback\0", scroll_cb as *mut c_void) {
        PREV_SCROLL.store(p, Ordering::SeqCst);
        any = true;
    }
    if let Some(p) = set_cb(window, b"glfwSetCharCallback\0", char_cb as *mut c_void) {
        PREV_CHAR.store(p, Ordering::SeqCst);
        any = true;
    }
    INSTALLED.store(any, Ordering::SeqCst);
    any
}

/// Put the game's own callbacks back.
///
/// # Safety
/// `window` must be the window `install` was called with.
pub unsafe fn remove(window: *mut c_void) -> bool {
    if !INSTALLED.swap(false, Ordering::SeqCst) {
        return true;
    }
    let _ = set_cb(window, b"glfwSetKeyCallback\0", PREV_KEY.swap(std::ptr::null_mut(), Ordering::SeqCst));
    let _ = set_cb(window, b"glfwSetMouseButtonCallback\0", PREV_MOUSE.swap(std::ptr::null_mut(), Ordering::SeqCst));
    let _ = set_cb(window, b"glfwSetCursorPosCallback\0", PREV_CURSOR.swap(std::ptr::null_mut(), Ordering::SeqCst));
    let _ = set_cb(window, b"glfwSetScrollCallback\0", PREV_SCROLL.swap(std::ptr::null_mut(), Ordering::SeqCst));
    let _ = set_cb(window, b"glfwSetCharCallback\0", PREV_CHAR.swap(std::ptr::null_mut(), Ordering::SeqCst));
    true
}

/// Call one of the `glfwSet*Callback` setters, handing back what it displaced.
unsafe fn set_cb(window: *mut c_void, name: &[u8], cb: *mut c_void) -> Option<*mut c_void> {
    let f = crate::glfw::proc(name)?;
    let f: unsafe extern "C" fn(*mut c_void, *mut c_void) -> *mut c_void = std::mem::transmute(f);
    Some(f(window, cb))
}

/// True when the menu wants this event to itself.
fn menu_open() -> bool {
    state::with(|s| s.menu_open).unwrap_or(false)
}

unsafe extern "C" fn key_cb(w: *mut c_void, key: i32, scancode: i32, action: i32, mods: i32) {
    let vk = glfw_key_to_vk(key);
    if action != RELEASE {
        // A rebind in progress captures the next key first — including a new
        // menu key — so it is never mistaken for a toggle or a module bind.
        if state::with(|s| s.binding.is_some()).unwrap_or(false) {
            state::with(|s| s.handle_key(vk));
            return;
        }
        let menu_key = state::with(|s| s.cfg.menu_key).unwrap_or(0x2D);
        if vk == menu_key {
            state::with(|s| s.toggle_menu());
            return;
        }
        // Module binds fire only while the menu is shut.
        let handled =
            state::with(|s| if !s.menu_open { s.handle_key(vk) } else { false }).unwrap_or(false);
        if handled {
            return;
        }
    }
    if menu_open() {
        state::with(|s| s.events.push(UiInput::Key(vk, action != RELEASE)));
        return;
    }
    if let Some(prev) = nonnull(&PREV_KEY) {
        let prev: KeyCb = std::mem::transmute(prev);
        prev(w, key, scancode, action, mods);
    }
}

unsafe extern "C" fn mouse_cb(w: *mut c_void, button: i32, action: i32, mods: i32) {
    let down = action != RELEASE;
    // Tracked whether or not the menu is open: the cheats ask for this, and a
    // click that opened the menu should not leave the flag stuck on.
    if button == 1 {
        RMB.store(down && !menu_open(), Ordering::Relaxed);
    }
    if menu_open() {
        let b = match button {
            0 => 0u8,
            1 => 1,
            _ => 2,
        };
        state::with(|s| s.events.push(UiInput::MouseButton(b, down)));
        return;
    }
    if let Some(prev) = nonnull(&PREV_MOUSE) {
        let prev: MouseCb = std::mem::transmute(prev);
        prev(w, button, action, mods);
    }
}

unsafe extern "C" fn cursor_cb(w: *mut c_void, x: f64, y: f64) {
    if menu_open() {
        let (x, y) = (x as f32, y as f32);
        state::with(|s| {
            s.pointer = (x, y);
            s.events.push(UiInput::MouseMove(x, y));
        });
        return;
    }
    if let Some(prev) = nonnull(&PREV_CURSOR) {
        let prev: CursorCb = std::mem::transmute(prev);
        prev(w, x, y);
    }
}

unsafe extern "C" fn scroll_cb(w: *mut c_void, dx: f64, dy: f64) {
    if menu_open() {
        state::with(|s| s.events.push(UiInput::Wheel(dy as f32)));
        return;
    }
    if let Some(prev) = nonnull(&PREV_SCROLL) {
        let prev: ScrollCb = std::mem::transmute(prev);
        prev(w, dx, dy);
    }
}

unsafe extern "C" fn char_cb(w: *mut c_void, codepoint: u32) {
    if menu_open() {
        if let Some(c) = char::from_u32(codepoint) {
            if !c.is_control() {
                state::with(|s| s.events.push(UiInput::Char(c)));
            }
        }
        return;
    }
    if let Some(prev) = nonnull(&PREV_CHAR) {
        let prev: CharCb = std::mem::transmute(prev);
        prev(w, codepoint);
    }
}

fn nonnull(slot: &AtomicPtr<c_void>) -> Option<*mut c_void> {
    let p = slot.load(Ordering::SeqCst);
    if p.is_null() {
        None
    } else {
        Some(p)
    }
}

/// GLFW key code to Win32 virtual-key, so binds mean the same thing on both
/// platforms. Letters, digits and space already share their numbering; the rest
/// are named constants on one side and a contiguous block on the other.
fn glfw_key_to_vk(key: i32) -> u32 {
    match key {
        // A-Z and 0-9 are ASCII in GLFW and the same values as the VK codes.
        32 => 0x20,                         // space
        48..=57 => key as u32,              // 0-9
        65..=90 => key as u32,              // A-Z
        256 => 0x1B,                        // escape
        257 => 0x0D,                        // enter
        258 => 0x09,                        // tab
        259 => 0x08,                        // backspace
        260 => 0x2D,                        // insert
        261 => 0x2E,                        // delete
        262 => 0x27,                        // right
        263 => 0x25,                        // left
        264 => 0x28,                        // down
        265 => 0x26,                        // up
        266 => 0x21,                        // page up
        267 => 0x22,                        // page down
        268 => 0x24,                        // home
        269 => 0x23,                        // end
        280 => 0x14,                        // caps lock
        290..=301 => 0x70 + (key - 290) as u32, // F1-F12
        320..=329 => 0x60 + (key - 320) as u32, // numpad 0-9
        340 | 344 => 0x10,                  // shift
        341 | 345 => 0x11,                  // control
        342 | 346 => 0x12,                  // alt
        // Anything else keeps its GLFW number, offset well clear of the VK
        // range so it can still be bound without colliding with a real key.
        other => 0x1000 + other as u32,
    }
}