diff --git a/crates/sim/src/bullet_collision.rs b/crates/sim/src/bullet_collision.rs index 4fa21ac..daff3d6 100644 --- a/crates/sim/src/bullet_collision.rs +++ b/crates/sim/src/bullet_collision.rs @@ -9,6 +9,8 @@ use weapon_iw4::{ pub const CONTENTS_SOLID: u32 = 0x0000_0001; +pub const CONTENTS_SKY: u32 = 0x0000_0800; + pub const MASK_PLAYER_SOLID: u32 = 0x0281_0011; pub const MASK_SHOT: u32 = 0x0280_6831; diff --git a/crates/sim/src/equipment.rs b/crates/sim/src/equipment.rs index bd802fb..14431a0 100644 --- a/crates/sim/src/equipment.rs +++ b/crates/sim/src/equipment.rs @@ -647,6 +647,8 @@ pub(crate) fn think_projectile(world: &mut FrameWorld, tick: Tick, entnum: i32) } return; } + let remote = crate::remote_missile::is_remote(world, projectile.id); + let mask = crate::remote_missile::projectile_mask(remote); let mut trace_start = start; let mut hops = 0u32; let mut hop_capped = false; @@ -665,7 +667,7 @@ pub(crate) fn think_projectile(world: &mut FrameWorld, tick: Tick, entnum: i32) &BulletTraceQuery { start: trace_start, end, - mask: MASK_BULLET_WORLD, + mask, ignore: Some(projectile.owner), ignore_hit: None, ignore_model: None, @@ -737,6 +739,22 @@ pub(crate) fn think_projectile(world: &mut FrameWorld, tick: Tick, entnum: i32) } break outcome; }; + let outcome = if remote { + let sky = crate::world::clip_trace( + brushes, + bsp, + mesh, + start, + end, + [0.0; 3], + [0.0; 3], + crate::bullet_collision::CONTENTS_SKY, + &|_| true, + ); + crate::remote_missile::sky_wall_outcome(outcome, start, &sky) + } else { + outcome + }; if world.publishes_snapshot() && projectile.live && crate::script_player::means( diff --git a/crates/sim/src/frame.rs b/crates/sim/src/frame.rs index 055b48a..b6c2f02 100644 --- a/crates/sim/src/frame.rs +++ b/crates/sim/src/frame.rs @@ -608,6 +608,8 @@ impl FrameWorld<'_> { pub fn shutdown_game(&mut self) { *self.deref_mut() = SimState::new(); clear_payload_rows(self.ecs); + self.ecs + .remove_resource::(); } } diff --git a/crates/sim/src/remote_missile.rs b/crates/sim/src/remote_missile.rs index cbdef31..23cf438 100644 --- a/crates/sim/src/remote_missile.rs +++ b/crates/sim/src/remote_missile.rs @@ -1,6 +1,9 @@ +use crate::ProjectileId; +use crate::bullet_collision::{CONTENTS_SKY, ColliderId, MASK_BULLET_WORLD, TraceOutcome}; use crate::frame::FrameWorld; use crate::match_state::ClientLifecycle; use crate::world::{ClientId, Tick}; +use bevy_ecs::prelude::{Resource, World}; const PITCH_RANGE: [f32; 2] = [1.0, 87.0]; const PITCH_RATE: f32 = 15.0; @@ -9,6 +12,87 @@ const SPEED_RANGE: [f32; 2] = [3_000.0, 6_000.0]; const SPEED_UP: f32 = 2_000.0; const SPEED_DOWN: f32 = 500.0; +const SKY_CEILING_MIN_NORMAL_Z: f32 = 0.7; + +// The mark is kept for the missile's whole flight. The pilot's control link is +// dropped when the pilot dies, and the missile must still pass the sky ceiling then. +#[derive(Resource, Debug, Default)] +pub(crate) struct RemoteMissiles(Vec); + +pub(crate) fn mark_remote(world: &mut World, id: ProjectileId) { + let mut marked = world.get_resource_or_insert_with(RemoteMissiles::default); + if !marked.0.contains(&id) { + marked.0.push(id); + } +} + +pub(crate) fn is_remote(world: &mut FrameWorld, id: ProjectileId) -> bool { + world + .ecs() + .get_resource::() + .is_some_and(|marked| marked.0.contains(&id)) +} + +fn forget_gone_missiles(world: &mut FrameWorld) { + let marked = match world.ecs().get_resource::() { + Some(marked) if !marked.0.is_empty() => marked.0.clone(), + _ => return, + }; + let mut flying = Vec::with_capacity(marked.len()); + world.visit_projectiles(|p| { + if marked.contains(&p.id) { + flying.push(p.id); + } + }); + if flying.len() != marked.len() { + world.ecs().resource_mut::().0 = flying; + } +} + +pub(crate) fn projectile_mask(remote: bool) -> u32 { + if remote { + MASK_BULLET_WORLD & !CONTENTS_SKY + } else { + MASK_BULLET_WORLD + } +} + +pub(crate) fn sky_wall_outcome( + outcome: TraceOutcome, + start: [f32; 3], + sky: &trace_iw4::Trace, +) -> TraceOutcome { + if sky.fraction >= 1.0 + || sky.startsolid != 0 + || sky.allsolid != 0 + || sky.normal[2] > SKY_CEILING_MIN_NORMAL_Z + { + return outcome; + } + let reach = match outcome { + TraceOutcome::Hit { end, .. } | TraceOutcome::Miss { end } => distance(start, end), + TraceOutcome::StartSolid { .. } | TraceOutcome::Invalid { .. } => return outcome, + }; + if distance(start, sky.endpos) >= reach { + return outcome; + } + TraceOutcome::Hit { + fraction: sky.fraction, + end: sky.endpos, + normal: sky.normal, + collider: ColliderId::World { + surface_flags: sky.surface_flags, + contents: sky.contents, + glass_encoded: 0, + }, + } +} + +fn distance(a: [f32; 3], b: [f32; 3]) -> f32 { + let d = [b[0] - a[0], b[1] - a[1], b[2] - a[2]]; + (d[0] * d[0] + d[1] * d[1] + d[2] * d[2]).sqrt() +} + pub(crate) fn steer( world: &mut FrameWorld, id: ClientId, @@ -49,6 +133,7 @@ pub(crate) fn advance(world: &mut FrameWorld, tick: Tick) { if !world.publishes_snapshot() { return; } + forget_gone_missiles(world); let now = crate::level_time_ms(tick); for id in world.client_ids_sorted() { let Some(mut link) = world.client_meta(id).and_then(|m| m.remote_missile) else { diff --git a/crates/sim/src/script/host/weapons.rs b/crates/sim/src/script/host/weapons.rs index ecc27f4..5651d54 100644 --- a/crates/sim/src/script/host/weapons.rs +++ b/crates/sim/src/script/host/weapons.rs @@ -170,6 +170,7 @@ pub(crate) fn register(registry: &mut NativeRegistry) { angles, ..Default::default() }); + crate::remote_missile::mark_remote(frame.ecs(), id); } Ok(Value::Undefined) });