From 665d4ff6df2967b5e659bc124776a9e2bed8aaa3 Mon Sep 17 00:00:00 2001 From: awsms <48278661+awsms@users.noreply.github.com> Date: Mon, 28 Sep 2026 17:51:18 +0300 Subject: [PATCH] Hold aim alongside toggle aim; mouse wheel directions in key bindings Controls list Aim Down the Sight and Hold Aim Down the Sight; pressing hold clears a toggle latch so release always leaves ADS. MWHEELUP and MWHEELDOWN bind from the menu, the console and saved settings, and wheel scroll reaches the game as discrete presses, so weapon cycling works on the wheel. Closes #6 Closes #7 --- crates/console/src/binds.rs | 56 +++++++++++++++++++++++++++-- crates/console/src/plugin.rs | 27 ++++++++++++-- crates/console/src/user_settings.rs | 16 +++++++-- crates/input_iw4/src/lib.rs | 3 ++ crates/ui/src/screens.rs | 1 + docs/RUN.md | 3 ++ 6 files changed, 98 insertions(+), 8 deletions(-) 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;