Hold use to pick up a weapon; tap use+reload over one to reload

Picking up a dropped weapon took the press itself. It now takes use held
for ITEM_USE_HOLD_MS (250 ms) on the same weapon; thrown items such as
knives still go on the press. The press start travels in the client
match state, so the protocol is 110.

Over a weapon, use+reload used to do nothing on a tap. The client holds
the reload back while the button is down and sends it on a release
before the hold time, if the same item is still selected, so a tap
reloads and a hold picks the weapon up.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
neptune
2026-10-05 18:22:42 -04:00
parent d82d178ecd
commit fc7ea850d5
8 changed files with 61 additions and 8 deletions
+7
View File
@@ -64,6 +64,11 @@ pub struct ClientActionInput {
pub pad_lockon: Option<u64>,
pub pad_autoaim: Option<(u64, f32)>,
pub pad_was_ads: bool,
/// Use+reload is down.
pub use_reload_down: bool,
/// When use+reload went down over an item, and the item's entity: a tap
/// reloads once released, a hold is the item's.
pub use_reload_over_item: Option<(i32, i32)>,
}
impl Default for ClientActionInput {
@@ -98,6 +103,8 @@ impl Default for ClientActionInput {
pad_lockon: None,
pad_autoaim: None,
pad_was_ads: false,
use_reload_down: false,
use_reload_over_item: None,
}
}
}
+31 -5
View File
@@ -749,6 +749,36 @@ fn apply_weapon_switch_requests(
}
}
/// Use+reload reloads, except over an item: there a hold is the item's
/// (`sim::ITEM_USE_HOLD_MS`, the authority takes it) and a tap still reloads,
/// sent once it is released, if the item is still the one it began on.
fn use_reload_as_reload(
actions: &mut ClientActionInput,
cmd: &mut playerstate_iw4::UserCmd,
ps: Option<&playerstate_iw4::PlayerState>,
) {
use playerstate_iw4::buttons::{RELOAD, USE_RELOAD};
let down = cmd.buttons & USE_RELOAD != 0;
let now = actions.now_msec;
let item = ps
.filter(|ps| ps.cursor_hint != 0)
.map(|ps| ps.cursor_hint_ent_index);
if down {
if !actions.use_reload_down {
actions.use_reload_over_item = item.map(|ent| (now, ent));
}
if actions.use_reload_over_item.is_none() && item.is_none() {
cmd.buttons |= RELOAD;
}
} else if let Some((since, ent)) = actions.use_reload_over_item.take()
&& now.wrapping_sub(since) < sim::ITEM_USE_HOLD_MS
&& item == Some(ent)
{
cmd.buttons |= RELOAD;
}
actions.use_reload_down = down;
}
pub fn sample_client_input(
time: Res<Time<Real>>,
mut actions: ResMut<ClientActionInput>,
@@ -1063,11 +1093,7 @@ pub fn sample_client_input(
actions.client.airborne =
ps.is_some_and(|ps| ps.ground_entity_num == playerstate_iw4::ENTITYNUM_NONE);
let mut cmd = build_usercmd(&mut actions, &look, 0);
if cmd.buttons & playerstate_iw4::buttons::USE_RELOAD != 0
&& ps.is_some_and(|ps| ps.cursor_hint == 0)
{
cmd.buttons |= playerstate_iw4::buttons::RELOAD;
}
use_reload_as_reload(&mut actions, &mut cmd, ps);
look.angles = cmd.angles;
if !frozen
&& aim.live
+1 -1
View File
@@ -174,6 +174,6 @@ pub use transport::wire::{WireError, WireReader, WireWriter};
pub use svc_script_audio::SvcScriptAudio;
pub const PROTOCOL_VERSION: u32 = 109;
pub const PROTOCOL_VERSION: u32 = 110;
pub use client::fire_verdict::{FireVerdictState, PredictedFireStatus};
+3
View File
@@ -1343,6 +1343,7 @@ fn encode_client_meta(out: &mut WireWriter, meta: &ClientSnapshotMeta) {
out.put_u32(meta.life_sequence.0);
out.put_i32(meta.item_use_spawn_ms);
encode_optional_entity_ref(out, meta.item_use_entity);
out.put_i32(meta.item_use_press_ms);
out.put_i32(meta.ammo_clip);
out.put_i32(meta.ammo_stock);
out.put_i32(meta.score);
@@ -1564,6 +1565,7 @@ fn decode_client_meta(input: &mut WireReader<'_>) -> Result<ClientSnapshotMeta,
let life_sequence = LifeSequence(input.get_u32()?);
let item_use_spawn_ms = input.get_i32()?;
let item_use_entity = decode_optional_entity_ref(input)?;
let item_use_press_ms = input.get_i32()?;
let ammo_clip = input.get_i32()?;
let ammo_stock = input.get_i32()?;
let score = input.get_i32()?;
@@ -1781,6 +1783,7 @@ fn decode_client_meta(input: &mut WireReader<'_>) -> Result<ClientSnapshotMeta,
life_sequence,
item_use_spawn_ms,
item_use_entity,
item_use_press_ms,
ammo_clip,
ammo_stock,
score,
+13 -2
View File
@@ -27,6 +27,9 @@ pub const WEAP_INVENTORY_PRIMARY: i32 = 0;
pub const ITEM_MINS: [f32; 3] = [0.0, 0.0, 0.0];
pub const ITEM_MAXS: [f32; 3] = [1.0, 1.0, 1.0];
/// How long use must stay down on a dropped weapon before it is taken.
pub const ITEM_USE_HOLD_MS: i32 = 250;
pub const PLAYER_DROP_Z: f32 = (PLAYER_MAXS[2] - PLAYER_MINS[2]) * 0.5;
#[derive(Clone, Copy, Debug, PartialEq)]
@@ -909,14 +912,22 @@ pub(crate) fn phase_use_items(
.expect("occupied item")
});
let meta = world.client_meta_mut(id);
if !held || selected.is_none() {
// A press counts only while it stays on the item it began on.
if !held || selected.is_none() || selected_ref != meta.item_use_entity {
meta.item_use_entity = None;
}
if pressed {
meta.item_use_entity = selected_ref;
meta.item_use_press_ms = now;
}
let pending = meta.item_use_entity;
let ready = now - meta.item_use_spawn_ms >= 500;
// A weapon is taken once use is held; a thrown item on the press.
let hold_ms = if selected.is_some_and(|item| item.projectile) {
0
} else {
ITEM_USE_HOLD_MS
};
let ready = now - meta.item_use_spawn_ms >= 500 && now - meta.item_use_press_ms >= hold_ms;
if held && ready && pending.is_some() && selected_ref == pending {
let item = selected.expect("selected use item");
if item.projectile {
+1
View File
@@ -18,6 +18,7 @@ pub mod identities;
pub mod input;
mod item;
mod local_profile;
pub use item::ITEM_USE_HOLD_MS;
pub use local_profile::LocalPlayerProfile;
mod mantle_xanim;
pub mod match_state;
+4
View File
@@ -94,6 +94,8 @@ pub struct ClientMatchState {
pub item_use_spawn_ms: i32,
pub item_use_entity: Option<crate::EntityRef>,
/// When the use press on `item_use_entity` began.
pub item_use_press_ms: i32,
pub(crate) ammo_clip: i32,
@@ -222,6 +224,7 @@ impl ClientMatchState {
life_sequence: self.life_sequence,
item_use_spawn_ms: self.item_use_spawn_ms,
item_use_entity: self.item_use_entity,
item_use_press_ms: self.item_use_press_ms,
ammo_clip: self.ammo_clip,
ammo_stock: self.ammo_stock,
score: self.score,
@@ -280,6 +283,7 @@ impl ClientMatchState {
self.life_sequence = meta.life_sequence;
self.item_use_spawn_ms = meta.item_use_spawn_ms;
self.item_use_entity = meta.item_use_entity;
self.item_use_press_ms = meta.item_use_press_ms;
self.ammo_clip = meta.ammo_clip;
self.ammo_stock = meta.ammo_stock;
self.score = meta.score;
@@ -24,6 +24,7 @@ pub struct ClientSnapshotMeta {
pub item_use_spawn_ms: i32,
pub item_use_entity: Option<crate::EntityRef>,
pub item_use_press_ms: i32,
pub ammo_clip: i32,