diff --git a/crates/console/src/binds.rs b/crates/console/src/binds.rs index 9c7a185..117d47c 100644 --- a/crates/console/src/binds.rs +++ b/crates/console/src/binds.rs @@ -1,10 +1,10 @@ -use input_iw4::{command_id_lookup, command_name}; +use input_iw4::{ClientInput, cl_key_event, command_id_lookup, command_name}; use std::collections::HashMap; use bevy::input::ButtonInput; use bevy::input::keyboard::KeyCode; -use bevy::input::mouse::MouseButton; +use bevy::input::mouse::{MouseButton, MouseScrollUnit}; use bevy::prelude::Resource; pub const DEFAULT_CONTROLS: &str = include_str!("../assets/default_controls.cfg"); @@ -73,12 +73,40 @@ pub const BINDABLE_KEYS: &[&str] = &[ "mouse3", "mouse4", "mouse5", + "mwheelup", + "mwheeldown", ]; #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum BindButton { Key(KeyCode), Mouse(MouseButton), + WheelUp, + WheelDown, +} + +pub(crate) fn wheel_button(y: f32) -> Option { + if y > 0.0 { + Some(BindButton::WheelUp) + } else if y < 0.0 { + Some(BindButton::WheelDown) + } else { + None + } +} + +pub(crate) fn wheel_detents(unit: MouseScrollUnit, y: f32, carry: &mut f32) -> i32 { + if !y.is_finite() { + return 0; + } + let delta = match unit { + MouseScrollUnit::Line => y, + MouseScrollUnit::Pixel => y / 100.0, + }; + *carry = (*carry + delta).clamp(-32.0, 32.0); + let detents = carry.trunc() as i32; + *carry -= detents as f32; + detents } pub struct BindInputs<'a> { @@ -95,6 +123,7 @@ impl<'a> BindInputs<'a> { match button { BindButton::Key(key) => self.keys.pressed(key), BindButton::Mouse(btn) => self.mouse.pressed(btn), + BindButton::WheelUp | BindButton::WheelDown => false, } } @@ -102,6 +131,7 @@ impl<'a> BindInputs<'a> { match button { BindButton::Key(key) => self.keys.just_pressed(key), BindButton::Mouse(btn) => self.mouse.just_pressed(btn), + BindButton::WheelUp | BindButton::WheelDown => false, } } @@ -109,6 +139,7 @@ impl<'a> BindInputs<'a> { match button { BindButton::Key(key) => self.keys.just_released(key), BindButton::Mouse(btn) => self.mouse.just_released(btn), + BindButton::WheelUp | BindButton::WheelDown => false, } } } @@ -261,6 +292,20 @@ impl KeyBinds { } } +pub(crate) fn pulse_wheel_binding( + binds: &KeyBinds, + client: &mut ClientInput, + button: BindButton, + now_msec: i32, + frame_msec: u32, +) { + let Some(id) = binds.get(button) else { return }; + let key_num = host_keynum(button); + client.keys[key_num].binding = id; + cl_key_event(client, key_num, true, now_msec, frame_msec); + cl_key_event(client, key_num, false, now_msec, frame_msec); +} + pub fn host_keynum(button: BindButton) -> usize { match button { BindButton::Mouse(MouseButton::Left) => 180, @@ -269,6 +314,8 @@ pub fn host_keynum(button: BindButton) -> usize { BindButton::Mouse(MouseButton::Back) => 183, BindButton::Mouse(MouseButton::Forward) => 184, BindButton::Mouse(_) => 185, + BindButton::WheelUp => 186, + BindButton::WheelDown => 187, BindButton::Key(key) => keycode_keynum(key), } } @@ -424,6 +471,8 @@ pub fn parse_button_name(name: &str) -> Option> { "mouse3" | "mousemiddle" | "mmb" => BindButton::Mouse(MouseButton::Middle), "mouse4" => BindButton::Mouse(MouseButton::Back), "mouse5" => BindButton::Mouse(MouseButton::Forward), + "mwheelup" | "wheelup" => BindButton::WheelUp, + "mwheeldown" | "wheeldown" => BindButton::WheelDown, _ => return None, }; Some(vec![button]) @@ -442,6 +491,8 @@ pub fn display_button(button: BindButton) -> String { BindButton::Mouse(MouseButton::Back) => "MOUSE4".into(), BindButton::Mouse(MouseButton::Forward) => "MOUSE5".into(), BindButton::Mouse(other) => format!("{other:?}"), + BindButton::WheelUp => "MWHEELUP".into(), + BindButton::WheelDown => "MWHEELDOWN".into(), } } @@ -509,3 +560,4 @@ fn display_key(key: KeyCode) -> String { } .to_owned() } + diff --git a/crates/console/src/plugin.rs b/crates/console/src/plugin.rs index e409c52..4619f04 100644 --- a/crates/console/src/plugin.rs +++ b/crates/console/src/plugin.rs @@ -2,7 +2,7 @@ use bevy::{ input::ButtonState, input::InputSystems, input::keyboard::{Key, KeyCode, KeyboardInput, NativeKeyCode}, - input::mouse::{MouseButton, MouseMotion}, + input::mouse::{MouseButton, MouseMotion, MouseWheel}, input_focus::{FocusCause, InputFocus, InputFocusSystems}, picking::{ events::{Drag, Pointer, Press, Release}, @@ -20,8 +20,8 @@ use render_frontend::prepare::scene::world::WorldScene; use ui::{MenuEnabled, UiLayer}; use crate::{ - BINDABLE_KEYS, ConsoleCommand, ConsoleEditor, ConsoleInputState, KeyBinds, - binds::{BindInputs, host_keynum}, + BINDABLE_KEYS, BindButton, ConsoleCommand, ConsoleEditor, ConsoleInputState, KeyBinds, + binds::{BindInputs, host_keynum, pulse_wheel_binding, wheel_detents}, is_bind_command, registry::ConsoleRegistry, suggest::{SuggestSpan, SuggestTone, suggestion_spans}, @@ -419,6 +419,8 @@ fn publish_client_action_input( keys: Res>, mouse_buttons: Res>, mut motion: MessageReader, + mut wheel: MessageReader, + mut wheel_carry: Local, binds: Res, mut scripted: ResMut, console: Res, @@ -464,9 +466,14 @@ fn publish_client_action_input( let now = out.now_msec; let frame = out.frame_msec; + if binds.is_changed() { + *wheel_carry = 0.0; + } if console.open || menu.0 || keys.just_pressed(KeyCode::Escape) { for _ in motion.read() {} + for _ in wheel.read() {} + *wheel_carry = 0.0; for key_num in 0..input_iw4::KEY_COUNT { if out.client.keys[key_num].down != 0 { cl_key_event(&mut out.client, key_num, false, now, frame); @@ -489,6 +496,20 @@ fn publish_client_action_input( cl_key_event(&mut out.client, key_num, false, now, frame); } } + for event in wheel.read() { + let steps = wheel_detents(event.unit, event.y, &mut wheel_carry); + if steps == 0 { + continue; + } + let button = if steps > 0 { + BindButton::WheelUp + } else { + BindButton::WheelDown + }; + for _ in 0..steps.unsigned_abs() { + pulse_wheel_binding(&binds, &mut out.client, button, now, frame); + } + } let scripted_now: std::collections::BTreeSet = scripted.ids().collect(); let down: Vec = scripted_now diff --git a/crates/console/src/user_settings.rs b/crates/console/src/user_settings.rs index a5569bc..ecf3778 100644 --- a/crates/console/src/user_settings.rs +++ b/crates/console/src/user_settings.rs @@ -2,11 +2,15 @@ use std::{fs, path::PathBuf}; use bevy::{ audio::{AudioSink, AudioSinkPlayback, GlobalVolume, Volume}, - input::{ButtonInput, keyboard::KeyCode, mouse::MouseButton}, + input::{ + ButtonInput, + keyboard::KeyCode, + mouse::{MouseButton, MouseWheel}, + }, prelude::*, }; -use crate::{BindButton, KeyBinds, display_button}; +use crate::{BindButton, KeyBinds, binds::wheel_button, display_button}; #[derive(Resource, Default)] pub(crate) struct PendingMenuBinding { @@ -47,10 +51,14 @@ pub(crate) fn consume_menu_binding( mut intents: MessageReader, keys: Res>, mouse: Res>, + mut wheel: MessageReader, mut pending: ResMut, mut binds: ResMut, mut view: ResMut, ) { + let wheel_direction = wheel + .read() + .fold(None, |first, event| first.or_else(|| wheel_button(event.y))); let mut began = false; for intent in intents.read() { if let ui::UiIntent::BeginBinding { id } = intent { @@ -82,7 +90,8 @@ pub(crate) fn consume_menu_binding( .copied() .map(BindButton::Mouse) .next() - }); + }) + .or(wheel_direction); let Some(button) = button else { return }; binds.clear_command(id); binds.set(button, id); @@ -305,3 +314,4 @@ fn parse_settings(source: &str, settings: &mut frame::GameSettings, binds: &mut binds.set(BindButton::Key(KeyCode::Digit4), 21); } } + diff --git a/crates/input_iw4/src/lib.rs b/crates/input_iw4/src/lib.rs index 308e9a8..6091b42 100644 --- a/crates/input_iw4/src/lib.rs +++ b/crates/input_iw4/src/lib.rs @@ -289,6 +289,9 @@ pub fn cl_input_cmd( 9 | 10 => panic!("+breath_sprint kbutton EAX unread; not merged with +sprint"), 11 | 12 => apply_pair(&mut client.kb.usereload, cmd_id, key, now_msec, frame_msec), 13 | 14 => { + if pair_down(cmd_id) { + client.using_ads = false; + } apply_pair(&mut client.kb.speed, cmd_id, key, now_msec, frame_msec); apply_pair(&mut client.kb.throw_btn, cmd_id, key, now_msec, frame_msec); } diff --git a/crates/ui/src/screens.rs b/crates/ui/src/screens.rs index 8becede..ba4c671 100644 --- a/crates/ui/src/screens.rs +++ b/crates/ui/src/screens.rs @@ -1239,6 +1239,7 @@ const MOVEMENT_BINDS: &[(u32, &str)] = &[ const ACTION_BINDS: &[(u32, &str)] = &[ (1, "Fire Weapon"), (57, "Aim Down the Sight"), + (13, "Hold Aim Down the Sight"), (51, "Reload"), (66, "Switch Weapon"), (3, "Melee"), diff --git a/docs/RUN.md b/docs/RUN.md index 4498f04..7f31a77 100644 --- a/docs/RUN.md +++ b/docs/RUN.md @@ -52,6 +52,9 @@ on host and clients — the host records authority, a client the snapshots it received plus its own presented state; `demo LATEST` plays it back. * ADS is `hold +speed_throw`, not `+speed`; +* Aim binding: `bind MOUSE2 +speed_throw` aims only while held; + `bind MOUSE2 +toggleads_throw` toggles aim on each press. Both are available + under Options → Controls → Actions, and the chosen bind is saved in settings. * bolt action (`fire_type=1`): `hold +attack` is **one** shot, and a full magazine will not reload itself (`press +attack` ×N, then `press +reload`); * `look` without `LookState` only writes `ps.viewangles` — no aiming;