Let the Predator fly down through the sky ceiling

The Predator script starts the missile 14000 units from its target, or 14000
above the player on maps without Predator spawn points. On Sub Base,
Wasteland, Karachi, Invasion, Rundown, Carnival, Storm and Trailer Park that
start is above the map's sky ceiling, a slab of sky brushes (contents 0x800;
on Sub Base at z 8128-8192), and on Fuel a rooftop Predator starts inside
it. The projectile trace uses MASK_SHOT, which includes the sky bit, so the
missile hit the top of the slab about 1.75 s after launch and blew up some
7,900 units above Sub Base, with the view still a wash of fog. The Predator
works on those maps in MW2, so it has to pass the ceiling there.

A missile is now marked as remote-controlled when a player is linked to it
as its pilot, and keeps the mark for its whole flight, since the control
link is dropped when the pilot dies and the missile must still pass the
ceiling then. The mark goes when the projectile is gone and on game
shutdown. The marked missile's world trace leaves out sky brushes. A second,
sky-only trace over the same tick still stops it on a sky wall or underside
(a sky face with normal z of 0.7 or less) through the usual contact
detonation, so a Predator steered sideways below the ceiling still ends at
the edge of the map. A sky trace that starts inside a sky brush is ignored,
which covers a missile inside the slab and the Fuel start. Every other
projectile keeps the full mask: an RPG fired straight up still explodes on
the ceiling.

Checked on the Mac on Sub Base: the RPG still explodes at z 8127.9. The
unsteered Predator passes z 8192 and reaches the ground about 5.4 s after
launch, with a clear view of the base before impact (before: explosion at
z 8192.1 after 1.75 s). A Predator whose pilot is killed about 1.5 s after
launch still passes the ceiling and lands. One steered flat below the
ceiling explodes on the south sky wall at y -15391.9. On our iPhone build
in a Sub Base match, Predators reached the ground with a clear view before
impact.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
JohnM
2026-10-13 20:18:13 +10:00
parent 73315a0d77
commit f5ab715755
5 changed files with 109 additions and 1 deletions
+2
View File
@@ -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;
+19 -1
View File
@@ -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(
+2
View File
@@ -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::<crate::remote_missile::RemoteMissiles>();
}
}
+85
View File
@@ -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<ProjectileId>);
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::<RemoteMissiles>()
.is_some_and(|marked| marked.0.contains(&id))
}
fn forget_gone_missiles(world: &mut FrameWorld) {
let marked = match world.ecs().get_resource::<RemoteMissiles>() {
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::<RemoteMissiles>().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 {
+1
View File
@@ -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)
});