Merge treuenten gameplay, effects and map-loading fixes

Includes PRs #100, #101, #102, #103, #104, #105, #106, #107 and #108.
This commit is contained in:
46 changed files with 1378 additions and 276 deletions
+30
View File
@@ -274,6 +274,8 @@ pub struct OwnedFxElemDef {
pub trail_def: Option<OwnedFxTrailDef>,
pub spark_fountain_def: Option<OwnedFxSparkFountainDef>,
pub phys_pieces: Vec<Option<fx_iw4::FxPhysPiece>>,
}
impl OwnedFxElemDef {
@@ -338,6 +340,11 @@ impl OwnedFxElemDef {
}
}
pub fn phys_piece(&self, random_seed: u64) -> Option<fx_iw4::FxPhysPiece> {
let idx = fx_iw4::elem_visual_index(self.view.visual_count, random_seed);
self.phys_pieces.get(idx).copied().flatten()
}
pub fn decal_mark_pair(&self, random_seed: u64) -> Option<&OwnedFxVisual> {
if self.view.elem_type != elem_type::DECAL {
return None;
@@ -953,6 +960,15 @@ impl FxDefinitions {
*edge = model_hint_edge(ns, hint.as_deref(), models);
}
}
elem.phys_pieces =
if fx_iw4::elem_skips_position_update(elem.view.elem_type, elem.view.flags) {
elem.visuals
.iter()
.map(|vis| model_phys_piece(vis, models))
.collect()
} else {
Vec::new()
};
}
}
}
@@ -1446,6 +1462,7 @@ fn capture_elem(
has_extended,
trail_def,
spark_fountain_def,
phys_pieces: Vec::new(),
})
}
@@ -1695,6 +1712,17 @@ fn model_hint_edge(
}
}
fn model_phys_piece(
vis: &OwnedFxVisual,
models: &crate::FxModelCatalog,
) -> Option<fx_iw4::FxPhysPiece> {
let OwnedFxVisual::Model { edge, .. } = vis else {
return None;
};
let (mid, half) = models.get_at(edge.bound_index()?)?.skel.bounds?;
fx_iw4::FxPhysPiece::from_bounds(mid, half)
}
fn resolve_material_visual(
s: &impl FxRead,
materials: &MaterialCatalog,
@@ -2056,6 +2084,7 @@ fn leftover_capture_elem_t5(
has_extended,
trail_def: None,
spark_fountain_def: None,
phys_pieces: Vec::new(),
})
}
@@ -2544,5 +2573,6 @@ fn capture_elem_t6(
has_extended: false,
trail_def: None,
spark_fountain_def: None,
phys_pieces: Vec::new(),
})
}
+18
View File
@@ -552,6 +552,23 @@ pub const HUD_CHROME_MENUS: &[&str] = &[
"perks_info_hd",
];
fn cover_wide_hud_overlays(catalog: &mut MenuCatalog) {
for name in HUD_CHROME_MENUS {
let Some(menu) = catalog.menus.get_mut(*name) else {
continue;
};
for item in &mut menu.items {
let rect = &item.rect;
if !item.background.is_empty()
&& (rect.x, rect.y, rect.w, rect.h) == (-427.0, -240.0, 854.0, 480.0)
&& (rect.horz_align, rect.vert_align) == (2, 2)
{
item.cover = true;
}
}
}
}
pub fn ui_games_root(games: &GamesRoot) -> Result<GamesRoot, String> {
let anchor = find_zone_file(games, "iw4:common_mp")?;
game_root_for_zone(&anchor.path).map(GamesRoot)
@@ -1025,6 +1042,7 @@ impl MenuSink {
fn finish_and_take(mut self, games: Option<&Path>) -> MenuCatalog {
self.finish_ui_images(games);
cover_wide_hud_overlays(&mut self.catalog);
self.catalog
}
+32 -2
View File
@@ -36,6 +36,8 @@ pub struct MatchLoadRequest {
struct MatchLoadTask {
task: Task<Result<Option<PreparedMatchReady>, MapLoadFailed>>,
request_id: u64,
load_key: frame::LocalLoadKey,
zone: String,
progress: LoadProgress,
}
@@ -141,6 +143,7 @@ fn start_match_load(
let load_key = request.load_key;
let zone = request.zone.clone();
let zone_accepted = zone.clone();
let zone_task = zone.clone();
let cancel_handle = progress.clone();
// The process outlives the overlay and every task this spawns: it is what
// the table reads, what admission finishes, and what the bench still has
@@ -224,6 +227,8 @@ fn start_match_load(
MatchLoadTask {
task,
request_id,
load_key,
zone: zone_task,
progress: cancel_handle,
},
frame::MatchScope::Loading,
@@ -356,8 +361,13 @@ fn poll_match_load(
task.request_id
);
}
let Some(outcome) = future::block_on(future::poll_once(&mut task.task)) else {
return;
let polled = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
future::block_on(future::poll_once(&mut task.task))
}));
let outcome = match polled {
Ok(None) => return,
Ok(Some(outcome)) => outcome,
Err(panic) => Err(walk_panic_failure(task, panic.as_ref())),
};
let request_id = task.request_id;
inflight.0 = false;
@@ -418,6 +428,26 @@ fn poll_match_load(
});
}
fn walk_panic_failure(task: &MatchLoadTask, panic: &(dyn std::any::Any + Send)) -> MapLoadFailed {
let message = panic
.downcast_ref::<&str>()
.copied()
.or_else(|| panic.downcast_ref::<String>().map(String::as_str))
.unwrap_or("no message");
diag::error!(
World,
"match load: walk request #{} (`{}`) panicked: {message} — reported as a failed map load",
task.request_id,
task.zone
);
MapLoadFailed {
request_id: task.request_id,
load_key: task.load_key,
zone: task.zone.clone(),
error: format!("The load stopped on an internal error: {message}"),
}
}
fn expire_match_load_abort(
mut commands: Commands,
abort: Option<Res<MatchLoadAbort>>,
+1 -1
View File
@@ -205,7 +205,7 @@ pub struct FxElemLightInstance {
pub is_spot: bool,
pub origin: [f32; 3],
pub radius: f32,
pub color_bgr: [f32; 3],
pub color_rgb: [f32; 3],
pub axis: [[f32; 3]; 3],
}
+3 -1
View File
@@ -37,7 +37,9 @@ pub use marks::{
FxMarksSystemHost, GfxMarkMeshCensus, GfxMarkMeshSurf, MarkImpactRequest, MarkImpactResult,
MarkReceiverEnable, MarkTraceRecord,
};
pub use motion::{evaluate_elem_collide_motion, evaluate_elem_motion};
pub use motion::{
evaluate_elem_collide_motion, evaluate_elem_motion, evaluate_elem_physics_motion,
};
pub use play::{
FxPlayPose, FxPlayRequest, PlayResult, axis_from_hit_normal, axis_from_impact_velocity,
play_at_origin, play_bolted, play_oriented, spawn_impact_or_death_effect, spawn_oriented,
+145 -7
View File
@@ -1,10 +1,11 @@
use fx_iw4::{
FX_ELEM_AT_REST_NONE, collide_marks_at_rest, collide_substep_schedule,
collision_reflect_base_vel_delta, elem_dies_on_touch, elem_gravity_accel_z,
elem_update_has_velocity_graph, elem_uses_collision, elem_uses_vel_local, elem_uses_vel_world,
get_at_rest_fraction, get_velocity_at_time_sampled, impact_child_speed_allows,
integrate_velocity_graph_sampled, orientation_pos_from_world, orientation_pos_to_world,
sample_reflection_factor, trace_mask, vec3_length_sq,
FX_ELEM_AT_REST_NONE, FX_PHYS_PIECE_HALF, collide_marks_at_rest, collide_on_ground,
collide_substep_schedule, collision_reflect_base_vel_delta, elem_dies_on_touch,
elem_gravity_accel_z, elem_launch_velocity, elem_update_has_velocity_graph,
elem_uses_collision, elem_uses_vel_local, elem_uses_vel_world, get_at_rest_fraction,
get_velocity_at_time_sampled, impact_child_speed_allows, integrate_velocity_graph_sampled,
orientation_pos_from_world, orientation_pos_to_world, sample_reflection_factor, trace_mask,
vec3_length_sq,
};
use crate::update::{FxElemMotionQuery, FxElemMotionResult, FxElemTraceHit, FxImpactSpawn};
@@ -41,6 +42,7 @@ pub fn evaluate_elem_collide_motion(
coll_maxs: [f32; 3],
use_item_clip: bool,
has_effect_on_impact: bool,
leaves_start_solid: bool,
vel_graph_local: &[fx_iw4::FxElemVec3Range],
vel_graph_world: &[fx_iw4::FxElemVec3Range],
q: FxElemMotionQuery<'_>,
@@ -125,7 +127,7 @@ pub fn evaluate_elem_collide_motion(
orientation_pos_to_world(q.orient.origin, q.orient.axis, end_local),
);
let hit = trace(start, end, coll_mins, coll_maxs, mask);
if hit.startsolid || hit.allsolid {
if hit.allsolid || (hit.startsolid && !leaves_start_solid) {
base_vel = [0.0; 3];
break;
}
@@ -224,6 +226,142 @@ pub fn evaluate_elem_collide_motion(
})
}
const FX_PHYS_GRAVITY: f32 = 800.0;
const FX_PHYS_BOUNCE: f32 = 0.2;
const FX_PHYS_FRICTION: f32 = 0.5;
const FX_PHYS_REST_SPEED: f32 = 20.0;
const FX_PHYS_UNSTICK_MAX_LIFT: f32 = 16.0;
const FX_PHYS_CONTACT_SLOP: f32 = 0.125;
pub fn evaluate_elem_physics_motion(
flags: i32,
use_item_clip: bool,
vel_graph_local: &[fx_iw4::FxElemVec3Range],
vel_graph_world: &[fx_iw4::FxElemVec3Range],
q: FxElemMotionQuery<'_>,
mut trace: impl FnMut([f32; 3], [f32; 3], [f32; 3], [f32; 3], u32) -> FxElemTraceHit,
) -> FxElemMotionResult {
let mut landed = (q.at_rest_fraction != FX_ELEM_AT_REST_NONE).then_some(q.at_rest_fraction);
let mut result = FxElemMotionResult {
origin_delta: [0.0; 3],
base_vel: q.base_vel,
remove: false,
at_rest_fraction: landed,
spawn_impact: None,
};
if landed.is_some() && q.base_vel == [0.0; 3] {
return result;
}
let from = q.prev_msec.max(q.msec_begin);
if q.msec_now <= from {
return result;
}
let mut vel = if q.prev_msec <= q.msec_begin {
elem_launch_velocity(
flags,
vel_graph_local,
vel_graph_world,
q.orient.axis,
q.motion_random.velocity,
)
} else {
q.base_vel
};
let mask = trace_mask(use_item_clip);
let (mins, maxs) = ([-FX_PHYS_PIECE_HALF; 3], [FX_PHYS_PIECE_HALF; 3]);
let mut pos = orientation_pos_to_world(q.orient.origin, q.orient.axis, q.origin);
for step in collide_substep_schedule(from, q.msec_now) {
let dt = (step.msec_end.saturating_sub(step.msec_start)).max(0) as f32 * 0.001;
vel[2] -= FX_PHYS_GRAVITY * dt;
let end = [
pos[0] + vel[0] * dt,
pos[1] + vel[1] * dt,
pos[2] + vel[2] * dt,
];
let hit = trace(pos, end, mins, maxs, mask);
if hit.allsolid {
vel = [0.0; 3];
match lift_out_of_solid(pos, mins, maxs, mask, &mut trace) {
Some(lifted) => {
pos = lifted;
continue;
}
None => {
landed.get_or_insert(at_rest_byte(step.msec_start, q.msec_begin, q.life_ms));
break;
}
}
}
if hit.fraction >= 1.0 {
pos = end;
continue;
}
pos = [
pos[0] + (end[0] - pos[0]) * hit.fraction,
pos[1] + (end[1] - pos[1]) * hit.fraction,
pos[2] + (end[2] - pos[2]) * hit.fraction,
];
let n = hit.normal;
let into = dot(vel, n);
if into < 0.0 {
let push = (1.0 + FX_PHYS_BOUNCE) * into;
for (v, n) in vel.iter_mut().zip(n) {
*v -= push * n;
}
let along = dot(vel, n);
for (v, n) in vel.iter_mut().zip(n) {
let normal_part = along * n;
*v = normal_part + (*v - normal_part) * (1.0 - FX_PHYS_FRICTION);
}
}
if collide_on_ground(n[2]) {
landed.get_or_insert(at_rest_byte(step.msec_end, q.msec_begin, q.life_ms));
if vec3_length_sq(vel) < FX_PHYS_REST_SPEED * FX_PHYS_REST_SPEED {
vel = [0.0; 3];
break;
}
}
}
let stored = orientation_pos_from_world(q.orient.origin, q.orient.axis, pos);
result.origin_delta = [
stored[0] - q.origin[0],
stored[1] - q.origin[1],
stored[2] - q.origin[2],
];
result.base_vel = vel;
result.at_rest_fraction = landed;
result
}
fn lift_out_of_solid(
pos: [f32; 3],
mins: [f32; 3],
maxs: [f32; 3],
mask: u32,
trace: &mut impl FnMut([f32; 3], [f32; 3], [f32; 3], [f32; 3], u32) -> FxElemTraceHit,
) -> Option<[f32; 3]> {
let mut dz = -mins[2] + FX_PHYS_CONTACT_SLOP;
let cap = dz.max(FX_PHYS_UNSTICK_MAX_LIFT);
while dz <= cap {
let lifted = [pos[0], pos[1], pos[2] + dz];
let probe = trace(lifted, lifted, mins, maxs, mask);
if !probe.startsolid && !probe.allsolid {
return Some(lifted);
}
dz *= 2.0;
}
None
}
fn at_rest_byte(msec: i32, msec_begin: i32, life_ms: f32) -> u8 {
let frac = get_at_rest_fraction(msec as f32, msec_begin as f32, 1.0 / life_ms.max(1.0));
frac.clamp(0.0, 254.0) as u8
}
fn dot(a: [f32; 3], b: [f32; 3]) -> f32 {
a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
}
fn integrate_free_flight(
flags: i32,
gravity_base: f32,
+15 -3
View File
@@ -860,6 +860,14 @@ impl FxSystemHost {
Some(idx as u8)
}
fn free_bolt_record(&mut self, index: u8) {
let Some(next) = self.bolt_next.get_mut(usize::from(index)) else {
return;
};
*next = self.bolt_first_free;
self.bolt_first_free = i32::from(index);
}
pub fn spawn_effect(
&mut self,
def_name: &str,
@@ -873,6 +881,9 @@ impl FxSystemHost {
if wants_spotlight && self.spotlight_count >= FX_SPOT_LIGHT_LIMIT {
return Err(SpawnFail::TooManySpotlights);
}
if self.ring_occupancy() >= FX_EFFECT_HANDLE_RING_SIZE as i32 {
return Err(SpawnFail::RingFull);
}
let bolt_index = if bolt == FX_SPAWN_BOLT_NONE {
0xff
} else {
@@ -883,9 +894,6 @@ impl FxSystemHost {
};
let alloc = self.first_free_effect;
if self.ring_occupancy() >= FX_EFFECT_HANDLE_RING_SIZE as i32 {
return Err(SpawnFail::RingFull);
}
self.first_free_effect = alloc.wrapping_add(1);
let ring_i = (alloc as u32 & (FX_EFFECT_HANDLE_RING_SIZE - 1)) as usize;
@@ -986,6 +994,10 @@ impl FxSystemHost {
self.set_handle_at_ring(active_index as u32, handle);
active_index = active_index.wrapping_add(1);
if let Some(slot) = handle_to_slot(handle) {
let bolt = self.effects[slot].bolt;
if bolt != 0xff {
self.free_bolt_record(bolt);
}
self.effects[slot] = FxEffectSlot::default();
}
}
+3 -3
View File
@@ -2,8 +2,8 @@ use fx_iw4::{
FX_ELEM_TYPE_SPARK_CLOUD, FX_ELEM_TYPE_SPARK_FOUNTAIN, FX_STATUS_HAS_PENDING_LOOP_ELEMS,
FX_STATUS_REF_COUNT_MASK_IW4, FxUpdateEffectBolt, axis_to_quat,
begin_iterating_over_effects_exclusive, bolt_compose_orientation, bolt_mark_lost,
elem_norm_time, elem_uses_collision, end_iterating_over_effects, end_iterating_runs_gc,
unit_quat_to_axis, update_effect_bolt, vector_vectors,
elem_norm_time, elem_skips_position_update, elem_uses_collision, end_iterating_over_effects,
end_iterating_runs_gc, unit_quat_to_axis, update_effect_bolt, vector_vectors,
};
use std::collections::HashMap;
@@ -384,7 +384,7 @@ fn pending_collide_for_elem(
if msec_now >= death {
return None;
}
if !elem_uses_collision(elem.flags) {
if !elem_uses_collision(elem.flags) && !elem_skips_position_update(elem.elem_type, elem.flags) {
return None;
}
let def_index = elem.def_index;
+56
View File
@@ -119,3 +119,59 @@ pub fn collision_reflect_base_vel_delta(
reflected[2] - pre_impact_vel[2],
]
}
pub const FX_PHYS_PIECE_HALF: f32 = 1.0;
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct FxPhysPiece {
flat_axis: usize,
flat_mid: f32,
flat_half: f32,
}
impl FxPhysPiece {
pub fn from_bounds(mid: [f32; 3], half: [f32; 3]) -> Option<Self> {
if !mid.iter().chain(&half).all(|v| v.is_finite()) || half.iter().any(|v| *v <= 0.0) {
return None;
}
let flat_axis = (0..3).min_by(|&a, &b| half[a].total_cmp(&half[b]))?;
Some(Self {
flat_axis,
flat_mid: mid[flat_axis],
flat_half: half[flat_axis],
})
}
pub fn rest_pose(&self, axis: [[f32; 3]; 3], scale: f32) -> ([[f32; 3]; 3], f32) {
let a = axis[self.flat_axis];
let up = if a[2] >= 0.0 { 1.0 } else { -1.0 };
let v = [a[1] * up, -a[0] * up, 0.0];
let k = 1.0 / (1.0 + a[2] * up);
let turn = |p: [f32; 3]| {
let vxp = cross(v, p);
let vxvxp = cross(v, vxp);
[
p[0] + vxp[0] + vxvxp[0] * k,
p[1] + vxp[1] + vxvxp[1] * k,
p[2] + vxp[2] + vxvxp[2] * k,
]
};
let lowest = if up > 0.0 {
self.flat_mid - self.flat_half
} else {
-(self.flat_mid + self.flat_half)
};
(
[turn(axis[0]), turn(axis[1]), turn(axis[2])],
-FX_PHYS_PIECE_HALF - lowest * scale,
)
}
}
fn cross(a: [f32; 3], b: [f32; 3]) -> [f32; 3] {
[
a[1] * b[2] - a[2] * b[1],
a[2] * b[0] - a[0] * b[2],
a[0] * b[1] - a[1] * b[0],
]
}
+3 -6
View File
@@ -94,11 +94,8 @@ pub fn cull_elem_light(
}
#[inline]
pub fn elem_light_color_bgr(color_rgba: [u8; 4], scale: f32) -> [f32; 3] {
pub fn elem_light_color_rgb(color_rgba: [u8; 4], scale: f32) -> [f32; 3] {
let s = scale * (crate::FX_RECIP_255 as f32);
[
color_rgba[2] as f32 * s,
color_rgba[1] as f32 * s,
color_rgba[0] as f32 * s,
]
let [r, g, b, _a] = color_rgba;
[r as f32 * s, g as f32 * s, b as f32 * s]
}
+9 -6
View File
@@ -88,14 +88,15 @@ pub use bolt::{
update_effect_bolt,
};
pub use collide::{
FX_COLLIDE_SUBSTEP_MS, FX_COLLISION_REFLECT_SCALE, FX_IMPACT_CHILD_MIN_SPEED_SQ, FX_TRACE_MASK,
FX_TRACE_MASK_ITEM_CLIP, FxCollideSubstep, collide_marks_at_rest, collide_on_ground,
collide_substep_schedule, collision_reflect_base_vel_delta, impact_child_speed_allows,
sample_reflection_factor, trace_mask,
FX_COLLIDE_SUBSTEP_MS, FX_COLLISION_REFLECT_SCALE, FX_IMPACT_CHILD_MIN_SPEED_SQ,
FX_PHYS_PIECE_HALF, FX_TRACE_MASK, FX_TRACE_MASK_ITEM_CLIP, FxCollideSubstep, FxPhysPiece,
collide_marks_at_rest, collide_on_ground, collide_substep_schedule,
collision_reflect_base_vel_delta, impact_child_speed_allows, sample_reflection_factor,
trace_mask,
};
pub use cull::{
FX_ELEM_FLAG_CULL_DRAW_5_PLANES, cull_cloud, cull_cloud_plane_count, cull_cloud_radius,
cull_elem_for_spawn_allows, cull_elem_light, cull_sphere, elem_light_color_bgr,
cull_elem_for_spawn_allows, cull_elem_light, cull_sphere, elem_light_color_rgb,
};
pub use draw::{FX_DRAW_ELEM_HANDLER_PRESENT, FxElemType, draw_elem_handler_present};
pub use effect_def::FxEffectDef;
@@ -341,7 +342,9 @@ pub use vec::{
FX_PERP_VECTOR_UNIT, effect_orient_arc, perpendicular_vector, vec3_distance, vec3_length_sq,
vec3_normalize, vector_vectors,
};
pub use velocity::{FX_VEL_AT_TIME_SCALE, get_velocity_at_time, get_velocity_at_time_sampled};
pub use velocity::{
FX_VEL_AT_TIME_SCALE, elem_launch_velocity, get_velocity_at_time, get_velocity_at_time_sampled,
};
pub use view::{
FX_EFFECT_DEF_SIZE, FX_ELEM_DEF_STRIDE, FxEffectDefView, FxElemDefView, effect_def_view,
elem_def_gravity_accel_z, elem_def_view, elem_def_view_x64,
+33
View File
@@ -75,3 +75,36 @@ pub fn get_velocity_at_time_sampled(
}
out
}
#[inline]
pub fn elem_launch_velocity(
flags: i32,
local_samples: &[crate::FxElemVec3Range],
world_samples: &[crate::FxElemVec3Range],
orient_axis: [[f32; 3]; 3],
random: [f32; 3],
) -> [f32; 3] {
let mut out = [0.0f32; 3];
if elem_uses_vel_world(flags) && world_samples.len() >= 2 {
let s = sample_vel_graph_at_age_sampled(world_samples, 0.0, random);
let scale = graph_speed_scale(world_samples.len());
for (out, s) in out.iter_mut().zip(s) {
*out += s * scale;
}
}
if elem_uses_vel_local(flags) && local_samples.len() >= 2 {
let s = sample_vel_graph_at_age_sampled(local_samples, 0.0, random);
let scale = graph_speed_scale(local_samples.len());
for (axis, out) in out.iter_mut().enumerate() {
let w = s[0] * orient_axis[0][axis]
+ s[1] * orient_axis[1][axis]
+ s[2] * orient_axis[2][axis];
*out += w * scale;
}
}
out
}
fn graph_speed_scale(fenceposts: usize) -> f32 {
(fenceposts - 1) as f32 * FX_VEL_AT_TIME_SCALE as f32
}
+4 -2
View File
@@ -948,10 +948,12 @@ fn push_stretch(
);
if rest.w > f32::EPSILON && rest.w < surface.width() {
let grow = surface.width() / rest.w;
let centre = applied.x + applied.w * 0.5;
let centre_x = applied.x + applied.w * 0.5;
let centre_y = applied.y + applied.h * 0.5;
applied.w *= grow;
applied.h *= grow;
applied.x = centre - applied.w * 0.5;
applied.x = centre_x - applied.w * 0.5;
applied.y = centre_y - applied.h * 0.5;
}
}
let color = style.fill_color(item);
+8 -1
View File
@@ -106,7 +106,14 @@ impl WeaponbarExprHost<'_> {
impl ExprHost for WeaponbarExprHost<'_> {
fn binding_label(&self, command: &str) -> Option<&str> {
self.input?.binding_keys.get(command).map(String::as_str)
let keys = &self.input?.binding_keys;
keys.get(command)
.or_else(|| match command {
"+activate" => keys.get("+usereload"),
"+usereload" => keys.get("+activate"),
_ => None,
})
.map(String::as_str)
}
fn ui_active(&self) -> Result<i32, ExprError> {
+6 -6
View File
@@ -212,10 +212,10 @@ pub fn add_omni_light_to_scene_allows(
Ok(())
}
pub fn omni_light_pack(origin: [f32; 3], radius: f32, color_bgr: [f32; 3]) -> GfxLightPack {
pub fn omni_light_pack(origin: [f32; 3], radius: f32, color_rgb: [f32; 3]) -> GfxLightPack {
GfxLightPack {
light_type: GFX_LIGHT_TYPE_OMNI,
color: color_bgr,
color: color_rgb,
direction: [0.0; 3],
origin,
radius,
@@ -281,7 +281,7 @@ pub fn spot_light_pack(
origin: [f32; 3],
forward: [f32; 3],
radius: f32,
color_bgr: [f32; 3],
color_rgb: [f32; 3],
cone: SpotLightConeDvars,
) -> GfxLightPack {
let end = spot_light_clamp_end(cone.start_radius, cone.end_radius, radius);
@@ -301,9 +301,9 @@ pub fn spot_light_pack(
GfxLightPack {
light_type: GFX_LIGHT_TYPE_SPOT,
color: [
color_bgr[0] * cone.brightness,
color_bgr[1] * cone.brightness,
color_bgr[2] * cone.brightness,
color_rgb[0] * cone.brightness,
color_rgb[1] * cone.brightness,
color_rgb[2] * cone.brightness,
],
direction: dir,
origin,
+3 -3
View File
@@ -83,9 +83,9 @@ pub fn vec_to_yaw(x: f32, y: f32) -> f32 {
const VECT_TO_ANGLES_PITCH_SCALE: f32 = -57.2957763671875_f32;
const VECT_TO_ANGLES_PITCH_DOWN: f32 = 270.0;
const VECT_TO_ANGLES_PITCH_DOWN: f32 = 90.0;
const VECT_TO_ANGLES_PITCH_UP: f32 = 90.0;
const VECT_TO_ANGLES_PITCH_UP: f32 = 270.0;
pub fn vect_to_angles(forward: [f32; 3]) -> [f32; 3] {
let [x, y, z] = forward;
@@ -118,7 +118,7 @@ fn axis_to_angles_roll_atan(v: [f32; 3]) -> f32 {
let [x, y, z] = v;
if y == 0.0 && x == 0.0 {
return if 0.0 <= -z {
VECT_TO_ANGLES_PITCH_UP
VECT_TO_ANGLES_PITCH_DOWN
} else {
AXIS_TO_ANGLES_ROLL_PITCH_NEG
};
@@ -64,9 +64,11 @@ impl CEntityRuntime {
return;
};
if !self.in_next_snap()
|| !matches!(self.pose_e_type as i32, ET_PLAYER | ET_PLAYER_CORPSE)
|| previous.pos.tr_type != TR_INTERPOLATE
|| self.current.pos.tr_type != TR_INTERPOLATE
|| !lerps_between_snapshots(
self.pose_e_type as i32,
previous.pos.tr_type,
self.current.pos.tr_type,
)
{
return;
}
@@ -92,6 +94,19 @@ impl CEntityRuntime {
}
}
fn lerps_between_snapshots(e_type: i32, previous_tr_type: i32, current_tr_type: i32) -> bool {
match e_type {
ET_PLAYER_CORPSE => true,
ET_PLAYER => previous_tr_type == TR_INTERPOLATE && current_tr_type == TR_INTERPOLATE,
_ => false,
}
}
fn another_body(current: &EntityState, next: &EntityState) -> bool {
next.e_type == ET_PLAYER_CORPSE
&& (current.time2 != next.time2 || current.client_num != next.client_num)
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct PlayerDrawGate {
pub eyes_entity_num: i32,
@@ -246,8 +261,9 @@ pub fn apply_existing(rt: &mut CEntityRuntime, next: EntityState, at_time_ms: i3
let was_in = rt.in_next_snap();
let e_type_changed = rt.next_state.e_type != next.e_type;
let teleport = (rt.next_state.e_flags ^ next.e_flags) & EFLAGS_TELEPORT != 0;
if !was_in || e_type_changed || teleport {
let new_entity = !was_in || e_type_changed;
let other_body = another_body(&rt.next_state, &next);
if !was_in || e_type_changed || teleport || other_body {
let new_entity = !was_in || e_type_changed || other_body;
reset_entity(rt, next, at_time_ms, new_entity);
return;
}
@@ -335,6 +351,7 @@ pub fn corpse_slot_to_entity_state(slot: &PlayerCorpseSlot) -> EntityState {
es.number = slot.entnum;
es.e_type = ET_PLAYER_CORPSE;
es.client_num = i32::try_from(slot.victim.0).unwrap_or(0);
es.time2 = slot.spawn_time_ms;
es.e_flags = slot.e_flags;
es.tr_type = slot.tr_type;
es.tr_time = slot.tr_time;
@@ -194,6 +194,10 @@ impl PresentedSnapshot {
self.trajectory_time_ms.unwrap_or(render_time_ms)
}
pub fn is_archived_replay(&self) -> bool {
self.trajectory_time_ms.is_some()
}
pub(crate) fn set_trajectory_sample(
&mut self,
time_ms: Option<i32>,
+1 -1
View File
@@ -175,6 +175,6 @@ pub use transport::wire::{WireError, WireReader, WireWriter};
pub use svc_script_audio::SvcScriptAudio;
pub const PROTOCOL_VERSION: u32 = 116;
pub const PROTOCOL_VERSION: u32 = 118;
pub use client::fire_verdict::{FireVerdictState, PredictedFireStatus};
+1 -1
View File
@@ -86,7 +86,7 @@ pub(crate) fn play_script_seats(
focus_life,
focus,
ends_at_ms: i32::MAX,
final_kill: false,
final_kill: seat.final_kill && seat.archive_ms > 0,
started_at_ms,
killcamoffset_ms: seat.archive_ms,
predelay_ms: 0,
+41
View File
@@ -273,6 +273,13 @@ fn seat_snapshot(
return (live.clone(), None);
}
let archived = lookup.tick.and_then(|tick| archive.frame(tick));
overlay_archived_cameras(
&mut out,
live,
archived.map(|f| &f.snapshot),
viewer,
session.focus_client,
);
overlay_killcam_hud(
&mut out,
live,
@@ -341,6 +348,31 @@ fn overlay_archived_world(
out
}
fn overlay_archived_cameras(
out: &mut Snapshot,
live: &Snapshot,
archived: Option<&Snapshot>,
viewer: ClientId,
focus: ClientId,
) {
let focus_row = archived.and_then(|snap| snap.meta.for_client(focus));
let delta_ms = archived.map_or(0, |snap| {
sim::level_time_ms(live.tick).wrapping_sub(sim::level_time_ms(snap.tick))
});
let linked_weapon_view = focus_row.and_then(|meta| meta.linked_weapon_view);
let remote_missile =
focus_row
.and_then(|meta| meta.remote_missile)
.map(|link| sim::RemoteMissile {
unlink_at_ms: link.unlink_at_ms.map(|ms| ms.wrapping_add(delta_ms)),
..link
});
if let Some((_, meta)) = out.meta.clients.iter_mut().find(|(id, _)| *id == viewer) {
meta.linked_weapon_view = linked_weapon_view;
meta.remote_missile = remote_missile;
}
}
fn rebase_entity_state(es: &mut entity_iw4::EntityState, delta_ms: i32) {
if es.tr_time != 0 {
es.tr_time = es.tr_time.wrapping_add(delta_ms);
@@ -377,6 +409,15 @@ fn rebase_archived_world(out: &mut Snapshot, viewer: ClientId, delta_ms: i32) {
p.detonate_at_ms = p.detonate_at_ms.map(|t| t.wrapping_add(delta_ms));
p.cleanup_at_ms = p.cleanup_at_ms.wrapping_add(delta_ms);
}
let bodies = out.meta.corpses.slots.iter_mut();
for body in bodies.filter(|body| body.occupied) {
if body.tr_time != 0 {
body.tr_time = body.tr_time.wrapping_add(delta_ms);
}
if body.spawn_time_ms != 0 {
body.spawn_time_ms = body.spawn_time_ms.wrapping_add(delta_ms);
}
}
for (id, ps) in &mut out.players {
if *id != viewer {
rebase_archived_timers(ps, delta_ms);
+17
View File
@@ -2210,6 +2210,9 @@ fn encode_corpse_pool(out: &mut WireWriter, pool: &PlayerCorpsePool) {
}
out.put_u8(u8::from(slot.falling));
out.put_i32(slot.ground_entity_num);
out.put_i32(slot.spawn_time_ms);
out.put_u32(slot.shield.map_or(0, |shield| shield.weapon));
out.put_u8(u8::from(slot.shield.is_some_and(|shield| shield.on_back)));
}
}
@@ -2243,6 +2246,8 @@ fn decode_corpse_pool(input: &mut WireReader<'_>) -> Result<PlayerCorpsePool, Wi
tr_base: [input.get_f32()?, input.get_f32()?, input.get_f32()?],
falling: input.get_u8()? != 0,
ground_entity_num: input.get_i32()?,
spawn_time_ms: input.get_i32()?,
shield: decode_corpse_shield(input)?,
};
}
_ => return Err(WireError::Malformed("bad corpse slot tag")),
@@ -2251,6 +2256,18 @@ fn decode_corpse_pool(input: &mut WireReader<'_>) -> Result<PlayerCorpsePool, Wi
Ok(pool)
}
fn decode_corpse_shield(
input: &mut WireReader<'_>,
) -> Result<Option<sim::ShieldAttachment>, WireError> {
let weapon = input.get_u32()?;
let on_back = match input.get_u8()? {
0 => false,
1 => true,
_ => return Err(WireError::Malformed("bad corpse shield tag")),
};
Ok((weapon != 0).then_some(sim::ShieldAttachment { weapon, on_back }))
}
fn encode_entity_states(out: &mut WireWriter, rows: &[entity_iw4::EntityState]) {
debug_assert!(rows.len() <= u16::MAX as usize);
out.put_u16(rows.len() as u16);
@@ -12,7 +12,7 @@ pub struct PersistentRemoteTree {
pub(crate) legs_restart: bool,
pub(crate) torso_restart: bool,
pub(crate) occupation_tr_time: Option<i32>,
pub(crate) occupation_key: Option<CorpseOccupationKey>,
pub(crate) legs_rate_sample: ClientAnimSample,
@@ -88,16 +88,36 @@ pub fn skip_frozen_corpse_dobj(
is_corpse && tree_reused && last_was_cache_hit && has_cache
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct CorpseOccupationKey {
pub spawn_time_ms: i32,
pub victim: i32,
}
pub fn corpse_occupation_key(es: &entity_iw4::EntityState) -> CorpseOccupationKey {
CorpseOccupationKey {
spawn_time_ms: es.time2,
victim: es.client_num,
}
}
pub fn fresh_corpse_clip_time(age_ms: i32, clip_length_ms: i32) -> f32 {
if clip_length_ms <= 0 || age_ms <= 0 {
return 0.0;
}
(age_ms as f32 / clip_length_ms as f32).min(1.0)
}
pub fn clone_corpse_tree_from_victim(
trees: &mut RemoteBodyTrees,
corpse_ent: u32,
victim: u32,
tr_time: i32,
key: CorpseOccupationKey,
) {
if trees
.by_ent
.get(&corpse_ent)
.is_some_and(|slot| slot.occupation_tr_time == Some(tr_time))
.is_some_and(|slot| slot.occupation_key == Some(key))
{
return;
}
@@ -108,7 +128,7 @@ pub fn clone_corpse_tree_from_victim(
let mut dst = src;
dst.dobj = None;
dst.reuse_key = None;
dst.occupation_tr_time = Some(tr_time);
dst.occupation_key = Some(key);
trees.by_ent.insert(corpse_ent, dst);
}
@@ -129,6 +149,7 @@ pub fn advance_remote_tree(
dt: f32,
origin: [f32; 3],
pose_time_ms: i32,
corpse_age_ms: Option<i32>,
trees: &mut RemoteBodyTrees,
) -> Result<AdvancedRemoteTree, String> {
let tree = binding.tree();
@@ -139,7 +160,7 @@ pub fn advance_remote_tree(
let legs_restart = legs.restart_toggle();
let torso_restart = torso.restart_toggle();
let previous = trees.by_ent.remove(&persist_key);
let occupation_tr_time = previous.as_ref().and_then(|slot| slot.occupation_tr_time);
let occupation_key = previous.as_ref().and_then(|slot| slot.occupation_key);
let prev = previous.filter(|slot| Arc::ptr_eq(&slot.binding, binding));
let prev_restart_changed_legs = prev
.as_ref()
@@ -178,6 +199,7 @@ pub fn advance_remote_tree(
let overlayed = overlay_legs_clip(&clip, &torso_clip);
(Arc::new(overlayed), Some(torso_clip))
};
let fresh = prev.is_none();
let old_legs = prev.as_ref().map(|slot| slot.legs).unwrap_or(0);
let old_torso = prev.as_ref().map(|slot| slot.torso).unwrap_or(0);
let old_legs_moving = prev
@@ -225,7 +247,7 @@ pub fn advance_remote_tree(
torso: 0,
legs_restart: false,
torso_restart: false,
occupation_tr_time,
occupation_key,
legs_rate_sample: ClientAnimSample::default(),
torso_rate_sample: ClientAnimSample::default(),
},
@@ -292,6 +314,23 @@ pub fn advance_remote_tree(
.set_state(id, state)
.map_err(|error| error.to_string())?;
}
if fresh && let Some(age_ms) = corpse_age_ms {
for (index, leaf_clip) in [
(legs_index, Some(&clip)),
(torso_index, torso_clip.as_ref()),
] {
let Some(leaf_clip) = leaf_clip else {
continue;
};
let time = fresh_corpse_clip_time(age_ms, leaf_clip.length_msec());
let mut state = slot.runtime.states()[index as usize];
state.time = time;
state.old_time = time;
slot.runtime
.set_state(xmodel_runtime::XAnimNodeId(index), state)
.map_err(|error| error.to_string())?;
}
}
clips = Some(PoseClips {
clip,
torso_clip,
@@ -17,6 +17,10 @@ const MSEC_STEP: i32 = 17;
const MAX_ITERS: i32 = 2;
const CONTENTS_SOLID: u32 = 1;
const CONTENTS_ITEMCLIP: u32 = 0x400;
const WAKE_MASKS: [u32; 2] = [CONTENTS_SOLID | CONTENTS_ITEMCLIP, CONTENTS_SOLID];
const WAKE_LIFTS: [f32; 6] = [0.0, CONTACT_SLOP, 0.25, 0.5, 1.0, 2.0];
const WAKE_FOOTPRINTS: [f32; 3] = [1.0, 0.75, 0.5];
const SLEEP_SPEED: f32 = 4.0;
const GROUND_NZ: f32 = 0.7;
const CONTACT_SLOP: f32 = 0.125;
@@ -56,6 +60,7 @@ struct DynEntPhysBody {
maxs: [f32; 3],
bounce: f32,
friction: f32,
mask: u32,
on_ground: bool,
asleep_ms: i32,
}
@@ -99,6 +104,7 @@ pub fn step_phys_world0(
}
let (mins, maxs) = xmodel_phys_hull(catalog, &inst.current_model);
let add_vel = impulse.impulse / preset.mass;
let phys_clip = clip.0.as_deref();
world
.bodies
.entry(entity)
@@ -106,15 +112,22 @@ pub fn step_phys_world0(
body.vel += add_vel;
body.asleep_ms = 0;
})
.or_insert_with(|| DynEntPhysBody {
origin: transform.translation,
vel: add_vel,
mins,
maxs,
bounce: preset.bounce,
friction: preset.friction,
on_ground: false,
asleep_ms: 0,
.or_insert_with(|| {
let pose = phys_clip
.and_then(|clip| clear_wake_pose(clip, transform.translation, mins, maxs));
let (origin, mins, maxs, mask) =
pose.unwrap_or((transform.translation, mins, maxs, CONTENTS_SOLID));
DynEntPhysBody {
origin,
vel: add_vel,
mins,
maxs,
bounce: preset.bounce,
friction: preset.friction,
mask,
on_ground: false,
asleep_ms: 0,
}
});
}
@@ -185,6 +198,39 @@ pub fn step_phys_world0(
}
}
fn clear_wake_pose(
clip: &ClipCollision,
origin: Vec3,
mins: [f32; 3],
maxs: [f32; 3],
) -> Option<(Vec3, [f32; 3], [f32; 3], u32)> {
for mask in WAKE_MASKS {
for scale in WAKE_FOOTPRINTS {
let (mins, maxs) = narrowed_footprint(mins, maxs, scale);
for lift in WAKE_LIFTS {
let at = (origin + Vec3::Z * lift).to_array();
let probe = clip.sweep_box(at, at, mins, maxs, mask);
if !probe.startsolid && !probe.allsolid {
return Some((Vec3::from_array(at), mins, maxs, mask));
}
}
}
}
None
}
fn narrowed_footprint(mins: [f32; 3], maxs: [f32; 3], scale: f32) -> ([f32; 3], [f32; 3]) {
let mut lo = mins;
let mut hi = maxs;
for axis in 0..2 {
let mid = (mins[axis] + maxs[axis]) * 0.5;
let half = (maxs[axis] - mins[axis]) * 0.5 * scale;
lo[axis] = mid - half;
hi[axis] = mid + half;
}
(lo, hi)
}
fn integrate_free(body: &mut DynEntPhysBody, dt: f32) {
body.vel.z += GRAVITY_Z * dt;
body.origin += body.vel * dt;
@@ -200,7 +246,7 @@ fn integrate_body(body: &mut DynEntPhysBody, clip: &ClipCollision, dt: f32) {
end.to_array(),
body.mins,
body.maxs,
CONTENTS_SOLID,
body.mask,
);
if hit.allsolid || hit.startsolid {
body.vel = Vec3::ZERO;
+39 -17
View File
@@ -3,10 +3,10 @@ use crate::anim::remote_body::{
RemoteBodySkinnedItem, RemoteBodySkinnedQueue, RemoteBodyTrees, RemoteModelLods,
RemoteSkinModels, RemoteSkinPoseHashes, SkinAfterPose, WorldGunGap, advance_remote_tree,
bind_remote_skin_models, clone_corpse_tree_from_victim, commit_assembled_body,
ensure_remote_dobj, hash_skin_matrices, occupy_lod_byte, occupy_remote_kit_dobj, packed_anim,
pose_remote_dobj, push_cached_surfaces, radii, remote_player_controller, select_remote_lods,
select_remote_models, skin_after_pose, skin_slot_need, skip_frozen_corpse_dobj,
take_unique_geom, validate_remote_tracks, zero_anim,
corpse_occupation_key, ensure_remote_dobj, hash_skin_matrices, occupy_lod_byte,
occupy_remote_kit_dobj, packed_anim, pose_remote_dobj, push_cached_surfaces, radii,
remote_player_controller, select_remote_lods, select_remote_models, skin_after_pose,
skin_slot_need, skip_frozen_corpse_dobj, take_unique_geom, validate_remote_tracks, zero_anim,
};
use crate::anim::scene_submission::{AnimDObjSceneSkels, AnimDObjSceneSubmission, AnimSceneSubmit};
use crate::anim::world_weapon::PreparedItemCompositions;
@@ -52,6 +52,26 @@ pub struct RemotePlayer {
pub shield: Option<sim::ShieldAttachment>,
}
fn drawn_shield(
snapshot: &sim::Snapshot,
number: u16,
client: sim::ClientId,
) -> Option<sim::ShieldAttachment> {
let body = snapshot
.meta
.corpses
.slots
.iter()
.find(|slot| slot.occupied && slot.entnum == i32::from(number));
match body {
Some(body) => body.shield,
None => snapshot
.meta
.for_client(client)
.and_then(|meta| meta.shield),
}
}
#[derive(Component, Default)]
pub struct RemoteFxBolts {
pub flash: Option<fx::FxBoltTarget>,
@@ -384,7 +404,9 @@ fn occupy_remote_scene_ents(
if !soldier.owns_bodies(&bodies.0) {
continue;
}
let shield = meta.and_then(|meta| meta.shield);
let shield = presented
.snapshot()
.and_then(|snap| drawn_shield(snap, identity.number(), client));
let held = remote_pose_sample(runtime).weapon;
let (kit_models, radius, hide_part_bits) = match live_kits.filter(|_| shield.is_none()) {
Some((kits, _world)) => {
@@ -556,7 +578,9 @@ fn sync_remote_bodies(
let marker = RemotePlayer {
ffa_team,
client_state_team,
shield: meta.and_then(|meta| meta.shield),
shield: presented
.snapshot()
.and_then(|snap| drawn_shield(snap, identity.number(), client)),
};
if remote.is_some() {
commands.entity(entity).insert((pose, marker));
@@ -868,16 +892,13 @@ impl<'a> RemotePoseFrame<'a> {
let persist_key = u32::from(identity.number());
let is_corpse = runtime.next_state.e_type == ET_PLAYER_CORPSE
|| runtime.pose_e_type == ET_PLAYER_CORPSE as u8;
let occupation_tr_time = is_corpse.then_some(runtime.next_state.tr_time);
if is_corpse {
let occupation_key = is_corpse.then(|| corpse_occupation_key(&runtime.next_state));
if let Some(key) = occupation_key {
let victim = u32::try_from(runtime.next_state.client_num).unwrap_or(0);
clone_corpse_tree_from_victim(
self.trees,
persist_key,
victim,
runtime.next_state.tr_time,
);
clone_corpse_tree_from_victim(self.trees, persist_key, victim, key);
}
let corpse_age_ms =
occupation_key.map(|key| runtime.pose_time_ms.wrapping_sub(key.spawn_time_ms));
if runtime.previous_pose.is_none() {
if let Some(slot) = self.trees.get_mut(persist_key) {
@@ -955,6 +976,7 @@ impl<'a> RemotePoseFrame<'a> {
dt,
sample.rate_origin,
sample.rate_time_ms,
corpse_age_ms,
trees,
)?;
@@ -1051,11 +1073,11 @@ impl<'a> RemotePoseFrame<'a> {
}
Ok(PoseOneOutcome::Posed)
})();
if let Some(tr_time) = occupation_tr_time
if let Some(key) = occupation_key
&& let Some(slot) = trees.get_mut(persist_key)
&& slot.occupation_tr_time.is_none()
&& slot.occupation_key.is_none()
{
slot.occupation_tr_time = Some(tr_time);
slot.occupation_key = Some(key);
}
result
}
+154 -19
View File
@@ -1,11 +1,11 @@
use asset_world::ClipCollision;
use net::PresentedSnapshot;
use net::{AUTHORITY_MS, PresentedProjectile, PresentedSnapshot};
use playerstate_iw4::{
CG_CAMERA_PULLBACK_BOX_HALF, CG_CAMERA_PULLBACK_CLIPMASK, CG_THIRD_PERSON_RANGE_DEFAULT,
KillCamMode, OffsetThirdPersonViewInputs, ThirdPersonViewInputs, is_third_person_view,
offset_third_person_view,
};
use sim::ClientId;
use sim::{ClientId, LifeSequence, ProjectileId, RemoteMissile};
use render_scene::WorldCameraPose;
@@ -29,7 +29,7 @@ pub fn presented_is_third_person(
if in_killcam && ps.kill_cam_entity != playerstate_iw4::ENTITYNUM_NONE {
return true;
}
if remote_missile_camera(presented, local, 0).is_some() {
if remote_missile_link(presented, local).is_some() {
return true;
}
is_third_person_view(ThirdPersonViewInputs {
@@ -42,25 +42,160 @@ pub fn presented_is_third_person(
})
}
pub fn remote_missile_camera(
fn remote_missile_link(
presented: &PresentedSnapshot,
local: ClientId,
) -> Option<(RemoteMissile, LifeSequence)> {
let meta = presented.snapshot()?.meta.for_client(local)?;
Some((meta.remote_missile?, meta.life_sequence))
}
// The missile leaves the snapshot on impact, before the link goes and the
// static starts. The last pose is held until then so that neither the
// player's own view nor a black frame shows in between.
#[derive(Default)]
pub struct RemoteMissileView {
held: Option<HeldMissileView>,
turn: MissileTurn,
}
#[derive(Clone, Copy, Debug)]
struct HeldMissileView {
life: LifeSequence,
projectile: ProjectileId,
pose: WorldCameraPose,
}
const TURN_SAMPLES: usize = 4;
#[derive(Default)]
struct MissileTurn {
missile: Option<(LifeSequence, ProjectileId)>,
archived: bool,
samples: Vec<(i32, [f32; 3])>,
}
impl MissileTurn {
fn record(
&mut self,
missile: (LifeSequence, ProjectileId),
archived: bool,
snapshot_ms: i32,
angles: [f32; 3],
) {
// The final killcam of the pilot's own kill replays the same missile
// of the same life from seat snapshots stamped with the live tick,
// later than every sample of the flight, so a switch between live and
// archived presentation starts a fresh history.
if self.missile != Some(missile) || self.archived != archived {
self.missile = Some(missile);
self.archived = archived;
self.samples.clear();
}
match self.samples.last_mut() {
Some(last) if last.0 == snapshot_ms => last.1 = angles,
Some(last) if last.0 > snapshot_ms => {
self.samples.clear();
self.samples.push((snapshot_ms, angles));
}
_ => {
if self.samples.len() == TURN_SAMPLES {
self.samples.remove(0);
}
self.samples.push((snapshot_ms, angles));
}
}
}
fn angles_at(&self, at_time: i32) -> Option<[f32; 3]> {
let shown = at_time.saturating_sub(AUTHORITY_MS);
let &(first_ms, first) = self.samples.first()?;
if shown <= first_ms {
return Some(first);
}
let Some(pair) = self.samples.windows(2).find(|pair| shown < pair[1].0) else {
return self.samples.last().map(|&(_, newest)| newest);
};
let ((from_ms, from), (to_ms, to)) = (pair[0], pair[1]);
let fraction = (shown - from_ms) as f32 / (to_ms - from_ms) as f32;
Some(std::array::from_fn(|i| {
from[i] + math_iw4::angle_subtract(to[i], from[i]) * fraction
}))
}
}
impl RemoteMissileView {
pub fn camera(
&mut self,
presented: &PresentedSnapshot,
local: ClientId,
render_time: i32,
clip: Option<&ClipCollision>,
) -> Option<WorldCameraPose> {
let at_time = presented.trajectory_time_ms(render_time);
let Some((link, life)) = remote_missile_link(presented, local) else {
self.held = None;
self.turn = MissileTurn::default();
return None;
};
self.held = self
.held
.filter(|held| held.life == life && held.projectile == link.projectile);
let flying = link
.unlink_at_ms
.is_none()
.then(|| {
presented
.presented_projectiles()
.iter()
.find(|p| p.authoritative_id() == Some(link.projectile))
})
.flatten();
let snapshot_ms = presented.tick().map_or(at_time, sim::level_time_ms);
if let Some(missile) = flying {
self.turn.record(
(life, link.projectile),
presented.is_archived_replay(),
snapshot_ms,
link.angles,
);
let pose = WorldCameraPose {
origin: missile_eye(missile, snapshot_ms, at_time, clip),
angles: self.turn.angles_at(at_time).unwrap_or(link.angles),
};
self.held = Some(HeldMissileView {
life,
projectile: link.projectile,
pose,
});
}
self.held.map(|held| held.pose)
}
}
fn missile_eye(
missile: &PresentedProjectile,
snapshot_ms: i32,
at_time: i32,
) -> Option<WorldCameraPose> {
let link = presented
.snapshot()?
.meta
.for_client(local)?
.remote_missile
.filter(|link| link.unlink_at_ms.is_none())?;
let missile = presented
.presented_projectiles()
.iter()
.find(|p| p.authoritative_id() == Some(link.projectile))?;
Some(WorldCameraPose {
origin: missile.origin_at(at_time),
angles: link.angles,
})
clip: Option<&ClipCollision>,
) -> [f32; 3] {
let known = missile.origin_at(at_time.min(snapshot_ms));
if at_time <= snapshot_ms {
return known;
}
let ahead = missile.origin_at(at_time.min(snapshot_ms.saturating_add(AUTHORITY_MS)));
let fraction = clip.map_or(1.0, |clip| {
let half = CG_CAMERA_PULLBACK_BOX_HALF;
let hit = clip.sweep_box(
known,
ahead,
[-half; 3],
[half; 3],
CG_CAMERA_PULLBACK_CLIPMASK,
);
if hit.allsolid { 0.0 } else { hit.fraction }
});
std::array::from_fn(|i| known[i] + (ahead[i] - known[i]) * fraction.clamp(0.0, 1.0))
}
pub fn linked_weapon_camera(
@@ -21,7 +21,7 @@ use crate::anim::view_kick_state::{KickParams, ViewKickState, add_kick_to_viewan
use crate::anim::view_sway::ViewSwayState;
use crate::occupancy::remote_body::RemotePlayer;
use crate::occupancy::third_person::{
linked_weapon_camera, presented_is_third_person, remote_missile_camera, third_person_camera,
RemoteMissileView, linked_weapon_camera, presented_is_third_person, third_person_camera,
};
use render_scene::{FlyCamera, FpvLens, SimCamera, transform_from_iw_view};
use render_scene::{WorldCameraPose, WorldScriptModelInstance};
@@ -228,6 +228,7 @@ pub fn sync_camera_from_presented(
mut lenses: Query<&mut Projection, With<FpvLens>>,
view: Res<ViewSubject>,
death_cam_clip: Res<crate::occupancy::dyn_ent::DynEntPhysClip>,
mut missile_view: Local<RemoteMissileView>,
) {
kick.killcam_focus_distance = None;
if let Some(view) = sim_cam.view_override {
@@ -283,7 +284,12 @@ pub fn sync_camera_from_presented(
kick.killcam_focus_distance = focus_distance;
return;
}
if let Some(pose) = remote_missile_camera(&presented, local.0, clock.time()) {
if let Some(pose) = missile_view.camera(
&presented,
local.0,
clock.time(),
death_cam_clip.0.as_deref(),
) {
let pose = earthquake_pose(pose, &presented, clock.time());
let eye = transform_from_iw_view(pose);
for mut transform in &mut q {
@@ -52,6 +52,14 @@ pub struct DynEntWakeBroadphase {
pub query_ms: f32,
}
#[derive(Clone, Copy, Debug, Message)]
pub struct DynEntPelletHit {
pub world: frame::WorldStamp,
pub weapon: u32,
pub start: Vec3,
pub end: Vec3,
}
#[derive(Clone, Copy, Debug)]
struct DynEntWakeEntry {
entity: Entity,
@@ -787,6 +795,59 @@ pub(crate) fn on_entity_bullet_hit(
}
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn hit_dyn_ents_from_pellets(
mut hits: MessageReader<DynEntPelletHit>,
weapons: Option<Res<PreparedWeapons>>,
catalog: Option<Res<asset_world::MapXModelSceneCatalog>>,
mut broadphase: ResMut<DynEntWakeBroadphase>,
mut instances: Query<
(
Entity,
&mut WorldDynEntInstance,
&Transform,
&mut Visibility,
),
With<DynEntModelEntity>,
>,
mut impulses: MessageWriter<DynEntPhysImpulse>,
mut phys: ResMut<DynEntPhysWorld>,
mut host: Option<ResMut<HostFxSystem>>,
fx: Option<Res<PreparedFxCatalog>>,
scene: Option<Res<WorldScene>>,
entity_marks: Res<EntityMarks>,
) {
let catalog = catalog.as_deref();
let fx = fx.as_deref();
let scene = scene.as_deref();
let marks = entity_marks.as_ref();
for hit in hits.read() {
let Some((entity, pose, bullet_scale)) =
closest_segment_hit(hit.start, hit.end, catalog, &mut broadphase, &instances)
else {
continue;
};
let weapons = weapons
.as_deref()
.and_then(|weapons| weapons.for_event(hit.world).ok());
hit_one_dyn_ent(
entity,
pose,
hit.start,
hit.end,
bullet_scale,
weapon_hit_damage(weapons.as_ref(), hit.weapon),
&mut instances,
&mut impulses,
phys.as_mut(),
host.as_deref_mut(),
fx,
scene,
marks,
);
}
}
pub(crate) fn on_entity_event_sound(
sound: On<EntityEventSound>,
weapons: Option<Res<PreparedWeapons>>,
@@ -917,6 +978,7 @@ fn on_physics_sphere(
pub fn register_dyn_ent_wake(app: &mut App) {
app.scoped::<DynEntWakeBroadphase>(frame::MatchScope::Live)
.scoped_message::<DynEntPelletHit>(frame::MatchScope::Live)
.add_observer(on_entity_explosion)
.add_observer(on_physics_sphere)
.add_observer(on_entity_bullet_hit)
@@ -927,6 +989,12 @@ pub fn register_dyn_ent_wake(app: &mut App) {
.in_set(frame::InMatch)
.in_set(net::ClientSet::Receive),
)
.add_systems(
Update,
hit_dyn_ents_from_pellets
.in_set(frame::InMatch)
.in_set(net::ClientSet::Present),
)
.add_systems(
Update,
wake_player_overlap
@@ -47,6 +47,11 @@ pub(crate) fn generate(
models: &asset_world::MapXModelSceneCatalog,
) {
let mut state = marks.lock();
state.attached.retain(|a| {
host.marks
.constructed(a.handle)
.is_some_and(|m| m.context as u8 == 3 && (m.context >> 16) as u16 == a.entity)
});
let pending = std::mem::take(&mut state.pending);
let before = state.unsupported_triangles;
let before_receivers = state.unsupported_receivers;
@@ -378,6 +383,7 @@ fn stage(
host.marks
.set_material_name(handle, Some(&request.material));
host.note_models_go_fire();
state.attached.retain(|a| a.handle != handle);
state.attached.push(EntityMarkAttachment {
entity: request.entity,
model: model.to_string(),
@@ -707,7 +707,7 @@ fn commit_fx_transaction(
light.origin,
light.axis[0],
light.radius,
light.color_bgr,
light.color_rgb,
spot_cone,
)),
Err(lighting_iw4::AddOmniLightRefuse::Cap) => {
@@ -725,7 +725,7 @@ fn commit_fx_transaction(
Ok(()) => env.dlights.scene.push(lighting_iw4::omni_light_pack(
light.origin,
light.radius,
light.color_bgr,
light.color_rgb,
)),
Err(lighting_iw4::AddOmniLightRefuse::Cap) => {
env.dlights.cap_full = env.dlights.cap_full.saturating_add(1);
@@ -3261,12 +3261,13 @@ fn drain_pellet_fx(
tracers: Option<Res<PreparedTracers>>,
mut tracer_world: ResMut<TracerWorld>,
mut gate: ResMut<TracerDrawGate>,
(local, generation, timeline, mut occurrences, verdicts): (
(local, generation, timeline, mut occurrences, verdicts, mut dyn_ent_hits): (
Res<LocalPresentClient>,
Res<frame::WorldGeneration>,
Res<net::EntityEventCursor>,
ResMut<PresentedFireFx>,
Res<net::FireVerdictState>,
MessageWriter<crate::adapters::anim::dyn_ent_wake::DynEntPelletHit>,
),
mut host: ResMut<HostFxSystem>,
mut cursor: ResMut<FxJournalCursor>,
@@ -3280,6 +3281,14 @@ fn drain_pellet_fx(
if world != generation.stamp() {
continue;
}
if domain != net::EntityEventDomain::Predicted && record.normal != [0.0; 3] {
dyn_ent_hits.write(crate::adapters::anim::dyn_ent_wake::DynEntPelletHit {
world,
weapon: record.weapon,
start: Vec3::from_array(record.start),
end: Vec3::from_array(record.end),
});
}
let now = host.0.msec_now;
let request = FireFxRequest {
world,
+68 -15
View File
@@ -12,16 +12,17 @@ use fx::{
FxModelInstance, FxPackedLightingSrc, FxPlayPose, FxPlayRequest, FxSparkDrawQuery,
FxSparkFillQuery, FxSparkFillVisual, FxSpriteInstance, FxSystemHost, FxTrailCollideHit,
FxTrailDrawDef, FxTrailSampleVisual, PendingCollide, PlayResult, axis_from_hit_normal,
evaluate_elem_collide_motion, evaluate_elem_motion, generate_verts_with_trails, play_bolted,
play_oriented, spawn_impact_or_death_effect, spawn_looping_partial, spawn_oriented,
update as fx_update,
evaluate_elem_collide_motion, evaluate_elem_motion, evaluate_elem_physics_motion,
generate_verts_with_trails, play_bolted, play_oriented, spawn_impact_or_death_effect,
spawn_looping_partial, spawn_oriented, update as fx_update,
};
use fx_iw4::{
FX_ELEM_ATLAS_OFF, FX_ELEM_ATLAS_SIZE, FxElemType, FxRandomChannel, apply_lighting_frac_bgra,
build_cloud, clamp_elem_rotation_time, cull_cloud, cull_elem_light,
effect_def_needs_lighting_sample, elem_light_color_bgr, elem_uses_lighting_frac,
FX_ELEM_AT_REST_NONE, FX_ELEM_ATLAS_OFF, FX_ELEM_ATLAS_SIZE, FxElemType, FxRandomChannel,
apply_lighting_frac_bgra, build_cloud, clamp_elem_rotation_time, cull_cloud, cull_elem_light,
effect_def_needs_lighting_sample, elem_light_color_rgb, elem_uses_lighting_frac,
evaluate_rotation_total, evaluate_scale, evaluate_size0, evaluate_size1, get_elem_angles_axis,
get_velocity_at_time, sample_f32, sprite_atlas_uv, vec3_normalize, vis_blocker_add_prepared,
get_velocity_at_time, msec_for_sampling_axis, sample_f32, sprite_atlas_uv, vec3_normalize,
vis_blocker_add_prepared,
};
use math_iw4::angle_vectors;
use sim::SimWorld;
@@ -658,6 +659,14 @@ fn eval_pending_collide(
let Some(elem) = effect.elems.get(q.def_index as usize) else {
return out;
};
if fx_iw4::elem_skips_position_update(elem.view.elem_type, elem.view.flags) {
let Some(world) = clip_world else {
out.gap = Some(q.def_index);
return out;
};
out.result = Some(evaluate_physics_piece(elem, world, q));
return out;
}
if fx_iw4::elem_uses_collision(elem.view.flags) {
let Some(world) = clip_world else {
out.gap = Some(q.def_index);
@@ -673,6 +682,7 @@ fn eval_pending_collide(
elem.view.coll_maxs,
elem.view.use_item_clip != 0,
!elem.effect_on_impact.is_absent(),
elem.view.elem_type == FxElemType::Model as u8,
elem.vel_graph_local.as_slice(),
elem.vel_graph_world.as_slice(),
q,
@@ -699,6 +709,29 @@ fn eval_pending_collide(
out
}
fn evaluate_physics_piece(
elem: &asset_game::OwnedFxElemDef,
world: &SimWorld,
q: fx::FxElemMotionQuery<'_>,
) -> FxElemMotionResult {
evaluate_elem_physics_motion(
elem.view.flags,
elem.view.use_item_clip != 0,
elem.vel_graph_local.as_slice(),
elem.vel_graph_world.as_slice(),
q,
|start, end, mins, maxs, mask| {
let t = world.trace_world(start, end, mins, maxs, mask);
FxElemTraceHit {
fraction: t.fraction,
normal: t.normal,
startsolid: t.startsolid != 0,
allsolid: t.allsolid != 0,
}
},
)
}
const COLLIDE_JOBS_PER_WORKER: usize = 16;
fn collide_worker_count(jobs: usize) -> usize {
@@ -830,6 +863,13 @@ fn tick_fx_pass(
let out = (|| {
let effect = catalog_lookup(catalog, q.catalog_index)?;
let elem = effect.elems.get(q.def_index as usize)?;
if fx_iw4::elem_skips_position_update(elem.view.elem_type, elem.view.flags) {
let Some(world) = clip_world else {
collide_gap_indices.borrow_mut().push(q.def_index);
return None;
};
return Some(evaluate_physics_piece(elem, world, q));
}
if fx_iw4::elem_uses_collision(elem.view.flags) {
let Some(world) = clip_world else {
collide_gap_indices.borrow_mut().push(q.def_index);
@@ -845,6 +885,7 @@ fn tick_fx_pass(
elem.view.coll_maxs,
elem.view.use_item_clip != 0,
!elem.effect_on_impact.is_absent(),
elem.view.elem_type == FxElemType::Model as u8,
elem.vel_graph_local.as_slice(),
elem.vel_graph_world.as_slice(),
q,
@@ -1467,7 +1508,7 @@ pub fn build_fx_verts(
is_spot: ctx.elem_type == FxElemType::SpotLight as u8,
origin: ctx.origin,
radius: size0,
color_bgr: elem_light_color_bgr(color, scale),
color_rgb: elem_light_color_rgb(color, scale),
axis,
})
};
@@ -1521,24 +1562,36 @@ fn evaluate_fx_model_instance(
if scale == 0.0 {
return None;
}
let rot_t = clamp_elem_rotation_time(
ctx.age_msec as f32,
ctx.life_msec as f32,
ctx.at_rest_fraction,
);
let axis = get_elem_angles_axis(
let landed_piece = ctx.at_rest_fraction != FX_ELEM_AT_REST_NONE
&& fx_iw4::elem_skips_position_update(elem.view.elem_type, elem.view.flags);
let rot_t = if landed_piece {
msec_for_sampling_axis(ctx.at_rest_fraction, ctx.life_msec as f32).min(ctx.age_msec as f32)
} else {
clamp_elem_rotation_time(
ctx.age_msec as f32,
ctx.life_msec as f32,
ctx.at_rest_fraction,
)
};
let mut axis = get_elem_angles_axis(
elem.view.spawn_angles,
elem.view.angular_velocity,
ctx.elem_random_seed,
rot_t,
ctx.axis,
);
let mut origin = ctx.origin;
if landed_piece && let Some(piece) = elem.phys_piece(ctx.elem_random_seed) {
let (flat, lift) = piece.rest_pose(axis, scale);
axis = flat;
origin[2] += lift;
}
Some(FxModelInstance {
def_name: ctx.def_name.to_owned(),
def_index: ctx.def_index,
model_index,
elem_handle: ctx.elem_handle,
origin: ctx.origin,
origin,
axis,
scale,
flags: ctx.flags,
+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;
+220 -115
View File
@@ -1,6 +1,8 @@
use crate::frame::FrameWorld;
use crate::world::ClientId;
use entity_iw4::{TR_GRAVITY, TR_INTERPOLATE, Trajectory, evaluate_trajectory};
use entity_iw4::{
TR_GRAVITY, TR_INTERPOLATE, Trajectory, evaluate_trajectory, evaluate_trajectory_delta,
};
use playerstate_iw4::{
AnimPair, ENTITYNUM_NONE, LINK_FLAGS_WEAPON_VIEW_ONLY, MAX_CLIENT_CORPSES,
PLAYER_CORPSE_ENTITY_BASE, PlayerState,
@@ -34,6 +36,10 @@ pub struct PlayerCorpseSlot {
pub falling: bool,
pub ground_entity_num: i32,
pub spawn_time_ms: i32,
pub shield: Option<crate::ShieldAttachment>,
}
impl Default for PlayerCorpseSlot {
@@ -55,6 +61,8 @@ impl Default for PlayerCorpseSlot {
tr_base: [0.0; 3],
falling: false,
ground_entity_num: ENTITYNUM_NONE,
spawn_time_ms: 0,
shield: None,
}
}
}
@@ -155,6 +163,10 @@ pub(crate) fn occupy_player_clone(
ps: &PlayerState,
time_ms: i32,
) -> u8 {
let shield = world
.client_meta(victim)
.and_then(|meta| meta.shield)
.filter(|shield| shield.on_back);
let pool = world.corpses_mut();
let entnum = pool.alloc_clone_entity();
let slot = pool.get_free(ps.origin);
@@ -176,6 +188,8 @@ pub(crate) fn occupy_player_clone(
tr_delta: clamp_clone_tr_delta(ps.velocity),
tr_base: ps.origin,
falling: true,
spawn_time_ms: time_ms,
shield,
..PlayerCorpseSlot::default()
},
);
@@ -209,11 +223,8 @@ fn corpse_land_angles(yaw: f32, normal: [f32; 3]) -> [f32; 3] {
}
fn clamp_clone_tr_delta(mut delta: [f32; 3]) -> [f32; 3] {
if delta[0] > G_CLONE_PLAYER_MAX_VELOCITY {
delta[0] = G_CLONE_PLAYER_MAX_VELOCITY;
}
if delta[1] > G_CLONE_PLAYER_MAX_VELOCITY {
delta[1] = G_CLONE_PLAYER_MAX_VELOCITY;
for speed in &mut delta[..2] {
*speed = speed.clamp(-G_CLONE_PLAYER_MAX_VELOCITY, G_CLONE_PLAYER_MAX_VELOCITY);
}
delta
}
@@ -224,7 +235,9 @@ const CORPSE_ANIM_AXIS_Y_SCALE: f32 = -1.0;
const CORPSE_GROUND_PROBE_DROP: f32 = 1.0;
const CORPSE_ANIM_DELTA_TO_VELOCITY: f32 = 20.0;
const CORPSE_STEP_DOWN: f32 = 18.0;
const CORPSE_WALL_SLIDE_GAP: f32 = 0.125;
const CORPSE_UNSTICK_Z_LIFT: f32 = 32.0;
@@ -245,109 +258,210 @@ pub(crate) fn phase_run_corpse_move(world: &mut FrameWorld, time_ms: i32) {
if !slot.occupied {
continue;
}
let ragdoll = is_ragdoll_tr_type(slot.tr_type);
let anim_delta = world
.corpse_dobj_tree_delta(slot.entnum, crate::MATCH_TICK_MS as i32)
.filter(|d| {
d[0] * d[0] + d[1] * d[1] + d[2] * d[2] > CORPSE_ANIM_DELTA_LEN_SQ_MIN
&& (!slot.falling || !ragdoll)
});
if !slot.falling && anim_delta.is_none() {
continue;
}
let mut desired = evaluate_trajectory(&slot.trajectory(), time_ms);
let world_delta = anim_delta.map(|delta| corpse_anim_world_delta(slot.viewangles, delta));
if let Some(world_delta) = world_delta {
for axis in 0..3 {
desired[axis] += world_delta[axis];
}
}
let hit = world.trace_clip(
slot.origin,
desired,
PLAYER_MINS,
PLAYER_MAXS,
MASK_PLAYER_SOLID,
);
let mut fraction = hit.fraction;
let endpos = if fraction >= 1.0 { desired } else { hit.endpos };
world.corpses_mut().slots[i].origin = endpos;
if hit.startsolid != 0 {
fraction = 0.0;
}
if fraction >= 1.0 {
let Some(world_delta) = world_delta else {
continue;
};
{
let slot = &mut world.corpses_mut().slots[i];
slot.tr_type = if ragdoll {
TR_RAGDOLL_SETTLED
} else {
TR_INTERPOLATE
};
slot.tr_base = endpos;
slot.tr_time = 0;
slot.tr_duration = 0;
slot.tr_delta = [0.0; 3];
}
if ragdoll {
let mut probe = desired;
probe[2] -= CORPSE_GROUND_PROBE_DROP;
let ground =
world.trace_clip(endpos, probe, PLAYER_MINS, PLAYER_MAXS, MASK_PLAYER_SOLID);
if ground.fraction < 1.0 || ground.startsolid != 0 {
let slot = &mut world.corpses_mut().slots[i];
slot.falling = true;
let mut vel = slot.tr_delta;
for axis in 0..3 {
vel[axis] = (vel[axis] + world_delta[axis]) * CORPSE_ANIM_DELTA_TO_VELOCITY;
}
slot.tr_delta = vel;
slot.tr_type = TR_BOUNCE_GRAVITY;
slot.tr_time = time_ms;
slot.tr_duration = 0;
continue;
}
}
world.corpses_mut().slots[i].falling = false;
continue;
}
if !slot.falling {
continue;
}
if hit.allsolid != 0 {
let mut lifted = world.corpses().slots[i].origin;
lifted[2] += CORPSE_UNSTICK_Z_LIFT;
let retry = world.trace_clip(
lifted,
desired,
PLAYER_MINS,
PLAYER_MAXS,
MASK_PLAYER_SOLID & !CORPSE_UNSTICK_CLIPMASK_DROP,
);
if retry.allsolid == 0 {
let f = retry.fraction;
world.corpses_mut().slots[i].origin = [
lifted[0] + (desired[0] - lifted[0]) * f,
lifted[1] + (desired[1] - lifted[1]) * f,
lifted[2] + (desired[2] - lifted[2]) * f,
];
}
}
corpse_land(world, i, hit, time_ms);
let anim_delta = world.corpse_dobj_tree_delta(slot.entnum, crate::MATCH_TICK_MS as i32);
let frame = &*world;
let stepped = step_corpse(slot, anim_delta, time_ms, |start, end, mask| {
frame.trace_clip(start, end, PLAYER_MINS, PLAYER_MAXS, mask)
});
world.corpses_mut().slots[i] = stepped;
}
let live = world.corpses().entnums();
world.corpse_dobj_tree_retain(&live);
}
fn step_corpse(
mut slot: PlayerCorpseSlot,
anim_delta: Option<[f32; 3]>,
time_ms: i32,
mut trace: impl FnMut([f32; 3], [f32; 3], u32) -> trace_iw4::Trace,
) -> PlayerCorpseSlot {
let was_falling = slot.falling;
let ragdoll = is_ragdoll_tr_type(slot.tr_type);
let anim_delta = anim_delta.filter(|d| {
d[0] * d[0] + d[1] * d[1] + d[2] * d[2] > CORPSE_ANIM_DELTA_LEN_SQ_MIN
&& (!was_falling || !ragdoll)
});
if !was_falling && anim_delta.is_none() {
return slot;
}
let world_delta = anim_delta.map(|delta| {
let step = corpse_anim_world_delta(slot.viewangles, delta);
if was_falling {
step
} else {
[step[0], step[1], 0.0]
}
});
let mut desired = evaluate_trajectory(&slot.trajectory(), time_ms);
if let Some(step) = world_delta {
for axis in 0..3 {
desired[axis] += step[axis];
}
}
let hit = trace(slot.origin, desired, MASK_PLAYER_SOLID);
let endpos = if hit.fraction >= 1.0 {
desired
} else {
hit.endpos
};
slot.origin = endpos;
if hit.fraction >= 1.0 && hit.startsolid == 0 {
let Some(step) = world_delta else {
return slot;
};
if was_falling {
for (base, step) in slot.tr_base.iter_mut().zip(step) {
*base += step;
}
return slot;
}
slot.tr_type = if ragdoll {
TR_RAGDOLL_SETTLED
} else {
TR_INTERPOLATE
};
slot.tr_base = endpos;
slot.tr_time = 0;
slot.tr_duration = 0;
slot.tr_delta = [0.0; 3];
follow_ground_or_fall(&mut slot, time_ms, ragdoll, &mut trace);
return slot;
}
if !was_falling {
slot.tr_base = endpos;
if hit.fraction > 0.0 && hit.startsolid == 0 {
follow_ground_or_fall(&mut slot, time_ms, ragdoll, &mut trace);
}
return slot;
}
if hit.allsolid != 0 {
let mut lifted = slot.origin;
lifted[2] += CORPSE_UNSTICK_Z_LIFT;
let retry = trace(
lifted,
desired,
MASK_PLAYER_SOLID & !CORPSE_UNSTICK_CLIPMASK_DROP,
);
if retry.allsolid == 0 {
let f = retry.fraction;
slot.origin = [
lifted[0] + (desired[0] - lifted[0]) * f,
lifted[1] + (desired[1] - lifted[1]) * f,
lifted[2] + (desired[2] - lifted[2]) * f,
];
}
land_corpse(&mut slot, &hit);
return slot;
}
if hit.normal[2] <= 0.0 {
if hit.startsolid != 0 {
for axis in 0..3 {
slot.origin[axis] += hit.normal[axis];
}
slot.tr_base = slot.origin;
slot.tr_time = time_ms;
slot.tr_delta = [0.0; 3];
return slot;
}
slide_down_wall(&mut slot, &hit, desired, time_ms, &mut trace);
return slot;
}
land_corpse(&mut slot, &hit);
slot
}
fn follow_ground_or_fall(
slot: &mut PlayerCorpseSlot,
time_ms: i32,
ragdoll: bool,
trace: &mut impl FnMut([f32; 3], [f32; 3], u32) -> trace_iw4::Trace,
) {
let at = slot.origin;
let mut probe = at;
probe[2] -= CORPSE_GROUND_PROBE_DROP;
let ground = trace(at, probe, MASK_PLAYER_SOLID);
if ground.fraction < 1.0 || ground.startsolid != 0 {
slot.falling = false;
return;
}
let mut step_probe = at;
step_probe[2] -= CORPSE_STEP_DOWN;
let step = trace(at, step_probe, MASK_PLAYER_SOLID);
if step.fraction < 1.0 && step.startsolid == 0 && step.allsolid == 0 && step.normal[2] > 0.0 {
slot.origin = step.endpos;
slot.tr_base = step.endpos;
slot.falling = false;
slot.ground_entity_num = i32::from(trace_iw4::trace_get_entity_hit_id(
step.hit_type,
step.hit_id,
));
return;
}
slot.falling = true;
slot.tr_type = if ragdoll {
TR_BOUNCE_GRAVITY
} else {
TR_GRAVITY
};
slot.tr_base = at;
slot.tr_time = time_ms;
slot.tr_duration = 0;
slot.tr_delta = [0.0; 3];
}
fn slide_down_wall(
slot: &mut PlayerCorpseSlot,
hit: &trace_iw4::Trace,
desired: [f32; 3],
time_ms: i32,
trace: &mut impl FnMut([f32; 3], [f32; 3], u32) -> trace_iw4::Trace,
) {
let n = hit.normal;
let mut start = hit.endpos;
let mut rest = [0.0f32; 3];
for axis in 0..3 {
start[axis] += n[axis] * CORPSE_WALL_SLIDE_GAP;
rest[axis] = desired[axis] - hit.endpos[axis];
}
let into_wall = dot(rest, n);
let mut end = start;
for axis in 0..3 {
end[axis] += rest[axis] - into_wall * n[axis];
}
let slid = trace(start, end, MASK_PLAYER_SOLID);
if slid.allsolid == 0 && slid.startsolid == 0 {
slot.origin = if slid.fraction >= 1.0 {
end
} else {
slid.endpos
};
if slid.fraction < 1.0 && slid.normal[2] > 0.0 {
land_corpse(slot, &slid);
return;
}
}
let speed = evaluate_trajectory_delta(&slot.trajectory(), time_ms);
let mut velocity = [0.0, 0.0, speed[2]];
let into = dot(velocity, n);
if into < 0.0 {
for axis in 0..3 {
velocity[axis] -= into * n[axis];
}
}
slot.tr_base = slot.origin;
slot.tr_time = time_ms;
slot.tr_duration = 0;
slot.tr_delta = velocity;
}
fn dot(a: [f32; 3], b: [f32; 3]) -> f32 {
a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
}
fn corpse_anim_world_delta(angles: [f32; 3], delta: [f32; 3]) -> [f32; 3] {
let (mut fwd, right, up) = math_iw4::angle_vectors(angles);
let mut left = [
@@ -364,18 +478,9 @@ fn corpse_anim_world_delta(angles: [f32; 3], delta: [f32; 3]) -> [f32; 3] {
out
}
fn corpse_land(world: &mut FrameWorld, i: usize, hit: trace_iw4::Trace, time_ms: i32) {
let slot = &mut world.corpses_mut().slots[i];
fn land_corpse(slot: &mut PlayerCorpseSlot, hit: &trace_iw4::Trace) {
let ragdoll = is_ragdoll_tr_type(slot.tr_type);
slot.tr_delta = [0.0; 3];
if hit.allsolid == 0 && hit.normal[2] <= 0.0 {
for axis in 0..3 {
slot.origin[axis] += hit.normal[axis];
}
slot.tr_base = slot.origin;
slot.tr_time = time_ms;
return;
}
slot.falling = false;
slot.tr_type = if ragdoll {
TR_RAGDOLL_SETTLED
+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>();
}
}
@@ -6,6 +6,7 @@ pub struct ScriptSeat {
pub archive_ms: i32,
pub ps_offset_ms: i32,
pub length_ms: i32,
pub final_kill: bool,
}
impl Default for ScriptSeat {
@@ -17,6 +18,7 @@ impl Default for ScriptSeat {
archive_ms: 0,
ps_offset_ms: 0,
length_ms: 0,
final_kill: false,
}
}
}
+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 -1
View File
@@ -419,7 +419,7 @@ pub(crate) fn select_location(
pub(crate) fn command_buttons(command: &str) -> u32 {
match command {
"+attack" | "mouse1" => buttons::ATTACK,
"+activate" | "+usereload" => buttons::USE,
"+activate" | "+usereload" => buttons::USE | buttons::USE_RELOAD,
"+melee" => buttons::MELEE_CHARGE,
"+frag" => buttons::FRAG,
"+smoke" => buttons::SMOKE,
+17 -2
View File
@@ -1061,19 +1061,34 @@ impl PlayerSlot {
}
pub(crate) fn script_seats(world: &World) -> Vec<(ClientId, crate::ScriptSeat)> {
world
.resource::<RoundScript>()
let runtime = world.resource::<RoundScript>();
let final_kill = level_flag(runtime, "showingfinalkillcam");
runtime
.players
.iter()
.filter(|(_, slot)| &*slot.sessionstate == "spectator" && slot.spectator.target.is_some())
.map(|(client, slot)| {
let mut seat = slot.seat;
seat.spectator_client = slot.spectator.target.unwrap() as i32;
seat.final_kill = final_kill;
(ClientId(*client), seat)
})
.collect()
}
fn level_flag(runtime: &RoundScript, name: &str) -> bool {
let symbol = runtime
.program
.as_ref()
.and_then(|program| program.symbol_ids.get(name))
.or_else(|| runtime.dynamic_symbols.get(name));
match symbol.and_then(|symbol| runtime.objects.get(&0)?.get(symbol)) {
Some(Value::Int(n)) => *n != 0,
Some(Value::Float(f)) => *f != 0.0,
_ => false,
}
}
const SEAT_FIELDS: [&str; 6] = [
"forcespectatorclient",
"killcamentity",
+39 -22
View File
@@ -313,25 +313,54 @@ fn present(world: &mut World, now: i32) {
}
}
fn tag_lookup(world: &mut World, object: u64, tag: &str) -> Option<Option<[f32; 3]>> {
fn with_tag_dobj<T>(
world: &mut World,
object: u64,
read: impl FnOnce(&AuthorityDObjState) -> Option<T>,
) -> Option<T> {
let presence = world
.resource::<RoundScript>()
.entities
.get(&object)?
.presence?;
let frame = settled(world);
let dobj = frame
if let Some(dobj) = frame
.entity_collision_capabilities()
.iter()
.find(|row| row.owner.script_model() == Some(presence))?
.dobj
.as_ref()?;
Some(dobj.tag_world_pose(tag).map(|(at, _)| {
dobj.world_from_model
.inverse()
.transform_point3(glam::Vec3::from(at))
.to_array()
}))
.as_ref()
{
return read(dobj);
}
let (model, origin, angles) = {
let mut runtime = world.resource_mut::<RoundScript>();
(
model_field(&mut runtime, object)?,
vector(&mut runtime, object, "origin"),
vector(&mut runtime, object, "angles"),
)
};
let capability = FrameWorld::from_world(world)
.model_capability(&model)
.flatten()?;
read(&AuthorityDObjState::at_pose(
&model,
Some(capability),
origin,
angles,
))
}
fn tag_lookup(world: &mut World, object: u64, tag: &str) -> Option<Option<[f32; 3]>> {
with_tag_dobj(world, object, |dobj| {
Some(dobj.tag_world_pose(tag).map(|(at, _)| {
dobj.world_from_model
.inverse()
.transform_point3(glam::Vec3::from(at))
.to_array()
}))
})
}
pub(crate) fn tag_world(
@@ -339,19 +368,7 @@ pub(crate) fn tag_world(
object: u64,
tag: &str,
) -> Option<([f32; 3], [[f32; 3]; 3])> {
let presence = world
.resource::<RoundScript>()
.entities
.get(&object)?
.presence?;
let frame = settled(world);
let matrix = frame
.entity_collision_capabilities()
.iter()
.find(|row| row.owner.script_model() == Some(presence))?
.dobj
.as_ref()?
.tag_world_matrix(tag)?;
let matrix = with_tag_dobj(world, object, |dobj| dobj.tag_world_matrix(tag))?;
let axis = |v: glam::Vec4| v.truncate().normalize_or_zero().to_array();
Some((
matrix.w_axis.truncate().to_array(),
+29 -1
View File
@@ -97,6 +97,32 @@ pub(crate) fn advance(world: &mut World) {
update(world, true);
}
pub(crate) fn hold_killcam_viewer(
world: &mut World,
client: ClientId,
cmd: &mut playerstate_iw4::UserCmd,
) {
let seated = world
.get_resource::<RoundScript>()
.and_then(|runtime| runtime.players.get(&client.0))
.is_some_and(|slot| {
&*slot.sessionstate == "spectator"
&& slot.seat.archive_ms > 0
&& slot.spectator.target.is_some()
});
if !seated {
return;
}
use playerstate_iw4::buttons;
let held = FrameWorld::from_world(world)
.player(client)
.map_or(0, |ps| ps.weapon);
cmd.forwardmove = 0;
cmd.rightmove = 0;
cmd.buttons &= buttons::PRONE | buttons::CROUCH | buttons::STANCE_HELD;
cmd.weapon = held as u16;
}
fn update(world: &mut World, input: bool) {
let replacement = RoundScript::new(world.resource::<crate::script::MatchScript>());
let mut runtime = std::mem::replace(&mut *world.resource_mut::<RoundScript>(), replacement);
@@ -148,7 +174,9 @@ fn update(world: &mut World, input: bool) {
&& !archived
&& pressed & (playerstate_iw4::buttons::ATTACK | playerstate_iw4::buttons::ADS) != 0;
if let Some(forced) = forced {
slot.spectator.target = allowed.contains(&forced).then_some(forced);
let own_replay =
archived && forced == client && targets.iter().any(|(id, ..)| *id == client);
slot.spectator.target = (own_replay || allowed.contains(&forced)).then_some(forced);
slot.spectator.free = false;
} else if input
&& pressed & playerstate_iw4::buttons::USE != 0
+42 -13
View File
@@ -180,14 +180,45 @@ fn aim_point(world: &mut World, aim: TurretAim) -> Option<[f32; 3]> {
}
}
fn gunner_input(world: &mut World, client: u32) -> Option<([f32; 3], bool)> {
let mut frame = crate::frame::FrameWorld::from_world(world);
let angles = frame.player(crate::ClientId(client))?.viewangles;
let held = crate::script_player::buttons(&mut frame, crate::ClientId(client));
Some((
math_iw4::angle_vectors(angles).0,
held & playerstate_iw4::buttons::ATTACK != 0,
))
fn gunner_input(world: &mut World, heli: u64, client: u32) -> Option<([f32; 3], bool)> {
let id = crate::ClientId(client);
let (eye, angles, held) = {
let mut frame = crate::frame::FrameWorld::from_world(world);
let ps = *frame.player(id)?;
let eye = frame
.client_meta(id)
.and_then(|meta| meta.linked_weapon_view)
.map_or(
[
ps.origin[0],
ps.origin[1],
ps.origin[2] + ps.view_height_current,
],
|view| view.origin,
);
(
eye,
ps.viewangles,
crate::script_player::buttons(&mut frame, id),
)
};
let dir = math_iw4::angle_vectors(angles).0;
let end = std::array::from_fn(|i| eye[i] + dir[i] * GUNNER_RANGE);
let ignore = super::natives::engine::TraceIgnore {
client: Some(id),
..super::turrets::ignore_self(world, heli)
};
let point = match super::natives::engine::entity_trace(
world,
eye,
end,
crate::bullet_collision::MASK_SHOT,
ignore,
) {
crate::bullet_collision::TraceOutcome::Hit { end, .. } => end,
_ => end,
};
Some((point, held & playerstate_iw4::buttons::ATTACK != 0))
}
fn fire_weapon(world: &mut World, receiver: &Value, args: &[Value]) -> Result<Value, String> {
@@ -992,7 +1023,7 @@ pub(crate) fn advance(world: &mut World) {
.map_or((None, None), |heli| (heli.gunner, heli.weapon));
let control = gunner
.filter(|client| world.resource::<RoundScript>().players.contains_key(client))
.and_then(|client| gunner_input(world, client));
.and_then(|client| gunner_input(world, id, client));
let fire_ms = weapon
.and_then(|weapon| crate::frame::FrameWorld::from_world(world).combat_facts_for(weapon))
.map_or(100, |facts| facts.fire_time_ms.max(1));
@@ -1111,10 +1142,8 @@ pub(crate) fn advance(world: &mut World) {
}
let [pitch, yaw, roll] = angles;
match control {
Some((view, attack)) => {
heli.turret = Some(TurretAim::Point(std::array::from_fn(|i| {
next[i] + view[i] * GUNNER_RANGE
})));
Some((point, attack)) => {
heli.turret = Some(TurretAim::Point(point));
heli.on_target = true;
if attack && now >= heli.next_fire_ms {
heli.next_fire_ms = now + fire_ms;
+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)
});
+1
View File
@@ -37,6 +37,7 @@ pub(crate) use host::players::{
pub(crate) use host::presence::sync_presence;
pub use host::registry::{Native, NativeRegistry};
pub(crate) use host::restart::restart_level;
pub(crate) use host::spectators::hold_killcam_viewer;
pub(crate) use host::weapons::{publish_projectile_launches, sync_engine_events};
pub use ir::IR_VERSION;
pub(crate) use ir::{Binary, Callee, Function, Global, Op, Unary};
+6 -1
View File
@@ -49,6 +49,7 @@ pub(crate) fn spawn(
};
world.unlink_player_area(id);
world.client_meta_mut(id).shield = None;
let prev_health = world.player(id).map(|p| p.health);
let mut ps = spawn_player_state(origin, angles);
if let Some(max) = world
.client_meta(id)
@@ -56,8 +57,12 @@ pub(crate) fn spawn(
.filter(|&n| n > 0)
{
ps.max_health = max;
ps.health = max;
}
ps.health = match sessionstate {
"playing" => ps.max_health,
"dead" | "intermission" => prev_health.unwrap_or(0),
_ => 0,
};
let cmd = spawn_command_angles(world, id);
ps.delta_angles = std::array::from_fn(|axis| angles[axis] - cmd[axis] as f32 * SHORT2ANGLE);
let prev_teleport = world
+1
View File
@@ -393,6 +393,7 @@ fn run_players_system(ecs: &mut World) {
crate::script::player_commands(world.ecs(), id.0, cmd.buttons, old_buttons);
let mut cmd = *cmd;
crate::script_player::constrain_cmd(&mut world, *id, &mut cmd);
crate::script::hold_killcam_viewer(world.ecs(), *id, &mut cmd);
crate::script::select_location(world.ecs(), id.0, &mut cmd, old_buttons);
if world
.client_meta(*id)