Prepare weapon presentation and add alternate modes, device feedback and collision fixes

This commit is contained in:
vtroc7
2026-09-24 05:30:17 +03:00
parent f357ef16be
commit 69cf9ff9fa
133 changed files with 5933 additions and 1659 deletions
Generated
+105
View File
@@ -920,6 +920,22 @@ dependencies = [
"encase_derive_impl",
]
[[package]]
name = "bevy_gilrs"
version = "0.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7998fe83c89d527efa83c85c574a17f7bb6e34881d638ecb7f706d9d79e82f3c"
dependencies = [
"bevy_app",
"bevy_ecs",
"bevy_input",
"bevy_platform",
"bevy_time",
"gilrs",
"thiserror 2.0.19",
"tracing",
]
[[package]]
name = "bevy_gizmos"
version = "0.19.0"
@@ -1062,6 +1078,7 @@ dependencies = [
"bevy_derive",
"bevy_diagnostic",
"bevy_ecs",
"bevy_gilrs",
"bevy_gizmos",
"bevy_gizmos_render",
"bevy_image",
@@ -2754,6 +2771,12 @@ dependencies = [
"zlib-rs",
]
[[package]]
name = "fnv"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
name = "foldhash"
version = "0.1.5"
@@ -2996,6 +3019,40 @@ dependencies = [
"syn 2.0.119",
]
[[package]]
name = "gilrs"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "902fb00d3f6398e635be22e5c837b303c501835cca7ac11a47bba138f7aafdd8"
dependencies = [
"fnv",
"gilrs-core",
"log",
"uuid",
"vec_map",
]
[[package]]
name = "gilrs-core"
version = "0.6.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc7f0ce6237abcc0523f2a5502b1e3fe5802daaae47ac14e166fe49551301ea9"
dependencies = [
"inotify",
"js-sys",
"libc",
"libudev-sys",
"log",
"nix",
"objc2-core-foundation",
"objc2-io-kit",
"uuid",
"vec_map",
"wasm-bindgen",
"web-sys",
"windows",
]
[[package]]
name = "glam"
version = "0.32.1"
@@ -3171,6 +3228,7 @@ dependencies = [
"gamemode_iw4",
"hud_iw4",
"killcam_iw4",
"math_iw4",
"movement_iw4",
"net",
"playerstate_iw4",
@@ -3360,6 +3418,26 @@ dependencies = [
"hashbrown 0.17.1",
]
[[package]]
name = "inotify"
version = "0.11.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4cc00ea907cab49550b7da656f80ebb97be1b997d931fbcd28d39734e17ce592"
dependencies = [
"bitflags 2.13.1",
"inotify-sys",
"libc",
]
[[package]]
name = "inotify-sys"
version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c033f80b2c113cdf91ab7a33faa9cbc014726dcad99880c8609af2a370edf37d"
dependencies = [
"libc",
]
[[package]]
name = "input_iw4"
version = "0.1.0"
@@ -3553,6 +3631,16 @@ dependencies = [
"redox_syscall 0.9.1",
]
[[package]]
name = "libudev-sys"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c8469b4a23b962c1396b9b451dda50ef5b283e8dd309d69033475fa9b334324"
dependencies = [
"libc",
"pkg-config",
]
[[package]]
name = "lighting_iw4"
version = "0.1.0"
@@ -4162,6 +4250,17 @@ dependencies = [
"objc2-core-foundation",
]
[[package]]
name = "objc2-io-kit"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33fafba39597d6dc1fb709123dfa8289d39406734be322956a69f0931c73bb15"
dependencies = [
"bitflags 2.13.1",
"libc",
"objc2-core-foundation",
]
[[package]]
name = "objc2-io-surface"
version = "0.3.2"
@@ -6201,6 +6300,12 @@ dependencies = [
"syn 2.0.119",
]
[[package]]
name = "vec_map"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191"
[[package]]
name = "version_check"
version = "0.9.5"
+1
View File
@@ -90,6 +90,7 @@ bevy = { version = "0.19", default-features = false, features = [
"bevy_sprite_render",
"bevy_asset",
"bevy_audio",
"bevy_gilrs",
"multi_threaded",
"x11",
"wayland",
+1 -1
View File
@@ -53,7 +53,7 @@ pub use player_anim::{
};
pub use quat::{
QUAT_IDENTITY, Quat, VEC3_ZERO, Vec3, normalize, quat_add_weighted, quat_dot, quat_mul,
quat_neg, slerp, vec3_add_scaled, xanim_apply_additive,
quat_neg, vec3_add_scaled, xanim_apply_additive,
};
pub use sample::{FrameKind, sample_quat, sample_vec3, span, time_to_frame};
pub use skel::{DuplicatePart, Skel};
+5 -25
View File
@@ -32,32 +32,12 @@ pub fn quat_add_weighted(acc: Quat, q: Quat, weight: f32) -> Quat {
]
}
pub fn slerp(a: Quat, b: Quat, t: f32) -> Quat {
let mut b = b;
let mut dot = quat_dot(a, b);
if dot < 0.0 {
b = quat_neg(b);
dot = -dot;
}
if dot > 0.9995 {
return normalize([
a[0] + t * (b[0] - a[0]),
a[1] + t * (b[1] - a[1]),
a[2] + t * (b[2] - a[2]),
a[3] + t * (b[3] - a[3]),
]);
}
let theta_0 = libm::acosf(dot.clamp(-1.0, 1.0));
let theta = theta_0 * t;
let sin_theta = libm::sinf(theta);
let sin_theta_0 = libm::sinf(theta_0);
let s0 = libm::cosf(theta) - dot * sin_theta / sin_theta_0;
let s1 = sin_theta / sin_theta_0;
pub fn lerp_quat(a: Quat, b: Quat, t: f32) -> Quat {
[
s0 * a[0] + s1 * b[0],
s0 * a[1] + s1 * b[1],
s0 * a[2] + s1 * b[2],
s0 * a[3] + s1 * b[3],
a[0] + t * (b[0] - a[0]),
a[1] + t * (b[1] - a[1]),
a[2] + t * (b[2] - a[2]),
a[3] + t * (b[3] - a[3]),
]
}
+2 -2
View File
@@ -1,4 +1,4 @@
use crate::quat::{Quat, Vec3, lerp_vec3, slerp};
use crate::quat::{Quat, Vec3, lerp_quat, lerp_vec3};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FrameKind {
@@ -52,7 +52,7 @@ pub fn sample_quat(kind: FrameKind, frames: &[u16], values: &[Quat], frame: f32)
return crate::quat::QUAT_IDENTITY;
}
let (a, b, t) = span(kind, frames, values.len(), frame);
slerp(values[a], values[b], t)
lerp_quat(values[a], values[b], t)
}
pub fn sample_vec3(kind: FrameKind, frames: &[u16], values: &[Vec3], frame: f32) -> Vec3 {
+11 -2
View File
@@ -63,7 +63,16 @@ selection cannot move a replayed scene.
`iw4l-artifacts/logs/latest.log` and `iw4l-artifacts/runs/<id>/` perf run.
Assertions are liveness only: the map loaded, the players exist, simulation
advanced, input moved the player or the player died, the lifecycle
boundaries happened in order, and `quit` ended the process on its own. There
advanced, the authority applied the scene's movement commands to the local
player, the player covered a path of at least 32 units or died, the lifecycle
boundaries happened in order, and `quit` ended the process on its own.
Movement is three facts, not one. `mark` reports the local player's input
receipt — commands the authority applied, how many of them carried a move, and
the summed length of every applied move — so a scene that turns while it walks
is judged by the path it walked, not by where it ended up. Each scene records an
`outcome`: `moved`, `died`, `died_before_input`, `held_in_place` (commands
applied, alive, no path: collision or a frozen player) or `input_not_applied`
(the break the scenario exists to catch). There
is no frame-time assertion. A run the controller had to kill is a failure,
never a quit.
+48 -4
View File
@@ -375,7 +375,13 @@ pub fn finish(run_dir: &Path, manifest: &mut Value, phases: &[Phase], outcome: &
.next()
.and_then(|n| n.parse::<usize>().ok())
.map_or(0, |n| n.saturating_sub(1));
let ticks = SCENARIO.scenes.get(index).map_or(0, |s| s.ticks);
let declared = SCENARIO.scenes.get(index);
let ticks = declared.map_or(0, |s| s.ticks);
let commands_move = declared.is_some_and(|s| {
s.hold
.iter()
.any(|h| matches!(*h, "+forward" | "+back" | "+moveleft" | "+moveright"))
});
let phase_log = log.get(&phase);
let spawn = phase_log
.and_then(|l| l.forced_spawn.as_deref())
@@ -405,6 +411,25 @@ pub fn finish(run_dir: &Path, manifest: &mut Value, phases: &[Phase], outcome: &
.and_then(|s| s.num("local_deaths"))
.zip(done.and_then(|d| d.num("local_deaths")))
.is_some_and(|(a, b)| b > a);
let receipt = |key: &str| {
spawned
.and_then(|s| s.num(key))
.zip(done.and_then(|d| d.num(key)))
.map(|(a, b)| b - a)
};
let applied_cmds = receipt("local_cmds");
let moving_cmds = receipt("local_moving_cmds");
let path = receipt("local_path");
let input_applied = applied_cmds.is_some_and(|n| n >= 1.0)
&& (!commands_move || moving_cmds.is_some_and(|n| n >= 1.0));
let moved = path.is_some_and(|p| p >= 32.0);
let outcome = match (input_applied, moved, died) {
(false, _, true) => "died_before_input",
(false, _, false) => "input_not_applied",
(true, true, _) => "moved",
(true, false, true) => "died",
(true, false, false) => "held_in_place",
};
let lives = spawned
.and_then(|s| s.num("local_life"))
.zip(done.and_then(|d| d.num("local_life")))
@@ -433,9 +458,24 @@ pub fn finish(run_dir: &Path, manifest: &mut Value, phases: &[Phase], outcome: &
json!({ "alive_at_end": done.and_then(|d| d.num("alive")) }),
);
asserts.check(
&format!("{phase}.input_moved_or_died"),
travel.is_some_and(|t| t >= 32.0) || died,
json!({ "travel_units": travel, "died": died }),
&format!("{phase}.input_applied"),
input_applied,
json!({
"applied_cmds": applied_cmds,
"moving_cmds": moving_cmds,
"scene_commands_move": commands_move,
"died": died,
}),
);
asserts.check(
&format!("{phase}.movement_observed"),
moved || died,
json!({
"outcome": outcome,
"path_units": path,
"net_displacement_units": travel,
"died": died,
}),
);
scene["observed"] = json!({
@@ -445,6 +485,10 @@ pub fn finish(run_dir: &Path, manifest: &mut Value, phases: &[Phase], outcome: &
"at_inputs_done": done.map(facts),
"ticks": tick_delta,
"travel_units": travel,
"path_units": path,
"applied_cmds": applied_cmds,
"moving_cmds": moving_cmds,
"outcome": outcome,
"died": died,
"lives": lives,
"kills_deaths_in_match": kills_deaths,
+35
View File
@@ -640,6 +640,10 @@ impl SoundCatalog {
found
}
pub fn sound_at(&self, index: usize) -> Option<&CapturedSound> {
self.sounds.get(index)
}
pub fn sound_in(&self, ns: AssetNamespace, alias: &str) -> Option<&CapturedSound> {
self.index_in(ns, alias).map(|i| &self.sounds[i])
}
@@ -920,6 +924,16 @@ impl SoundCatalog {
self.streamed_from_row(ns, row)
}
pub fn streamed_for_variant_at(
&self,
index: usize,
variant: usize,
) -> Option<(AssetNamespace, String, String)> {
let sound = self.sounds.get(index)?;
let row = sound.aliases.get(variant)?;
self.streamed_from_row(ns_of(sound.game), row)
}
fn streamed_from_row(
&self,
ns: AssetNamespace,
@@ -1075,6 +1089,16 @@ impl SoundCatalog {
self.pick_loaded_outcome_depth(ns, alias, rng, avoid, 0)
}
pub fn pick_loaded_outcome_at<'a>(
&'a self,
index: usize,
rng: &mut u32,
avoid: Option<usize>,
) -> Option<PickLoadedOutcome<'a>> {
let sound = self.sounds.get(index)?;
self.pick_from_sound(ns_of(sound.game), sound, rng, avoid, 0)
}
fn pick_loaded_outcome_depth<'a>(
&'a self,
ns: AssetNamespace,
@@ -1084,6 +1108,17 @@ impl SoundCatalog {
depth: u8,
) -> Option<PickLoadedOutcome<'a>> {
let sound = self.sound_in(ns, alias)?;
self.pick_from_sound(ns, sound, rng, avoid, depth)
}
fn pick_from_sound<'a>(
&'a self,
ns: AssetNamespace,
sound: &'a CapturedSound,
rng: &mut u32,
avoid: Option<usize>,
depth: u8,
) -> Option<PickLoadedOutcome<'a>> {
if sound.aliases.is_empty() {
return Some(PickLoadedOutcome {
variant_index: 0,
+8
View File
@@ -30,6 +30,8 @@ pub struct FpvAssemblyTags {
pub brass: Option<u16>,
pub knife: Option<u16>,
pub laser: Option<u16>,
pub tracker_screen: [Option<u16>; 3],
pub tracker_light: Option<u16>,
}
#[derive(Debug)]
@@ -166,6 +168,12 @@ impl FpvAssembly {
brass: tag("tag_brass"),
knife: tag("tag_knife_fx"),
laser: tag(fx_iw4::FX_LASER_TAG),
tracker_screen: [
tag("tag_screen_tl"),
tag("tag_screen_bl"),
tag("tag_screen_br"),
],
tracker_light: tag("tag_motion_tracker_fx"),
};
let paired_bones = specs[0].0.num_bones + specs[1].0.num_bones;
let parts = parts
+1
View File
@@ -416,6 +416,7 @@ pub const HUD_CHROME_MENUS: &[&str] = &[
"weaponbar_sd",
"hud_fullscreen",
"dpad_hd",
"javelin_overlay_hd",
"dpad_sd",
"splash",
"challenge",
@@ -122,6 +122,8 @@ pub struct WeaponAnimations {
pub melee_charge_time_ms: i32,
pub raise_time_ms: i32,
pub alternate_raise_time_ms: i32,
pub alternate_drop_time_ms: i32,
pub drop_time_ms: i32,
@@ -175,6 +177,8 @@ impl WeaponAnimations {
melee_time_ms: 0,
melee_charge_time_ms: 0,
raise_time_ms: 0,
alternate_raise_time_ms: 0,
alternate_drop_time_ms: 0,
drop_time_ms: 0,
quick_drop_time_ms: 0,
quick_raise_time_ms: 0,
@@ -269,6 +273,8 @@ impl WeaponAnimations {
melee_time_ms: 0,
melee_charge_time_ms: 0,
raise_time_ms: 0,
alternate_raise_time_ms: 0,
alternate_drop_time_ms: 0,
drop_time_ms: 0,
quick_drop_time_ms: 0,
quick_raise_time_ms: 0,
@@ -291,6 +297,8 @@ impl WeaponAnimations {
fn with_registry_facts(mut self, registry: &WeaponRegistry, index: u32) -> Self {
if let Some(facts) = registry.facts_of(index) {
self.alternate_raise_time_ms = facts.alternate_raise_time_ms;
self.alternate_drop_time_ms = facts.alternate_drop_time_ms;
self.melee_time_ms = facts.melee_time_ms;
self.melee_charge_time_ms = facts.melee_charge_time_ms;
}
+287 -14
View File
@@ -30,6 +30,8 @@ pub struct WeaponBodyFacts {
pub raise_time_ms: i32,
pub drop_time_ms: i32,
pub alternate_raise_time_ms: i32,
pub alternate_drop_time_ms: i32,
pub fire_delay_ms: i32,
@@ -426,6 +428,7 @@ pub fn cac_offhand_bucket(offhand_class: i32) -> Option<CacOffhandBucket> {
#[derive(Clone, Debug)]
pub struct CatalogWeapon {
pub name: String,
pub alternate_weapon: Option<String>,
pub weap_def: Option<(u8, u32)>,
@@ -540,6 +543,14 @@ pub struct Iw5ScopeRow {
pub aim_assist: Option<fastfile_iw5::AttachmentAimAssist>,
pub ammunition: Option<fastfile_iw5::AttachmentAmmunition>,
pub damage: Option<fastfile_iw5::AttachmentDamage>,
pub projectile: Option<fastfile_iw5::AttachmentProjectile>,
pub projectile_model: Option<String>,
pub reticle: Option<WeaponReticleAssets>,
pub projectile_explosion_fx: Option<String>,
pub projectile_trail_fx: Option<String>,
pub projectile_ignition_fx: Option<String>,
pub projectile_explosion_sound: Option<String>,
pub projectile_ignition_sound: Option<String>,
pub location_damage: Option<[f32; 19]>,
pub idle_settings: Option<fastfile_iw5::AttachmentIdleSettings>,
pub ads_settings: Option<fastfile_iw5::AttachmentAdsSettings>,
@@ -1122,6 +1133,7 @@ impl CatalogWeapon {
) -> Self {
Self {
name: name.into(),
alternate_weapon: None,
weap_def,
display_name_key: None,
reticle: WeaponReticleAssets::default(),
@@ -1196,11 +1208,21 @@ impl WeaponCatalog {
&mut self,
stream: &fastfile_iw5::ZoneStream<'_>,
geometry: &fastfile_iw5::AttachmentGeometry,
fx_name_at_slot: &dyn Fn(fastfile_iw5::Ptr) -> Option<String>,
) {
let Some(name) = geometry.name.and_then(|ptr| leftover_cstr_iw5(stream, ptr)) else {
return;
};
let facts = &geometry.facts;
let projectile_fx = |x86, x64| {
let body = facts.projectile?.body;
match stream.ptr_at(body, stream.layout(x86, x64)).ok()? {
fastfile_iw5::ZonePtr::Offset(slot) => fx_name_at_slot(stream.resolve_alias(slot)),
_ => None,
}
};
let projectile_sound =
|x86, x64| leftover_iw5_snd_alias(stream, facts.projectile?.body, x86, x64);
let ads_settings = facts.ads_settings;
self.iw5_attachments.insert(
name.clone(),
@@ -1246,9 +1268,36 @@ impl WeaponCatalog {
reload: facts.reload,
add_ons: facts.add_ons,
general: facts.general,
reticle: facts.general.and_then(|general| general.body).map(|body| {
let center = leftover_iw5_material_name(stream, Some(body), 8, 8);
let side = leftover_iw5_material_name(stream, Some(body), 12, 16);
WeaponReticleAssets {
center_authored: center.is_some(),
side_authored: side.is_some(),
center_material: center,
side_material: side,
center_size: stream.i32_at(body, stream.layout(16, 24)).unwrap_or(0),
side_size: stream.i32_at(body, stream.layout(20, 28)).unwrap_or(0),
..Default::default()
}
}),
aim_assist: facts.aim_assist,
ammunition: facts.ammunition,
damage: facts.damage,
projectile: facts.projectile,
projectile_explosion_fx: projectile_fx(40, 48),
projectile_trail_fx: projectile_fx(76, 104),
projectile_ignition_fx: projectile_fx(84, 120),
projectile_explosion_sound: projectile_sound(48, 64),
projectile_ignition_sound: projectile_sound(88, 128),
projectile_model: facts.projectile.and_then(|p| p.model).and_then(|model| {
match stream.ptr_at(model, 0).ok()? {
fastfile_iw5::ZonePtr::Offset(name) => {
leftover_cstr_iw5(stream, stream.resolve_alias(name))
}
_ => None,
}
}),
location_damage: facts.location_damage,
idle_settings: facts.idle_settings,
ads_settings,
@@ -1320,6 +1369,9 @@ impl WeaponCatalog {
.unwrap_or([const { None }; WEAPON_ANIM_SLOTS]);
let hide_tags = read_hide_tags(stream, &self.strings, geometry.hide_tags);
self.entries.push(CatalogWeapon {
alternate_weapon: geometry
.alternate_weapon_name
.and_then(|p| read_name(stream, p)),
name: name.to_owned(),
weap_def: geometry.weap_def.map(ptr_key),
display_name_key: geometry
@@ -1542,6 +1594,8 @@ impl WeaponCatalog {
impact_type: geometry.impact_type,
raise_time_ms: geometry.raise_time_ms,
drop_time_ms: geometry.drop_time_ms,
alternate_raise_time_ms: geometry.alternate_raise_time_ms,
alternate_drop_time_ms: geometry.alternate_drop_time_ms,
fire_delay_ms: geometry.fire_delay_ms,
hold_fire_time_ms: geometry.hold_fire_time_ms,
weap_type: geometry.weap_type,
@@ -2014,6 +2068,9 @@ impl WeaponCatalog {
let leftover_anim_overrides = leftover_iw5_anim_overrides(stream, &geometry);
apply_leftover_default_anim_overrides(&mut sz_xanims, &leftover_anim_overrides);
self.entries.push(CatalogWeapon {
alternate_weapon: geometry
.alternate_weapon_name
.and_then(|p| leftover_cstr_iw5(stream, p)),
reticle_center_slot: leftover_iw5_asset_slot(
stream,
geometry.weap_def,
@@ -2135,6 +2192,11 @@ impl WeaponCatalog {
.map(|arr| read_sz_xanims_t5(stream, arr))
.unwrap_or([const { None }; WEAPON_ANIM_SLOTS]);
self.entries.push(CatalogWeapon {
alternate_weapon: geometry
.alternate_weapon_name
.and_then(|p| stream.cstr(p).ok())
.filter(|name| !name.is_empty())
.map(str::to_owned),
reticle_center_slot: leftover_t5_asset_slot(
stream,
geometry.weap_def,
@@ -2900,6 +2962,8 @@ fn capture_t5_body_facts(
ads_move_speed_scale: geometry.ads_move_speed_scale,
rechamber_time_ms: geometry.rechamber_time_ms,
drop_time_ms: geometry.drop_time_ms,
alternate_raise_time_ms: geometry.alternate_raise_time_ms,
alternate_drop_time_ms: geometry.alternate_drop_time_ms,
raise_time_ms: geometry.raise_time_ms,
bolt_action: geometry.bolt_action,
select_requires_ammo_at_0x667: Some(leftover_t5_select_requires_ammo()),
@@ -3354,6 +3418,8 @@ fn capture_iw5_body_facts(
facts.reload_start_add_time_ms =
i32_at_iw5(stream, body, sz::WEAPON_DEF_RELOAD_START_ADD_TIME_OFF, 984);
facts.reload_end_time_ms = i32_at_iw5(stream, body, sz::WEAPON_DEF_RELOAD_END_TIME_OFF, 988);
facts.alternate_raise_time_ms = geometry.alternate_raise_time_ms;
facts.alternate_drop_time_ms = geometry.alternate_drop_time_ms;
facts.drop_time_ms = i32_at_iw5(stream, body, sz::WEAPON_DEF_DROP_TIME_OFF, 992);
facts.raise_time_ms = i32_at_iw5(stream, body, sz::WEAPON_DEF_RAISE_TIME_OFF, 996);
facts.quick_drop_time_ms = i32_at_iw5(stream, body, sz::WEAPON_DEF_QUICK_DROP_TIME_OFF, 1004);
@@ -4718,6 +4784,8 @@ fn merge_body_facts(dst: &mut WeaponBodyFacts, src: WeaponBodyFacts) {
#[derive(Clone, Debug)]
struct WeaponRow {
name: String,
alternate_weapon: Option<String>,
alternate_index: u32,
namespace: crate::AssetNamespace,
@@ -4829,6 +4897,8 @@ impl Default for WeaponRow {
fn default() -> Self {
Self {
name: String::new(),
alternate_weapon: None,
alternate_index: 0,
namespace: crate::AssetNamespace::Iw4,
facts: WeaponBodyFacts::default(),
weap_def: None,
@@ -4917,6 +4987,7 @@ pub struct WeaponRegistry {
families: crate::WeaponFamilies,
alternate_fpv: HashMap<(u32, u32), [Option<Result<crate::FpvSideAssemblies, String>>; 2]>,
fpv_clip_tracks: Arc<crate::FpvClipTracks>,
revision: u64,
@@ -4991,7 +5062,10 @@ impl WeaponBuild {
.registry
.resolve_iw5_attachment_slots(base_id, &selection.attachments)
.ok()
.and_then(|native| self.registry.compose_iw5_configuration(base_id, native));
.and_then(|native| {
self.registry
.compose_iw5_configuration(base_id, native, false)
});
match row {
Some(row) => prepared.push((base_id, selection, row)),
None => census.refused.push(selection),
@@ -5013,6 +5087,52 @@ impl WeaponBuild {
self.combat_slots.push(slots);
self.registry.rows.push(row);
}
let primary_count = self.registry.rows.len();
for parent in 1..primary_count {
let Some((base, native)) = self.registry.rows[parent].iw5_configuration else {
continue;
};
let Some(mut alternate) = self.registry.compose_iw5_configuration(base, native, true)
else {
continue;
};
let index = self.registry.rows.len() as u32;
let primary = &self.registry.rows[parent];
let underbarrel = native.underbarrel as usize + 5;
let shared = self.registry.rows[base as usize].iw5_attachment_slots[underbarrel]
.as_deref()
.and_then(|name| self.registry.iw5_attachments.get(name))
.is_some_and(|a| a.share_ammo_with_alt);
alternate.facts.ammo_index = if shared {
if primary.facts.ammo_index != 0 {
primary.facts.ammo_index
} else {
parent as i32
}
} else {
(parent as i32) | 0x1000000
};
alternate.facts.clip_index = if shared {
if primary.facts.clip_index != 0 {
primary.facts.clip_index
} else {
parent as i32
}
} else {
(parent as i32) | 0x1000000
};
alternate.prepared_attachments = primary.prepared_attachments.clone();
alternate.alternate_weapon = None;
alternate.alternate_index = 0;
self.registry.rows[parent].alternate_index = index;
self.combat_slots.push(
self.combat_slots
.get(base as usize)
.copied()
.unwrap_or_default(),
);
self.registry.rows.push(alternate);
}
self.registry.rebuild_name_maps();
self.registry.revision = mint_weapon_revision();
census
@@ -5275,11 +5395,38 @@ impl WeaponBuild {
HashMap::new();
let mut tracks = crate::FpvClipTracks::default();
let mut census = FpvAssemblyCensus::default();
let hide_tags: Vec<Vec<String>> = (0..self.registry.rows.len() as u32)
.map(|id| crate::effective_hide_tags(&self.registry, id))
self.registry.alternate_fpv.clear();
let mut variants = Vec::new();
for parent in 1..self.registry.rows.len() as u32 {
let alternate = self.registry.alternate_of(parent);
if alternate != 0
&& self
.registry
.facts_of(alternate)
.is_some_and(|f| f.inventory_type == 3)
{
variants.push((alternate, parent));
}
}
let preparations: Vec<_> = (0..self.registry.rows.len() as u32)
.map(|id| (id, 0))
.chain(variants)
.map(|(id, parent)| {
(
id,
parent,
crate::effective_hide_tags(
&self.registry,
if parent == 0 { id } else { parent },
),
)
})
.collect();
for (row, hide_tags) in self.registry.rows.iter_mut().zip(hide_tags) {
row.fpv_assemblies = [None, None];
for (id, parent, hide_tags) in preparations {
let row = &mut self.registry.rows[id as usize];
if parent == 0 {
row.fpv_assemblies = [None, None];
}
let Some(Ok(mounts)) = &row.fpv_mount_plan else {
continue;
};
@@ -5342,7 +5489,11 @@ impl WeaponBuild {
Err(_) => census.refused += 1,
}
}
row.fpv_assemblies = sides;
if parent == 0 {
row.fpv_assemblies = sides;
} else {
self.registry.alternate_fpv.insert((id, parent), sides);
}
}
census.clip_tables = tracks.len();
self.registry.fpv_clip_tracks = Arc::new(tracks);
@@ -5534,6 +5685,9 @@ impl WeaponBuild {
}
std::collections::hash_map::Entry::Occupied(mut slot) => {
let existing = slot.get_mut();
if existing.alternate_weapon.is_none() {
existing.alternate_weapon = entry.alternate_weapon;
}
if existing.gun_xmodel.is_none() {
existing.gun_xmodel = entry.gun_xmodel;
}
@@ -5638,6 +5792,8 @@ impl WeaponBuild {
combat_slots.push(entry.combat_slots);
rows.push(WeaponRow {
name,
alternate_weapon: entry.alternate_weapon,
alternate_index: 0,
namespace: crate::AssetNamespace::Iw4,
facts: entry.facts,
weap_def: entry.weap_def,
@@ -5714,6 +5870,7 @@ impl WeaponBuild {
by_namespaced: HashMap::new(),
item_groups: HashMap::new(),
families: crate::WeaponFamilies::default(),
alternate_fpv: HashMap::new(),
fpv_clip_tracks: Arc::default(),
revision: mint_weapon_revision(),
};
@@ -6025,6 +6182,7 @@ impl WeaponRegistry {
&self,
base_id: u32,
native: Iw5AttachmentSelection,
alternate: bool,
) -> Option<WeaponRow> {
use fastfile_iw5::size as sz;
let base = self.rows.get(base_id as usize)?;
@@ -6073,10 +6231,49 @@ impl WeaponRegistry {
row.attachment_view_models = view;
row.attachment_world_models = world;
let assets = self.iw5_primary_attachment_assets(base_id, native)?;
apply_iw5_parameter_blocks(&mut row.facts, &assets);
let primary_assets = self.iw5_primary_attachment_assets(base_id, native)?;
let alternate_assets;
let assets = if alternate {
let underbarrel = underbarrel.filter(|asset| asset.weapon_class != 0)?;
row.facts.weap_type = remap_iw5_weap_type(underbarrel.weapon_type);
row.facts.weap_class = underbarrel.weapon_class;
row.facts.inventory_type = 3;
alternate_assets = if underbarrel.share_ammo_with_alt {
primary_assets
} else {
vec![underbarrel]
};
&alternate_assets
} else {
&primary_assets
};
apply_iw5_parameter_blocks(&mut row.facts, assets);
if let Some(reticle) = assets.iter().find_map(|asset| asset.reticle.as_ref()) {
row.reticle = reticle.clone();
}
if alternate {
if let Some(projectile) = iw5_first_block(assets, |asset| asset.projectile) {
row.facts.explosion_radius = projectile.explosion_radius;
row.facts.explosion_inner_damage = projectile.explosion_inner_damage;
row.facts.explosion_outer_damage = projectile.explosion_outer_damage;
row.facts.projectile_speed = projectile.speed;
row.facts.projectile_speed_up = projectile.speed_up;
row.facts.projectile_activate_dist = projectile.activate_distance;
row.facts.projectile_explosion_type = projectile.explosion_type;
row.facts.proj_impact_explode = projectile.impact_explode;
let asset = assets.iter().find(|asset| asset.projectile.is_some())?;
row.projectile_model = asset.projectile_model.clone();
row.combat_fx.explosion_hint = asset.projectile_explosion_fx.clone();
row.proj_trail = asset.projectile_trail_fx.clone();
row.proj_trail_from_slot = row.proj_trail.is_some();
row.proj_ignition = asset.projectile_ignition_fx.clone();
row.proj_ignition_from_slot = row.proj_ignition.is_some();
row.sounds.proj_explosion = asset.projectile_explosion_sound.clone();
row.sounds.proj_ignition_sound = asset.projectile_ignition_sound.clone();
}
}
if let Some(scope) = scope {
if let Some(scope) = assets.iter().find(|asset| asset.overlay.is_some()) {
if let Some(overlay) = scope
.overlay
.as_deref()
@@ -6104,12 +6301,21 @@ impl WeaponRegistry {
let Some(slot) = iw5_anim_tree_type_to_iw4_slot(anim_type) else {
continue;
};
if let Some(anim) = chosen.override_anim.clone() {
if let Some(anim) = if alternate {
chosen.altmode_anim.clone()
} else {
chosen.override_anim.clone()
} {
row.sz_xanims[slot] = Some(anim);
}
if chosen.anim_time_ms > 0 {
let time = if alternate {
chosen.alt_time_ms
} else {
chosen.anim_time_ms
};
if time > 0 {
if let Some(timer) = iw5_anim_timer(&mut row.facts, slot) {
*timer = chosen.anim_time_ms;
*timer = time;
}
}
}
@@ -6131,7 +6337,13 @@ impl WeaponRegistry {
] {
if let Some(sound) = self
.select_iw5_sound_override(base_id, native, sound_type)
.and_then(|chosen| chosen.override_sound.clone())
.and_then(|chosen| {
if alternate {
chosen.altmode_sound.clone()
} else {
chosen.override_sound.clone()
}
})
{
*target = Some(sound);
}
@@ -6145,7 +6357,13 @@ impl WeaponRegistry {
] {
if let Some(fx) = self
.select_iw5_fx_override(base_id, native, fx_type)
.and_then(|chosen| chosen.override_fx.clone())
.and_then(|chosen| {
if alternate {
chosen.altmode_fx.clone()
} else {
chosen.override_fx.clone()
}
})
{
*hint = Some(fx);
}
@@ -6176,6 +6394,15 @@ impl WeaponRegistry {
.insert((row.namespace, row.name.clone()), id as u32);
self.by_name.entry(row.name.clone()).or_insert(id as u32);
}
for row in &mut self.rows {
if let Some(name) = row.alternate_weapon.as_deref() {
row.alternate_index = self
.by_namespaced
.get(&(row.namespace, normalize_weapon_name(name)))
.copied()
.unwrap_or(0);
}
}
}
pub fn reticle_of(&self, index: u32) -> Option<&WeaponReticleAssets> {
@@ -6210,6 +6437,17 @@ impl WeaponRegistry {
.get(&note.to_ascii_lowercase())
}
pub fn notetrack_actions_of(
&self,
index: u32,
) -> impl Iterator<Item = (&str, &LinkedNotetrackAction)> {
self.rows
.get(index as usize)
.into_iter()
.flat_map(|row| row.notetrack_actions.iter())
.map(|(note, action)| (note.as_str(), action))
}
pub fn notetrack_sound_aliases_of(&self, index: u32) -> impl Iterator<Item = &str> {
self.rows
.get(index as usize)
@@ -6304,6 +6542,25 @@ impl WeaponRegistry {
.ok()
}
pub fn alternate_fpv_pairs(&self) -> impl Iterator<Item = (u32, u32)> + '_ {
self.alternate_fpv.keys().copied()
}
pub fn fpv_assemblies_for(
&self,
index: u32,
parent: u32,
axis: bool,
) -> Option<&crate::FpvSideAssemblies> {
if parent != 0 && self.facts_of(index).is_some_and(|f| f.inventory_type == 3) {
return self.alternate_fpv.get(&(index, parent))?[usize::from(axis)]
.as_ref()?
.as_ref()
.ok();
}
self.fpv_assemblies_of(index, axis)
}
pub fn fpv_assemblies_of(&self, index: u32, axis: bool) -> Option<&crate::FpvSideAssemblies> {
self.rows.get(index as usize)?.fpv_assemblies[usize::from(axis)]
.as_ref()?
@@ -6615,6 +6872,20 @@ impl WeaponRegistry {
self.rows.get(index as usize).map(|row| row.facts)
}
pub fn alternate_name_of(&self, index: u32) -> Option<&str> {
self.rows
.get(index as usize)?
.alternate_weapon
.as_deref()
.filter(|name| !name.is_empty())
}
pub fn alternate_of(&self, index: u32) -> u32 {
self.rows
.get(index as usize)
.map_or(0, |row| row.alternate_index)
}
pub fn resolve_index(&self, name: &str) -> Result<Option<u32>, UnknownWeaponName> {
if name.is_empty() {
return Ok(None);
@@ -7476,6 +7747,8 @@ fn iw5_anim_timer(facts: &mut WeaponBodyFacts, slot: usize) -> Option<&mut i32>
weap_anim::RELOAD_EMPTY => &mut facts.reload_empty_time_ms,
weap_anim::RELOAD_START => &mut facts.reload_start_time_ms,
weap_anim::RELOAD_END => &mut facts.reload_end_time_ms,
weap_anim::ALT_RAISE => &mut facts.alternate_raise_time_ms,
weap_anim::ALT_DROP => &mut facts.alternate_drop_time_ms,
weap_anim::RAISE => &mut facts.raise_time_ms,
weap_anim::DROP => &mut facts.drop_time_ms,
weap_anim::QUICK_RAISE => &mut facts.quick_raise_time_ms,
+22 -8
View File
@@ -91,6 +91,7 @@ pub struct MaterialConstant {
#[derive(Clone, Debug)]
pub struct AuthoredShader {
pub namespace: crate::AssetNamespace,
pub name: AssetRef,
pub kind: AssetType,
@@ -646,11 +647,11 @@ impl MaterialCatalog {
}
fn link_shader(&mut self, incoming: AuthoredShader) -> usize {
if let Some(index) = self
.shaders
.iter()
.position(|owned| owned.kind == incoming.kind && owned.name.same_name(&incoming.name))
{
if let Some(index) = self.shaders.iter().position(|owned| {
owned.namespace == incoming.namespace
&& owned.kind == incoming.kind
&& owned.name.same_name(&incoming.name)
}) {
let take_body = AssetRef::incoming_owns_slot(&self.shaders[index].name, &incoming.name)
&& !(incoming.program.is_empty() && !self.shaders[index].program.is_empty());
if take_body {
@@ -1003,7 +1004,9 @@ impl MaterialCatalog {
let shader_ids = shaders
.into_iter()
.map(|shader| {
if let Some(index) = self.real_shader_index(&shader.name, shader.kind) {
if let Some(index) =
self.real_shader_index(shader.namespace, &shader.name, shader.kind)
{
return index;
}
self.link_shader(shader)
@@ -1063,9 +1066,17 @@ impl MaterialCatalog {
.position(|owned| owned.name.is_real() && owned.name.same_name(name))
}
fn real_shader_index(&self, name: &AssetRef, kind: AssetType) -> Option<usize> {
fn real_shader_index(
&self,
namespace: crate::AssetNamespace,
name: &AssetRef,
kind: AssetType,
) -> Option<usize> {
self.shaders.iter().position(|owned| {
owned.kind == kind && owned.name.is_real() && owned.name.same_name(name)
owned.namespace == namespace
&& owned.kind == kind
&& owned.name.is_real()
&& owned.name.same_name(name)
})
}
@@ -1674,6 +1685,7 @@ impl MaterialCatalog {
}
}
Some(self.take_shader_slot(AuthoredShader {
namespace: self.capture_ns,
name,
kind,
program: bytes,
@@ -2613,6 +2625,7 @@ impl MaterialCatalog {
}
}
Some(self.take_shader_slot(AuthoredShader {
namespace: self.capture_ns,
name,
kind,
program: bytes,
@@ -2934,6 +2947,7 @@ impl MaterialCatalog {
_ => return None,
};
Some(self.take_shader_slot(AuthoredShader {
namespace: self.capture_ns,
name,
kind,
program: bytes,
+4 -1
View File
@@ -45,6 +45,7 @@ pub enum StageId {
Pipelines,
/// Consecutive frames drawn with nothing left compiling.
RenderFrames,
FirstPerson,
/// Converting the clips this match's aliases resolve to.
Audio,
/// Baking the bot navigation graph.
@@ -54,7 +55,7 @@ pub enum StageId {
}
impl StageId {
pub const ALL: [Self; 16] = [
pub const ALL: [Self; 17] = [
Self::MapAssets,
Self::CommonAssets,
Self::Localization,
@@ -68,6 +69,7 @@ impl StageId {
Self::GpuTextures,
Self::Pipelines,
Self::RenderFrames,
Self::FirstPerson,
Self::Audio,
Self::Navigation,
Self::Admission,
@@ -88,6 +90,7 @@ impl StageId {
Self::GpuTextures => "gpu_textures",
Self::Pipelines => "pipelines",
Self::RenderFrames => "render_frames",
Self::FirstPerson => "first_person",
Self::Audio => "audio",
Self::Navigation => "navigation",
Self::Admission => "admission",
+30
View File
@@ -409,6 +409,16 @@ fn extract_mesh_tables(
mesh.tri_indices.push(id);
}
}
if let Some(walk_ptr) = g.tri_edge_is_walkable {
let n = g.tri_count.saturating_mul(3).div_ceil(32) * 4;
mesh.tri_edge_is_walkable.reserve(n);
for i in 0..n {
let b = s
.u8_at(walk_ptr, i)
.map_err(|_| ClipCollisionError::Truncated)?;
mesh.tri_edge_is_walkable.push(b);
}
}
extract_mesh_materials(s, g, out)?;
extract_aabb_forest(s, g, out)?;
Ok(())
@@ -1293,6 +1303,16 @@ fn extract_iw5_mesh_tables(
mesh.tri_indices.push(id);
}
}
if let Some(walk_ptr) = g.tri_edge_is_walkable {
let n = g.tri_count.saturating_mul(3).div_ceil(32) * 4;
mesh.tri_edge_is_walkable.reserve(n);
for i in 0..n {
let b = s
.u8_at(walk_ptr, i)
.map_err(|_| ClipCollisionError::Truncated)?;
mesh.tri_edge_is_walkable.push(b);
}
}
extract_iw5_mesh_materials(s, g, out)?;
extract_iw5_aabb_forest(s, g, out)?;
Ok(())
@@ -1776,6 +1796,16 @@ fn extract_t5_mesh_tables(
mesh.tri_indices.push(id);
}
}
if let Some(walk_ptr) = g.tri_edge_is_walkable {
let n = g.tri_count.saturating_mul(3).div_ceil(32) * 4;
mesh.tri_edge_is_walkable.reserve(n);
for i in 0..n {
let b = s
.u8_at(walk_ptr, i)
.map_err(|_| ClipCollisionError::Truncated)?;
mesh.tri_edge_is_walkable.push(b);
}
}
use fastfile_t5::size as t5;
if let Some(materials) = g.materials {
for i in 0..g.material_count {
+11 -1
View File
@@ -508,7 +508,17 @@ impl fastfile_iw5::AssetLinkSink for CommonWalkSink {
stream: &fastfile_iw5::ZoneStream<'_>,
geometry: &fastfile_iw5::AttachmentGeometry,
) -> fastfile_iw5::Result<()> {
self.weapons.capture_iw5_attachment(stream, geometry);
let fx_name_at_slot = |mut slot| {
for _ in 0..8 {
if let Some(name) = self.fx_names_iw5.get(&slot) {
return Some(name.clone());
}
slot = *self.fx_aliases_iw5.get(&slot)?;
}
None
};
self.weapons
.capture_iw5_attachment(stream, geometry, &fx_name_at_slot);
Ok(())
}
@@ -598,6 +598,9 @@ async fn prepare_common(key: CommonKey) -> Arc<CommonSet> {
weapons.set_family_tables(cac_tables.clone());
let iw5_prepared = weapons.prepare_iw5_configurations();
weapons.resolve_fpv_mesh_edges(&fpv_meshes);
weapons.resolve_fpv_hands(&fpv_meshes, &crate::BodyMeshCatalog::default());
weapons.resolve_world_model_edges(&world_weapons);
common_report.push(format!(
"IW5 configurations: prepared={} refused={} {:?}",
iw5_prepared.prepared,
+12 -4
View File
@@ -498,6 +498,7 @@ impl ClipStore {
pub(crate) struct PendingOneshot {
pub namespace: AssetNamespace,
pub alias: String,
pub bound: Option<usize>,
pub variant: usize,
pub volume: f32,
pub pitch: f32,
@@ -596,6 +597,7 @@ pub(crate) fn clip_key_for_variant(
bank: &SoundCatalog,
ns: AssetNamespace,
alias: &str,
bound: Option<usize>,
variant: usize,
loaded_name: Option<&str>,
loaded_ns: Option<AssetNamespace>,
@@ -605,15 +607,21 @@ pub(crate) fn clip_key_for_variant(
{
return Some(ClipKey::Loaded(idx));
}
if let Some(idx) = bank
.sound_in(ns, alias)
let sound = match bound {
Some(index) => bank.sound_at(index),
None => bank.sound_in(ns, alias),
};
if let Some(idx) = sound
.and_then(|s| s.aliases.get(variant))
.and_then(|row| row.loaded.bound_index())
{
return Some(ClipKey::Loaded(idx));
}
bank.streamed_for_variant(ns, alias, variant)
.map(|(sns, dir, name)| ClipKey::Streamed { ns: sns, dir, name })
match bound {
Some(index) => bank.streamed_for_variant_at(index, variant),
None => bank.streamed_for_variant(ns, alias, variant),
}
.map(|(sns, dir, name)| ClipKey::Streamed { ns: sns, dir, name })
}
static WORKERS: AtomicU64 = AtomicU64::new(0);
+199 -35
View File
@@ -1,4 +1,7 @@
use assets::{LinkedNotetrackAction, PreparedWeapons, WeaponSoundSlot};
use std::collections::{HashMap, HashSet};
use std::sync::Arc;
use assets::{PreparedWeapons, WeaponSoundSlot};
use bevy::prelude::*;
use net::{CEntity, EntityEventKind, LocalPresentClient, PresentedSnapshot};
@@ -290,62 +293,223 @@ pub(crate) fn register_entity_event_audio(app: &mut App) {
.add_observer(cg_grenade_contact);
}
#[derive(Clone, Debug)]
enum NotetrackSound {
Bound(usize),
Unbound(String),
}
#[derive(Clone, Debug)]
struct BoundNotetrack {
sound: Option<NotetrackSound>,
rumble: Option<Result<Arc<crate::rumble::Rumble>, String>>,
}
#[derive(Resource, Default)]
pub(crate) struct NotetrackSoundTable {
owner: Option<(Arc<assets::SoundCatalog>, Arc<assets::WeaponRegistry>)>,
actions: HashMap<(u32, String), BoundNotetrack>,
reported: HashSet<(u32, String)>,
}
impl NotetrackSoundTable {
fn owns(
&self,
bank: &Arc<assets::SoundCatalog>,
weapons: &Arc<assets::WeaponRegistry>,
) -> bool {
self.owner
.as_ref()
.is_some_and(|(b, w)| Arc::ptr_eq(b, bank) && Arc::ptr_eq(w, weapons))
}
fn bind(bank: &Arc<assets::SoundCatalog>, weapons: &Arc<assets::WeaponRegistry>) -> Self {
let mut actions = HashMap::new();
let mut unbound = 0usize;
let mut rumbles = HashMap::new();
for weapon in 1..=weapons.len() as u32 {
let namespace = weapons
.namespace_of(weapon)
.unwrap_or(assets::AssetNamespace::Iw4);
for (note, action) in weapons.notetrack_actions_of(weapon) {
let sound = action.sound_alias.as_deref().map(|alias| {
match bank.index_in(namespace, alias) {
Some(index) => NotetrackSound::Bound(index),
None => {
unbound += 1;
NotetrackSound::Unbound(alias.to_owned())
}
}
});
actions.insert(
(weapon, note.to_owned()),
BoundNotetrack {
sound,
rumble: action.rumble_alias.as_ref().map(|alias| {
rumbles
.entry((namespace, alias.clone()))
.or_insert_with(|| {
crate::rumble::Rumble::prepare(bank, namespace, alias)
})
.clone()
}),
},
);
}
}
for ((namespace, alias), rumble) in &rumbles {
if let Err(error) = rumble {
diag::warn!(
Audio,
"audio: {namespace:?} rumble `{alias}` refused during binding: {error}"
);
}
}
diag::info!(
Audio,
"audio: viewmodel notetracks bound to the sound bank: actions={} unbound_sounds={unbound} rumbles={} refused_rumbles={}",
actions.len(),
rumbles.len(),
rumbles.values().filter(|value| value.is_err()).count()
);
Self {
owner: Some((Arc::clone(bank), Arc::clone(weapons))),
actions,
reported: HashSet::new(),
}
}
fn report_once(&mut self, weapon: u32, note: &str, message: impl FnOnce() -> String) {
if self.reported.insert((weapon, note.to_owned())) {
diag::warn!(Audio, "{}", message());
}
}
}
pub(crate) fn bind_notetrack_sounds(
bank: Option<Res<SoundBank>>,
weapons: Option<Res<PreparedWeapons>>,
mut table: ResMut<NotetrackSoundTable>,
) {
let (Some(bank), Some(weapons)) = (bank, weapons) else {
if table.owner.is_some() {
*table = NotetrackSoundTable::default();
}
return;
};
if table.owns(&bank.0, &weapons.0) {
return;
}
*table = NotetrackSoundTable::bind(&bank.0, &weapons.0);
}
pub(crate) fn play_viewmodel_notetrack_messages(
mut notes: MessageReader<crate::ViewmodelNotetracks>,
generation: Res<frame::WorldGeneration>,
presented: Res<net::PresentedSnapshot>,
local: Res<net::LocalPresentClient>,
weapons: Option<Res<PreparedWeapons>>,
mut output: MessageWriter<WeaponSound>,
bank: Option<Res<SoundBank>>,
mut table: ResMut<NotetrackSoundTable>,
mut output: MessageWriter<crate::BoundWeaponSound>,
mut rumbles: MessageWriter<crate::rumble::PlayRumble>,
) {
let bound_bank = bank
.as_deref()
.zip(weapons.as_deref())
.filter(|(bank, weapons)| table.owns(&bank.0, &weapons.0))
.map(|(bank, _)| Arc::clone(&bank.0));
for batch in notes.read() {
let namespace = weapons
.as_deref()
.and_then(|weapons| weapons.0.namespace_of(batch.weapon))
.unwrap_or(assets::AssetNamespace::Iw4);
if batch.generation != *generation
|| batch.client != local.0
|| !presented
.snapshot()
.and_then(|snapshot| snapshot.meta.for_client(local.0))
.is_some_and(|meta| {
meta.life_sequence == batch.life
&& meta.lifecycle == sim::ClientLifecycle::Alive
})
{
continue;
}
for name in &batch.names {
let action = weapons
.as_deref()
.and_then(|weapons| weapons.0.notetrack_action_of(batch.weapon, name));
apply_viewmodel_notetrack(name, action, namespace, &mut output);
apply_viewmodel_notetrack(
batch.weapon,
name,
bound_bank.as_deref(),
&mut table,
&mut output,
&mut rumbles,
);
}
}
}
fn apply_viewmodel_notetrack(
weapon: u32,
note: &str,
action: Option<&LinkedNotetrackAction>,
namespace: assets::AssetNamespace,
output: &mut MessageWriter<WeaponSound>,
bank: Option<&assets::SoundCatalog>,
table: &mut NotetrackSoundTable,
output: &mut MessageWriter<crate::BoundWeaponSound>,
rumbles: &mut MessageWriter<crate::rumble::PlayRumble>,
) {
if note.eq_ignore_ascii_case("end") {
return;
}
if note.eq_ignore_ascii_case("NVG_on_powerup") || note.eq_ignore_ascii_case("NVG_off_powerdown")
{
diag::warn!(
Audio,
"audio: NVG notetrack `{note}` has no cgMedia alias (typed gap)"
);
table.report_once(weapon, note, || {
format!("audio: NVG notetrack `{note}` has no cgMedia alias (typed gap)")
});
}
let rumble = action.and_then(|action| action.rumble_alias.as_deref());
if let Some(rumble) = rumble {
diag::warn!(
Audio,
"audio: notetrack rumble `{rumble}` is not ported (typed gap)"
);
}
if let Some(alias) = action.and_then(|action| action.sound_alias.as_deref()) {
output.write(WeaponSound {
namespace,
alias: alias.to_owned(),
origin_inches: None,
snd_ent: Some(crate::SND_ENT_LOCAL),
let Some(bank) = bank else {
table.report_once(weapon, note, || {
format!("audio: notetrack `{note}` dropped — notetracks are not bound to the installed sound bank")
});
return;
};
let key = (weapon, note.to_ascii_lowercase());
let Some(action) = table.actions.get(&key).cloned() else {
table.report_once(weapon, note, || {
format!("audio: notetrack `{note}` has no sound or rumble mapping")
});
return;
};
if let Some(rumble) = &action.rumble {
match rumble {
Ok(rumble) => {
rumbles.write(crate::rumble::PlayRumble {
bank_revision: bank.revision(),
rumble: Arc::clone(rumble),
});
}
Err(error) => table.report_once(weapon, note, || {
format!("audio: notetrack `{note}` rumble refused: {error}")
}),
}
}
if rumble.is_none() {
diag::warn!(
Audio,
"audio: notetrack `{note}` has no sound or rumble mapping"
);
match action.sound {
Some(NotetrackSound::Bound(index)) => {
output.write(crate::BoundWeaponSound {
bank_revision: bank.revision(),
index,
origin_inches: None,
snd_ent: Some(crate::SND_ENT_LOCAL),
});
}
Some(NotetrackSound::Unbound(alias)) => {
table.report_once(weapon, note, || {
format!(
"audio: notetrack `{note}` sound `{alias}` is not in the sound bank (refused when bound)"
)
});
}
None if action.rumble.is_none() => {
table.report_once(weapon, note, || {
format!("audio: notetrack `{note}` has no sound or rumble mapping")
});
}
None => {}
}
}
+5 -2
View File
@@ -11,6 +11,7 @@ mod pcm;
mod playback;
mod plugin;
pub mod policy;
mod rumble;
mod shellshock;
mod space;
mod start;
@@ -30,8 +31,8 @@ pub use emit::{BobCycleTracker, emit_footstep_on_bob_wrap, emit_weapon_fire};
pub use frontend::FrontendAudio;
pub use match_set::{AudioReady, AudioSilent};
pub use messages::{
AliasCommand, Footstep, LandSound, PlayAlias, SND_ENT_LOCAL, ViewmodelNotetracks, WeaponSound,
snd_ent_from_number,
AliasCommand, BoundWeaponSound, Footstep, LandSound, PlayAlias, SND_ENT_LOCAL,
ViewmodelNotetracks, WeaponSound, snd_ent_from_number,
};
pub use pcm::{LivePan, LoopingPcmAudio, PcmAudio, decode_audio_bytes};
pub use playback::{
@@ -49,3 +50,5 @@ pub use voice::VoiceOccupancy;
mod destructible_loops;
mod map_doors;
mod match_set;
mod weapon_lock;
+3
View File
@@ -225,6 +225,9 @@ fn queue_match_clips(
}
for alias in MATCH_HUD_PULSE
.iter()
.chain([&"motiontracker_ping", &"motiontracker_pong"])
.chain(crate::weapon_lock::ALIASES.iter())
.chain([&gamemode_iw4::damage_feedback::HIT_ALERT_ALIAS])
.chain(MATCH_CLOCK)
.chain(crate::objectives::EFFECTS)
{
+15
View File
@@ -17,6 +17,10 @@ pub struct PlayAlias {
#[derive(Message, Clone, Debug)]
pub enum AliasCommand {
Play(PlayAlias),
PlayPitched {
sound: PlayAlias,
pitch: f32,
},
Stop {
namespace: AssetNamespace,
alias: String,
@@ -40,8 +44,19 @@ pub struct WeaponSound {
pub snd_ent: Option<u32>,
}
#[derive(Message, Clone, Debug)]
pub struct BoundWeaponSound {
pub bank_revision: u64,
pub index: usize,
pub origin_inches: Option<[f32; 3]>,
pub snd_ent: Option<u32>,
}
#[derive(Message, Clone, Debug)]
pub struct ViewmodelNotetracks {
pub generation: frame::WorldGeneration,
pub client: sim::ClientId,
pub life: sim::LifeSequence,
pub weapon: u32,
pub names: Vec<String>,
}
+154 -11
View File
@@ -19,7 +19,8 @@ use crate::clip_store::{
clip_keys_for_alias, deadline_for,
};
use crate::messages::{
AliasCommand, Footstep, LandSound, PlayAlias, SND_ENT_LOCAL, ViewmodelNotetracks, WeaponSound,
AliasCommand, BoundWeaponSound, Footstep, LandSound, PlayAlias, SND_ENT_LOCAL,
ViewmodelNotetracks, WeaponSound,
};
use crate::pcm::{LoopingPcmAudio, PcmAudio};
use crate::space::{distance_inches, transform_inches};
@@ -157,7 +158,9 @@ impl Plugin for PlayerSoundPlugin {
.add_message::<AliasCommand>()
.add_message::<Footstep>()
.add_message::<WeaponSound>()
.add_message::<BoundWeaponSound>()
.add_message::<ViewmodelNotetracks>()
.init_resource::<crate::entity_events::NotetrackSoundTable>()
.add_message::<LandSound>()
.add_systems(
Update,
@@ -168,6 +171,7 @@ impl Plugin for PlayerSoundPlugin {
.before(play_alias_messages)
.before(play_footstep_messages)
.before(play_weapon_sound_messages)
.before(play_bound_weapon_sounds)
.before(play_land_sound_messages),
drain_pending_oneshots
.after(play_alias_messages)
@@ -181,8 +185,10 @@ impl Plugin for PlayerSoundPlugin {
play_alias_messages.after(FxSoundPublished),
play_footstep_messages,
crate::entity_events::play_viewmodel_notetrack_messages
.before(play_weapon_sound_messages),
.before(play_weapon_sound_messages)
.before(play_bound_weapon_sounds),
play_weapon_sound_messages,
play_bound_weapon_sounds.after(drain_pending_oneshots),
play_land_sound_messages,
crate::map_doors::update.before(crate::ambient::update_map_emitter_gain),
crate::destructible_loops::update
@@ -199,6 +205,8 @@ impl Plugin for PlayerSoundPlugin {
.after(crate::ambient::stop_map_ambient_on_match_end),
crate::ambient::install_sound_bank
.after(crate::ambient::start_sound_bank_compose),
crate::entity_events::bind_notetrack_sounds
.after(crate::ambient::install_sound_bank),
crate::ambient::stop_map_ambient_on_match_end.after(SessionSwapApplied),
reset_clip_prep_on_match_torn_down,
)
@@ -312,8 +320,9 @@ fn play_alias_messages(
let pose = listener_pose(&listeners);
let iwd = iwd.as_deref().map(|s| s.0.as_ref());
for command in events.read() {
let event = match command {
AliasCommand::Play(event) => event,
let (event, pitch_scale) = match command {
AliasCommand::Play(event) => (event, 1.0),
AliasCommand::PlayPitched { sound, pitch } => (sound, *pitch),
AliasCommand::Stop {
namespace,
alias,
@@ -348,7 +357,7 @@ fn play_alias_messages(
drop_without_bank(std::iter::once(event.alias.as_str()), &mut decisions);
continue;
};
let outcome = play_alias_oneshot(
let outcome = play_oneshot_recorded(
&mut commands,
&mut pcm_assets,
&mut shared,
@@ -356,6 +365,7 @@ fn play_alias_messages(
iwd,
event.namespace,
&event.alias,
None,
event.origin_inches,
pose,
&mut pick,
@@ -366,13 +376,14 @@ fn play_alias_messages(
event.snd_ent,
SoundClass::World,
epoch.0,
pitch_scale,
);
finish_binding_start(
outcome,
&event.alias,
event.fallback.as_deref(),
|fb| {
play_alias_oneshot(
play_oneshot_recorded(
&mut commands,
&mut pcm_assets,
&mut shared,
@@ -380,6 +391,7 @@ fn play_alias_messages(
iwd,
event.namespace,
fb,
None,
event.origin_inches,
pose,
&mut pick,
@@ -390,6 +402,7 @@ fn play_alias_messages(
event.snd_ent,
SoundClass::World,
epoch.0,
pitch_scale,
)
},
&mut gaps,
@@ -494,6 +507,63 @@ fn play_weapon_sound_messages(
}
}
fn play_bound_weapon_sounds(
mut events: MessageReader<BoundWeaponSound>,
mut commands: Commands,
mut pcm_assets: ResMut<Assets<PcmAudio>>,
mut shared: ResMut<SharedPlayAssets>,
mut pick: ResMut<SoundPickState>,
mut gaps: ResMut<MissingAliasGaps>,
mut clips: Option<ResMut<ClipStore>>,
mut pending: ResMut<PendingStarts>,
mut occupancy: ResMut<VoiceOccupancy>,
mut decisions: ResMut<StartDecisions>,
bank: Option<Res<SoundBank>>,
iwd: Option<Res<SoundIwd>>,
epoch: Res<MatchEpoch>,
listeners: Query<&Transform, With<AmbientListener>>,
) {
let Some(bank) = bank else {
for _ in events.read() {}
return;
};
let pose = listener_pose(&listeners);
let iwd = iwd.as_deref().map(|s| s.0.as_ref());
for event in events.read() {
if event.bank_revision != bank.0.revision() {
continue;
}
let Some(alias) = bank.0.name_at(event.index) else {
continue;
};
let namespace = bank.0.namespace_of_alias(event.index);
let outcome = play_oneshot_recorded(
&mut commands,
&mut pcm_assets,
&mut shared,
&bank.0,
iwd,
namespace,
alias,
Some(event.index),
event.origin_inches,
pose,
&mut pick,
clips.as_deref_mut(),
&mut pending,
&mut occupancy,
&mut decisions,
event.snd_ent,
SoundClass::Weapon,
epoch.0,
1.0,
);
if outcome.allows_binding_fallback() {
gaps.record(alias);
}
}
}
fn play_land_sound_messages(
mut events: MessageReader<LandSound>,
mut commands: Commands,
@@ -722,6 +792,7 @@ fn drain_pending_oneshots(
iwd,
entry.namespace,
&entry.alias,
entry.bound,
entry.origin_inches,
pose,
&mut pick,
@@ -780,6 +851,51 @@ pub(crate) fn play_alias_oneshot(
snd_ent: Option<u32>,
class: SoundClass,
epoch: u64,
) -> StartOutcome {
play_oneshot_recorded(
commands,
pcm_assets,
shared,
bank,
iwd,
namespace,
alias,
None,
origin_inches,
listener,
pick,
clips,
pending,
occupancy,
decisions,
snd_ent,
class,
epoch,
1.0,
)
}
#[allow(clippy::too_many_arguments)]
fn play_oneshot_recorded(
commands: &mut Commands,
pcm_assets: &mut Assets<PcmAudio>,
shared: &mut SharedPlayAssets,
bank: &SoundCatalog,
iwd: Option<&NamespaceSoundIwd>,
namespace: AssetNamespace,
alias: &str,
bound: Option<usize>,
origin_inches: Option<[f32; 3]>,
listener: Option<(Vec3, Vec3)>,
pick: &mut SoundPickState,
clips: Option<&mut ClipStore>,
pending: &mut PendingStarts,
occupancy: &mut VoiceOccupancy,
decisions: &mut StartDecisions,
snd_ent: Option<u32>,
class: SoundClass,
epoch: u64,
pitch_scale: f32,
) -> StartOutcome {
let started = play_alias_oneshot_at(
commands,
@@ -789,6 +905,7 @@ pub(crate) fn play_alias_oneshot(
iwd,
namespace,
alias,
bound,
origin_inches,
listener,
pick,
@@ -799,6 +916,7 @@ pub(crate) fn play_alias_oneshot(
0,
class,
epoch,
pitch_scale,
);
if let Some((sec, sec_out)) = &started.secondary
&& !sec_out.is_open()
@@ -920,6 +1038,7 @@ fn play_alias_oneshot_at(
iwd: Option<&NamespaceSoundIwd>,
namespace: AssetNamespace,
alias: &str,
bound: Option<usize>,
origin_inches: Option<[f32; 3]>,
listener: Option<(Vec3, Vec3)>,
pick: &mut SoundPickState,
@@ -930,12 +1049,17 @@ fn play_alias_oneshot_at(
depth: u8,
class: SoundClass,
epoch: u64,
pitch_scale: f32,
) -> OneshotStart {
let avoid = pick
.last_variant
.get(&(namespace, alias.to_owned()))
.copied();
let Some(outcome) = bank.pick_loaded_outcome(namespace, alias, &mut pick.lcg, avoid) else {
let picked = match bound {
Some(index) => bank.pick_loaded_outcome_at(index, &mut pick.lcg, avoid),
None => bank.pick_loaded_outcome(namespace, alias, &mut pick.lcg, avoid),
};
let Some(outcome) = picked else {
return OneshotStart::failed(StartFailure::MissingAlias);
};
let variant_index = outcome.variant_index;
@@ -948,6 +1072,7 @@ fn play_alias_oneshot_at(
bank,
namespace,
alias,
bound,
variant_index,
loaded_name,
loaded_ns,
@@ -957,14 +1082,22 @@ fn play_alias_oneshot_at(
let (volume, pitch, layer) = if let Some(picked) = &outcome.picked {
(picked.volume, picked.pitch, picked.layer.clone())
} else {
let row = bank
.sound_in(namespace, alias)
.and_then(|s| s.aliases.get(variant_index));
let row = match bound {
Some(index) => bank.sound_at(index),
None => bank.sound_in(namespace, alias),
}
.and_then(|s| s.aliases.get(variant_index));
let (volume, pitch) = row
.map(|row| streamed_row_volume_pitch(row, &mut pick.lcg))
.unwrap_or((1.0, 1.0));
(volume, pitch, row.and_then(|r| r.secondary.clone()))
};
let pitch = pitch
* if pitch_scale.is_finite() && pitch_scale > 0.0 {
pitch_scale
} else {
1.0
};
let pcm = match take_or_pending_clip(
bank,
clips.as_deref_mut(),
@@ -972,6 +1105,7 @@ fn play_alias_oneshot_at(
clip.clone(),
namespace,
alias,
bound,
variant_index,
volume,
pitch,
@@ -1002,6 +1136,7 @@ fn play_alias_oneshot_at(
iwd,
namespace,
alias,
bound,
origin_inches,
listener,
pick,
@@ -1034,6 +1169,7 @@ fn take_or_pending_clip(
clip: ClipKey,
namespace: AssetNamespace,
alias: &str,
bound: Option<usize>,
variant: usize,
volume: f32,
pitch: f32,
@@ -1059,6 +1195,7 @@ fn take_or_pending_clip(
pending.push_oneshot(PendingOneshot {
namespace,
alias: alias.to_owned(),
bound,
variant,
volume,
pitch,
@@ -1086,6 +1223,7 @@ fn submit_prepared_oneshot(
iwd: Option<&NamespaceSoundIwd>,
namespace: AssetNamespace,
alias: &str,
bound: Option<usize>,
origin_inches: Option<[f32; 3]>,
listener: Option<(Vec3, Vec3)>,
pick: &mut SoundPickState,
@@ -1103,7 +1241,10 @@ fn submit_prepared_oneshot(
pitch: f32,
layer: Option<&str>,
) -> OneshotStart {
let sound = bank.sound_in(namespace, alias);
let sound = match bound {
Some(index) => bank.sound_at(index),
None => bank.sound_in(namespace, alias),
};
let row = sound.and_then(|s| s.aliases.get(variant_index));
let channel = sound.and_then(|s| s.ent_channel(variant_index));
let mut world_detail: Option<String> = None;
@@ -1299,6 +1440,7 @@ fn play_secondary_layer(
iwd,
namespace,
sec,
None,
origin_inches,
listener,
pick,
@@ -1309,6 +1451,7 @@ fn play_secondary_layer(
depth + 1,
class,
epoch,
1.0,
);
Some((sec.to_owned(), started.outcome))
}
+2
View File
@@ -12,5 +12,7 @@ impl Plugin for AudioPlugin {
crate::frontend::register_frontend_audio(app);
crate::policy::music::register_script_music(app);
crate::entity_events::register_entity_event_audio(app);
crate::weapon_lock::register(app);
crate::rumble::register(app);
}
}
+224
View File
@@ -0,0 +1,224 @@
use std::sync::Arc;
use bevy::input::gamepad::{GamepadRumbleIntensity, GamepadRumbleRequest};
use bevy::prelude::*;
use net::{CgFrameClock, LocalPresentClient, PresentedSnapshot};
#[derive(Clone, Debug)]
pub(crate) struct Rumble {
duration_ms: i32,
low: Vec<(f32, f32)>,
high: Vec<(f32, f32)>,
}
impl Rumble {
pub(crate) fn prepare(
bank: &assets::SoundCatalog,
namespace: assets::AssetNamespace,
name: &str,
) -> Result<Arc<Self>, String> {
let text = |name: &str| -> Result<&str, String> {
let key = format!("rumble/{name}");
let bytes = bank
.rawfiles
.get(&(namespace, key.clone()))
.ok_or_else(|| format!("missing {key}"))?;
std::str::from_utf8(bytes).map_err(|_| format!("invalid text in {key}"))
};
let definition = text(name)?.trim_end_matches('\0');
let mut fields = definition.split('\\');
if fields.next() != Some("RUMBLE") {
return Err(format!("invalid rumble header: {name}"));
}
let mut duration = None;
let mut low = None;
let mut high = None;
while let Some(key) = fields.next() {
let value = fields
.next()
.ok_or_else(|| format!("missing {key} value: {name}"))?;
match key {
"duration" => duration = value.parse::<f32>().ok(),
"lowRumbleFile" => low = Some(value),
"highRumbleFile" => high = Some(value),
_ => {}
}
}
let duration = duration
.filter(|d| d.is_finite() && *d > 0.0)
.ok_or_else(|| format!("invalid rumble duration: {name}"))?;
let graph = |name: Option<&str>| -> Result<Vec<(f32, f32)>, String> {
let name = name
.filter(|name| !name.is_empty())
.ok_or_else(|| "missing rumble graph name".to_owned())?;
let mut tokens = text(name)?.trim_end_matches('\0').split_whitespace();
if tokens.next() != Some("RUMBLEGRAPHFILE") {
return Err(format!("invalid rumble graph header: {name}"));
}
let count = tokens
.next()
.and_then(|n| n.parse::<usize>().ok())
.filter(|&n| n > 0 && n <= 16)
.ok_or_else(|| format!("invalid rumble graph count: {name}"))?;
let mut points = Vec::with_capacity(count);
for _ in 0..count {
let mut number = || {
tokens
.next()
.and_then(|n| n.parse::<f32>().ok())
.filter(|v| v.is_finite())
.ok_or_else(|| format!("invalid rumble graph point: {name}"))
};
let x = number()?;
let y = number()?;
if points.last().is_some_and(|&(previous, _)| previous > x) {
return Err(format!("unordered rumble graph: {name}"));
}
points.push((x, y));
}
Ok(points)
};
Ok(Arc::new(Self {
duration_ms: (duration * 1000.0).max(1.0) as i32,
low: graph(low)?,
high: graph(high)?,
}))
}
fn intensity(&self, elapsed: i32) -> GamepadRumbleIntensity {
let fraction = (elapsed as f32 / self.duration_ms as f32).clamp(0.0, 1.0);
GamepadRumbleIntensity {
strong_motor: sample(&self.low, fraction),
weak_motor: sample(&self.high, fraction),
}
}
}
fn sample(points: &[(f32, f32)], fraction: f32) -> f32 {
let mut previous = points[0];
if fraction <= previous.0 {
return previous.1.clamp(0.0, 1.0);
}
for &next in &points[1..] {
if fraction <= next.0 {
let t = if next.0 > previous.0 {
(fraction - previous.0) / (next.0 - previous.0)
} else {
1.0
};
return (previous.1 + (next.1 - previous.1) * t).clamp(0.0, 1.0);
}
previous = next;
}
previous.1.clamp(0.0, 1.0)
}
#[derive(Message)]
pub(crate) struct PlayRumble {
pub bank_revision: u64,
pub rumble: Arc<Rumble>,
}
#[derive(Default)]
struct RumblePlayback {
owner: Option<(
frame::WorldGeneration,
sim::ClientId,
sim::LifeSequence,
u64,
)>,
last_time: i32,
active: Vec<(i32, Arc<Rumble>)>,
output: Option<(Entity, GamepadRumbleIntensity)>,
}
pub(crate) fn register(app: &mut App) {
app.add_message::<PlayRumble>().add_systems(
Update,
update
.in_set(net::ClientSet::Effects)
.after(crate::entity_events::play_viewmodel_notetrack_messages),
);
}
fn update(
mut requests: MessageReader<PlayRumble>,
bank: Option<Res<crate::SoundBank>>,
presented: Res<PresentedSnapshot>,
local: Res<LocalPresentClient>,
generation: Res<frame::WorldGeneration>,
clock: Res<CgFrameClock>,
gamepads: Query<Entity, With<Gamepad>>,
mut state: Local<RumblePlayback>,
mut output: MessageWriter<GamepadRumbleRequest>,
) {
let now = clock.time();
let owner = bank.as_ref().and_then(|bank| {
let meta = presented.snapshot()?.meta.for_client(local.0)?;
(meta.lifecycle == sim::ClientLifecycle::Alive).then_some((
*generation,
local.0,
meta.life_sequence,
bank.0.revision(),
))
});
if owner != state.owner || now < state.last_time {
state.active.clear();
if let Some((gamepad, _)) = state.output.take() {
output.write(GamepadRumbleRequest::Stop { gamepad });
}
state.owner = owner;
}
state.last_time = now;
state
.active
.retain(|(start, rumble)| now.saturating_sub(*start) < rumble.duration_ms);
for request in requests.read() {
if owner.is_none_or(|owner| owner.3 != request.bank_revision) {
continue;
}
if state.active.len() == 32 {
if let Some((index, _)) = state
.active
.iter()
.enumerate()
.min_by_key(|(_, (start, rumble))| start.saturating_add(rumble.duration_ms))
{
state.active.swap_remove(index);
}
}
state.active.push((now, Arc::clone(&request.rumble)));
}
let mut intensity = GamepadRumbleIntensity {
strong_motor: 0.0,
weak_motor: 0.0,
};
for (start, rumble) in &state.active {
let current = rumble.intensity(now.saturating_sub(*start));
intensity.strong_motor = intensity.strong_motor.max(current.strong_motor);
intensity.weak_motor = intensity.weak_motor.max(current.weak_motor);
}
let selected = gamepads.iter().min_by_key(|entity| entity.to_bits());
if state
.output
.is_some_and(|(entity, _)| Some(entity) != selected)
{
if let Some((gamepad, _)) = state.output.take() {
output.write(GamepadRumbleRequest::Stop { gamepad });
}
}
if let Some(gamepad) = selected {
let zero = intensity.strong_motor == 0.0 && intensity.weak_motor == 0.0;
if !zero || state.output != Some((gamepad, intensity)) {
output.write(GamepadRumbleRequest::Stop { gamepad });
if !zero {
output.write(GamepadRumbleRequest::Add {
gamepad,
duration: std::time::Duration::from_millis(100),
intensity,
});
}
}
state.output = Some((gamepad, intensity));
}
}
+77
View File
@@ -0,0 +1,77 @@
use bevy::prelude::*;
use net::{CgFrameClock, LocalPresentClient, PresentedSnapshot};
use crate::{AliasCommand, PlayAlias, SND_ENT_LOCAL};
pub(crate) const ALIASES: [&str; 2] = ["javelin_clu_aquiring_lock", "javelin_clu_lock"];
#[derive(Default)]
struct LockAudio {
owner: Option<(
frame::WorldGeneration,
sim::ClientId,
sim::LifeSequence,
u32,
)>,
stage: u8,
last_time: i32,
next_ping: i32,
}
pub(crate) fn register(app: &mut App) {
app.add_systems(Update, present.in_set(net::ClientSet::Effects));
}
fn present(
presented: Res<PresentedSnapshot>,
local: Res<LocalPresentClient>,
generation: Res<frame::WorldGeneration>,
clock: Res<CgFrameClock>,
mut cursor: Local<LockAudio>,
mut commands: MessageWriter<AliasCommand>,
) {
let state = presented.snapshot().and_then(|snapshot| {
let meta = snapshot.meta.for_client(local.0)?;
let ps = presented.player(local.0)?;
(meta.lifecycle == sim::ClientLifecycle::Alive && ps.other_flags & 0x400 == 0).then_some((
(*generation, local.0, meta.life_sequence, ps.weapon),
if meta.weapon_lock.weapon == ps.weapon {
meta.weapon_lock.flags & 3
} else {
0
},
))
});
let now = clock.time();
let owner = state.map(|(owner, _)| owner);
let stage = state.map_or(0, |(_, stage)| stage);
if cursor.owner != owner || cursor.stage != stage || now < cursor.last_time {
if cursor.stage != 0 {
for alias in ALIASES {
commands.write(AliasCommand::Stop {
namespace: assets::AssetNamespace::Iw4,
alias: alias.to_owned(),
snd_ent: Some(SND_ENT_LOCAL),
});
}
}
cursor.owner = owner;
cursor.stage = stage;
cursor.next_ping = now;
}
cursor.last_time = now;
if stage != 0 && now >= cursor.next_ping {
commands.write(AliasCommand::Play(PlayAlias {
namespace: assets::AssetNamespace::Iw4,
alias: ALIASES[usize::from(stage & 2 != 0)].to_owned(),
fallback: None,
origin_inches: None,
snd_ent: Some(SND_ENT_LOCAL),
}));
cursor.next_ping = if stage & 2 != 0 {
i32::MAX
} else {
now.saturating_add(600)
};
}
}
+1 -1
View File
@@ -6,7 +6,7 @@ use sim::SimBrush;
use crate::observation::ModeObjective;
use crate::query::{QueryResult, QuerySubsystem, WalkSample, WorldQuery};
pub const NAV_SCHEMA: u32 = 7;
pub const NAV_SCHEMA: u32 = 8;
pub const GRID_IN: f32 = 48.0;
pub const SNAP_IN: f32 = 256.0;
pub const STEP_Z_IN: f32 = 18.0;
+119 -33
View File
@@ -6,6 +6,8 @@ use crate::TraceExtents;
const SURFACE_CLIP_EPSILON: f32 = 0.125;
const EQUAL_EPSILON: f32 = 0.001;
pub const VERTS_PER_SEGMENT: usize = 1024;
const WALKABLE_NORMAL_Z: f32 = 0.7;
const CONTENTS_SOLID: u32 = 1;
@@ -47,6 +49,8 @@ pub struct ClipMeshTables {
pub tri_indices: Vec<u16>,
pub tri_edge_is_walkable: Vec<u8>,
pub tri_surface_flags: Vec<u32>,
pub tri_content_flags: Vec<u32>,
@@ -62,6 +66,7 @@ impl ClipMeshTables {
ClipMeshRef {
verts: &self.verts,
tri_indices: &self.tri_indices,
tri_edge_is_walkable: &self.tri_edge_is_walkable,
tri_surface_flags: &self.tri_surface_flags,
tri_content_flags: &self.tri_content_flags,
aabb_trees: &self.aabb_trees,
@@ -81,6 +86,10 @@ pub struct ClipMeshRef<'a> {
pub tri_indices: &'a [u16],
/// One bit per triangle edge, `3 * tri + k`, where k = 0 is v1-v2, 1 is
/// v0-v2 and 2 is v0-v1.
pub tri_edge_is_walkable: &'a [u8],
pub tri_surface_flags: &'a [u32],
pub tri_content_flags: &'a [u32],
@@ -100,6 +109,7 @@ impl<'a> ClipMeshRef<'a> {
Self {
verts,
tri_indices,
tri_edge_is_walkable: &[],
tri_surface_flags,
tri_content_flags: &[],
aabb_trees: &[],
@@ -245,7 +255,7 @@ pub fn trace_through_mesh_into(
start[1] + delta[1] * best.fraction - cap.offset[1],
start[2] + delta[2] * best.fraction - cap.offset[2],
];
if best.walkable == 0 {
if best.walkable == 0 && best.startsolid == 0 {
best.walkable = u8::from(best.normal[2] >= WALKABLE_NORMAL_Z);
}
} else {
@@ -380,6 +390,12 @@ fn test_tri(
cflags
};
census.tris_tested = census.tris_tested.saturating_add(1);
let edge_walkable = [0, 1, 2].map(|k| {
let bit = ti * 3 + k;
mesh.tri_edge_is_walkable
.get(bit >> 3)
.is_some_and(|b| b & (1 << (bit & 7)) != 0)
});
capsule_through_triangle(
cap,
start,
@@ -387,6 +403,7 @@ fn test_tri(
v0,
v1,
v2,
edge_walkable,
mesh.tri_surface_flags.get(ti).copied().unwrap_or(0),
contents,
best,
@@ -425,6 +442,7 @@ fn capsule_through_triangle(
v0: [f32; 3],
v1: [f32; 3],
v2: [f32; 3],
edge_walkable: [bool; 3],
surface_flags: u32,
contents: u32,
trace: &mut Trace,
@@ -484,6 +502,7 @@ fn capsule_through_triangle(
let trace_plane = cross(delta, plane_pt);
let mut missed_edge = false;
let mut vert_to_check: Option<[f32; 3]> = None;
let mut vert_walkable = true;
let neg_v = dot(trace_plane, v0_v1);
if neg_v < 0.0 {
@@ -498,9 +517,18 @@ fn capsule_through_triangle(
contents,
trace,
) {
EdgeHit::Hits => return,
EdgeHit::MayV0 => vert_to_check = Some(v0),
EdgeHit::MayV1 => vert_to_check = Some(v1),
EdgeHit::Hits => {
trace.walkable = u8::from(edge_walkable[2]);
return;
}
EdgeHit::MayV0 => {
vert_to_check = Some(v0);
vert_walkable &= edge_walkable[2];
}
EdgeHit::MayV1 => {
vert_to_check = Some(v1);
vert_walkable &= edge_walkable[2];
}
EdgeHit::Miss => {}
}
}
@@ -517,9 +545,18 @@ fn capsule_through_triangle(
contents,
trace,
) {
EdgeHit::Hits => return,
EdgeHit::MayV0 => vert_to_check = Some(v0),
EdgeHit::MayV1 => vert_to_check = Some(v2),
EdgeHit::Hits => {
trace.walkable = u8::from(edge_walkable[1]);
return;
}
EdgeHit::MayV0 => {
vert_to_check = Some(v0);
vert_walkable &= edge_walkable[1];
}
EdgeHit::MayV1 => {
vert_to_check = Some(v2);
vert_walkable &= edge_walkable[1];
}
EdgeHit::Miss => {}
}
}
@@ -536,9 +573,18 @@ fn capsule_through_triangle(
contents,
trace,
) {
EdgeHit::Hits => return,
EdgeHit::MayV0 => vert_to_check = Some(v1),
EdgeHit::MayV1 => vert_to_check = Some(v2),
EdgeHit::Hits => {
trace.walkable = u8::from(edge_walkable[0]);
return;
}
EdgeHit::MayV0 => {
vert_to_check = Some(v1);
vert_walkable &= edge_walkable[0];
}
EdgeHit::MayV1 => {
vert_to_check = Some(v2);
vert_walkable &= edge_walkable[0];
}
EdgeHit::Miss => {}
}
}
@@ -550,6 +596,7 @@ fn capsule_through_triangle(
sphere_start,
delta,
vert,
vert_walkable,
surface_flags,
contents,
trace,
@@ -569,7 +616,7 @@ fn capsule_through_triangle(
trace.surface_flags = surface_flags;
trace.hit_type = HITTYPE_ENTITY;
trace.hit_id = ENTITYNUM_WORLD;
trace.walkable = u8::from(normal[2] >= WALKABLE_NORMAL_Z);
trace.walkable = 0;
}
#[derive(Clone, Copy, PartialEq, Eq)]
@@ -620,7 +667,32 @@ fn sphere_through_edge(
return EdgeHit::Miss;
}
if frac_enter < 0.0 {
return EdgeHit::Miss;
let scaled_proj = -dot(start_delta, v0_v1);
if scaled_proj <= 0.0 {
return EdgeHit::MayV0;
}
if scaled_proj >= edge_len_sq {
return EdgeHit::MayV1;
}
let along = scaled_proj / edge_len_sq;
let mut normal = [
along * v0_v1[0] + start_delta[0],
along * v0_v1[1] + start_delta[1],
along * v0_v1[2] + start_delta[2],
];
if dot(normal, delta) >= 0.0 {
return EdgeHit::Miss;
}
normalize_to(&mut normal);
let inner_disc = radius * radius * perp_len_sq - scaled_dist * scaled_dist;
trace.fraction = 0.0;
trace.normal = normal;
trace.startsolid = u8::from(edge_len_sq * inner_disc > t_scaled * t_scaled);
trace.contents = contents;
trace.surface_flags = surface_flags;
trace.hit_type = HITTYPE_ENTITY;
trace.hit_id = ENTITYNUM_WORLD;
return EdgeHit::Hits;
}
let hit_delta = [
frac_enter * delta[0] + start_delta[0],
@@ -650,7 +722,6 @@ fn sphere_through_edge(
trace.surface_flags = surface_flags;
trace.hit_type = HITTYPE_ENTITY;
trace.hit_id = ENTITYNUM_WORLD;
trace.walkable = u8::from(trace.normal[2] >= WALKABLE_NORMAL_Z);
EdgeHit::Hits
}
@@ -659,6 +730,7 @@ fn sphere_through_vertex(
sphere_start: [f32; 3],
delta: [f32; 3],
vert: [f32; 3],
is_walkable: bool,
surface_flags: u32,
contents: u32,
trace: &mut Trace,
@@ -668,34 +740,48 @@ fn sphere_through_vertex(
sphere_start[1] - vert[1],
sphere_start[2] - vert[2],
];
let a = len_sq(delta);
if a <= 0.0 {
let b = dot(delta, start_delta);
if b >= 0.0 {
return;
}
let b = 2.0 * dot(start_delta, delta);
let start_len_sq = len_sq(start_delta);
let r = radius + SURFACE_CLIP_EPSILON;
let c = len_sq(start_delta) - r * r;
let disc = b * b - 4.0 * a * c;
if disc < 0.0 {
return;
}
let frac = (-b - sqrtf(disc)) / (2.0 * a);
if frac < 0.0 || frac >= trace.fraction {
return;
}
let hit = [
start_delta[0] + frac * delta[0],
start_delta[1] + frac * delta[1],
start_delta[2] + frac * delta[2],
];
let inv_r = 1.0 / r;
let c = start_len_sq - r * r;
let (frac, normal) = if c > 0.0 {
let a = len_sq(delta);
let b_sq = b * b;
let disc = b_sq - a * c;
if disc < b_sq * EQUAL_EPSILON {
return;
}
let frac = (-sqrtf(disc) - b) / a;
if frac >= trace.fraction {
return;
}
let inv_r = 1.0 / r;
let mut normal = [
(start_delta[0] + frac * delta[0]) * inv_r,
(start_delta[1] + frac * delta[1]) * inv_r,
(start_delta[2] + frac * delta[2]) * inv_r,
];
let approx_recip_len = (3.0 - len_sq(normal)) * 0.5;
normal = normal.map(|c| c * approx_recip_len);
(frac, normal)
} else {
let mut normal = start_delta;
normalize_to(&mut normal);
if start_len_sq < radius * radius {
trace.startsolid = 1;
}
(0.0, normal)
};
trace.fraction = frac;
trace.normal = [hit[0] * inv_r, hit[1] * inv_r, hit[2] * inv_r];
trace.normal = normal;
trace.contents = contents;
trace.surface_flags = surface_flags;
trace.hit_type = HITTYPE_ENTITY;
trace.hit_id = ENTITYNUM_WORLD;
trace.walkable = u8::from(trace.normal[2] >= WALKABLE_NORMAL_Z);
trace.walkable = u8::from(is_walkable);
}
fn cross(a: [f32; 3], b: [f32; 3]) -> [f32; 3] {
+1
View File
@@ -194,6 +194,7 @@ pub fn trace_leaf_mesh_into(
let staged = ClipMeshRef {
verts: mesh.verts,
tri_indices: mesh.tri_indices,
tri_edge_is_walkable: mesh.tri_edge_is_walkable,
tri_surface_flags: mesh.tri_surface_flags,
tri_content_flags: mesh.tri_content_flags,
aabb_trees: mesh.aabb_trees,
+25 -2
View File
@@ -861,6 +861,25 @@ fn handle_input_commands(
settings: Res<ConsoleSettings>,
) {
for command in events.read() {
let mut command = command.clone();
if matches!(command.name.as_str(), "hold" | "press" | "release")
&& command
.args
.first()
.is_some_and(|arg| matches!(arg.as_str(), "+actionslot" | "-actionslot"))
&& command.args.len() >= 2
{
let slot = command.args.remove(1);
command.args[0].push(' ');
command.args[0].push_str(&slot);
} else if matches!(command.name.as_str(), "+actionslot" | "-actionslot")
&& command.args.len() == 1
{
command.name.push(' ');
command.name.push_str(&command.args[0]);
command.args.clear();
}
match command.name.as_str() {
name if name.starts_with('+') && is_console_input(name) => {
inputs.hold(name);
@@ -2167,8 +2186,12 @@ fn mark_match_facts(
out.push_str(&format!(" local={life}"));
if let Some(meta) = meta {
out.push_str(&format!(
" local_life={} local_deaths={}",
meta.life_sequence.0, meta.deaths
" local_life={} local_deaths={} local_cmds={} local_moving_cmds={} local_path={:.1}",
meta.life_sequence.0,
meta.deaths,
meta.input_receipt.applied_cmds,
meta.input_receipt.moving_cmds,
meta.input_receipt.path_units
));
}
if let Some(ps) = presented.and_then(|p| p.alive_player(local.0)) {
+3 -1
View File
@@ -128,7 +128,8 @@ pub(super) fn load_clipmap(s: &mut ZoneStream<'_>, links: &mut dyn AssetLinkSink
let verts = s.plain_array(p, s.layout(92, 176), 4, 12, vert_count)?;
let index_count = 3 * tri_count;
let tri_indices = s.plain_array(p, s.layout(100, 192), 2, 2, index_count)?;
s.plain_array(p, s.layout(104, 200), 1, 1, index_count.div_ceil(32) * 4)?;
let tri_edge_is_walkable =
s.plain_array(p, s.layout(104, 200), 1, 1, index_count.div_ceil(32) * 4)?;
s.plain_array(
p,
s.layout(112, 216),
@@ -259,6 +260,7 @@ pub(super) fn load_clipmap(s: &mut ZoneStream<'_>, links: &mut dyn AssetLinkSink
leafbrush_nodes,
verts,
tri_indices,
tri_edge_is_walkable,
collision_partitions,
partition_count,
collision_aabb_trees,
+7 -1
View File
@@ -87,7 +87,7 @@ pub(super) fn load_weapon(s: &mut ZoneStream<'_>, links: &mut dyn AssetLinkSink)
let display_name_at_0x8 = s.follow_string(p, s.layout(8, 16))?;
let hide_tags = s.plain_array(p, s.layout(12, 24), 2, 2, 32)?;
let sz_xanims = follow_string_array(s, p, s.layout(16, 32), sz::WEAPON_ANIM_COUNT)?;
follow_name(s, p, s.layout(60, 80))?;
let alternate_weapon_name = s.follow_string(p, s.layout(60, 80))?;
let kill_icon_fresh =
asset_ptr_at_linked(s, links, AssetType::Material, p.at(s.layout(72, 96)))?;
@@ -327,6 +327,12 @@ pub(super) fn load_weapon(s: &mut ZoneStream<'_>, links: &mut dyn AssetLinkSink)
},
};
s.record_weapon(WeaponGeometry {
alternate_weapon_name,
alternate_raise_time_ms: s.i32_at(p, s.layout(68, 92))?,
alternate_drop_time_ms: weap_def
.map(|body| s.i32_at(body, s.layout(0x290, 984)))
.transpose()?
.unwrap_or_default(),
view_flash_slot: authored_material_slot(s, weap_def, s.layout(0x48, 112))?,
world_flash_slot: authored_material_slot(s, weap_def, s.layout(0x4c, 120))?,
view_shell_eject_slot: authored_material_slot(s, weap_def, s.layout(0x110, 512))?,
+5
View File
@@ -676,6 +676,8 @@ pub struct ClipMapGeometry {
pub tri_indices: Option<Ptr>,
pub tri_edge_is_walkable: Option<Ptr>,
pub collision_partitions: Option<Ptr>,
pub partition_count: usize,
@@ -915,6 +917,9 @@ pub struct WeaponIdleCapture {
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct WeaponGeometry {
pub name: Option<Ptr>,
pub alternate_weapon_name: Option<Ptr>,
pub alternate_raise_time_ms: i32,
pub alternate_drop_time_ms: i32,
pub weap_def: Option<Ptr>,
+34
View File
@@ -16,6 +16,7 @@ pub struct AttachmentFacts {
pub aim_assist: Option<AttachmentAimAssist>,
pub ammunition: Option<AttachmentAmmunition>,
pub damage: Option<AttachmentDamage>,
pub projectile: Option<AttachmentProjectile>,
pub location_damage: Option<[f32; 19]>,
pub idle_settings: Option<AttachmentIdleSettings>,
pub ads_settings: Option<AttachmentAdsSettings>,
@@ -63,6 +64,7 @@ pub struct AttachmentAddOns {
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct AttachmentGeneral {
pub body: Option<Ptr>,
pub bolt_action: bool,
pub inherits_perks: bool,
pub enemy_crosshair_range: f32,
@@ -98,6 +100,20 @@ pub struct AttachmentDamage {
pub min_player_damage: i32,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct AttachmentProjectile {
pub body: Ptr,
pub explosion_radius: i32,
pub explosion_inner_damage: i32,
pub explosion_outer_damage: i32,
pub speed: i32,
pub speed_up: i32,
pub activate_distance: i32,
pub explosion_type: i32,
pub impact_explode: bool,
pub model: Option<Ptr>,
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct AttachmentIdleSettings {
pub hip_idle_amount: f32,
@@ -171,6 +187,23 @@ pub(crate) fn read_attachment_facts(s: &ZoneStream<'_>, header: Ptr) -> Attachme
weapon_type: i32_at(12, 20),
weapon_class: i32_at(16, 24),
load_index: i32_at(156, 256),
projectile: body_at(104, 200).and_then(|body| {
Some(AttachmentProjectile {
body,
explosion_radius: s.i32_at(body, 0).ok()?,
explosion_inner_damage: s.i32_at(body, 4).ok()?,
explosion_outer_damage: s.i32_at(body, 8).ok()?,
speed: s.i32_at(body, 16).ok()?,
speed_up: s.i32_at(body, 20).ok()?,
activate_distance: s.i32_at(body, 24).ok()?,
explosion_type: s.i32_at(body, s.layout(36, 40)).ok()?,
impact_explode: s.u8_at(body, s.layout(60, 88)).ok()? != 0,
model: match s.ptr_at(body, 32).ok()? {
ZonePtr::Offset(p) => Some(s.resolve_alias(p)),
_ => None,
},
})
}),
sight: body_at(sz::ATTACH_SIGHT_OFF, 64).and_then(|body| read_sight(s, body)),
ammo_general: body_at(32, 56).and_then(|body| read_ammo_general(s, body)),
reload: body_at(40, 72).and_then(|body| {
@@ -275,6 +308,7 @@ fn read_ammo_general(s: &ZoneStream<'_>, body: Ptr) -> Option<AttachmentAmmoGene
fn read_general(s: &ZoneStream<'_>, body: Ptr) -> Option<AttachmentGeneral> {
Some(AttachmentGeneral {
body: Some(body),
bolt_action: s.u8_at(body, 0).ok()? != 0,
inherits_perks: s.u8_at(body, 1).ok()? != 0,
enemy_crosshair_range: s.f32_at(body, 4).ok()?,
+2 -2
View File
@@ -15,8 +15,8 @@ pub use asset_type::AssetType;
pub use attachment::{
AttachmentAddOns, AttachmentAdsSettings, AttachmentAimAssist, AttachmentAmmoGeneral,
AttachmentAmmunition, AttachmentDamage, AttachmentFacts, AttachmentGeneral, AttachmentGunKick,
AttachmentHipSpread, AttachmentIdleSettings, AttachmentReload, AttachmentScales,
AttachmentSight,
AttachmentHipSpread, AttachmentIdleSettings, AttachmentProjectile, AttachmentReload,
AttachmentScales, AttachmentSight,
};
pub use content::{
AssetSink, AssetTable, ScriptStrings, XASSET_ENTRY_LEN, XASSET_LIST_LEN, load_zone,
+3 -1
View File
@@ -60,7 +60,8 @@ pub(super) fn load_clipmap(s: &mut ZoneStream<'_>, links: &mut dyn AssetLinkSink
let verts = s.plain_array(p, s.layout(0x68, 208), 4, 12, vert_count)?;
let index_count = 3 * tri_count;
let tri_indices = s.plain_array(p, s.layout(0x70, 224), 2, 2, index_count)?;
s.plain_array(p, s.layout(0x74, 232), 1, 1, index_count.div_ceil(32) * 4)?;
let tri_edge_is_walkable =
s.plain_array(p, s.layout(0x74, 232), 1, 1, index_count.div_ceil(32) * 4)?;
let border_count = s.i32_at(p, s.layout(0x78, 240))?.max(0) as usize;
s.plain_array(
p,
@@ -167,6 +168,7 @@ pub(super) fn load_clipmap(s: &mut ZoneStream<'_>, links: &mut dyn AssetLinkSink
leafbrush_nodes: info.leafbrush_nodes,
verts,
tri_indices,
tri_edge_is_walkable,
collision_partitions,
collision_aabb_trees,
cmodels,
+7 -1
View File
@@ -132,7 +132,7 @@ pub(super) fn load_weapon(s: &mut ZoneStream<'_>, links: &mut dyn AssetLinkSink)
}
}
follow_name(s, p, s.layout(116, 192))?;
let alternate_weapon_name = s.follow_string(p, s.layout(116, 192))?;
// The complete def lives in TEMP and is overwritten by the next asset: keep a slot that outlives it.
let kill_icon_cell = p.at(s.layout(sz::WEAPON_COMPLETE_KILL_ICON_OFF, 216));
let referenced = match s.ptr_at(kill_icon_cell, 0)? {
@@ -199,6 +199,12 @@ pub(super) fn load_weapon(s: &mut ZoneStream<'_>, links: &mut dyn AssetLinkSink)
_ => None,
};
s.record_weapon(WeaponGeometry {
alternate_weapon_name,
alternate_raise_time_ms: s.i32_at(p, s.layout(124, 208))?,
alternate_drop_time_ms: weap_def
.map(|body| s.i32_at(body, s.layout(0x298, 1000)))
.transpose()?
.unwrap_or_default(),
name,
kill_icon_slot,
kill_icon,
+1 -1
View File
@@ -92,7 +92,7 @@ impl WeaponGeometry {
.i32_at(row, s.layout(sz::ANIM_OVERRIDE_ANIM_TIME_OFF, 28))
.ok()?,
alt_time_ms: s
.i32_at(row, s.layout(sz::ANIM_OVERRIDE_ALT_TIME_OFF, 36))
.i32_at(row, s.layout(sz::ANIM_OVERRIDE_ALT_TIME_OFF, 32))
.ok()?,
})
})
+5
View File
@@ -336,6 +336,9 @@ pub struct AttachmentGeometry {
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct WeaponGeometry {
pub name: Option<Ptr>,
pub alternate_weapon_name: Option<Ptr>,
pub alternate_raise_time_ms: i32,
pub alternate_drop_time_ms: i32,
pub kill_icon_slot: Option<Ptr>,
@@ -664,6 +667,8 @@ pub struct ClipMapGeometry {
pub tri_indices: Option<Ptr>,
pub tri_edge_is_walkable: Option<Ptr>,
pub collision_partitions: Option<Ptr>,
pub collision_aabb_trees: Option<Ptr>,
+2 -1
View File
@@ -96,7 +96,7 @@ pub(super) fn load_clip_map(s: &mut ZoneStream<'_>, links: &mut dyn AssetLinkSin
always_array(s, p.at(0x5c), 4, 12 * num_brush_verts)?;
always_array(s, p.at(0x64), 2, 2 * nuinds)?;
let tri_indices = always_array(s, p.at(0x6c), 2, 2 * (3 * tri_count))?;
always_array(s, p.at(0x70), 1, 4 * ((3 * tri_count + 31) >> 5))?;
let tri_edge_is_walkable = always_array(s, p.at(0x70), 1, 4 * ((3 * tri_count + 31) >> 5))?;
always_array(s, p.at(0x78), 4, sz::COLLISION_BORDER * border_count)?;
let collision_partitions =
@@ -184,6 +184,7 @@ pub(super) fn load_clip_map(s: &mut ZoneStream<'_>, links: &mut dyn AssetLinkSin
brushes,
verts,
tri_indices,
tri_edge_is_walkable,
materials,
material_count: num_materials,
collision_partitions,
+7 -1
View File
@@ -25,7 +25,7 @@ pub(super) fn load_weapon(s: &mut ZoneStream<'_>, links: &mut dyn AssetLinkSink)
follow_name(s, p, 0xc)?;
follow_name(s, p, 0x14)?;
let alternate_weapon_name = s.follow_string(p, 0x14)?;
let sz_xanims = follow_string_array(s, p, 0x10, sz::WEAPON_XANIM_COUNT)?;
@@ -57,6 +57,12 @@ pub(super) fn load_weapon(s: &mut ZoneStream<'_>, links: &mut dyn AssetLinkSink)
_ => None,
};
s.record_weapon(WeaponGeometry {
alternate_weapon_name,
alternate_raise_time_ms: s.i32_at(p, 0x3c)?,
alternate_drop_time_ms: weap_def
.map(|body| s.i32_at(body, 0x400))
.transpose()?
.unwrap_or_default(),
name,
display_name,
weap_def,
+5
View File
@@ -381,6 +381,9 @@ pub struct FxEffectDefGeometry {
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct WeaponGeometry {
pub name: Option<Ptr>,
pub alternate_weapon_name: Option<Ptr>,
pub alternate_raise_time_ms: i32,
pub alternate_drop_time_ms: i32,
pub display_name: Option<Ptr>,
@@ -583,6 +586,8 @@ pub struct ClipMapGeometry {
pub tri_indices: Option<Ptr>,
pub tri_edge_is_walkable: Option<Ptr>,
pub materials: Option<Ptr>,
pub material_count: usize,
+1 -1
View File
@@ -296,7 +296,7 @@ fn draw_effect_sprites(
return;
}
for class in [0usize, 2] {
for class in 0..3 {
let mut handle = effect.first_elem_handle[class];
while handle != FX_ELEM_HANDLE_NONE {
let next = elem_slot_for_handle(handle)
+18
View File
@@ -619,6 +619,24 @@ impl FxSystemHost {
.filter(|e| e.ring_resident && e.has_refs())
}
pub fn stop_bolted(&mut self, def_name: &str, dobj: u32, bone: u16) {
let slots: Vec<_> = self
.effects
.iter()
.enumerate()
.filter_map(|(slot, effect)| {
(effect.ring_resident
&& effect.def_name == def_name
&& fx_iw4::fx_bolt_dobj(effect.bolt_packed) == dobj
&& fx_iw4::fx_bolt_bone(effect.bolt_packed) == u32::from(bone))
.then_some(slot)
})
.collect();
for slot in slots {
crate::spawn::stop_effect_non_recursive(self, slot);
}
}
pub fn refresh_bolt_poses(
&mut self,
mut resolve: impl FnMut(u32, u16) -> Option<FxResolvedBoltPose>,
+1
View File
@@ -17,6 +17,7 @@ entity_iw4.workspace = true
frame.workspace = true
net = { path = "../net" }
sim = { path = "../sim" }
math_iw4.workspace = true
gamemode_iw4.workspace = true
killcam_iw4 = { path = "../killcam_iw4" }
+31 -12
View File
@@ -15,10 +15,13 @@ const FLASH_WHITE_IMAGE: &str = "white";
#[derive(Resource, Default)]
pub(crate) struct FlashWhiteoutLatch {
pub(crate) saved: PackedList,
pub(crate) packed: PackedList,
last_alpha: Option<f32>,
last_alpha: Option<(f32, f32)>,
last_w: Option<f32>,
last_h: Option<f32>,
last_start: Option<i32>,
pub(crate) save_sequence: u64,
}
#[derive(Resource, Default, Clone, Copy, PartialEq, Eq)]
@@ -35,7 +38,7 @@ fn request_hide(job: &mut FlashGpuJob, packed_empty: bool) {
}
}
fn skip_rewrite(latch: &FlashWhiteoutLatch, alpha: f32, win_w: f32, win_h: f32) -> bool {
fn skip_rewrite(latch: &FlashWhiteoutLatch, alpha: (f32, f32), win_w: f32, win_h: f32) -> bool {
!latch.packed.is_empty()
&& latch.last_alpha == Some(alpha)
&& latch.last_w == Some(win_w)
@@ -60,7 +63,9 @@ pub(crate) fn update_flash_whiteout(
for ev in started.read() {
if ev.client == local.0.0 {
latch.packed = PackedList::default();
latch.saved = PackedList::default();
latch.last_alpha = None;
latch.last_start = None;
}
}
gaps.clear(HudGap::FlashWhiteout);
@@ -82,12 +87,18 @@ pub(crate) fn update_flash_whiteout(
ps.shellshock_duration,
SCREEN_BLEND_FLASHED,
);
let Some((white, _)) =
let Some((white, shot)) =
cg_shellshock_flash_blend(remaining, FLASHBANG_WHITE_FADE_MS, FLASHBANG_SHOT_FADE_MS)
else {
latch.last_start = None;
request_hide(&mut job, latch.packed.is_empty());
return;
};
if latch.last_start != Some(ps.shellshock_time) {
latch.last_start = Some(ps.shellshock_time);
latch.save_sequence = latch.save_sequence.wrapping_add(1);
latch.last_alpha = None;
}
let win_w = surface.width();
let win_h = surface.height();
let Some(color) = hud_images.get(
@@ -102,11 +113,11 @@ pub(crate) fn update_flash_whiteout(
request_hide(&mut job, latch.packed.is_empty());
return;
};
if skip_rewrite(&latch, white, win_w, win_h) {
if skip_rewrite(&latch, (white, shot), win_w, win_h) {
*job = FlashGpuJob::Write;
return;
}
let list = Draw2dList {
let full_screen = |alpha: f32, site: &'static str| Draw2dList {
cmds: vec![Draw2dCmd {
material_namespace: crate::images::HUD_CHROME_NAMESPACE,
x: 0.0,
@@ -117,22 +128,30 @@ pub(crate) fn update_flash_whiteout(
t0: 0.0,
s1: 1.0,
t1: 1.0,
color: [1.0, 1.0, 1.0, white],
color: [1.0, 1.0, 1.0, alpha],
material: FLASH_WHITE_IMAGE.into(),
op: Draw2dOp::StretchPic,
provenance: Draw2dProvenance::CgDraw {
site: "flash_whiteout",
},
provenance: Draw2dProvenance::CgDraw { site },
layer: 0,
}],
};
let Some(quad) = tessellate(&list).0.into_iter().next() else {
let (Some(saved_quad), Some(white_quad)) = (
tessellate(&full_screen(shot, "flash_saved_screen"))
.0
.into_iter()
.next(),
tessellate(&full_screen(white, "flash_whiteout"))
.0
.into_iter()
.next(),
) else {
request_hide(&mut job, latch.packed.is_empty());
return;
};
latch.packed = crate::gpu_list::pack_modulate(&quad, color);
latch.saved = crate::gpu_list::pack_saved_screen(&saved_quad, color.clone());
latch.packed = crate::gpu_list::pack_modulate(&white_quad, color);
latch.last_w = Some(win_w);
latch.last_h = Some(win_h);
latch.last_alpha = Some(white);
latch.last_alpha = Some((white, shot));
*job = FlashGpuJob::Write;
}
+12
View File
@@ -32,6 +32,8 @@ pub enum HudTessTechnique {
Modulate,
SplatterAlt,
SavedScreen,
}
#[derive(Clone, Debug)]
@@ -58,6 +60,8 @@ pub struct HudTessGpuFrame {
pub surface_w: f32,
pub surface_h: f32,
pub visible: bool,
pub saved_screen_sequence: u64,
}
impl HudTessGpuFrame {
@@ -461,6 +465,14 @@ pub(crate) fn pack_splatter_alt(
packed
}
pub(crate) fn pack_saved_screen(quad: &Draw2dQuad, stand_in: Handle<Image>) -> PackedList {
let mut packed = pack_modulate(quad, stand_in);
for batch in &mut packed.batches {
batch.technique = HudTessTechnique::SavedScreen;
}
packed
}
pub(crate) fn pack_modulate(quad: &Draw2dQuad, image: Handle<Image>) -> PackedList {
let mut packed = PackedList::default();
packed.vertices.extend(pack_quad_verts(quad));
+2
View File
@@ -214,11 +214,13 @@ fn flush_flash_tess(
mut frame: ResMut<crate::gpu_list::HudTessGpuFrame>,
) {
let _body = gpu_list::TessBody::open();
frame.saved_screen_sequence = latch.save_sequence;
match *job {
FlashGpuJob::Write => {
if latch.packed.is_empty() {
return;
}
frame.append_packed(&latch.saved);
frame.append_packed(&latch.packed);
}
FlashGpuJob::Hide | FlashGpuJob::Idle => {}
+83 -14
View File
@@ -72,6 +72,7 @@ fn hide(pass: &mut HudTessPass) {
}
struct WeaponbarExprHost<'a> {
weapons: Option<&'a PreparedWeapons>,
ps: Option<&'a PlayerState>,
input: Option<&'a frame::HudInputView>,
ms: i32,
@@ -110,15 +111,13 @@ impl ExprHost for WeaponbarExprHost<'_> {
.ok()
.and_then(|i| ps.action_slot_type.get(i))
})
.is_some_and(|kind| !matches!(kind, 0 | 1))
.is_some_and(|kind| !matches!(kind, 0 | 1 | 2))
{
return Err(ExprError::Host(
"alternate/nightvision action slot not hosted",
));
return Err(ExprError::Host("nightvision action slot not hosted"));
}
Ok(i32::from(
self.ps
.and_then(|ps| action_slot_weapon(ps, slot - 1))
.and_then(|ps| action_slot_weapon(ps, slot - 1, self.weapons))
.is_some(),
))
}
@@ -182,12 +181,18 @@ impl ExprHost for WeaponbarExprHost<'_> {
fn weapon_lock(&self) -> Result<hud_iw4::WeaponLockView, ExprError> {
self.lock.ok_or(ExprError::Host("weapon lock"))
}
fn emp_jammed(&self) -> Result<i32, ExprError> {
Ok(i32::from(
self.ps.is_some_and(|ps| ps.other_flags & 0x400 != 0),
))
}
fn dvar_int(&self, name: &str) -> Result<i32, ExprError> {
if name.eq_ignore_ascii_case("scr_gameended") {
Ok(i32::from(self.game_ended))
} else if name.eq_ignore_ascii_case("g_hardcore")
|| name.eq_ignore_ascii_case("onlinegame")
|| name.eq_ignore_ascii_case("xblive_privatematch")
|| name.eq_ignore_ascii_case("cg_thirdPersonSpectator")
{
Ok(0)
} else if name.eq_ignore_ascii_case("scr_showperksonspawn") {
@@ -480,7 +485,14 @@ fn background_stem(background: &str) -> Option<String> {
const WEAPOVERLAYINTERFACE_JAVELIN: i32 = 1;
fn weapon_lock_view(ps: &PlayerState, weapons: &PreparedWeapons) -> hud_iw4::WeaponLockView {
fn weapon_lock_view(
ps: &PlayerState,
weapons: &PreparedWeapons,
meta: Option<&sim::ClientSnapshotMeta>,
time_ms: i32,
projection: Option<&Projection>,
surface: &crate::surface::Hud2dSurface,
) -> hud_iw4::WeaponLockView {
let viewmodel = bg_get_viewmodel_weapon_index(ps);
let ads_javelin = viewmodel > 0
&& ps.f_weapon_pos_frac == 1.0
@@ -488,9 +500,35 @@ fn weapon_lock_view(ps: &PlayerState, weapons: &PreparedWeapons) -> hud_iw4::Wea
.0
.facts_of(viewmodel)
.is_some_and(|facts| facts.overlay_interface == WEAPOVERLAYINTERFACE_JAVELIN);
let lock = meta
.map(|m| m.weapon_lock)
.filter(|lock| lock.weapon == ps.weapon && ps.health > 0)
.unwrap_or_default();
let mut screen_pos = [0.0; 2];
if lock.flags & 3 != 0 {
if let Some(Projection::Perspective(projection)) = projection {
let eye = Vec3::from_array(ps.origin) + Vec3::Z * ps.view_height_current;
let delta = Vec3::from_array(lock.target) - eye;
let (forward, right, up) = math_iw4::angle_vectors(ps.viewangles);
let depth = delta.dot(Vec3::from_array(forward));
if depth > 0.0 {
let scale = surface.height() * 0.5 / ((projection.fov * 0.5).tan() * depth);
screen_pos = [
surface.width() * 0.5 + delta.dot(Vec3::from_array(right)) * scale,
surface.height() * 0.5 - delta.dot(Vec3::from_array(up)) * scale,
];
}
}
}
hud_iw4::WeaponLockView {
ads_javelin,
..Default::default()
time_ms,
attack_top: lock.flags & 4 != 0,
attack_direct: lock.flags & 8 != 0,
locking: lock.locking(),
locked: lock.locked(),
too_close: lock.too_close(),
screen_pos,
}
}
@@ -499,9 +537,10 @@ fn ammo_hud_hidden(ps: &PlayerState) -> bool {
}
#[derive(bevy::ecs::system::SystemParam)]
pub(crate) struct WeaponbarInput<'w> {
pub(crate) struct WeaponbarInput<'w, 's> {
select: Res<'w, CgWeaponSelect>,
input: Option<Res<'w, frame::HudInputView>>,
cameras: Query<'w, 's, &'static Projection, With<Camera3d>>,
}
#[allow(clippy::too_many_arguments)]
@@ -566,6 +605,7 @@ pub(crate) fn update_weaponbar(
.map(|w| offhand_ammo(ps, w, ps.offhand_secondary))
.unwrap_or(0);
let mut host = WeaponbarExprHost {
weapons: weapons.as_deref(),
ps: Some(ps),
input: client_input.input.as_deref(),
ms: sys_milliseconds() as i32,
@@ -586,7 +626,16 @@ pub(crate) fn update_weaponbar(
lock: Some(
weapons
.as_deref()
.map(|weapons| weapon_lock_view(ps, weapons))
.map(|weapons| {
weapon_lock_view(
ps,
weapons,
meta,
cg_clock.time(),
client_input.cameras.iter().next(),
&surface,
)
})
.unwrap_or_default(),
),
frag_ammo,
@@ -648,7 +697,10 @@ pub(crate) fn update_weaponbar(
});
}
if let Some(menu) = catalog.get("dpad_hd") {
for name in ["dpad_hd", "javelin_overlay_hd"] {
let Some(menu) = catalog.get(name) else {
continue;
};
host.menu = Some(menu);
let mut hook = |args: OwnerDrawArgs<'_>, frame: &mut ChromeFrame| {
paint_owner(&owner_state, args, frame)
@@ -787,9 +839,25 @@ pub(crate) fn update_weaponbar(
pass.weaponbar = TessJob::Quads(quads);
}
fn action_slot_weapon(ps: &PlayerState, slot: i32) -> Option<u32> {
fn action_slot_weapon(
ps: &PlayerState,
slot: i32,
weapons: Option<&PreparedWeapons>,
) -> Option<u32> {
let slot = usize::try_from(slot).ok()?;
if ps.action_slot_type.get(slot) != Some(&1) || ps.weap_flags & 2 != 0 {
if ps.weap_flags & 2 != 0 {
return None;
}
if ps.action_slot_type.get(slot) == Some(&2) {
let weapons = &weapons?.0;
let weapon = if weapons.facts_of(ps.weapon)?.inventory_type == 3 {
ps.weapon_primary
} else {
weapons.alternate_of(ps.weapon)
};
return (weapon != 0).then_some(weapon);
}
if ps.action_slot_type.get(slot) != Some(&1) {
return None;
}
let weapon = *ps.action_slot_param.get(slot)?;
@@ -805,11 +873,12 @@ fn paint_action_slot(
.ps
.action_slot_type
.get((args.item.owner_draw - 171) as usize)
.is_some_and(|kind| !matches!(kind, 0 | 1))
.is_some_and(|kind| !matches!(kind, 0 | 1 | 2))
{
return OwnerDrawPaint::Gap(ChromeGapKind::OwnerDraw);
}
let Some(weapon) = action_slot_weapon(state.ps, args.item.owner_draw - 171) else {
let Some(weapon) = action_slot_weapon(state.ps, args.item.owner_draw - 171, state.weapons)
else {
return OwnerDrawPaint::Painted;
};
let Some(weapons) = state.weapons else {
+13 -4
View File
@@ -149,7 +149,7 @@ pub enum ExprError {
pub struct WeaponLockView {
pub ads_javelin: bool,
pub blink: bool,
pub time_ms: i32,
pub attack_top: bool,
@@ -638,12 +638,21 @@ fn run_op(
data.push(Operand::Int(host.selecting_location()?));
Ok(())
}
OP_ADSJAVELIN | OP_WEAPLOCKBLINK | OP_WEAPATTACKTOP | OP_WEAPATTACKDIRECT
| OP_WEAPLOCKING | OP_WEAPLOCKED | OP_WEAPLOCKTOOCLOSE => {
OP_WEAPLOCKBLINK => {
let rate = source_float(&pop_data(data)?);
let lock = host.weapon_lock()?;
let period = (1000.0 / rate) as i32;
let visible = lock.locked
|| (lock.locking
&& (rate <= 0.0 || period <= 0 || (lock.time_ms / period) % 2 != 0));
data.push(Operand::Int(i32::from(visible)));
Ok(())
}
OP_ADSJAVELIN | OP_WEAPATTACKTOP | OP_WEAPATTACKDIRECT | OP_WEAPLOCKING | OP_WEAPLOCKED
| OP_WEAPLOCKTOOCLOSE => {
let lock = host.weapon_lock()?;
let flag = match op {
OP_ADSJAVELIN => lock.ads_javelin,
OP_WEAPLOCKBLINK => lock.blink,
OP_WEAPATTACKTOP => lock.attack_top,
OP_WEAPATTACKDIRECT => lock.attack_direct,
OP_WEAPLOCKING => lock.locking,
+7 -1
View File
@@ -239,6 +239,7 @@ pub struct ClientInput {
pub stance_latch: i32,
pub weapon_cycles: Vec<bool>,
pub action_slots: Vec<usize>,
pub offhand_hold_cancel: bool,
}
@@ -251,6 +252,7 @@ impl Default for ClientInput {
using_ads: false,
stance_latch: 0,
weapon_cycles: Vec::new(),
action_slots: Vec::new(),
offhand_hold_cancel: false,
}
}
@@ -290,7 +292,11 @@ pub fn cl_input_cmd(
apply_pair(&mut client.kb.speed, cmd_id, key, now_msec, frame_msec);
apply_pair(&mut client.kb.throw_btn, cmd_id, key, now_msec, frame_msec);
}
15..=22 => panic!("+actionslot N not this slice"),
15..=22 => {
if pair_down(cmd_id) {
client.action_slots.push(((cmd_id - 15) / 2) as usize);
}
}
23 | 24 => panic!("+stance writes the stance latch; not a movedown alias"),
25 | 26 => {
apply_pair(&mut client.kb.gostand, cmd_id, key, now_msec, frame_msec);
+6 -3
View File
@@ -20,18 +20,21 @@ pub fn weapon_cycle_allowed(
pub fn cycle_weapon(
weapons: &[i32; 15],
selected: u32,
mapped: u32,
last_primary: u32,
next: bool,
mut inventory_type: impl FnMut(u32) -> i32,
) -> Option<u32> {
let selected = if selected != 0 && inventory_type(selected) == 3 {
mapped
} else {
selected
};
let current_type = if selected == 0 {
0
} else {
inventory_type(selected)
};
if current_type == 3 {
panic!("alternate-mode parent selection and remembered alt mode");
}
if current_type != 0
&& current_type != 4
&& last_primary != 0
+60 -1
View File
@@ -210,6 +210,7 @@ pub struct CgWeaponSelect {
pub held_index: u32,
pub last_primary: u32,
pub mapped_index: u32,
}
pub fn cg_follow_held_weapon_select(
@@ -226,6 +227,7 @@ pub fn cg_follow_held_weapon_select(
|| (select.index == select.held_index && ps_weapon != select.held_index)
{
select.index = ps_weapon;
select.mapped_index = ps_weapon;
select.last_primary = 0;
select.life_sequence = life_sequence;
select.time = cg_time;
@@ -270,6 +272,7 @@ pub fn cg_cycle_weapon_select(
let Some(target) = cycle_weapon(
&ps.weapons,
select.index,
select.mapped_index,
select.last_primary,
next,
|weapon| facts(weapon).inventory_type,
@@ -292,6 +295,7 @@ pub fn cg_cycle_weapon_select(
select.last_primary = select.index;
}
select.index = target;
select.mapped_index = target;
input_iw4::cl_set_ads(input, false);
}
}
@@ -722,6 +726,7 @@ pub fn sample_client_input(
push_phase(trace, "Input");
if !gate.local_cmds_enabled {
actions.client.weapon_cycles.clear();
actions.client.action_slots.clear();
return;
}
actions.frame_msec = key_frame_msec(time.delta_secs());
@@ -780,6 +785,60 @@ pub fn sample_client_input(
.map(|meta| meta.life_sequence.0)
.unwrap_or(0);
cg_follow_held_weapon_select(&mut select, ps_weapon, life_sequence, clock.time());
if let Some(ps) = ps {
if select.index == ps.weapon {
select.mapped_index = if prediction
.0
.world()
.weapon_combat_row(ps.weapon)
.is_some_and(|facts| facts.inventory_type == 3)
{
ps.weapon_primary
} else {
ps.weapon
};
}
}
let slots = std::mem::take(&mut actions.client.action_slots);
if let Some(ps) = ps.filter(|_| !frozen) {
for slot in slots {
if !input_iw4::weapon_select::weapon_cycle_allowed(ps, clock.time(), select.time, 0, 0)
{
continue;
}
let target = match ps.action_slot_type.get(slot).copied() {
Some(1) => ps.action_slot_param[slot].max(0) as u32,
Some(2) => match prediction.0.world().weapon_combat_row(select.index) {
Some(facts) if facts.inventory_type == 3 => select.mapped_index,
Some(facts) => facts.alternate_weapon,
None => 0,
},
_ => 0,
};
if target == 0 {
continue;
}
let parent = if ps.weapons.contains(&(target as i32)) {
target
} else {
select.mapped_index
};
let owned = ps.weapons.contains(&(parent as i32))
&& (parent == target
|| prediction
.0
.world()
.weapon_combat_row(parent)
.is_some_and(|f| f.alternate_weapon == target));
if !owned {
continue;
}
select.index = target;
select.mapped_index = parent;
select.time = clock.time();
input_iw4::cl_set_ads(&mut actions.client, false);
}
}
let cycles = std::mem::take(&mut actions.client.weapon_cycles);
let in_killcam = view.is_some_and(|v| v.in_killcam());
if let Some(ps) = ps {
@@ -808,7 +867,7 @@ pub fn sample_client_input(
look.angles = cmd.angles;
cmd.weapon = select.index as u16;
cmd.weapon_mapped = select.index as u16;
cmd.weapon_mapped = select.mapped_index as u16;
if let Some(loadout) = presented
.snapshot()
.and_then(|snapshot| snapshot.meta.for_client(local.0))
+1 -1
View File
@@ -169,4 +169,4 @@ pub use transport::udp_session::{CommittedAdmission, UdpAuthorityHub, UdpClientL
pub use transport::udp_socket::{DEFAULT_RECV_BUDGET_PER_TICK, UdpDatagramSocket, UdpSendError};
pub use transport::wire::{WireError, WireReader, WireWriter};
pub const PROTOCOL_VERSION: u32 = 76;
pub const PROTOCOL_VERSION: u32 = 77;
+25
View File
@@ -1203,6 +1203,18 @@ fn encode_client_meta(out: &mut WireWriter, meta: &ClientSnapshotMeta) {
out.put_u32(meta.player_card_icon);
out.put_u32(meta.player_card_title);
out.put_u32(meta.player_card_nameplate);
let lock = meta.weapon_lock;
out.put_u32(lock.weapon);
out.put_u32(lock.life);
out.put_u8(lock.flags);
for value in lock.target.into_iter().chain(lock.point_sum) {
out.put_f32(value);
}
out.put_u8(lock.point_count);
out.put_u8(lock.misses);
out.put_i32(lock.sampled_at);
out.put_i32(lock.out_of_ads_at);
out.put_i32(lock.acquire_started_at);
}
fn decode_client_meta(input: &mut WireReader<'_>) -> Result<ClientSnapshotMeta, WireError> {
@@ -1266,7 +1278,20 @@ fn decode_client_meta(input: &mut WireReader<'_>) -> Result<ClientSnapshotMeta,
let player_card_icon = input.get_u32()?;
let player_card_title = input.get_u32()?;
let player_card_nameplate = input.get_u32()?;
let weapon_lock = sim::WeaponLock {
weapon: input.get_u32()?,
life: input.get_u32()?,
flags: input.get_u8()?,
target: [input.get_f32()?, input.get_f32()?, input.get_f32()?],
point_sum: [input.get_f32()?, input.get_f32()?, input.get_f32()?],
point_count: input.get_u8()?,
misses: input.get_u8()?,
sampled_at: input.get_i32()?,
out_of_ads_at: input.get_i32()?,
acquire_started_at: input.get_i32()?,
};
Ok(ClientSnapshotMeta {
weapon_lock,
killcam_hud,
lifecycle,
loadout,
+1
View File
@@ -340,6 +340,7 @@ ps_netfields! {
offhand_primary: i32 = "offhandPrimary", 0x2ac, Replication::Replicated, Validation::Exact;
offhand_secondary: i32 = "offhandSecondary", 0x2b0, Replication::Replicated, Validation::Exact;
weapon: u32 = "weapon", 0x2b4, Replication::Replicated, Validation::Exact;
weapon_primary: u32 = "weaponPrimary", 0x2b8, Replication::Replicated, Validation::Exact;
weap_flags: u32 = "weapFlags", 0x2bc, Replication::Replicated, Validation::Exact;
f_weapon_pos_frac: f32 = "fWeaponPosFrac", 0x2c0, Replication::Replicated, Validation::Exact;
aim_spread_scale: f32 = "aimSpreadScale", 0x2c4, Replication::Replicated, Validation::Exact;
@@ -87,6 +87,7 @@ pub struct PlayerState {
pub offhand_primary: i32,
pub offhand_secondary: i32,
pub weapon: u32,
pub weapon_primary: u32,
pub weap_flags: u32,
pub f_weapon_pos_frac: f32,
pub aim_spread_scale: f32,
@@ -265,6 +266,7 @@ impl PlayerState {
offhand_primary: 0,
offhand_secondary: 0,
weapon: 0,
weapon_primary: 0,
weap_flags: 0,
f_weapon_pos_frac: 0.0,
aim_spread_scale: 0.0,
+11 -30
View File
@@ -175,6 +175,8 @@ pub struct FpvAuthoritySample {
pub perks0: u32,
pub clip_ammo: Option<i32>,
pub left_clip_ammo: Option<i32>,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
@@ -238,7 +240,7 @@ pub enum WeapAnimEdge {
Idle,
EmptyIdle,
ForceIdle,
Dispatch(usize),
}
@@ -382,22 +384,16 @@ pub fn tick_equipped_fpv_with_extra_events(
present.last_weap_anim = None;
}
}
WeapAnimEdge::EmptyIdle => {
equipped.controller.apply_empty_idle_weap_anim();
WeapAnimEdge::ForceIdle => {
equipped
.controller
.apply_force_idle_weap_anim(sample.clip_ammo == Some(0));
}
WeapAnimEdge::Dispatch(slot) => {
report_dispatch_slot(slot, equipped.controller.dispatch_sz_xanim_index(slot));
}
}
}
if let Some(sample) = sample {
reconcile_sprint_latch(&mut equipped.controller, sample.sprinting);
if let Some(left) = equipped.left.as_mut()
&& sample.last_weapon_hand == 1
{
reconcile_sprint_latch(left, sample.sprinting);
}
}
let crate::AdvanceResult { notifies } = equipped.controller.advance(dt_secs);
let ads_frac = sample
.map(|s| s.ads_frac)
@@ -410,12 +406,12 @@ pub fn tick_equipped_fpv_with_extra_events(
match present.observe_weap_anim_secondary_edge(sample.weap_anim_secondary) {
WeapAnimEdge::Unchanged => {}
WeapAnimEdge::Idle => {
if !left.apply_idle_weap_anim(false) {
if !left.apply_idle_weap_anim(sample.left_clip_ammo == Some(0)) {
present.last_weap_anim_secondary = None;
}
}
WeapAnimEdge::EmptyIdle => {
left.apply_empty_idle_weap_anim();
WeapAnimEdge::ForceIdle => {
left.apply_force_idle_weap_anim(sample.left_clip_ammo == Some(0));
}
WeapAnimEdge::Dispatch(slot) => {
report_dispatch_slot(slot, left.dispatch_sz_xanim_index(slot));
@@ -429,21 +425,6 @@ pub fn tick_equipped_fpv_with_extra_events(
(sample_pose(&equipped.controller), notifies)
}
fn reconcile_sprint_latch(controller: &mut ViewmodelController, sprinting: bool) {
if sprinting {
return;
}
if matches!(
controller.state(),
WeaponState::SprintIn | WeaponState::SprintLoop
) {
report_fpv_event_result(
PresentFpvEvent::SprintOut,
controller.handle(ViewmodelEvent::SprintOut),
);
}
}
fn observe_weap_anim_edge_on(previous: &mut Option<i32>, raw: i32) -> WeapAnimEdge {
let last = previous.replace(raw);
if last == Some(raw) {
@@ -452,7 +433,7 @@ fn observe_weap_anim_edge_on(previous: &mut Option<i32>, raw: i32) -> WeapAnimEd
let masked = raw as u32 & weapon_iw4::WEAP_ANIM_EVENT_MASK;
match masked {
0 => WeapAnimEdge::Idle,
1 => WeapAnimEdge::EmptyIdle,
1 => WeapAnimEdge::ForceIdle,
_ => match weapon_iw4::slot_for_weap_anim_event(masked) {
Some(slot) => WeapAnimEdge::Dispatch(slot),
None => WeapAnimEdge::Idle,
+19 -70
View File
@@ -1,15 +1,15 @@
use std::collections::HashMap;
use std::sync::Arc;
use bevy::math::{Mat4, Vec3};
use bevy::prelude::*;
use render_scene::SmodelPassMaterial;
use crate::anim::fpv::{
EquippedFpv, FpvAuthoritySample, FpvPresentState, tick_equipped_fpv_with_predicted_fire,
};
use crate::anim::fpv_pose::{FpvBoltFrame, PosedClip};
use crate::anim::fpv_rig::{FpvHandPose, FpvRigInputs, PreparedFpvRig};
use assets::{FpvClipTracks, FpvMeshCatalog, FpvMeshIndex, FpvSideAssemblies};
use crate::anim::fpv_prepared::FpvRigSet;
use crate::anim::fpv_rig::{FpvHandPose, PreparedFpvRig};
use assets::FpvMeshIndex;
#[derive(Resource, Default)]
pub struct FpvPresentCursor(pub FpvPresentState);
@@ -22,6 +22,7 @@ pub struct PendingFpvSpawnRequest {
pub gun_index: FpvMeshIndex,
pub catalog_id: u64,
pub weapon_id: u32,
pub parent_weapon: u32,
}
#[derive(Resource, Default)]
@@ -39,6 +40,8 @@ pub struct FpvHeldLife(pub Option<u32>);
#[derive(Resource, Default)]
pub struct FpvBoltTargets {
pub pose: [Option<FpvBoltFrame>; 2],
pub tracker_screen: Option<[Vec3; 3]>,
pub tracker_light: Option<fx::FxBoltTarget>,
pub flash: [Option<fx::FxBoltTarget>; 2],
pub brass: [Option<fx::FxBoltTarget>; 2],
pub knife: [Option<fx::FxBoltTarget>; 2],
@@ -48,6 +51,8 @@ pub struct FpvBoltTargets {
impl FpvBoltTargets {
pub fn clear(&mut self) {
self.pose = [None, None];
self.tracker_screen = None;
self.tracker_light = None;
self.flash = [None, None];
self.brass = [None, None];
self.knife = [None, None];
@@ -104,14 +109,9 @@ pub struct FpvPoseProduct {
pub struct FpvGenerateArgs<'a> {
pub dt: f32,
pub equipped: &'a mut EquippedFpv,
pub rigs: &'a mut Vec<PreparedFpvRig>,
pub active: &'a mut Option<usize>,
pub rigs: &'a FpvRigSet,
pub active: &'a mut Option<Arc<PreparedFpvRig>>,
pub cursor: &'a mut FpvPresentState,
pub catalog: &'a FpvMeshCatalog,
pub materials: &'a [SmodelPassMaterial],
pub material_by_authored: &'a HashMap<usize, u32>,
pub assemblies: &'a FpvSideAssemblies,
pub clip_tracks: &'a FpvClipTracks,
pub rocket: bool,
pub sample: Option<FpvAuthoritySample>,
pub predicted_fire: bool,
@@ -126,11 +126,6 @@ pub fn generate_fpv_pose(args: FpvGenerateArgs<'_>) -> FpvPoseKind {
rigs,
active,
cursor,
catalog,
materials,
material_by_authored,
assemblies,
clip_tracks,
rocket,
sample,
predicted_fire,
@@ -167,64 +162,18 @@ pub fn generate_fpv_pose(args: FpvGenerateArgs<'_>) -> FpvPoseKind {
};
let dual_drawn = !left.is_empty();
let rocket = rocket && assemblies.rocket.is_some();
let assembly = assemblies.pick(rocket);
let left_orders: &[Option<usize>] = match (dual_drawn, equipped.left.as_ref()) {
(true, Some(left)) => left.weapon().clip_orders(),
_ => &[],
};
let orders = [
equipped.controller.weapon().clip_orders().as_slice(),
left_orders,
];
*active = rigs
.iter()
.position(|rig| rig.matches(assembly, dual_drawn, orders));
if active.is_none() {
let rig_started = std::time::Instant::now();
match PreparedFpvRig::build(
std::sync::Arc::clone(assembly),
dual_drawn,
FpvRigInputs {
catalog,
materials,
material_by_authored,
clip_tracks,
clip_orders: orders,
},
) {
Ok(prepared) => {
diag::info!(
Fpv,
"fpv: laid out `{}` in {:.2}ms — {} draws, {} vertices, {} surfaces skipped{}{}",
equipped.gun_xmodel,
rig_started.elapsed().as_secs_f64() * 1000.0,
prepared.geometry.plan_draw_n,
prepared.geometry.dest_n,
prepared.geometry.plan_skip_n,
if dual_drawn { ", dual" } else { "" },
if rocket { ", rocket" } else { "" }
);
rigs.push(prepared);
*active = Some(rigs.len() - 1);
}
Err(error) => {
diag::info!(
Fpv,
"fpv: cannot lay out `{}` — {error}",
equipped.gun_xmodel
);
return FpvPoseKind::Refuse(FpvPoseRefuse::EyePoseFailed {
gun_xmodel: equipped.gun_xmodel.clone(),
});
}
}
}
let Some(prepared) = active.and_then(|index| rigs.get_mut(index)) else {
let Some(prepared) = rigs.pick(rocket, dual_drawn) else {
*active = None;
return FpvPoseKind::Refuse(FpvPoseRefuse::EyePoseFailed {
gun_xmodel: equipped.gun_xmodel.clone(),
});
};
if active
.as_ref()
.is_none_or(|current| current.generation() != prepared.generation())
{
*active = Some(Arc::clone(prepared));
}
let Some(right_pose) = prepared.pose_hand(0, &right, Vec3::ZERO) else {
return FpvPoseKind::Refuse(FpvPoseRefuse::EyePoseFailed {
+2
View File
@@ -48,6 +48,8 @@ pub struct FpvBoltTags {
pub knife: Option<u16>,
pub laser: Option<u16>,
pub tracker_screen: [Option<u16>; 3],
pub tracker_light: Option<u16>,
}
#[derive(Clone, Debug, Default)]
+971
View File
@@ -0,0 +1,971 @@
use std::collections::{HashMap, HashSet};
use std::sync::Arc;
use assets::{
AssetEdge, AssetNamespace, FpvHands, FpvMeshCatalog, FpvMeshIndex, FpvSideAssemblies,
PreparedFpvMeshes, PreparedWeapons, PreparedXAnims, TS_COLOR_MAP, WEAPON_ANIM_SLOTS,
WeaponAnimations, WeaponBodyFacts, WeaponRegistry, XAnimCatalog,
};
use bevy::prelude::*;
use render_material::{RuntimeMaterialCatalog, RuntimeSortedMaterialTable};
use render_scene::{SmodelPassMaterial, TessMaterials, WorldModelLightingAtlas};
use crate::anim::fpv_rig::{
FpvMandatoryRefusal, FpvMaterialAdmission, FpvSurfaceVerdict, PreparedFpvComposition,
PreparedFpvModel, PreparedFpvRig, compose_clip_tracks,
};
use crate::gaps::RenderGapCause;
const PREPARE_FRAME_BUDGET: std::time::Duration = std::time::Duration::from_millis(6);
const NO_COLOUR_MAP: &str = "technique samples no colour map";
#[derive(Default)]
pub struct FpvRigSet {
bare: [Option<Arc<PreparedFpvRig>>; 2],
rocket: [Option<Arc<PreparedFpvRig>>; 2],
}
impl FpvRigSet {
pub fn pick(&self, rocket: bool, dual: bool) -> Option<&Arc<PreparedFpvRig>> {
let hand = usize::from(dual);
match (rocket, &self.rocket[hand]) {
(true, Some(rig)) => Some(rig),
_ => self.bare[hand].as_ref(),
}
}
}
#[derive(Clone, Debug, Default)]
pub struct FpvViewCensus {
pub gun_colormap_skip_n: u32,
pub gun_colormap_skip_names: Vec<String>,
pub mat_hints: Vec<String>,
}
pub struct FpvWeaponView {
pub gun_name: String,
pub gun_index: FpvMeshIndex,
pub hands: FpvHands,
pub hands_index: FpvMeshIndex,
pub namespace: AssetNamespace,
pub assemblies: FpvSideAssemblies,
pub right: WeaponAnimations,
pub left: Option<WeaponAnimations>,
pub idle_name: Option<String>,
pub rigs: FpvRigSet,
pub census: FpvViewCensus,
}
pub enum FpvWeaponSlot {
Absent,
Ready(Arc<FpvWeaponView>),
Refused(RenderGapCause),
}
static ABSENT_SLOT: FpvWeaponSlot = FpvWeaponSlot::Absent;
#[derive(Clone, Copy, Debug, Default)]
pub struct FpvPreparationCensus {
pub models: usize,
pub layouts: usize,
pub compositions: usize,
pub rigs: usize,
pub materials: usize,
pub elapsed_ms: f64,
}
pub struct FpvWeaponTable {
material_catalog: Arc<RuntimeMaterialCatalog>,
catalog_id: u64,
facts: Vec<Option<WeaponBodyFacts>>,
hud_iris: Vec<bool>,
guns: Vec<Option<FpvMeshIndex>>,
slots: Vec<[FpvWeaponSlot; 2]>,
alternate_slots: HashMap<(u32, u32), [FpvWeaponSlot; 2]>,
census: FpvPreparationCensus,
}
impl FpvWeaponTable {
pub fn census(&self) -> FpvPreparationCensus {
self.census
}
pub fn material_catalog(&self) -> &Arc<RuntimeMaterialCatalog> {
&self.material_catalog
}
pub fn catalog_id(&self) -> u64 {
self.catalog_id
}
pub fn facts_of(&self, weapon: u32) -> Option<WeaponBodyFacts> {
self.facts.get(weapon as usize).copied().flatten()
}
pub fn overlay_is_hud_iris(&self, weapon: u32) -> bool {
self.hud_iris.get(weapon as usize).copied().unwrap_or(false)
}
pub fn gun_index(&self, weapon: u32) -> Option<FpvMeshIndex> {
self.guns.get(weapon as usize).copied().flatten()
}
pub fn slot(&self, weapon: u32, parent: u32, axis: bool) -> &FpvWeaponSlot {
if parent != 0 && self.facts_of(weapon).is_some_and(|f| f.inventory_type == 3) {
return self
.alternate_slots
.get(&(weapon, parent))
.map_or(&ABSENT_SLOT, |s| &s[usize::from(axis)]);
}
self.slots
.get(weapon as usize)
.map_or(&ABSENT_SLOT, |sides| &sides[usize::from(axis)])
}
}
struct FpvPreparationOwner {
materials: Arc<RuntimeMaterialCatalog>,
images: Arc<Vec<Option<Handle<Image>>>>,
weapons: Arc<WeaponRegistry>,
catalog_id: u64,
atlas: Option<bevy::asset::AssetId<Image>>,
}
impl FpvPreparationOwner {
fn same(&self, other: &Self) -> bool {
Arc::ptr_eq(&self.materials, &other.materials)
&& Arc::ptr_eq(&self.images, &other.images)
&& Arc::ptr_eq(&self.weapons, &other.weapons)
&& self.catalog_id == other.catalog_id
&& self.atlas == other.atlas
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum FpvPreparationStage {
Admit,
Layout,
Weapons,
Done,
}
type RigKey = (usize, bool, Vec<Option<usize>>, Vec<Option<usize>>);
struct FpvPreparationJob {
owner: FpvPreparationOwner,
stage: FpvPreparationStage,
started: std::time::Instant,
progress: Option<assets::StageHandle>,
work_total: u64,
work_done: u64,
models: Vec<usize>,
next_model: usize,
admission: FpvMaterialAdmission,
image_cache: HashMap<u32, Handle<Image>>,
layouts_queue: Vec<(usize, Option<[u32; 6]>)>,
next_layout: usize,
layouts: HashMap<(usize, Option<[u32; 6]>), Result<Arc<PreparedFpvModel>, String>>,
next_weapon: u32,
alternate_queue: Vec<(u32, u32)>,
compositions: HashMap<usize, Result<Arc<PreparedFpvComposition>, String>>,
tracks: HashMap<(usize, usize), Option<Arc<[u16]>>>,
rigs: HashMap<RigKey, Arc<PreparedFpvRig>>,
facts: Vec<Option<WeaponBodyFacts>>,
hud_iris: Vec<bool>,
guns: Vec<Option<FpvMeshIndex>>,
slots: Vec<[FpvWeaponSlot; 2]>,
alternate_slots: HashMap<(u32, u32), [FpvWeaponSlot; 2]>,
refused: Vec<String>,
}
#[derive(Resource, Default)]
pub struct PreparedFpv {
job: Option<FpvPreparationJob>,
table: Option<(FpvPreparationOwner, Arc<FpvWeaponTable>)>,
}
impl PreparedFpv {
pub fn table(&self) -> Option<&Arc<FpvWeaponTable>> {
self.table.as_ref().map(|(_, table)| table)
}
pub fn settled_for(&self, materials: &Arc<RuntimeMaterialCatalog>) -> bool {
self.job.is_none()
&& self
.table()
.is_some_and(|table| Arc::ptr_eq(&table.material_catalog, materials))
}
pub fn clear(&mut self) {
if let Some(job) = self.job.take()
&& let Some(stage) = job.progress
{
stage.cancel();
}
self.table = None;
}
}
fn assembly_key(assembly: &Arc<assets::FpvAssembly>) -> usize {
Arc::as_ptr(assembly) as usize
}
fn composition_key(composition: &Arc<PreparedFpvComposition>) -> usize {
Arc::as_ptr(composition) as usize
}
#[allow(clippy::too_many_arguments)]
fn admit_surface(
global: &RuntimeMaterialCatalog,
images: &[Option<Handle<Image>>],
entry: &assets::FpvMeshEntry,
surface_index: usize,
lighting: &WorldModelLightingAtlas,
admission: &mut FpvMaterialAdmission,
image_cache: &mut HashMap<u32, Handle<Image>>,
) -> FpvSurfaceVerdict {
use lighting_iw4::{
MODEL_LIGHTING_INV_ATLAS_WIDTH, MODEL_LIGHTING_VOLUME_W, model_lighting_inv_image_height,
model_lighting_lookup_scale,
};
let edge = entry
.material_edges
.get(surface_index)
.copied()
.unwrap_or(AssetEdge::Absent);
let leftover = entry
.material_names
.get(surface_index)
.and_then(|name| name.as_deref())
.unwrap_or("-");
let refused = |material: &str, cause: &'static str| FpvSurfaceVerdict::Refused {
material: material.to_owned(),
cause,
};
let bound = match edge {
AssetEdge::Absent => return FpvSurfaceVerdict::Inapplicable("no material"),
AssetEdge::Unresolved(_) => return refused(leftover, edge.edge_kind()),
AssetEdge::Bound(_) => match edge.bound_index() {
Some(bound) => bound,
None => return refused(leftover, "bound edge without an index"),
},
};
let Some(present_name) = entry.material_present_name(surface_index) else {
return refused(leftover, "bound material has no name");
};
let Some(mat_i) = entry
.skel
.surface_materials
.get(surface_index)
.copied()
.flatten()
.map(|index| index.get())
else {
return refused(present_name, "surface carries no authored material");
};
if let Some(&row) = admission.by_authored.get(&mat_i) {
return FpvSurfaceVerdict::Admitted(row);
}
let Some(authored) = global.materials.get(bound) else {
return refused(present_name, "material outside the session catalog");
};
let Some(ordinal) = global.ordinal_for_material_name(present_name) else {
return refused(present_name, "material has no sorted ordinal");
};
let Some(color_image) = authored
.textures
.iter()
.find_map(|(_, texture)| texture.filter(|binding| binding.semantic == TS_COLOR_MAP))
.map(|binding| binding.image.0)
else {
return FpvSurfaceVerdict::Inapplicable(NO_COLOUR_MAP);
};
let mut image = |image_index: u32| -> Option<Handle<Image>> {
if let Some(cached) = image_cache.get(&image_index) {
return Some(cached.clone());
}
let handle = images.get(image_index as usize).cloned().flatten()?;
image_cache.insert(image_index, handle.clone());
Some(handle)
};
let Some(color) = image(color_image) else {
return refused(present_name, "colour map not uploaded");
};
let specular = authored
.textures
.iter()
.find_map(|(_, texture)| {
texture.filter(|binding| binding.semantic == assets::TS_SPECULAR_MAP)
})
.and_then(|binding| image(binding.image.0));
const ENV_MAP_PARMS: u32 = 1_033_475_292;
let env_map_parms = authored
.constants
.iter()
.find(|(hash, _)| *hash == ENV_MAP_PARMS)
.map(|(_, words)| words.map(f32::from_bits))
.unwrap_or([0.0; 4]);
let Some(inv_h) = model_lighting_inv_image_height(lighting.dims.image_height) else {
return refused(present_name, "model lighting atlas has no rows");
};
let scale = model_lighting_lookup_scale(inv_h);
let cull_mode =
authored
.state_bits_table
.first()
.and_then(|bits| match assets::cull_face_from_state_bits(*bits) {
assets::MaterialCullFace::Back => Some(bevy::render::render_resource::Face::Back),
assets::MaterialCullFace::Front => Some(bevy::render::render_resource::Face::Front),
assets::MaterialCullFace::None => None,
});
let material = SmodelPassMaterial {
model_lighting_required: true,
color: Some(color),
specular,
probe: None,
atlas: Some(lighting.image.clone()),
alpha_mode: AlphaMode::Opaque,
draw_mode: None,
cull_mode,
env_map_parms,
lighting_lookup_scale: [scale.u, scale.v, scale.w, scale.q],
atlas_lookup: [
MODEL_LIGHTING_INV_ATLAS_WIDTH as f32,
inv_h,
MODEL_LIGHTING_VOLUME_W,
0.0,
],
sort_key: authored.sort_key,
material_sorted_index: Some(ordinal.get()),
};
let row = admission.materials.len() as u32;
admission.materials.push(material);
admission.by_authored.insert(mat_i, row);
FpvSurfaceVerdict::Admitted(row)
}
impl FpvPreparationJob {
fn new(
owner: FpvPreparationOwner,
fpv: &FpvMeshCatalog,
progress: Option<assets::StageHandle>,
) -> Self {
let registry = Arc::clone(&owner.weapons);
let weapon_n = registry.len() as u32;
let mut models: Vec<usize> = Vec::new();
let mut seen_models: HashSet<usize> = HashSet::new();
let mut layouts_queue: Vec<(usize, Option<[u32; 6]>)> = Vec::new();
let mut seen_layouts: HashSet<(usize, Option<[u32; 6]>)> = HashSet::new();
let mut seen_assemblies: HashSet<usize> = HashSet::new();
for (id, parent) in (1..=weapon_n)
.map(|id| (id, 0))
.chain(registry.alternate_fpv_pairs())
{
for axis in [false, true] {
let Some(sides) = registry.fpv_assemblies_for(id, parent, axis) else {
continue;
};
for assembly in std::iter::once(&sides.bare).chain(&sides.rocket) {
if !seen_assemblies.insert(assembly_key(assembly)) {
continue;
}
for part in &assembly.parts {
let order = part.model.order();
if fpv.get_at(order).is_none() {
continue;
}
if seen_models.insert(order) {
models.push(order);
}
if seen_layouts.insert((order, part.hide)) {
layouts_queue.push((order, part.hide));
}
}
}
}
}
let work_total =
(models.len() + layouts_queue.len() + registry.alternate_fpv_pairs().count()) as u64
+ u64::from(weapon_n);
if let Some(stage) = &progress {
stage.set_total(work_total);
}
Self {
owner,
stage: FpvPreparationStage::Admit,
started: std::time::Instant::now(),
progress,
work_total,
work_done: 0,
models,
next_model: 0,
admission: FpvMaterialAdmission::default(),
image_cache: HashMap::new(),
layouts_queue,
next_layout: 0,
layouts: HashMap::new(),
next_weapon: 0,
alternate_queue: {
let mut pairs: Vec<_> = registry.alternate_fpv_pairs().collect();
pairs.sort_unstable();
pairs.reverse();
pairs
},
compositions: HashMap::new(),
tracks: HashMap::new(),
rigs: HashMap::new(),
facts: Vec::with_capacity(weapon_n as usize + 1),
hud_iris: Vec::with_capacity(weapon_n as usize + 1),
guns: Vec::with_capacity(weapon_n as usize + 1),
slots: Vec::with_capacity(weapon_n as usize + 1),
alternate_slots: HashMap::new(),
refused: Vec::new(),
}
}
fn tick(&mut self) {
self.work_done = self.work_done.saturating_add(1);
if let Some(stage) = &self.progress {
stage.set_completed(self.work_done.min(self.work_total));
}
}
fn advance(
&mut self,
deadline: std::time::Instant,
fpv: &FpvMeshCatalog,
xanims: &XAnimCatalog,
tess: &TessMaterials,
lighting: &WorldModelLightingAtlas,
) {
while self.stage != FpvPreparationStage::Done {
if std::time::Instant::now() >= deadline {
return;
}
match self.stage {
FpvPreparationStage::Admit => self.admit_next(fpv, tess, lighting),
FpvPreparationStage::Layout => self.lay_out_next(fpv),
FpvPreparationStage::Weapons => self.prepare_next_weapon(fpv, xanims),
FpvPreparationStage::Done => {}
}
}
}
fn admit_next(
&mut self,
fpv: &FpvMeshCatalog,
tess: &TessMaterials,
lighting: &WorldModelLightingAtlas,
) {
let Some(&order) = self.models.get(self.next_model) else {
self.stage = FpvPreparationStage::Layout;
return;
};
self.next_model += 1;
if let Some(entry) = fpv.get_at(order) {
let verdicts = entry
.skel
.surfaces_for_lod(0)
.map(|surface| {
let verdict = admit_surface(
&tess.catalog,
tess.material_images.as_ref(),
entry,
surface,
lighting,
&mut self.admission,
&mut self.image_cache,
);
(surface, verdict)
})
.collect();
self.admission.record(order, verdicts);
}
self.tick();
}
fn lay_out_next(&mut self, fpv: &FpvMeshCatalog) {
let Some(&(order, hide)) = self.layouts_queue.get(self.next_layout) else {
self.stage = FpvPreparationStage::Weapons;
self.next_weapon = 0;
return;
};
self.next_layout += 1;
let prepared = PreparedFpvModel::build(fpv, order, hide.as_ref(), &self.admission)
.map(Arc::new)
.map_err(|error| error.to_string());
self.layouts.insert((order, hide), prepared);
self.tick();
}
fn composition(
&mut self,
assembly: &Arc<assets::FpvAssembly>,
) -> Result<Arc<PreparedFpvComposition>, String> {
let key = assembly_key(assembly);
if let Some(done) = self.compositions.get(&key) {
return done.clone();
}
let layouts = &self.layouts;
let composed = PreparedFpvComposition::compose(Arc::clone(assembly), |order, hide| {
layouts
.get(&(order, hide))
.cloned()
.unwrap_or_else(|| Err("model missing from the first-person catalog".to_owned()))
})
.map(Arc::new);
self.compositions.insert(key, composed.clone());
composed
}
fn clip_tracks(
&mut self,
composition: &PreparedFpvComposition,
orders: &[Option<usize>],
) -> Vec<Option<Arc<[u16]>>> {
let registry = Arc::clone(&self.owner.weapons);
let assembly = composition.assembly();
let key = assembly_key(assembly);
orders
.iter()
.map(|order| {
let order = (*order)?;
self.tracks
.entry((key, order))
.or_insert_with(|| {
compose_clip_tracks(assembly, order, registry.fpv_clip_tracks())
})
.clone()
})
.collect()
}
fn rig(
&mut self,
composition: &Arc<PreparedFpvComposition>,
dual: bool,
right: &[Option<usize>],
left: &[Option<usize>],
) -> Arc<PreparedFpvRig> {
let key: RigKey = (
composition_key(composition),
dual,
right.to_vec(),
left.to_vec(),
);
if let Some(rig) = self.rigs.get(&key) {
return Arc::clone(rig);
}
let tracks = [
self.clip_tracks(composition, right),
self.clip_tracks(composition, left),
];
let rig = Arc::new(PreparedFpvRig::build(
Arc::clone(composition),
dual,
&self.admission,
tracks,
));
self.rigs.insert(key, Arc::clone(&rig));
rig
}
fn census_of(&self, fpv: &FpvMeshCatalog, gun: FpvMeshIndex) -> FpvViewCensus {
let mut census = FpvViewCensus::default();
let order = gun.order();
let Some(entry) = fpv.get_at(order) else {
return census;
};
for (surface, verdict) in self.admission.verdicts_of(order) {
let name = entry
.material_names
.get(*surface)
.and_then(|name| name.as_deref());
match verdict {
FpvSurfaceVerdict::Admitted(_) => {
if let Some(name) = name
&& census.mat_hints.len() < 12
&& !census.mat_hints.iter().any(|seen| seen == name)
{
census.mat_hints.push(name.to_owned());
}
}
FpvSurfaceVerdict::Inapplicable(NO_COLOUR_MAP) => {
census.gun_colormap_skip_n = census.gun_colormap_skip_n.saturating_add(1);
if let Some(name) = name
&& census.gun_colormap_skip_names.len() < 8
&& !census
.gun_colormap_skip_names
.iter()
.any(|seen| seen == name)
{
census.gun_colormap_skip_names.push(name.to_owned());
}
}
_ => {}
}
}
census
}
fn prepare_next_weapon(&mut self, fpv: &FpvMeshCatalog, xanims: &XAnimCatalog) {
let registry = Arc::clone(&self.owner.weapons);
let (id, parent) = if self.next_weapon as usize > registry.len() {
let Some(pair) = self.alternate_queue.pop() else {
self.stage = FpvPreparationStage::Done;
return;
};
pair
} else {
let id = self.next_weapon;
self.next_weapon += 1;
self.facts.push(registry.facts_of(id));
self.hud_iris.push(registry.overlay_is_hud_iris(id));
(id, 0)
};
let gun = registry
.gun_xmodel_edge_of(id)
.and_then(|edge| edge.bound_index())
.filter(|&order| fpv.get_at(order).is_some())
.map(FpvMeshIndex::from_order);
if parent == 0 {
self.guns.push(gun);
}
if id == 0 {
self.slots
.push([FpvWeaponSlot::Absent, FpvWeaponSlot::Absent]);
return;
}
let Some(gun_index) = gun else {
let cause = || RenderGapCause::FpvGunXModelUnresolved { weapon_id: id };
let sides = [
FpvWeaponSlot::Refused(cause()),
FpvWeaponSlot::Refused(cause()),
];
if parent == 0 {
self.slots.push(sides);
} else {
self.alternate_slots.insert((id, parent), sides);
}
self.tick();
return;
};
let right_edges = registry.sz_xanim_right_edges_of(id);
let right_idle_bound =
right_edges.is_some_and(|edges| edges[assets::weap_anim::IDLE].is_bound());
let no_dual = registry
.facts_of(id)
.map(|f| u8::from(f.no_dual_wield))
.unwrap_or(1);
let create_lr = no_dual == 0 && right_idle_bound;
let right = if create_lr {
WeaponAnimations::from_registry_edges(&registry, id, right_edges, xanims)
} else {
WeaponAnimations::from_registry(&registry, id, xanims)
};
let left_edges = registry.sz_xanim_left_edges_of(id);
let left_idle_bound =
left_edges.is_some_and(|edges| edges[assets::weap_anim::IDLE].is_bound());
let left = (create_lr && left_idle_bound)
.then(|| WeaponAnimations::from_registry_edges(&registry, id, left_edges, xanims));
let idle_name = registry
.sz_xanim_edges_of(id)
.and_then(|row| row[assets::weap_anim::IDLE].bound_index())
.and_then(|order| xanims.clip_at(order))
.map(|clip| clip.name.clone());
let namespace = registry.namespace_of(id).unwrap_or(AssetNamespace::Iw4);
let gun_name = fpv
.get_at(gun_index.order())
.map(|entry| entry.skel.name.clone())
.unwrap_or_else(String::new);
let census = self.census_of(fpv, gun_index);
let right_orders: Vec<Option<usize>> = right.clip_orders().to_vec();
let left_orders: Vec<Option<usize>> = left
.as_ref()
.map(|left| left.clip_orders().to_vec())
.unwrap_or_else(Vec::new);
debug_assert_eq!(right_orders.len(), WEAPON_ANIM_SLOTS);
let sides: [FpvWeaponSlot; 2] = [false, true].map(|axis| {
let (Some((hands, hands_index)), Some(assemblies)) = (
registry.fpv_hands_of(id, axis),
registry.fpv_assemblies_for(id, parent, axis),
) else {
return FpvWeaponSlot::Refused(RenderGapCause::FpvDependencyUnresolved {
weapon_id: id,
role: "FPV skeleton",
name: registry.fpv_assembly_gap_of(id, axis),
});
};
let bare = self.composition(&assemblies.bare);
let rocket = assemblies
.rocket
.as_ref()
.map(|rocket| self.composition(rocket));
let (bare, rocket) = match (bare, rocket.transpose()) {
(Ok(bare), Ok(rocket)) => (bare, rocket),
(Err(error), _) | (_, Err(error)) => {
return FpvWeaponSlot::Refused(RenderGapCause::FpvDependencyUnresolved {
weapon_id: id,
role: "FPV layout",
name: error,
});
}
};
let refusal: Option<FpvMandatoryRefusal> = bare
.refusal()
.or_else(|| rocket.as_ref().and_then(|rocket| rocket.refusal()))
.cloned();
if let Some(refusal) = refusal {
return FpvWeaponSlot::Refused(RenderGapCause::FpvDependencyUnresolved {
weapon_id: id,
role: "FPV material",
name: refusal.to_string(),
});
}
let mut rigs = FpvRigSet::default();
rigs.bare[0] = Some(self.rig(&bare, false, &right_orders, &[]));
if left.is_some() {
rigs.bare[1] = Some(self.rig(&bare, true, &right_orders, &left_orders));
}
if let Some(rocket) = &rocket {
rigs.rocket[0] = Some(self.rig(rocket, false, &right_orders, &[]));
if left.is_some() {
rigs.rocket[1] = Some(self.rig(rocket, true, &right_orders, &left_orders));
}
}
FpvWeaponSlot::Ready(Arc::new(FpvWeaponView {
gun_name: gun_name.clone(),
gun_index,
hands: hands.clone(),
hands_index,
namespace,
assemblies: assemblies.clone(),
right: right.clone(),
left: left.clone(),
idle_name: idle_name.clone(),
rigs,
census: census.clone(),
}))
});
for (axis, side) in sides.iter().enumerate() {
if let FpvWeaponSlot::Refused(cause) = side
&& self.refused.len() < 16
{
self.refused.push(format!(
"{} ({}): {cause}",
registry.name_of(id),
if axis == 0 { "allies" } else { "axis" }
));
}
}
if parent == 0 {
self.slots.push(sides);
} else {
self.alternate_slots.insert((id, parent), sides);
}
self.tick();
}
fn finish(self) -> (FpvPreparationOwner, FpvWeaponTable) {
let ready = self
.slots
.iter()
.chain(self.alternate_slots.values())
.flatten()
.filter(|slot| matches!(slot, FpvWeaponSlot::Ready(_)))
.count();
let refused_n = self
.slots
.iter()
.chain(self.alternate_slots.values())
.flatten()
.filter(|slot| matches!(slot, FpvWeaponSlot::Refused(_)))
.count();
let inapplicable = self
.models
.iter()
.flat_map(|&order| self.admission.verdicts_of(order))
.filter(|(_, verdict)| matches!(verdict, FpvSurfaceVerdict::Inapplicable(_)))
.count();
let census = FpvPreparationCensus {
models: self.models.len(),
layouts: self.layouts.len(),
compositions: self.compositions.len(),
rigs: self.rigs.len(),
materials: self.admission.materials.len(),
elapsed_ms: self.started.elapsed().as_secs_f64() * 1000.0,
};
diag::info!(
Fpv,
"fpv prepared before Ready: models={} layouts={} compositions={} rigs={} materials={} inapplicable_surfaces={inapplicable} sides ready={ready} refused={refused_n} elapsed={:.1}ms",
census.models,
census.layouts,
census.compositions,
census.rigs,
census.materials,
census.elapsed_ms,
);
for line in &self.refused {
diag::info!(Fpv, "fpv refused before use: {line}");
}
if let Some(stage) = self.progress {
stage.done();
}
let table = FpvWeaponTable {
material_catalog: Arc::clone(&self.owner.materials),
catalog_id: self.owner.catalog_id,
facts: self.facts,
hud_iris: self.hud_iris,
guns: self.guns,
slots: self.slots,
alternate_slots: self.alternate_slots,
census,
};
(self.owner, table)
}
}
fn refused_table(
owner: &FpvPreparationOwner,
fpv: &FpvMeshCatalog,
cause: RenderGapCause,
) -> FpvWeaponTable {
let registry = &owner.weapons;
let weapon_n = registry.len() as u32;
let mut table = FpvWeaponTable {
material_catalog: Arc::clone(&owner.materials),
catalog_id: owner.catalog_id,
facts: Vec::with_capacity(weapon_n as usize + 1),
hud_iris: Vec::with_capacity(weapon_n as usize + 1),
guns: Vec::with_capacity(weapon_n as usize + 1),
slots: Vec::with_capacity(weapon_n as usize + 1),
alternate_slots: HashMap::new(),
census: FpvPreparationCensus::default(),
};
for id in 0..=weapon_n {
table.facts.push(registry.facts_of(id));
table.hud_iris.push(registry.overlay_is_hud_iris(id));
table.guns.push(
registry
.gun_xmodel_edge_of(id)
.and_then(|edge| edge.bound_index())
.filter(|&order| fpv.get_at(order).is_some())
.map(FpvMeshIndex::from_order),
);
table.slots.push(if id == 0 {
[FpvWeaponSlot::Absent, FpvWeaponSlot::Absent]
} else {
[
FpvWeaponSlot::Refused(cause.clone()),
FpvWeaponSlot::Refused(cause.clone()),
]
});
}
table
}
#[derive(bevy::ecs::system::SystemParam)]
pub struct PrepareFpvInputs<'w> {
weapons: Option<Res<'w, PreparedWeapons>>,
fpv_meshes: Option<Res<'w, PreparedFpvMeshes>>,
xanims: Option<Res<'w, PreparedXAnims>>,
tess: Res<'w, TessMaterials>,
lighting: Option<Res<'w, WorldModelLightingAtlas>>,
load: Option<Res<'w, assets::MapLoadProcess>>,
}
pub fn prepare_fpv_compositions(inputs: PrepareFpvInputs, mut prepared: ResMut<PreparedFpv>) {
let PrepareFpvInputs {
weapons,
fpv_meshes,
xanims,
tess,
lighting,
load,
} = inputs;
let (Some(weapons), Some(fpv_meshes), Some(xanims)) = (weapons, fpv_meshes, xanims) else {
if prepared.table.is_some() || prepared.job.is_some() {
prepared.clear();
}
return;
};
if tess.material_images.is_empty() {
return;
}
let sorted_ready = matches!(
tess.catalog.sorted_materials,
RuntimeSortedMaterialTable::Ready { .. }
);
let owner = FpvPreparationOwner {
materials: Arc::clone(&tess.catalog),
images: Arc::clone(&tess.material_images),
weapons: Arc::clone(&weapons.0),
catalog_id: fpv_meshes.0.identity(),
atlas: lighting.as_ref().map(|atlas| atlas.image.id()),
};
if prepared.job.is_none()
&& prepared
.table
.as_ref()
.is_some_and(|(settled, _)| settled.same(&owner))
{
return;
}
let restart = prepared
.job
.as_ref()
.is_none_or(|job| !job.owner.same(&owner));
if restart {
prepared.clear();
let shared_refusal = if !sorted_ready {
Some((
RenderGapCause::FpvCatalogMissing,
"the material generation has no sorted table",
))
} else if lighting.is_none() {
Some((
RenderGapCause::FpvNoLightingAtlas,
"no model lighting atlas in this world",
))
} else {
None
};
if let Some((cause, why)) = shared_refusal {
diag::info!(
Fpv,
"fpv: {why} — every first-person weapon refused before Ready"
);
let table = Arc::new(refused_table(&owner, &fpv_meshes.0, cause));
prepared.table = Some((owner, table));
return;
}
let progress = load
.as_deref()
.filter(|process| !process.is_complete())
.map(|process| process.progress.begin(assets::StageId::FirstPerson, None));
prepared.job = Some(FpvPreparationJob::new(owner, &fpv_meshes.0, progress));
}
let Some(lighting) = lighting.as_deref() else {
return;
};
let deadline = std::time::Instant::now() + PREPARE_FRAME_BUDGET;
let done = {
let Some(job) = prepared.job.as_mut() else {
return;
};
job.advance(deadline, &fpv_meshes.0, &xanims.0, &tess, lighting);
job.stage == FpvPreparationStage::Done
};
if done && let Some(job) = prepared.job.take() {
let (owner, table) = job.finish();
prepared.table = Some((owner, Arc::new(table)));
}
}
+340 -216
View File
@@ -1,9 +1,5 @@
//! The prepared first-person rig.
//!
//! The rig owns the whole first-person plan. Both hands of a dual-wield
//! composition are slots in one layout, so the left gun is a second destination
//! range over the same prepared model rather than a second pass over the same
//! decisions.
use std::collections::HashMap;
use std::sync::Arc;
@@ -17,60 +13,79 @@ use crate::anim::fpv_pose::{
use crate::anim::xmodel_pose::{FpvSurfOwner, SkinLayout, build_skin_layout, skin_packed_into};
use crate::draw::FpvSurfaceDraw;
use anim_iw4::dobj_surface_hidden;
use assets::{
AnimInstance, FpvAssembly, FpvClipTracks, FpvMeshCatalog, FpvPartRole, FpvSkel, PartBits,
};
use assets::{AnimInstance, FpvAssembly, FpvClipTracks, FpvMeshCatalog, FpvPartRole, PartBits};
/// One model inside the combined skeleton: where its bones start, and how its
/// surfaces map onto a contiguous run of destination vertices.
struct PreparedModel {
catalog_entry: usize,
owner: FpvSurfOwner,
bone_base: usize,
posed_surface_n: usize,
layout: SkinLayout,
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum FpvSurfaceVerdict {
Admitted(u32),
Inapplicable(&'static str),
Refused {
material: String,
cause: &'static str,
},
}
/// One model drawn for one hand. The same prepared model appears twice in a
/// dual-wield rig — once per hand — with a destination range of its own.
struct PlanSlot {
hand: usize,
model: usize,
dest_base: usize,
}
/// The rows the plan publishes for as long as the composition holds.
pub struct PreparedFpvGeometry {
pub indices: Vec<u32>,
pub surface_ranges: Vec<(u32, u32)>,
#[derive(Default)]
pub struct FpvMaterialAdmission {
pub materials: Vec<SmodelPassMaterial>,
pub draws: Vec<FpvSurfaceDraw>,
pub dest_n: usize,
pub packed_ok: bool,
pub hands_plan_n: u32,
pub gun_plan_n: u32,
pub scope_plan_n: u32,
pub scope_house_plan_n: u32,
pub scope_lens_plan_n: u32,
pub plan_draw_n: u32,
pub plan_skip_n: u32,
pub by_authored: HashMap<usize, u32>,
verdicts: HashMap<usize, Vec<(usize, FpvSurfaceVerdict)>>,
}
pub struct FpvHandPose {
skin: Vec<Mat4>,
eye_from_world: Mat4,
offset: Vec3,
pub lens: Mat4,
pub bolt: FpvBoltFrame,
impl FpvMaterialAdmission {
pub fn record(&mut self, catalog_entry: usize, verdicts: Vec<(usize, FpvSurfaceVerdict)>) {
self.verdicts.insert(catalog_entry, verdicts);
}
pub fn verdict(&self, catalog_entry: usize, surface: usize) -> Option<&FpvSurfaceVerdict> {
self.verdicts
.get(&catalog_entry)?
.iter()
.find(|(index, _)| *index == surface)
.map(|(_, verdict)| verdict)
}
pub fn verdicts_of(&self, catalog_entry: usize) -> &[(usize, FpvSurfaceVerdict)] {
self.verdicts
.get(&catalog_entry)
.map_or(&[], |verdicts| verdicts.as_slice())
}
}
pub struct FpvRigInputs<'a> {
pub catalog: &'a FpvMeshCatalog,
pub materials: &'a [SmodelPassMaterial],
pub material_by_authored: &'a HashMap<usize, u32>,
pub clip_tracks: &'a FpvClipTracks,
pub clip_orders: [&'a [Option<usize>]; 2],
#[derive(Clone, Copy, Debug)]
struct PreparedFpvSurface {
index_start: u32,
index_count: u32,
material: u32,
authored: usize,
lens_named: bool,
}
pub struct PreparedFpvModel {
catalog_entry: usize,
posed_surface_n: usize,
packed_ok: bool,
layout: SkinLayout,
surfaces: Vec<PreparedFpvSurface>,
refusal: Option<FpvMandatoryRefusal>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct FpvMandatoryRefusal {
pub model: String,
pub surface: usize,
pub material: String,
pub cause: &'static str,
}
impl core::fmt::Display for FpvMandatoryRefusal {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(
f,
"{} surface {} `{}`: {}",
self.model, self.surface, self.material, self.cause
)
}
}
#[derive(Debug)]
@@ -88,47 +103,112 @@ impl core::fmt::Display for FpvRigError {
}
}
pub struct PreparedFpvRig {
assembly: Arc<FpvAssembly>,
dual: bool,
generation: u64,
parts: PartBits,
tags: FpvBoltTags,
models: Vec<PreparedModel>,
slots: Vec<PlanSlot>,
orders: [Vec<Option<usize>>; 2],
tracks: [Vec<Option<Vec<Option<usize>>>>; 2],
pub geometry: PreparedFpvGeometry,
}
pub fn leftover_scope_surf_is_lens(name: &str) -> bool {
name.contains("lens")
}
/// Which surfaces of one model reach the plan: not hidden by this weapon's hide
/// tags, and carrying an authored material the session admitted.
fn surface_admitted(
skel: &FpvSkel,
words: Option<&[u32; 6]>,
admitted_materials: &HashMap<usize, u32>,
surface_index: usize,
) -> bool {
if let Some(words) = words
&& let Some(bits) = skel.surface_part_bits.get(surface_index)
&& dobj_surface_hidden(bits, words, 0)
{
return false;
impl PreparedFpvModel {
pub fn build(
catalog: &FpvMeshCatalog,
catalog_entry: usize,
hide: Option<&[u32; 6]>,
admission: &FpvMaterialAdmission,
) -> Result<Self, FpvRigError> {
let entry = catalog
.get_at(catalog_entry)
.ok_or(FpvRigError::Catalog("model"))?;
let skel = &entry.skel;
let lod_range = skel.surfaces_for_lod(0);
let first_surface = lod_range.start;
let hidden = |surface: usize| {
hide.is_some_and(|words| {
skel.surface_part_bits
.get(surface)
.is_some_and(|bits| dobj_surface_hidden(bits, words, 0))
})
};
let admitted = |surface: usize| match admission.verdict(catalog_entry, surface) {
Some(FpvSurfaceVerdict::Admitted(row)) => Some(*row),
_ => None,
};
let layout = build_skin_layout(skel, 0, |lod_local| {
let surface = first_surface + lod_local;
!hidden(surface) && admitted(surface).is_some()
})
.ok_or(FpvRigError::Layout("skin layout refused the model"))?;
let mut refusal = None;
for surface in lod_range.clone() {
if hidden(surface) {
continue;
}
if let Some(FpvSurfaceVerdict::Refused { material, cause }) =
admission.verdict(catalog_entry, surface)
{
refusal = Some(FpvMandatoryRefusal {
model: skel.name.clone(),
surface,
material: material.clone(),
cause: *cause,
});
break;
}
}
let mut surfaces = Vec::new();
for surface in &layout.surfaces {
if !surface.visible || surface.index_count == 0 {
continue;
}
let Some(authored) = skel
.surface_materials
.get(surface.surface_index)
.copied()
.flatten()
.map(|index| index.get())
else {
continue;
};
let Some(material) = admitted(surface.surface_index) else {
continue;
};
surfaces.push(PreparedFpvSurface {
index_start: surface.index_start,
index_count: surface.index_count,
material,
authored,
lens_named: entry
.material_names
.get(surface.surface_index)
.and_then(|name| name.as_deref())
.is_some_and(leftover_scope_surf_is_lens),
});
}
Ok(Self {
catalog_entry,
posed_surface_n: lod_range.len(),
packed_ok: skel.packed_vertices.len() == skel.positions.len(),
layout,
surfaces,
refusal,
})
}
let Some(authored) = skel
.surface_materials
.get(surface_index)
.copied()
.flatten()
.map(|index| index.get())
else {
return false;
};
admitted_materials.contains_key(&authored)
pub fn refusal(&self) -> Option<&FpvMandatoryRefusal> {
self.refusal.as_ref()
}
}
struct ComposedPart {
model: Arc<PreparedFpvModel>,
owner: FpvSurfOwner,
bone_base: usize,
}
pub struct PreparedFpvComposition {
assembly: Arc<FpvAssembly>,
parts: Vec<ComposedPart>,
refusal: Option<FpvMandatoryRefusal>,
}
fn surf_owner(role: FpvPartRole) -> FpvSurfOwner {
@@ -140,6 +220,112 @@ fn surf_owner(role: FpvPartRole) -> FpvSurfOwner {
}
}
impl PreparedFpvComposition {
pub fn compose(
assembly: Arc<FpvAssembly>,
mut model_of: impl FnMut(usize, Option<[u32; 6]>) -> Result<Arc<PreparedFpvModel>, String>,
) -> Result<Self, String> {
let mut parts = Vec::with_capacity(assembly.parts.len());
let mut refusal = None;
for part in &assembly.parts {
let model = model_of(part.model.order(), part.hide)?;
if refusal.is_none() {
refusal = model.refusal().cloned();
}
parts.push(ComposedPart {
model,
owner: surf_owner(part.role),
bone_base: part.bone_base,
});
}
Ok(Self {
assembly,
parts,
refusal,
})
}
pub fn assembly(&self) -> &Arc<FpvAssembly> {
&self.assembly
}
pub fn refusal(&self) -> Option<&FpvMandatoryRefusal> {
self.refusal.as_ref()
}
}
pub fn compose_clip_tracks(
assembly: &FpvAssembly,
clip: usize,
tracks: &FpvClipTracks,
) -> Option<Arc<[u16]>> {
let composed = assembly.compose_tracks(clip, tracks)?;
Some(
composed
.into_iter()
.map(|bone| {
bone.and_then(|bone| u16::try_from(bone).ok())
.unwrap_or(FpvClipTracks::NONE)
})
.collect(),
)
}
/// One model drawn for one hand. The same prepared model appears twice in a
/// dual-wield rig — once per hand — with a destination range of its own.
struct PlanSlot {
hand: usize,
part: usize,
dest_base: usize,
}
pub struct PreparedFpvGeometry {
segments: Vec<(Arc<PreparedFpvModel>, u32)>,
pub index_n: usize,
pub surface_ranges: Vec<(u32, u32)>,
pub materials: Vec<SmodelPassMaterial>,
pub draws: Vec<FpvSurfaceDraw>,
pub dest_n: usize,
pub packed_ok: bool,
pub hands_plan_n: u32,
pub gun_plan_n: u32,
pub scope_plan_n: u32,
pub scope_house_plan_n: u32,
pub scope_lens_plan_n: u32,
pub plan_draw_n: u32,
pub plan_skip_n: u32,
}
impl PreparedFpvGeometry {
pub fn write_indices(&self, out: &mut Vec<u32>) {
out.clear();
out.reserve(self.index_n);
for (model, rebase) in &self.segments {
out.extend(model.layout.indices.iter().map(|index| index + rebase));
}
}
}
pub struct FpvHandPose {
skin: Vec<Mat4>,
eye_from_world: Mat4,
offset: Vec3,
pub lens: Mat4,
pub bolt: FpvBoltFrame,
}
pub struct PreparedFpvRig {
composition: Arc<PreparedFpvComposition>,
dual: bool,
generation: u64,
parts: PartBits,
tags: FpvBoltTags,
slots: Vec<PlanSlot>,
tracks: [Vec<Option<Arc<[u16]>>>; 2],
pub geometry: PreparedFpvGeometry,
}
impl PreparedFpvRig {
/// A plan holding this generation is a plan whose indices, ranges,
/// materials and draws are this rig's.
@@ -147,53 +333,17 @@ impl PreparedFpvRig {
self.generation
}
pub fn matches(
&self,
assembly: &Arc<FpvAssembly>,
dual: bool,
orders: [&[Option<usize>]; 2],
) -> bool {
Arc::ptr_eq(&self.assembly, assembly)
&& self.dual == dual
&& self.orders[0] == orders[0]
&& self.orders[1] == orders[1]
pub fn is_dual(&self) -> bool {
self.dual
}
pub fn build(
assembly: Arc<FpvAssembly>,
composition: Arc<PreparedFpvComposition>,
dual: bool,
inputs: FpvRigInputs<'_>,
) -> Result<Self, FpvRigError> {
let catalog = inputs.catalog;
let skel_of = |index: usize| catalog.get_at(index).map(|entry| &entry.skel);
let mut models: Vec<PreparedModel> = Vec::with_capacity(assembly.parts.len());
let mut packed_ok = true;
for part in &assembly.parts {
let index = part.model.order();
let skel = skel_of(index).ok_or(FpvRigError::Catalog("model"))?;
let lod_range = skel.surfaces_for_lod(0);
let first_surface = lod_range.start;
let layout = build_skin_layout(skel, 0, |lod_local| {
surface_admitted(
skel,
part.hide.as_ref(),
inputs.material_by_authored,
first_surface + lod_local,
)
})
.ok_or(FpvRigError::Layout("skin layout refused the model"))?;
if skel.packed_vertices.len() != skel.positions.len() {
packed_ok = false;
}
models.push(PreparedModel {
catalog_entry: index,
owner: surf_owner(part.role),
bone_base: part.bone_base,
posed_surface_n: lod_range.len(),
layout,
});
}
admission: &FpvMaterialAdmission,
tracks: [Vec<Option<Arc<[u16]>>>; 2],
) -> Self {
let parts_n = composition.parts.len();
// Every hand's view hands first, then every hand's gun and whatever
// hangs off it.
@@ -203,22 +353,23 @@ impl PreparedFpvRig {
order.push((hand, 0));
}
for hand in 0..hand_n {
for model in 1..models.len() {
for part in 1..parts_n {
// The scope and the rocket hang off the right gun only.
if hand == 1 && model != 1 {
if hand == 1 && part != 1 {
continue;
}
order.push((hand, model));
order.push((hand, part));
}
}
let mut geometry = PreparedFpvGeometry {
indices: Vec::new(),
segments: Vec::with_capacity(order.len()),
index_n: 0,
surface_ranges: Vec::new(),
materials: Vec::new(),
draws: Vec::new(),
dest_n: 0,
packed_ok,
packed_ok: true,
hands_plan_n: 0,
gun_plan_n: 0,
scope_plan_n: 0,
@@ -227,60 +378,29 @@ impl PreparedFpvRig {
plan_draw_n: 0,
plan_skip_n: 0,
};
let mut slots: Vec<PlanSlot> = Vec::new();
let mut slots: Vec<PlanSlot> = Vec::with_capacity(order.len());
let mut material_key: HashMap<usize, u32> = HashMap::new();
let mut dest_base = 0usize;
let mut posed_surface_n = 0usize;
for (hand, model_index) in order {
let model = &models[model_index];
let skel = skel_of(model.catalog_entry).ok_or(FpvRigError::Catalog("model"))?;
let entry = catalog
.get_at(model.catalog_entry)
.ok_or(FpvRigError::Catalog("model"))?;
for (hand, part_index) in order {
let part = &composition.parts[part_index];
let model = &part.model;
posed_surface_n += model.posed_surface_n;
for surface in &model.layout.surfaces {
if !surface.visible || surface.index_count == 0 {
continue;
}
let Some(authored) = skel
.surface_materials
.get(surface.surface_index)
.copied()
.flatten()
.map(|index| index.get())
else {
geometry.packed_ok &= model.packed_ok;
let index_base = geometry.index_n as u32;
for surface in &model.surfaces {
let Some(material) = admission.materials.get(surface.material as usize) else {
continue;
};
let Some(&session_index) = inputs.material_by_authored.get(&authored) else {
continue;
};
let Some(material) = inputs.materials.get(session_index as usize).cloned() else {
continue;
};
let is_scope = model.owner == FpvSurfOwner::Scope;
let is_lens = is_scope
&& entry
.material_names
.get(surface.surface_index)
.and_then(|name| name.as_deref())
.is_some_and(leftover_scope_surf_is_lens);
let index_start = geometry.indices.len() as u32;
let src = surface.index_start as usize;
let end = src.saturating_add(surface.index_count as usize);
let rebase = dest_base as u32;
geometry.indices.extend(
model.layout.indices[src..end]
.iter()
.map(|index| index + rebase),
);
let is_scope = part.owner == FpvSurfOwner::Scope;
let is_lens = is_scope && surface.lens_named;
let range_index = geometry.surface_ranges.len() as u32;
geometry
.surface_ranges
.push((index_start, surface.index_count));
let material_index = *material_key.entry(authored).or_insert_with(|| {
.push((index_base + surface.index_start, surface.index_count));
let material_index = *material_key.entry(surface.authored).or_insert_with(|| {
let index = geometry.materials.len() as u32;
geometry.materials.push(material);
geometry.materials.push(material.clone());
index
});
geometry.draws.push(FpvSurfaceDraw {
@@ -289,7 +409,7 @@ impl PreparedFpvRig {
is_scope: is_scope && !is_lens,
});
if hand == 0 {
match model.owner {
match part.owner {
FpvSurfOwner::Hands => geometry.hands_plan_n += 1,
_ => geometry.gun_plan_n += 1,
}
@@ -303,9 +423,13 @@ impl PreparedFpvRig {
}
}
}
geometry
.segments
.push((Arc::clone(model), dest_base as u32));
geometry.index_n += model.layout.indices.len();
slots.push(PlanSlot {
hand,
model: model_index,
part: part_index,
dest_base,
});
dest_base += model.layout.dest_vertex_n;
@@ -314,40 +438,34 @@ impl PreparedFpvRig {
geometry.plan_draw_n = geometry.draws.len() as u32;
geometry.plan_skip_n = posed_surface_n.saturating_sub(geometry.draws.len()) as u32;
let orders = inputs.clip_orders.map(<[Option<usize>]>::to_vec);
let tracks = [0, 1].map(|hand| {
orders[hand]
.iter()
.map(|order| assembly.compose_tracks((*order)?, inputs.clip_tracks))
.collect()
});
let assembly = &composition.assembly;
let tags = FpvBoltTags {
flash: assembly.tags.flash,
flash_silenced: assembly.tags.flash_silenced,
brass: assembly.tags.brass,
knife: assembly.tags.knife,
laser: assembly.tags.laser,
tracker_screen: assembly.tags.tracker_screen,
tracker_light: assembly.tags.tracker_light,
};
let parts = assembly.dobj.all_parts();
static GENERATION: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
let parts = assembly.dobj.all_parts();
Ok(Self {
assembly,
Self {
composition,
dual,
generation: GENERATION.fetch_add(1, std::sync::atomic::Ordering::Relaxed),
parts,
tags,
models,
slots,
orders,
tracks,
geometry,
})
}
}
/// One hand's bones for this frame. No skeleton is built here.
pub fn pose_hand(
&mut self,
&self,
hand: usize,
anims: &[PosedClip<'_>],
offset: Vec3,
@@ -355,40 +473,45 @@ impl PreparedFpvRig {
if anims.is_empty() {
return None;
}
let instances: Vec<AnimInstance<'_>> = anims
let bound: Vec<(&PosedClip<'_>, Vec<Option<usize>>)> = anims
.iter()
.filter_map(|anim| {
let tracks = self.tracks[hand].get(anim.node)?.as_ref()?;
Some(AnimInstance {
clip: anim.clip,
tracks,
time: anim.time,
weight: anim.weight,
parts: None,
})
let bones = tracks
.iter()
.map(|&bone| (bone != FpvClipTracks::NONE).then_some(usize::from(bone)))
.collect();
Some((anim, bones))
})
.collect();
if !instances
if !bound
.iter()
.any(|instance| instance.tracks.iter().any(Option::is_some))
.any(|(_, bones)| bones.iter().any(Option::is_some))
{
return None;
}
let world = self
.assembly
.dobj
.pose(&instances, &self.parts, Mat4::IDENTITY);
let skin = self.assembly.dobj.skin_matrices(&world);
let eye_from_world = tag_view_to_bevy_camera() * world[self.assembly.view_bone].inverse();
let lens = self
.assembly
let instances: Vec<AnimInstance<'_>> = bound
.iter()
.map(|(anim, bones)| AnimInstance {
clip: anim.clip,
tracks: bones,
time: anim.time,
weight: anim.weight,
parts: None,
})
.collect();
let assembly = &self.composition.assembly;
let world = assembly.dobj.pose(&instances, &self.parts, Mat4::IDENTITY);
let skin = assembly.dobj.skin_matrices(&world);
let eye_from_world = tag_view_to_bevy_camera() * world[assembly.view_bone].inverse();
let lens = assembly
.camera_bone
.map(|camera| tag_camera_lens_local(world[self.assembly.view_bone], world[camera]))
.map(|camera| tag_camera_lens_local(world[assembly.view_bone], world[camera]))
.unwrap_or(Mat4::IDENTITY);
let published = if hand == 0 {
world.len()
} else {
self.assembly.paired_bones.min(world.len())
assembly.paired_bones.min(world.len())
};
let shift = (offset != Vec3::ZERO).then(|| Mat4::from_translation(offset));
let bones = world[..published]
@@ -425,7 +548,8 @@ impl PreparedFpvRig {
let Some(pose) = poses[slot.hand].as_ref() else {
continue;
};
let model = &self.models[slot.model];
let part = &self.composition.parts[slot.part];
let model = &part.model;
let Some(skel) = catalog.get_at(model.catalog_entry).map(|entry| &entry.skel) else {
continue;
};
@@ -436,7 +560,7 @@ impl PreparedFpvRig {
let rows = &mut dest[slot.dest_base..end];
skin_packed_into(
skel,
|bone| pose.eye_from_world * pose.skin[model.bone_base + bone],
|bone| pose.eye_from_world * pose.skin[part.bone_base + bone],
|_| false,
0,
&model.layout,
+2
View File
@@ -3,7 +3,9 @@ pub mod dobj_pose;
pub mod fpv;
pub mod fpv_host;
pub mod fpv_pose;
pub mod fpv_prepared;
pub mod fpv_rig;
pub mod model_materials;
pub mod pose_types;
pub mod remote_body;
pub mod scene_submission;
@@ -0,0 +1,305 @@
use std::collections::HashMap;
use std::sync::Arc;
use bevy::prelude::*;
use render_material::RuntimeMaterialCatalog;
use render_scene::{SmodelPassMaterial, TessMaterials, WorldModelLightingAtlas};
type ModelMaterialsOwner = (
Arc<RuntimeMaterialCatalog>,
Option<bevy::asset::AssetId<Image>>,
Option<usize>,
Option<u64>,
);
#[derive(Resource, Default)]
pub struct PreparedModelMaterials {
owner: Option<ModelMaterialsOwner>,
by_name: HashMap<String, SmodelPassMaterial>,
by_authored: HashMap<assets::MaterialIndex, SmodelPassMaterial>,
scene_dobjs: HashMap<String, Arc<assets::DObj>>,
projectile_materials: HashMap<assets::MaterialIndex, SmodelPassMaterial>,
projectile_dobjs: HashMap<String, Arc<assets::DObj>>,
}
impl PreparedModelMaterials {
fn owns(&self, owner: &ModelMaterialsOwner) -> bool {
self.owner
.as_ref()
.is_some_and(|(catalog, atlas, bodies, world)| {
Arc::ptr_eq(catalog, &owner.0)
&& *atlas == owner.1
&& *bodies == owner.2
&& *world == owner.3
})
}
pub fn settled_for(&self, catalog: &Arc<RuntimeMaterialCatalog>) -> bool {
self.owner
.as_ref()
.is_some_and(|(owned, ..)| Arc::ptr_eq(owned, catalog))
}
pub fn material(
&self,
catalog: &Arc<RuntimeMaterialCatalog>,
name: &str,
) -> Option<&SmodelPassMaterial> {
if !self.settled_for(catalog) {
return None;
}
self.by_name.get(name)
}
pub fn authored(
&self,
catalog: &Arc<RuntimeMaterialCatalog>,
authored: assets::MaterialIndex,
) -> Option<&SmodelPassMaterial> {
if !self.settled_for(catalog) {
return None;
}
self.by_authored.get(&authored)
}
pub fn scene_dobj(&self, model: &str) -> Option<&Arc<assets::DObj>> {
self.scene_dobjs.get(model)
}
pub fn projectile_material(
&self,
catalog: &Arc<RuntimeMaterialCatalog>,
authored: assets::MaterialIndex,
) -> Option<&SmodelPassMaterial> {
if !self.settled_for(catalog) {
return None;
}
self.projectile_materials.get(&authored)
}
pub fn projectile_dobj(&self, model: &str) -> Option<&Arc<assets::DObj>> {
self.projectile_dobjs.get(model)
}
pub fn clear(&mut self) {
*self = Self::default();
}
}
fn admit_names<'a>(
names: impl IntoIterator<Item = &'a str>,
atlas: &WorldModelLightingAtlas,
catalog: &RuntimeMaterialCatalog,
by_name: &mut HashMap<String, SmodelPassMaterial>,
refused: &mut Vec<String>,
) {
for name in names {
if by_name.contains_key(name) || refused.iter().any(|seen| seen == name) {
continue;
}
match crate::body_lit_pass_material(atlas, catalog, name) {
Some(material) => {
by_name.insert(name.to_owned(), material);
}
None => refused.push(name.to_owned()),
}
}
}
pub fn scene_lit_pass_material(
atlas: &WorldModelLightingAtlas,
tess: &TessMaterials,
authored: assets::MaterialIndex,
) -> Option<SmodelPassMaterial> {
let world_material = tess.catalog.derived(authored)?;
let ordinal = tess
.catalog
.sorted_materials
.ordinal_for_asset_id(authored.order())?;
let maps =
render_scene::runtime_maps(Some(authored), &tess.catalog, tess.material_images.as_ref());
let inv_h = lighting_iw4::model_lighting_inv_image_height(atlas.dims.image_height)?;
let scale = lighting_iw4::model_lighting_lookup_scale(inv_h);
Some(SmodelPassMaterial {
model_lighting_required: true,
color: maps.color,
specular: maps.specular,
probe: None,
atlas: Some(atlas.image.clone()),
alpha_mode: maps.alpha_mode,
draw_mode: maps.draw_mode,
cull_mode: maps.cull_mode,
env_map_parms: maps.env_map_parms,
lighting_lookup_scale: [scale.u, scale.v, scale.w, scale.q],
atlas_lookup: [
lighting_iw4::MODEL_LIGHTING_INV_ATLAS_WIDTH as f32,
inv_h,
lighting_iw4::MODEL_LIGHTING_VOLUME_W,
0.0,
],
sort_key: world_material.sort_key,
material_sorted_index: Some(ordinal.get()),
})
}
fn present_names<'a>(
names: &'a [Option<String>],
edges: &'a [assets::AssetEdge<assets::MaterialSpace>],
) -> impl Iterator<Item = &'a str> {
names
.iter()
.zip(edges)
.filter(|(_, edge)| edge.is_bound())
.filter_map(|(name, _)| name.as_deref())
}
pub fn prepare_model_materials(
tess: Res<TessMaterials>,
atlas: Option<Res<WorldModelLightingAtlas>>,
bodies: Option<Res<assets::PreparedBodies>>,
world_weapons: Option<Res<assets::PreparedWorldWeapons>>,
scene_models: Option<Res<assets::MapXModelSceneCatalog>>,
projectiles: Option<Res<assets::PreparedProjectileMeshes>>,
mut prepared: ResMut<PreparedModelMaterials>,
) {
if tess.material_images.is_empty() {
return;
}
let owner: ModelMaterialsOwner = (
Arc::clone(&tess.catalog),
atlas.as_ref().map(|atlas| atlas.image.id()),
bodies
.as_ref()
.map(|bodies| Arc::as_ptr(&bodies.0) as usize),
world_weapons.as_ref().map(|world| world.0.identity()),
);
if prepared.owns(&owner) {
return;
}
let (Some(atlas), Some(bodies), Some(world)) = (atlas, bodies, world_weapons) else {
diag::info!(
World,
"model materials: nothing to admit (lighting atlas or model catalogs missing)"
);
*prepared = PreparedModelMaterials {
owner: Some(owner),
..Default::default()
};
return;
};
let started = std::time::Instant::now();
let mut by_name = HashMap::new();
let mut refused = Vec::new();
for name in bodies.0.names() {
if let Some(entry) = bodies.0.get(name) {
admit_names(
present_names(&entry.material_names, &entry.material_edges),
&atlas,
&tess.catalog,
&mut by_name,
&mut refused,
);
}
}
for index in 0..world.0.len() {
if let Some(entry) = world.0.get_at(index) {
admit_names(
present_names(&entry.material_names, &entry.material_edges),
&atlas,
&tess.catalog,
&mut by_name,
&mut refused,
);
}
}
let mut by_authored = HashMap::new();
let mut scene_dobjs = HashMap::new();
if let Some(scene_models) = scene_models.as_deref() {
for (key, asset) in scene_models.iter() {
let skel = match asset {
assets::MapXModelSceneAsset::Iw4(skel)
| assets::MapXModelSceneAsset::Iw5(skel)
| assets::MapXModelSceneAsset::T5(skel) => skel,
assets::MapXModelSceneAsset::Unavailable { .. } => continue,
};
for surface in 0..skel.surface_materials.len() {
let Some(authored) = scene_models.surface_material(key, surface) else {
continue;
};
if by_authored.contains_key(&authored) {
continue;
}
if let Some(material) = scene_lit_pass_material(&atlas, &tess, authored) {
by_authored.insert(authored, material);
}
}
if let Some(pose) = skel.pose.as_ref()
&& let Ok(dobj) = assets::DObj::build(&[(pose, None)])
{
scene_dobjs.insert(key.0.clone(), Arc::new(dobj));
}
}
}
let mut projectile_materials = HashMap::new();
let mut projectile_dobjs = HashMap::new();
if let Some(projectiles) = projectiles.as_deref() {
for name in projectiles.0.names() {
let Some(entry) = projectiles.0.get(name) else {
continue;
};
for surface in 0..entry.material_edges.len() {
let Some(authored) = entry.material_index(surface) else {
continue;
};
if projectile_materials.contains_key(&authored) {
continue;
}
if let Some(material) = crate::authored_lit_xmodel_pass_material(
&atlas,
&tess.catalog,
tess.material_images.as_ref(),
authored,
) {
projectile_materials.insert(authored, material);
}
}
if let Some(pose) = entry.skel.pose.as_ref()
&& let Ok(dobj) = assets::DObj::build(&[(pose, None)])
{
projectile_dobjs.insert(name.to_owned(), Arc::new(dobj));
}
}
}
diag::info!(
World,
"model materials prepared before Ready: admitted={} map_model_materials={} map_model_dobjs={} projectile_materials={} projectile_dobjs={} refused={} elapsed={:.1}ms{}",
by_name.len(),
by_authored.len(),
scene_dobjs.len(),
projectile_materials.len(),
projectile_dobjs.len(),
refused.len(),
started.elapsed().as_secs_f64() * 1000.0,
if refused.is_empty() {
String::new()
} else {
format!(
" first_refused=[{}]",
refused
.iter()
.take(8)
.cloned()
.collect::<Vec<_>>()
.join(",")
)
}
);
*prepared = PreparedModelMaterials {
owner: Some(owner),
by_name,
by_authored,
scene_dobjs,
projectile_materials,
projectile_dobjs,
};
}
+178 -5
View File
@@ -14,7 +14,7 @@ use crate::anim::xmodel_pose::PosedSmodelSurface;
#[derive(Clone, Debug)]
pub struct PersistentRemoteTree {
pub runtime: assets::dobj::XAnimTreeRuntime,
pub dobj: Option<assets::DObj>,
pub dobj: Option<std::sync::Arc<assets::DObj>>,
pub reuse_key: Option<assets::dobj::DObjReuseKey>,
pub legs: u16,
pub torso: u16,
@@ -557,10 +557,166 @@ pub fn kit_dobj_radius(radii: impl IntoIterator<Item = f32>) -> Option<f32> {
.fold(None::<f32>, |acc, r| Some(acc.map_or(r, |a| a.max(r))))
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum KitSource<'a> {
Body(&'a str),
World(usize),
}
#[derive(Clone, Debug, PartialEq, Eq)]
enum PreparedKitSource {
Body(String),
World(usize),
}
pub struct PreparedRemoteKit {
sources: Vec<PreparedKitSource>,
pub radius: Option<f32>,
pub hide_part_bits: [u32; 6],
pub dobj: Option<std::sync::Arc<assets::DObj>>,
pub bolt_bones: [Option<u16>; 4],
pub head_bone: Option<usize>,
}
impl PreparedRemoteKit {
pub fn models<'a>(
&'a self,
bodies: &'a assets::PreparedBodies,
world: &'a assets::PreparedWorldWeapons,
) -> Option<Vec<KitModel<'a>>> {
self.sources
.iter()
.map(|source| match source {
PreparedKitSource::Body(key) => {
let entry = bodies.0.get(key)?;
Some(KitModel {
name: entry.skel.name.as_str(),
skel: &entry.skel,
hide_tags: Vec::new(),
source: KitSource::Body(key.as_str()),
})
}
PreparedKitSource::World(index) => {
let entry = world.0.get_at(*index)?;
Some(KitModel {
name: entry.skel.name.as_str(),
skel: &entry.skel,
hide_tags: Vec::new(),
source: KitSource::World(*index),
})
}
})
.collect()
}
}
#[derive(bevy::prelude::Resource, Default)]
pub struct PreparedRemoteKits {
owner: Option<(
std::sync::Arc<assets::BodyMeshCatalog>,
std::sync::Arc<assets::WeaponRegistry>,
u64,
)>,
kits: std::collections::HashMap<(bool, u32), PreparedRemoteKit>,
}
impl PreparedRemoteKits {
pub fn owned_by(
&self,
bodies: &assets::PreparedBodies,
weapons: &assets::PreparedWeapons,
world_weapons: &assets::PreparedWorldWeapons,
) -> bool {
self.owner.as_ref().is_some_and(|(b, w, catalog)| {
std::sync::Arc::ptr_eq(b, &bodies.0)
&& std::sync::Arc::ptr_eq(w, &weapons.0)
&& *catalog == world_weapons.0.identity()
})
}
pub fn get(&self, axis: bool, weapon: u32) -> Option<&PreparedRemoteKit> {
self.kits.get(&(axis, weapon))
}
pub fn prepare(
bodies: &assets::PreparedBodies,
weapons: &assets::PreparedWeapons,
world_weapons: &assets::PreparedWorldWeapons,
) -> Self {
let mut kits = std::collections::HashMap::new();
for axis in [false, true] {
for weapon in 0..=weapons.0.len() as u32 {
let Some((models, radius)) = occupy_remote_kit_dobj(
bodies,
Some(weapons),
Some(world_weapons),
axis,
weapon,
true,
) else {
continue;
};
let sources = models
.iter()
.map(|model| match model.source {
KitSource::Body(key) => PreparedKitSource::Body(key.to_owned()),
KitSource::World(index) => PreparedKitSource::World(index),
})
.collect();
let dobj =
select_remote_models(bodies, Some(weapons), Some(world_weapons), axis, weapon)
.ok()
.and_then(|set| assets::DObj::build(&set.dobj_models).ok())
.map(std::sync::Arc::new);
kits.insert(
(axis, weapon),
PreparedRemoteKit {
sources,
radius,
hide_part_bits: kit_hide_part_bits(&models),
bolt_bones: [
"tag_flash",
"tag_brass",
"tag_knife_fx",
fx_iw4::FX_LASER_TAG,
]
.map(|tag| {
dobj.as_ref()
.and_then(|dobj| dobj.find(tag))
.and_then(|bone| u16::try_from(bone).ok())
}),
head_bone: dobj.as_ref().and_then(|dobj| dobj.find("j_head")),
dobj,
},
);
}
}
Self {
owner: Some((
std::sync::Arc::clone(&bodies.0),
std::sync::Arc::clone(&weapons.0),
world_weapons.0.identity(),
)),
kits,
}
}
}
pub fn kit_hide_part_bits(models: &[KitModel<'_>]) -> [u32; 6] {
let mut bits = [0u32; 6];
let mut base = 0usize;
for model in models {
render_scene::hide_part_bits_from_tags(&mut bits, model.skel, base, &model.hide_tags);
base += model.skel.bones.len();
}
bits
}
pub struct KitModel<'a> {
pub name: &'a str,
pub skel: &'a assets::ModelSkel,
pub hide_tags: Vec<String>,
pub source: KitSource<'a>,
}
pub fn occupy_remote_kit_dobj<'a>(
@@ -569,6 +725,7 @@ pub fn occupy_remote_kit_dobj<'a>(
world_weapons: Option<&'a assets::PreparedWorldWeapons>,
axis: bool,
weapon: u32,
with_hide_tags: bool,
) -> Option<(Vec<KitModel<'a>>, Option<f32>)> {
let kits = bodies.0.kits();
let kit = kits.kit(axis)?;
@@ -581,6 +738,7 @@ pub fn occupy_remote_kit_dobj<'a>(
name: body.skel.name.as_str(),
skel: &body.skel,
hide_tags: Vec::new(),
source: KitSource::Body(kit.body.as_str()),
}];
if let Some(name) = kit.head.as_deref() {
if let Some(entry) = bodies.0.get(name) {
@@ -592,13 +750,20 @@ pub fn occupy_remote_kit_dobj<'a>(
name: entry.skel.name.as_str(),
skel: &entry.skel,
hide_tags: Vec::new(),
source: KitSource::Body(name),
});
}
}
}
if weapon != 0 {
if let (Some(registry), Some(catalog)) = (weapons, world_weapons) {
if let Some(entry) = registry.0.world_model_entry(weapon, &catalog.0) {
let gun_index = registry
.0
.world_model_edge_of(weapon)
.and_then(|edge| edge.bound_index());
if let (Some(entry), Some(gun_index)) =
(registry.0.world_model_entry(weapon, &catalog.0), gun_index)
{
if let (Some(_gun_pose), Some(_tag)) = (
entry.skel.pose.as_ref(),
assets::tp_weapon_attach_tag(&body.skel.bone_names),
@@ -607,14 +772,17 @@ pub fn occupy_remote_kit_dobj<'a>(
name: entry.skel.name.as_str(),
skel: &entry.skel,
hide_tags: weapons
.filter(|_| with_hide_tags)
.map(|registry| assets::effective_hide_tags(&registry.0, weapon))
.unwrap_or_default(),
.unwrap_or_else(Vec::new),
source: KitSource::World(gun_index),
});
for attachment in world_attachments(&registry.0, &catalog.0, weapon) {
skels.push(KitModel {
name: attachment.entry.skel.name.as_str(),
skel: &attachment.entry.skel,
hide_tags: Vec::new(),
source: KitSource::World(attachment.index.order()),
});
}
}
@@ -871,6 +1039,7 @@ pub fn ensure_remote_dobj(
e_type: i32,
persist_key: u32,
trees: &mut RemoteBodyTrees,
prepared: Option<&std::sync::Arc<assets::DObj>>,
) -> Result<(), String> {
let gun_name = models
.gun
@@ -890,8 +1059,12 @@ pub fn ensure_remote_dobj(
);
let slot = trees.get_mut(persist_key).expect("tree slot inserted");
if !remote_dobj_reuses(slot.dobj.is_some(), slot.reuse_key, reuse_key) {
slot.dobj =
Some(assets::DObj::build(&models.dobj_models).map_err(|error| error.to_string())?);
slot.dobj = Some(std::sync::Arc::clone(prepared.ok_or_else(|| {
format!(
"remote representation not prepared: body={} head={} weapon={gun_name}",
models.body_name, models.head_name
)
})?));
slot.reuse_key = Some(reuse_key);
}
Ok(())
@@ -30,9 +30,54 @@ pub struct AnimDObjSceneSubmission {
#[derive(Resource, Default)]
pub struct AnimDObjSceneSkels {
by_name: HashMap<String, Arc<assets::ModelSkel>>,
seeded_for: Option<(usize, u64)>,
}
impl AnimDObjSceneSkels {
pub fn seed(&mut self, bodies: &assets::PreparedBodies, world: &assets::PreparedWorldWeapons) {
let owner = (Arc::as_ptr(&bodies.0) as usize, world.0.identity());
if self.seeded_for == Some(owner) {
return;
}
let kits = bodies.0.kits();
for kit in [kits.kit(false), kits.kit(true)].into_iter().flatten() {
for name in std::iter::once(&kit.body).chain(kit.head.as_ref()) {
let Some(entry) = bodies.0.get(name) else {
continue;
};
self.by_name
.entry(entry.skel.name.clone())
.or_insert_with(|| Arc::new(entry.skel.clone()));
}
}
for index in 0..world.0.len() {
if let Some(entry) = world.0.get_at(index) {
self.by_name
.entry(entry.skel.name.clone())
.or_insert_with(|| Arc::clone(&entry.skel));
}
}
self.seeded_for = Some(owner);
}
pub fn shared(
&mut self,
name: &str,
skel: &Arc<assets::ModelSkel>,
lod: i8,
) -> AnimDObjSceneModel {
let skel = Arc::clone(
self.by_name
.entry(name.to_owned())
.or_insert_with(|| Arc::clone(skel)),
);
AnimDObjSceneModel {
lod,
bone_count: u8::try_from(skel.bones.len()).unwrap_or(u8::MAX),
skel,
}
}
pub fn model(&mut self, name: &str, skel: &assets::ModelSkel, lod: i8) -> AnimDObjSceneModel {
let skel = match self.by_name.get(name) {
Some(skel) => Arc::clone(skel),
@@ -264,7 +264,14 @@ impl ViewmodelController {
positive_ms(self.weapon.quick_raise_time_ms),
),
WeaponAnimSlot::EmptyRaise | WeaponAnimSlot::AltRaise => (
WeaponAnimSlot::AltRaise => (
WeaponState::Raising {
first: false,
quick: false,
},
positive_ms(self.weapon.alternate_raise_time_ms),
),
WeaponAnimSlot::EmptyRaise => (
WeaponState::Raising {
first: false,
quick: false,
@@ -311,7 +318,11 @@ impl ViewmodelController {
positive_ms(self.weapon.quick_drop_time_ms),
),
WeaponAnimSlot::EmptyDrop | WeaponAnimSlot::AltDrop => (WeaponState::Dropping, None),
WeaponAnimSlot::AltDrop => (
WeaponState::Dropping,
positive_ms(self.weapon.alternate_drop_time_ms),
),
WeaponAnimSlot::EmptyDrop => (WeaponState::Dropping, None),
WeaponAnimSlot::SprintIn => (
WeaponState::SprintIn,
positive_ms(self.weapon.sprint_raise_time_ms),
@@ -360,8 +371,9 @@ impl ViewmodelController {
true
}
pub fn apply_empty_idle_weap_anim(&mut self) {
self.start_idle_family(true, true);
pub fn apply_force_idle_weap_anim(&mut self, empty_mag: bool) {
let interrupt = self.dispatch_range_unfinished();
self.start_idle_family(empty_mag, interrupt);
}
pub fn settle_fire_to_idle(&mut self) {
+1 -2
View File
@@ -735,8 +735,7 @@ pub fn install_prepared_fpv_plan(
geometry: &crate::anim::fpv_rig::PreparedFpvGeometry,
lighting_handle: u32,
) {
plan.indices.clear();
plan.indices.extend_from_slice(&geometry.indices);
geometry.write_indices(&mut plan.indices);
plan.surface_ranges.clear();
plan.surface_ranges
.extend_from_slice(&geometry.surface_ranges);
+5
View File
@@ -14,6 +14,11 @@ pub use anim::fpv_host::{
FpvPoseRefuse, FpvPosedFrame, FpvPresentCursor, PendingFpvNotetracks, PendingFpvSpawn,
PendingFpvSpawnRequest, generate_fpv_pose,
};
pub use anim::fpv_prepared::{
FpvPreparationCensus, FpvRigSet, FpvWeaponSlot, FpvWeaponTable, FpvWeaponView, PreparedFpv,
prepare_fpv_compositions,
};
pub use anim::model_materials::{PreparedModelMaterials, prepare_model_materials};
pub use anim::viewmodel_controller::{
AdvanceResult, EventResult, ViewmodelController, ViewmodelEvent, WeaponState,
};
+6 -48
View File
@@ -1,5 +1,3 @@
use std::collections::HashMap;
use bevy::prelude::*;
use dpvs_iw4::{
Bounds, DpvsPlanes, dyn_ent_cell_bits_len, dyn_ent_client_word_count, dyn_ent_in_cell,
@@ -480,6 +478,7 @@ fn pose_dyn_ents(
instances: Query<(Entity, &WorldDynEntInstance, &Transform, &Visibility)>,
cameras: Query<(&GlobalTransform, &Projection, &Camera), With<FpvLens>>,
lod_skinned: Res<render_scene::LodRampSkinnedDvar>,
prepared: Res<crate::anim::model_materials::PreparedModelMaterials>,
mut product: ResMut<DynEntPoseProduct>,
) {
product.owners.clear();
@@ -526,10 +525,7 @@ fn pose_dyn_ents(
{
index
} else {
let Some(pose) = skel.pose.as_ref() else {
continue;
};
let Some(dobj) = assets::DObj::build(&[(pose, None)]).ok() else {
let Some(dobj) = prepared.scene_dobj(&inst.current_model.0) else {
continue;
};
let dobj_state =
@@ -540,7 +536,7 @@ fn pose_dyn_ents(
let Some((surfaces, authored)) = pose_script_dobj_with_materials(
Some(catalog),
&[skel],
&dobj,
dobj,
&request,
Some(DObjLodView {
origin: inst.lighting_origin,
@@ -581,6 +577,7 @@ fn append_dynent_draws(
mut plan: ResMut<DynEntDrawPlan>,
mut lighting_requests: ResMut<ModelLightingRequests>,
product: Res<DynEntPoseProduct>,
model_materials: Res<crate::anim::model_materials::PreparedModelMaterials>,
mut last_material_generation: Local<Option<render_material::MaterialGenerationId>>,
mut draws: Local<Vec<XModelSurfaceDraw>>,
mut owners: Local<Vec<DynEntOwnerDraw>>,
@@ -593,8 +590,8 @@ fn append_dynent_draws(
let catalog_changed = catalog.as_ref().is_some_and(|c| c.is_changed());
let atlas_changed = atlas.as_ref().is_some_and(|a| a.is_changed());
let tess_changed = tess.as_ref().is_some_and(|h| h.is_changed());
let atlas = atlas_ref.expect("checked");
let tess = tess.as_deref().expect("checked");
let tess_catalog = std::sync::Arc::clone(&tess.catalog);
let material_generation = tess.catalog.generation_id;
let catalog_reset = catalog_changed
|| atlas_changed
@@ -610,7 +607,6 @@ fn append_dynent_draws(
draws.clear();
owners.clear();
let mut material_cache: HashMap<assets::MaterialIndex, SmodelPassMaterial> = HashMap::new();
let mut drawn = 0u16;
let mut packed_assets: Vec<Option<Vec<(u32, u32)>>> = vec![None; product.assets.len()];
@@ -626,45 +622,7 @@ fn append_dynent_draws(
.iter()
.zip(posed.authored.iter().copied())
.map(|(_surface, authored)| {
let authored = authored?;
if let Some(material) = material_cache.get(&authored) {
return Some(material.clone());
}
let world_material = tess.catalog.derived(authored)?;
let ordinal = tess
.catalog
.sorted_materials
.ordinal_for_asset_id(authored.order())?;
let maps = render_scene::runtime_maps(
Some(authored),
&tess.catalog,
tess.material_images.as_ref(),
);
let inv_h =
lighting_iw4::model_lighting_inv_image_height(atlas.dims.image_height)?;
let scale = lighting_iw4::model_lighting_lookup_scale(inv_h);
let material = SmodelPassMaterial {
model_lighting_required: true,
color: maps.color,
specular: maps.specular,
probe: None,
atlas: Some(atlas.image.clone()),
alpha_mode: maps.alpha_mode,
draw_mode: maps.draw_mode,
cull_mode: maps.cull_mode,
env_map_parms: maps.env_map_parms,
lighting_lookup_scale: [scale.u, scale.v, scale.w, scale.q],
atlas_lookup: [
lighting_iw4::MODEL_LIGHTING_INV_ATLAS_WIDTH as f32,
inv_h,
lighting_iw4::MODEL_LIGHTING_VOLUME_W,
0.0,
],
sort_key: world_material.sort_key,
material_sorted_index: Some(ordinal.get()),
};
material_cache.insert(authored, material.clone());
Some(material)
model_materials.authored(&tess_catalog, authored?).cloned()
})
.collect();
if materials.iter().all(Option::is_none) {
+149 -559
View File
@@ -1,11 +1,7 @@
use std::collections::HashMap;
use std::sync::Arc;
use anim_iw4::{DOBJ_RADIUS_PARENT_ROOT, dobj_compute_bounds_radius};
use assets::{
AssetEdge, AssetNamespace, FpvMeshIndex, MaterialSpace, PreparedFpvMeshes, PreparedWeapons,
PreparedXAnims, TS_COLOR_MAP, WeaponAnimations, WeaponRegistry,
};
use assets::PreparedFpvMeshes;
use bevy::ecs::system::SystemParam;
use bevy::prelude::*;
use frame::{LifeFrontPublished, PresentedPublished, ViewSubject, WorkerCmdSet};
@@ -18,6 +14,7 @@ use crate::anim::fpv::{
local_shot_identity,
};
use crate::anim::fpv_host::{FpvGenerateArgs, FpvPoseKind, FpvPoseRefuse, generate_fpv_pose};
use crate::anim::fpv_prepared::{FpvWeaponSlot, FpvWeaponTable, FpvWeaponView, PreparedFpv};
use crate::anim::fpv_rig::PreparedFpvRig;
use crate::anim::scene_submission::{AnimDObjSceneSubmission, AnimSceneSubmit};
use crate::anim::viewmodel_controller::ViewmodelController;
@@ -36,7 +33,7 @@ use hud_iw4::{
};
use math_iw4::angle_vectors;
use playerstate_iw4::PlayerState;
use render_material::{RuntimeMaterialCatalog, RuntimeSortedMaterialTable};
use render_material::RuntimeMaterialCatalog;
use render_scene::WorldScriptModelInstance;
use render_scene::{FlyCamera, FpvLens};
use render_scene::{HostGfxScene, scene_quat_from_viewmodel_axes};
@@ -75,33 +72,22 @@ pub struct SessionViewmodel(pub Option<SessionFpvMeshesHandles>);
pub struct SessionFpvMeshesHandles {
pub weapon_id: u32,
pub parent_weapon: u32,
pub catalog_id: u64,
pub assemblies: assets::FpvSideAssemblies,
pub axis: bool,
pub view: Arc<FpvWeaponView>,
pub fpv: EquippedFpv,
pub(crate) active_rig: Option<usize>,
pub(crate) materials: Vec<render_scene::SmodelPassMaterial>,
pub(crate) material_by_authored: HashMap<usize, u32>,
pub(crate) active_rig: Option<Arc<PreparedFpvRig>>,
pub(crate) material_catalog: Arc<RuntimeMaterialCatalog>,
}
#[derive(Resource, Default)]
pub struct FpvRigShelf {
owner: Option<(Arc<RuntimeMaterialCatalog>, u64)>,
rigs: Vec<PreparedFpvRig>,
}
impl FpvRigShelf {
fn hold(&mut self, materials: &Arc<RuntimeMaterialCatalog>, catalog_id: u64) {
let owned = self
.owner
.as_ref()
.is_some_and(|(owner, id)| Arc::ptr_eq(owner, materials) && *id == catalog_id);
if !owned {
self.rigs.clear();
self.owner = Some((Arc::clone(materials), catalog_id));
fn same_compositions(a: &assets::FpvSideAssemblies, b: &assets::FpvSideAssemblies) -> bool {
Arc::ptr_eq(&a.bare, &b.bare)
&& match (&a.rocket, &b.rocket) {
(Some(a), Some(b)) => Arc::ptr_eq(a, b),
(None, None) => true,
_ => false,
}
}
}
fn same_material_catalog(
@@ -165,20 +151,8 @@ impl core::fmt::Display for FpvState {
}
}
pub fn resolve_fpv_gun(
weapons: &assets::WeaponRegistry,
fpv: &assets::FpvMeshCatalog,
weapon_id: u32,
) -> Option<FpvMeshIndex> {
let order = weapons
.gun_xmodel_edge_of(weapon_id)
.and_then(|edge| edge.bound_index())?;
fpv.get_at(order)?;
Some(FpvMeshIndex::from_order(order))
}
fn fpv_rocket_should_attach(
weapons: &WeaponRegistry,
weapons: &FpvWeaponTable,
weapon_id: u32,
ps: Option<&PlayerState>,
) -> bool {
@@ -202,10 +176,7 @@ fn fpv_rocket_should_attach(
#[derive(SystemParam)]
pub struct SpawnPendingFpvInputs<'w> {
weapons: Option<Res<'w, PreparedWeapons>>,
fpv_meshes: Option<Res<'w, PreparedFpvMeshes>>,
xanims: Option<Res<'w, PreparedXAnims>>,
lighting: Option<Res<'w, render_scene::WorldModelLightingAtlas>>,
prepared: Res<'w, PreparedFpv>,
tess: Res<'w, render_scene::TessMaterials>,
presented: Res<'w, PresentedSnapshot>,
local: Res<'w, LocalPresentClient>,
@@ -224,10 +195,7 @@ pub fn spawn_pending_fpv(
gaps: Res<RenderPresentationGaps>,
) {
let SpawnPendingFpvInputs {
weapons,
fpv_meshes,
xanims,
lighting,
prepared,
tess,
presented,
local,
@@ -235,6 +203,13 @@ pub fn spawn_pending_fpv(
let Some(request) = pending.0.take() else {
return;
};
let Some(table) = prepared
.table()
.filter(|table| same_material_catalog(table.material_catalog(), Some(&*tess)))
else {
pending.0 = Some(request);
return;
};
let instance_started = std::time::Instant::now();
cursor.0.forget_weap_anim();
for entity in &existing_fpv {
@@ -242,65 +217,38 @@ pub fn spawn_pending_fpv(
}
session_vm.0 = None;
let idle_clip = weapons
.as_ref()
.zip(xanims.as_ref())
.and_then(|(reg, cat)| {
let row = reg.0.sz_xanim_edges_of(request.weapon_id)?;
let order = row[assets::weap_anim::IDLE].bound_index()?;
cat.0.clip_at(order)
});
let idle_from_table = idle_clip.is_some();
let idle_name = idle_clip.as_ref().map(|clip| clip.name.clone());
let idle_ns = weapons
.as_ref()
.and_then(|reg| reg.0.namespace_of(request.weapon_id))
.unwrap_or(AssetNamespace::Iw4);
let Some(fpv_cat) = fpv_meshes.as_ref() else {
gaps.raise(RenderGapCause::FpvCatalogMissing);
status.0 = Some(FpvState::Blocked(RenderGapCause::FpvCatalogMissing));
return;
};
if fpv_cat.0.identity() != request.catalog_id
|| weapons
.as_ref()
.and_then(|reg| resolve_fpv_gun(&reg.0, &fpv_cat.0, request.weapon_id))
!= Some(request.gun_index)
if table.catalog_id() != request.catalog_id
|| table.gun_index(request.weapon_id) != Some(request.gun_index)
{
gaps.raise(RenderGapCause::FpvGunXModelUnresolved {
let cause = RenderGapCause::FpvGunXModelUnresolved {
weapon_id: request.weapon_id,
});
status.0 = Some(FpvState::Blocked(RenderGapCause::FpvGunXModelUnresolved {
weapon_id: request.weapon_id,
}));
};
gaps.raise(cause.clone());
status.0 = Some(FpvState::Blocked(cause));
return;
}
let Some(gun_entry) = fpv_cat.0.get_at(request.gun_index.order()) else {
return;
};
let gun_name = gun_entry.skel.name.clone();
let reg = &weapons.as_ref().expect("validated weapon catalog").0;
let meta = presented
.snapshot()
.and_then(|snap| snap.meta.for_client(local.0));
let ffa_team = meta.and_then(|m| m.ffa_team);
let client_state_team = meta.map(|m| m.client_state_team).unwrap_or(0);
let axis = assets::kit_assignment_is_axis(client_state_team, ffa_team);
let (Some((hands, hands_index)), Some(assemblies)) = (
reg.fpv_hands_of(request.weapon_id, axis),
reg.fpv_assemblies_of(request.weapon_id, axis),
) else {
let cause = RenderGapCause::FpvDependencyUnresolved {
weapon_id: request.weapon_id,
role: "FPV skeleton",
name: reg.fpv_assembly_gap_of(request.weapon_id, axis),
};
gaps.raise(cause.clone());
status.0 = Some(FpvState::Blocked(cause));
return;
let view = match table.slot(request.weapon_id, request.parent_weapon, axis) {
FpvWeaponSlot::Ready(view) => Arc::clone(view),
FpvWeaponSlot::Refused(cause) => {
gaps.raise(cause.clone());
status.0 = Some(FpvState::Blocked(cause.clone()));
return;
}
FpvWeaponSlot::Absent => {
let cause = RenderGapCause::FpvGunXModelUnresolved {
weapon_id: request.weapon_id,
};
gaps.raise(cause.clone());
status.0 = Some(FpvState::Blocked(cause));
return;
}
};
let assemblies = assemblies.clone();
let hands = hands.clone();
let Ok(host) = cameras.single() else {
diag::info!(
Fpv,
@@ -312,107 +260,6 @@ pub fn spawn_pending_fpv(
return;
};
let Some(lighting) = lighting.as_ref() else {
diag::info!(
Fpv,
"fpv: no WorldSmodelLighting atlas — refuse Bevy-unlit glow (ModelLightingCache gap)"
);
gaps.raise(RenderGapCause::FpvNoLightingAtlas);
status.0 = Some(FpvState::Blocked(RenderGapCause::FpvNoLightingAtlas));
return;
};
if !matches!(
tess.catalog.sorted_materials,
RuntimeSortedMaterialTable::Ready { .. }
) {
diag::info!(
Fpv,
"fpv: global sorted material table not READY — re-queue (clone-index is not an ordinal)"
);
pending.0 = Some(request);
return;
}
let mut image_cache: HashMap<u32, Handle<Image>> = HashMap::new();
let mut materials: Vec<render_scene::SmodelPassMaterial> = Vec::new();
let mut material_by_authored: HashMap<usize, u32> = HashMap::new();
let mut ordinal_admitted = 0u32;
let mut ordinal_refused: Vec<String> = Vec::new();
let mut gun_skip = FpvGunSkipCensus::default();
let hands_entry = fpv_cat.0.get_at(hands_index.order());
let gun_entry = Some(gun_entry);
let mut plan_mat_hints: Vec<String> = Vec::new();
// Which materials this composition needs, walked off the models rather than
// off a pose: the rig the first frame prepares needs this table to decide
// the layout.
let mut admit_model = |label: &str, entry: Option<&assets::FpvMeshEntry>| {
let Some(entry) = entry else {
return;
};
for surface_index in entry.skel.surfaces_for_lod(0) {
let edge = entry
.material_edges
.get(surface_index)
.copied()
.unwrap_or(AssetEdge::Absent);
let present_name = entry.material_present_name(surface_index);
let leftover_hint = entry
.material_names
.get(surface_index)
.and_then(|name| name.as_deref());
let authored = entry
.skel
.surface_materials
.get(surface_index)
.copied()
.flatten()
.map(|index| index.get());
let admitted = fpv_pass_material(
&tess.catalog,
tess.material_images.as_ref(),
authored,
edge,
present_name,
leftover_hint,
&mut image_cache,
&mut material_by_authored,
&mut materials,
lighting,
label,
surface_index,
&mut ordinal_admitted,
&mut ordinal_refused,
&mut gun_skip,
);
if admitted.is_none() {
continue;
}
if let Some(hint) = leftover_hint.or(present_name)
&& plan_mat_hints.len() < 12
&& !plan_mat_hints.iter().any(|seen| seen == hint)
{
plan_mat_hints.push(hint.to_owned());
}
}
};
let mut admitted_models: Vec<FpvMeshIndex> = Vec::new();
for assembly in std::iter::once(&assemblies.bare).chain(&assemblies.rocket) {
for part in &assembly.parts {
if admitted_models.contains(&part.model) {
continue;
}
admitted_models.push(part.model);
let label = match part.role {
assets::FpvPartRole::Hands => "fpv hands",
_ => "fpv gun",
};
admit_model(label, fpv_cat.0.get_at(part.model.order()));
}
}
drop(admit_model);
commands.entity(host).with_children(|parent| {
parent.spawn((
FpvViewmodel,
@@ -422,328 +269,66 @@ pub fn spawn_pending_fpv(
));
});
// The plan belongs to the rig, and the rig is prepared by the first pose
// this composition takes. Until then the plan has nothing to publish.
crate::clear_fpv_draw_plan(&mut fpv_plan, 0);
fpv_plan.gun_colormap_skip_n = Some(gun_skip.colormap_n);
fpv_plan.gun_ordinal_skip_n = Some(gun_skip.ordinal_n);
fpv_plan.gun_colormap_skip_names =
(!gun_skip.colormap_names.is_empty()).then(|| gun_skip.colormap_names.join(","));
fpv_plan.plan_mat_hints = (!plan_mat_hints.is_empty()).then(|| plan_mat_hints.join(","));
let census = &view.census;
fpv_plan.gun_colormap_skip_n = Some(census.gun_colormap_skip_n);
fpv_plan.gun_ordinal_skip_n = Some(0);
fpv_plan.gun_colormap_skip_names = (!census.gun_colormap_skip_names.is_empty())
.then(|| census.gun_colormap_skip_names.join(","));
fpv_plan.plan_mat_hints = (!census.mat_hints.is_empty()).then(|| census.mat_hints.join(","));
let (controller, left) = match (weapons.as_ref(), xanims.as_ref()) {
(Some(reg), Some(cat)) => {
let right_idle_bound = reg
.0
.sz_xanim_right_edges_of(request.weapon_id)
.is_some_and(|edges| edges[assets::weap_anim::IDLE].is_bound());
let no_dual = reg
.0
.facts_of(request.weapon_id)
.map(|f| u8::from(f.no_dual_wield))
.unwrap_or(1);
let create_lr = no_dual == 0 && right_idle_bound;
let animations = if create_lr {
WeaponAnimations::from_registry_edges(
&reg.0,
request.weapon_id,
reg.0.sz_xanim_right_edges_of(request.weapon_id),
&cat.0,
)
} else {
WeaponAnimations::from_registry(&reg.0, request.weapon_id, &cat.0)
};
let resolved = animations.resolved_count();
let (fire_ms, raise_ms) = (animations.fire_time_ms, animations.raise_time_ms);
let switch_ms = format!(
"dropTime={}ms quickDropTime={}ms quickRaiseTime={}ms",
animations.drop_time_ms,
animations.quick_drop_time_ms,
animations.quick_raise_time_ms
);
let idle_meta = animations
.clip(assets::WeaponAnimSlot::Idle)
.map(|clip| {
format!(
"idle=`{}` frames={} loop={} dur={:.2}s",
clip.name,
clip.numframes,
clip.looping,
clip.duration()
)
})
.unwrap_or_else(|| "idle=missing".into());
let left_idle_bound = reg
.0
.sz_xanim_left_edges_of(request.weapon_id)
.is_some_and(|edges| edges[assets::weap_anim::IDLE].is_bound());
let left = if create_lr && left_idle_bound {
let left_anims = WeaponAnimations::from_registry_edges(
&reg.0,
request.weapon_id,
reg.0.sz_xanim_left_edges_of(request.weapon_id),
&cat.0,
);
let left_n = left_anims.resolved_count();
diag::info!(
Fpv,
"fpv: viewmodel controller `{}` — {resolved} clips R, {left_n} clips L, fireTime={fire_ms}ms raiseTime={raise_ms}ms {switch_ms} {idle_meta} (dual DObj; action from weapAnim)",
animations.name
);
Some(ViewmodelController::new(left_anims))
} else {
diag::info!(
Fpv,
"fpv: viewmodel controller `{}` — {resolved} clips, fireTime={fire_ms}ms raiseTime={raise_ms}ms {switch_ms} {idle_meta} (action from weapAnim; Fire predicted)",
animations.name
);
None
};
(ViewmodelController::new(animations), left)
}
_ => {
diag::warn!(
Fpv,
"fpv: weapon/xanim catalog missing — static idle pose only"
);
(ViewmodelController::new(WeaponAnimations::empty("")), None)
}
let controller = ViewmodelController::new(view.right.clone());
let left = view.left.clone().map(ViewmodelController::new);
diag::info!(
Fpv,
"fpv: viewmodel controller `{}` — {} clips{}, fireTime={}ms raiseTime={}ms (action from weapAnim)",
view.right.name,
view.right.resolved_count(),
view.left
.as_ref()
.map(|left| format!(" R, {} clips L (dual DObj)", left.resolved_count()))
.unwrap_or_else(String::new),
view.right.fire_time_ms,
view.right.raise_time_ms,
);
let idle_kind = match view.idle_name.as_deref() {
Some(name) => format!("idle `{name}` (szXAnims[IDLE])"),
None => "no szXAnims[IDLE] — guess forbidden".to_owned(),
};
let idle_sampled = view.idle_name.is_some();
diag::info!(
Fpv,
"fpv: equipped `{}` ({}; prepared before Ready) — instance {:.2}ms",
view.gun_name,
idle_kind,
instance_started.elapsed().as_secs_f64() * 1000.0,
);
session_vm.0 = Some(SessionFpvMeshesHandles {
weapon_id: request.weapon_id,
parent_weapon: request.parent_weapon,
catalog_id: request.catalog_id,
assemblies,
axis,
fpv: EquippedFpv::new(
gun_name.clone(),
request.gun_index,
hands_index,
idle_ns,
hands,
view.gun_name.clone(),
view.gun_index,
view.hands_index,
view.namespace,
view.hands.clone(),
controller,
left,
),
view,
active_rig: None,
materials,
material_by_authored,
material_catalog: Arc::clone(&tess.catalog),
material_catalog: Arc::clone(table.material_catalog()),
});
let idle_kind = if idle_from_table {
let name = idle_name.as_deref().unwrap_or("?");
format!("idle `{name}` (szXAnims[IDLE])")
} else {
"no szXAnims[IDLE] — guess forbidden".to_owned()
};
let refused_names = if ordinal_refused.is_empty() {
"none".to_owned()
} else {
ordinal_refused
.iter()
.take(8)
.cloned()
.collect::<Vec<_>>()
.join(",")
};
diag::info!(
Fpv,
"fpv: ordinal admission admitted={ordinal_admitted} refused={} names=[{refused_names}] (whole model incl. hideTags-hidden surfaces; global catalog, clone-index unused)",
ordinal_refused.len(),
);
let vert_census = |entry: Option<&assets::FpvMeshEntry>| match entry {
Some(entry) => format!("{}/{}", entry.skel.rigid_verts, entry.skel.blend_verts),
None => "-".to_owned(),
};
diag::info!(
Fpv,
"fpv: equipped `{}` ({}; {}; hands rigid/blend={} gun rigid/blend={}; skeleton prepared before Ready) — instance {:.2}ms on shared templates",
gun_name,
"linked gunXModel",
idle_kind,
vert_census(hands_entry),
vert_census(gun_entry),
instance_started.elapsed().as_secs_f64() * 1000.0,
);
gaps.clear(RenderGap::FpvViewmodel);
status.0 = Some(FpvState::Drawn {
idle_sampled: idle_from_table,
});
}
#[derive(Default)]
struct FpvGunSkipCensus {
colormap_n: u32,
ordinal_n: u32,
colormap_names: Vec<String>,
}
impl FpvGunSkipCensus {
fn note_colormap(&mut self, is_gun: bool, name: Option<&str>) {
if !is_gun {
return;
}
self.colormap_n = self.colormap_n.saturating_add(1);
let Some(name) = name else {
return;
};
if self.colormap_names.len() < 8 && !self.colormap_names.iter().any(|seen| seen == name) {
self.colormap_names.push(name.to_owned());
}
}
fn note_ordinal(&mut self, is_gun: bool) {
if is_gun {
self.ordinal_n = self.ordinal_n.saturating_add(1);
}
}
}
fn fpv_pass_material(
global: &RuntimeMaterialCatalog,
images: &[Option<Handle<Image>>],
material_index: Option<usize>,
edge: AssetEdge<MaterialSpace>,
present_name: Option<&str>,
leftover_hint: Option<&str>,
image_cache: &mut HashMap<u32, Handle<Image>>,
material_by_authored: &mut HashMap<usize, u32>,
materials: &mut Vec<render_scene::SmodelPassMaterial>,
lighting: &render_scene::WorldModelLightingAtlas,
label: &str,
surface_index: usize,
ordinal_admitted: &mut u32,
ordinal_refused: &mut Vec<String>,
gun_skip: &mut FpvGunSkipCensus,
) -> Option<render_scene::SmodelPassMaterial> {
use lighting_iw4::{
MODEL_LIGHTING_INV_ATLAS_WIDTH, MODEL_LIGHTING_VOLUME_W, model_lighting_inv_image_height,
model_lighting_lookup_scale,
};
use render_scene::SmodelPassMaterial;
let is_gun = label == "fpv gun";
if !edge.is_bound() {
diag::info!(
Fpv,
"{label} surface {surface_index}: materialHandles {} leftover=`{}` — skip (leftover name is not identity)",
edge.edge_kind(),
leftover_hint.unwrap_or("-")
);
return None;
}
let Some(present_name) = present_name else {
diag::info!(
Fpv,
"{label} surface {surface_index}: Bound materialHandles has no present name — skip"
);
return None;
};
let mat_i = material_index?;
if let Some(&idx) = material_by_authored.get(&mat_i) {
return materials.get(idx as usize).cloned();
}
let bound = edge.bound_index()?;
let authored = global.materials.get(bound)?;
let Some(ordinal) = global.ordinal_for_material_name(present_name) else {
diag::info!(
Fpv,
"{label} surface {surface_index}: Bound `{present_name}` global-index={bound} has no sorted ordinal — skip (not packed as 0)"
);
if !ordinal_refused.iter().any(|name| name == present_name) {
ordinal_refused.push(present_name.to_owned());
}
gun_skip.note_ordinal(is_gun);
return None;
};
*ordinal_admitted = ordinal_admitted.saturating_add(1);
let color_texture = {
let image_index = authored
.textures
.iter()
.find_map(|(_, texture)| texture.filter(|binding| binding.semantic == TS_COLOR_MAP))?
.image
.0;
if let Some(cached) = image_cache.get(&image_index) {
Some(cached.clone())
} else {
let Some(handle) = images.get(image_index as usize).cloned().flatten() else {
diag::info!(
Fpv,
"{label} surface {surface_index}: no uploaded color map — skip (no unlit stand-in)"
);
gun_skip.note_colormap(is_gun, Some(present_name).or(leftover_hint));
return None;
};
image_cache.insert(image_index, handle.clone());
Some(handle)
}
};
let specular_texture = authored
.textures
.iter()
.find_map(|(_, texture)| {
texture.filter(|binding| binding.semantic == assets::TS_SPECULAR_MAP)
})
.and_then(|binding| {
let image_index = binding.image.0;
if let Some(cached) = image_cache.get(&image_index) {
return Some(cached.clone());
}
let handle = images.get(image_index as usize).cloned().flatten()?;
image_cache.insert(image_index, handle.clone());
Some(handle)
});
const ENV_MAP_PARMS: u32 = 1_033_475_292;
let env_map_parms = authored
.constants
.iter()
.find(|(hash, _)| *hash == ENV_MAP_PARMS)
.map(|(_, words)| words.map(f32::from_bits))
.unwrap_or([0.0; 4]);
let alpha_mode = AlphaMode::Opaque;
let inv_h = model_lighting_inv_image_height(lighting.dims.image_height)?;
let scale = model_lighting_lookup_scale(inv_h);
let cull_mode =
authored
.state_bits_table
.first()
.and_then(|bits| match assets::cull_face_from_state_bits(*bits) {
assets::MaterialCullFace::Back => Some(bevy::render::render_resource::Face::Back),
assets::MaterialCullFace::Front => Some(bevy::render::render_resource::Face::Front),
assets::MaterialCullFace::None => None,
});
let mat = SmodelPassMaterial {
model_lighting_required: true,
color: color_texture,
specular: specular_texture,
probe: None,
atlas: Some(lighting.image.clone()),
alpha_mode,
draw_mode: None,
cull_mode,
env_map_parms,
lighting_lookup_scale: [scale.u, scale.v, scale.w, scale.q],
atlas_lookup: [
MODEL_LIGHTING_INV_ATLAS_WIDTH as f32,
inv_h,
MODEL_LIGHTING_VOLUME_W,
0.0,
],
sort_key: authored.sort_key,
material_sorted_index: Some(ordinal.get()),
};
let idx = materials.len() as u32;
materials.push(mat.clone());
material_by_authored.insert(mat_i, idx);
Some(mat)
status.0 = Some(FpvState::Drawn { idle_sampled });
}
fn viewweapon_drawgun_admit(
ps: &PlayerState,
weapons: Option<&WeaponRegistry>,
weapons: Option<&FpvWeaponTable>,
b_position_to_ads: bool,
) -> Option<(bool, Option<&'static str>)> {
let reg = weapons?;
@@ -755,15 +340,7 @@ fn viewweapon_drawgun_admit(
ads_zoom_in_frac: facts.ads_zoom_in_frac,
ads_zoom_out_frac: facts.ads_zoom_out_frac,
overlay_material: u32::from(hud_iris),
overlay_reticle: if hud_iris {
if facts.overlay_reticle != 0 {
facts.overlay_reticle
} else {
1
}
} else {
0
},
overlay_reticle: facts.overlay_reticle,
ads_overlay_width: facts.ads_overlay_width,
ads_overlay_height: facts.ads_overlay_height,
..WeaponAdsOverlayFacts::default()
@@ -777,7 +354,7 @@ fn viewweapon_drawgun_admit(
fn viewweapon_drawgun_value(
ps: &PlayerState,
weapons: Option<&WeaponRegistry>,
weapons: Option<&FpvWeaponTable>,
b_position_to_ads: bool,
) -> Option<i32> {
let (admit, _skip) = viewweapon_drawgun_admit(ps, weapons, b_position_to_ads)?;
@@ -810,7 +387,7 @@ pub fn occupy_fpv_scene(
presented: Res<PresentedSnapshot>,
local: Res<LocalPresentClient>,
view: Res<ViewSubject>,
weapons: Option<Res<PreparedWeapons>>,
prepared: Res<PreparedFpv>,
kick: Option<Res<SessionViewKick>>,
session_vm: Option<Res<SessionViewmodel>>,
tess: Option<Res<render_scene::TessMaterials>>,
@@ -824,7 +401,7 @@ pub fn occupy_fpv_scene(
};
let admit = viewweapon_drawgun_admit(
ps,
weapons.as_ref().map(|w| &*w.0),
prepared.table().map(|table| &**table),
kick.as_ref().map(|k| k.b_position_to_ads).unwrap_or(true),
);
if !admit.is_some_and(|(ok, _)| ok) {
@@ -899,7 +476,7 @@ pub fn tick_fpv_viewmodel(
presented: Res<PresentedSnapshot>,
local: Res<LocalPresentClient>,
mut cursor: ResMut<FpvPresentCursor>,
weapons: Option<Res<PreparedWeapons>>,
prepared: Res<PreparedFpv>,
fpv_meshes: Option<Res<PreparedFpvMeshes>>,
mut session_vm: ResMut<SessionViewmodel>,
tess: Option<Res<render_scene::TessMaterials>>,
@@ -910,8 +487,8 @@ pub fn tick_fpv_viewmodel(
mut bolts: ResMut<FpvBoltTargets>,
kick: Option<Res<SessionViewKick>>,
view: Res<ViewSubject>,
mut shelf: ResMut<FpvRigShelf>,
) {
let table = prepared.table().map(|table| &**table);
pending_notes.weapon = 0;
pending_notes.names.clear();
bolts.clear();
@@ -920,7 +497,7 @@ pub fn tick_fpv_viewmodel(
if !presented_is_third_person(&presented, local.0, view.in_killcam()) {
product.drawgun = viewweapon_drawgun_value(
ps,
weapons.as_ref().map(|w| &*w.0),
table,
kick.as_ref().map(|k| k.b_position_to_ads).unwrap_or(true),
);
}
@@ -961,26 +538,27 @@ pub fn tick_fpv_viewmodel(
if let Some(ps) = presented.viewweapon_player(local.0) {
let weapon = bg_get_viewmodel_weapon_index(ps);
if weapon != 0 && weapon != session.weapon_id {
if let Some(reg) = weapons.as_ref() {
match resolve_fpv_gun(&reg.0, &fpv.0, weapon) {
if weapon != 0
&& (weapon != session.weapon_id || ps.weapon_primary != session.parent_weapon)
{
if let Some(table) = table {
match table.gun_index(weapon) {
Some(gun_index) if gun_index == session.fpv.gun_index => {
let axis = session.axis;
let same = reg.0.fpv_assemblies_of(weapon, axis).is_some_and(|next| {
Arc::ptr_eq(&next.bare, &session.assemblies.bare)
&& match (&next.rocket, &session.assemblies.rocket) {
(Some(a), Some(b)) => Arc::ptr_eq(a, b),
(None, None) => true,
_ => false,
}
});
let same = match table.slot(weapon, ps.weapon_primary, session.axis) {
FpvWeaponSlot::Ready(next) => {
same_compositions(&next.assemblies, &session.view.assemblies)
}
_ => false,
};
if same {
session.weapon_id = weapon;
session.parent_weapon = ps.weapon_primary;
} else {
pending.0 = Some(PendingFpvSpawnRequest {
gun_index,
catalog_id: fpv.0.identity(),
weapon_id: weapon,
parent_weapon: ps.weapon_primary,
});
product.kind = FpvPoseKind::Hide;
return;
@@ -991,6 +569,7 @@ pub fn tick_fpv_viewmodel(
gun_index,
catalog_id: fpv.0.identity(),
weapon_id: weapon,
parent_weapon: ps.weapon_primary,
});
diag::info!(
Fpv,
@@ -1022,12 +601,12 @@ pub fn tick_fpv_viewmodel(
.unwrap_or(false);
let ads_frac = ps.map(|p| p.f_weapon_pos_frac).unwrap_or(0.0);
let weap_anim = ps.map(|p| p.weap_anim).unwrap_or(0);
let clip_ammo = match (ps, weapons.as_ref()) {
(Some(ps), Some(reg)) => {
let clip_ammo = |hand| match (ps, table) {
(Some(ps), Some(table)) => {
let viewmodel = bg_get_viewmodel_weapon_index(ps);
reg.0.facts_of(viewmodel).map(|facts| {
table.facts_of(viewmodel).map(|facts| {
let key = bg_clip_table_key(facts.clip_index, viewmodel);
bg_get_clip_for_hand(&ps.ammoclip, key, 0)
bg_get_clip_for_hand(&ps.ammoclip, key, hand)
})
}
_ => None,
@@ -1061,24 +640,23 @@ pub fn tick_fpv_viewmodel(
weap_anim_secondary: ps.map(|p| p.weap_anim_secondary).unwrap_or(0),
last_weapon_hand: ps.map(|p| p.last_weapon_hand).unwrap_or(0),
perks0: ps.map(|p| p.perks[0]).unwrap_or(0),
clip_ammo,
clip_ammo: clip_ammo(0),
left_clip_ammo: clip_ammo(1),
}),
predicted_fire,
)
}
None => (None, false),
};
let rocket_visible = weapons.as_ref().is_some_and(|reg| {
fpv_rocket_should_attach(&reg.0, session.weapon_id, presented.player(local.0))
let rocket_visible = table.is_some_and(|table| {
fpv_rocket_should_attach(table, session.weapon_id, presented.player(local.0))
});
let dual = presented
.viewweapon_player(local.0)
.is_some_and(|ps| ps.last_weapon_hand == 1);
let dual_offset = if dual {
presented.viewweapon_player(local.0).and_then(|ps| {
weapons
.as_ref()?
.0
table?
.facts_of(bg_get_viewmodel_weapon_index(ps))
.map(|f| f.dual_wield_view_model_offset)
})
@@ -1086,30 +664,18 @@ pub fn tick_fpv_viewmodel(
None
};
let weapon_id = session.weapon_id;
shelf.hold(&session.material_catalog, session.catalog_id);
let SessionFpvMeshesHandles {
fpv: equipped,
active_rig,
materials,
material_by_authored,
assemblies,
view: equipped_view,
..
} = session;
let Some(reg) = weapons.as_ref() else {
product.kind = FpvPoseKind::Refuse(FpvPoseRefuse::CatalogMissing);
return;
};
let mut kind = generate_fpv_pose(FpvGenerateArgs {
dt,
equipped,
rigs: &mut shelf.rigs,
rigs: &equipped_view.rigs,
active: active_rig,
cursor: &mut cursor.0,
catalog: &fpv.0,
materials,
material_by_authored,
assemblies,
clip_tracks: reg.0.fpv_clip_tracks(),
rocket: rocket_visible,
sample,
predicted_fire,
@@ -1136,7 +702,6 @@ pub fn tick_fpv_viewmodel(
fn skin_fpv_geometry(
product: Res<FpvPoseProduct>,
session_vm: Option<Res<SessionViewmodel>>,
shelf: Res<FpvRigShelf>,
tess: Option<Res<render_scene::TessMaterials>>,
fpv_meshes: Option<Res<PreparedFpvMeshes>>,
mut fpv_plan: ResMut<crate::FpvDrawPlan>,
@@ -1169,9 +734,7 @@ fn skin_fpv_geometry(
crate::clear_fpv_draw_plan(&mut fpv_plan, handle);
return;
}
let rig = session
.and_then(|session| session.active_rig)
.and_then(|index| shelf.rigs.get(index));
let rig = session.and_then(|session| session.active_rig.as_ref());
let (Some(rig), Some(catalog)) = (rig, fpv_meshes.as_ref()) else {
crate::clear_fpv_draw_plan(&mut fpv_plan, handle);
return;
@@ -1229,6 +792,8 @@ pub fn publish_fpv_dobj_pose(
bolts.clear();
return;
}
bolts.tracker_screen = None;
bolts.tracker_light = None;
for hand in 0..2usize {
let Some(frame) = bolts.pose[hand].take() else {
continue;
@@ -1252,6 +817,18 @@ pub fn publish_fpv_dobj_pose(
})
};
if hand == 0 && fpv_plan.placement_ok {
bolts.tracker_light = target(frame.tags.tracker_light);
bolts.tracker_screen = (|| {
let mut points = [Vec3::ZERO; 3];
for (point, bone) in points.iter_mut().zip(frame.tags.tracker_screen) {
*point = fpv_plan
.world_from_local
.transform_point3(frame.bones.get(usize::from(bone?))?.w_axis.truncate());
}
Some(points)
})();
}
bolts.flash[hand] = target(frame.tags.flash);
bolts.brass[hand] = target(frame.tags.brass);
bolts.knife[hand] = target(frame.tags.knife);
@@ -1264,7 +841,7 @@ pub fn apply_fpv_placement(
clock: Res<CgFrameClock>,
presented: Res<PresentedSnapshot>,
local: Res<LocalPresentClient>,
weapons: Option<Res<PreparedWeapons>>,
prepared: Res<PreparedFpv>,
mut kick: ResMut<SessionViewKick>,
cg_gun: Res<CgGunOffset>,
mut aim: ResMut<CgViewweaponAim>,
@@ -1290,11 +867,11 @@ pub fn apply_fpv_placement(
if presented_is_third_person(&presented, local.0, view.in_killcam()) {
return;
}
let Some(reg) = weapons.as_ref() else {
let Some(table) = prepared.table() else {
return;
};
let viewmodel = bg_get_viewmodel_weapon_index(ps);
let Some(facts) = reg.0.facts_of(viewmodel) else {
let Some(facts) = table.facts_of(viewmodel) else {
return;
};
@@ -1319,7 +896,7 @@ pub fn apply_fpv_placement(
let overlay_reticle = if facts.overlay_reticle != 0 {
facts.overlay_reticle
} else if reg.0.overlay_is_hud_iris(viewmodel) {
} else if table.overlay_is_hud_iris(viewmodel) {
1
} else {
0
@@ -1486,13 +1063,25 @@ fn flush_fpv_spawn(world: &mut World) {
}
fn publish_fpv_notetracks(
generation: Res<frame::WorldGeneration>,
presented: Res<PresentedSnapshot>,
local: Res<LocalPresentClient>,
pending: Res<PendingFpvNotetracks>,
mut notes: MessageWriter<audio::ViewmodelNotetracks>,
) {
if pending.weapon == 0 || pending.names.is_empty() {
return;
}
let Some(meta) = presented
.snapshot()
.and_then(|snapshot| snapshot.meta.for_client(local.0))
else {
return;
};
notes.write(audio::ViewmodelNotetracks {
generation: *generation,
client: local.0,
life: meta.life_sequence,
weapon: pending.weapon,
names: pending.names.clone(),
});
@@ -1501,7 +1090,8 @@ fn publish_fpv_notetracks(
pub fn register_fpv_present_systems(app: &mut App) {
app.init_resource::<LocalSpawnArmed>()
.init_resource::<SessionViewmodel>()
.init_resource::<FpvRigShelf>()
.init_resource::<PreparedFpv>()
.init_resource::<crate::anim::model_materials::PreparedModelMaterials>()
.init_resource::<SessionViewKick>()
.init_resource::<CgGunOffset>()
.init_resource::<CgViewweaponAim>()
+34 -38
View File
@@ -1,12 +1,12 @@
use assets::{PreparedFpvMeshes, PreparedWeapons};
use bevy::prelude::*;
use frame::{LaunchReport, LifeEnded, LifeFrontPublished, LifeStarted};
use net::{ClientSet, LocalPresentClient, PresentedSnapshot};
use crate::anim::fpv_prepared::PreparedFpv;
use crate::gaps::{RenderGap, RenderGapCause, RenderPresentationGaps};
use crate::occupancy::fpv_present::{
FpvHeldLife, FpvHeldSettled, FpvPlacementRoot, FpvState, FpvStatusGap, PendingFpvSpawn,
PendingFpvSpawnRequest, SessionViewmodel, resolve_fpv_gun,
PendingFpvSpawnRequest, SessionViewmodel,
};
use weapon_iw4::bg_get_viewmodel_weapon_index;
@@ -14,8 +14,7 @@ pub fn sync_fpv_to_held_weapon(
mut commands: Commands,
presented: Res<PresentedSnapshot>,
local: Res<LocalPresentClient>,
weapons: Option<Res<PreparedWeapons>>,
fpv_meshes: Option<Res<PreparedFpvMeshes>>,
prepared: Res<PreparedFpv>,
mut pending_fpv: ResMut<PendingFpvSpawn>,
mut session_vm: ResMut<SessionViewmodel>,
mut settled: ResMut<FpvHeldSettled>,
@@ -133,15 +132,13 @@ pub fn sync_fpv_to_held_weapon(
if pending_fpv
.0
.as_ref()
.is_some_and(|req| req.weapon_id == held)
.is_some_and(|req| req.weapon_id == held && req.parent_weapon == ps.weapon_primary)
{
return;
}
if session_vm
.0
.as_ref()
.is_some_and(|session| session.weapon_id == held)
{
if session_vm.0.as_ref().is_some_and(|session| {
session.weapon_id == held && session.parent_weapon == ps.weapon_primary
}) {
settled.0 = Some(held);
return;
}
@@ -150,37 +147,36 @@ pub fn sync_fpv_to_held_weapon(
return;
}
let state = match (
weapons.as_ref().map(|r| &r.0),
fpv_meshes.as_ref().map(|r| &r.0),
) {
(Some(reg), Some(fpv)) => match resolve_fpv_gun(reg, fpv, held) {
Some(gun_index) => {
for entity in &existing_fpv {
commands.entity(entity).try_despawn();
}
session_vm.0 = None;
pending_fpv.0 = Some(PendingFpvSpawnRequest {
gun_index,
catalog_id: fpv.identity(),
weapon_id: held,
});
settled.0 = None;
FpvState::Queued
let Some(table) = prepared.table() else {
let waiting = FpvState::Blocked(RenderGapCause::FpvCatalogMissing);
if status.0.as_ref() != Some(&waiting) {
status.0 = Some(waiting);
}
return;
};
let state = match table.gun_index(held) {
Some(gun_index) => {
for entity in &existing_fpv {
commands.entity(entity).try_despawn();
}
None => {
for entity in &existing_fpv {
commands.entity(entity).try_despawn();
}
session_vm.0 = None;
pending_fpv.0 = None;
settled.0 = Some(held);
FpvState::Blocked(RenderGapCause::FpvGunXModelUnresolved { weapon_id: held })
session_vm.0 = None;
pending_fpv.0 = Some(PendingFpvSpawnRequest {
gun_index,
catalog_id: table.catalog_id(),
weapon_id: held,
parent_weapon: ps.weapon_primary,
});
settled.0 = None;
FpvState::Queued
}
None => {
for entity in &existing_fpv {
commands.entity(entity).try_despawn();
}
},
_ => {
session_vm.0 = None;
pending_fpv.0 = None;
settled.0 = Some(held);
FpvState::Blocked(RenderGapCause::FpvCatalogMissing)
FpvState::Blocked(RenderGapCause::FpvGunXModelUnresolved { weapon_id: held })
}
};
+171 -104
View File
@@ -1,5 +1,3 @@
use std::collections::HashMap;
use bevy::prelude::*;
use crate::anim::scene_submission::{AnimDObjSceneSkels, AnimDObjSceneSubmission, AnimSceneSubmit};
@@ -7,7 +5,7 @@ use crate::anim::xmodel_pose::PosedModelSurface;
use crate::occupancy::script_model::pose_script_dobj_with_materials;
use crate::{
ItemAssetDraw, ItemDrawPlan, ItemOwnerDraw, XMODEL_OBJECT_ID_ITEM_BASE, append_item_surfaces,
body_lit_pass_material, dobj_lighting_box_half, stamp_plan_geometry, topology_fingerprint,
dobj_lighting_box_half, stamp_plan_geometry, topology_fingerprint,
};
use anim_iw4::DOBJ_RADIUS_PARENT_ROOT;
use entity_iw4::{ET_ITEM, Trajectory, bg_evaluate_trajectory};
@@ -19,6 +17,106 @@ use render_scene::{
pub const ITEM_LIGHTING_Z_OFS: f32 = 4.0;
struct ItemComposition {
name: String,
model: assets::WorldWeaponIndex,
attachments: Vec<assets::WorldWeaponIndex>,
key: String,
dobj: assets::DObj,
}
#[derive(Resource, Default)]
struct PreparedItemCompositions {
owner: Option<(usize, u64)>,
by_weapon: std::collections::HashMap<u32, std::sync::Arc<ItemComposition>>,
}
impl PreparedItemCompositions {
fn owned_by(
&self,
weapons: &assets::PreparedWeapons,
world: &assets::PreparedWorldWeapons,
) -> bool {
self.owner
== Some((
std::sync::Arc::as_ptr(&weapons.0) as usize,
world.0.identity(),
))
}
}
fn compose_item(
registry: &assets::WeaponRegistry,
catalog: &assets::WorldWeaponCatalog,
weapon: u32,
) -> Option<ItemComposition> {
let entry = registry.world_model_entry(weapon, catalog)?;
let model_index = registry.world_model_edge_of(weapon)?.bound_index()?;
let pose = entry.skel.pose.as_ref()?;
let attachments = crate::anim::remote_body::world_attachments(registry, catalog, weapon);
let mut key = entry.skel.name.clone();
let mut dobj_models = vec![(pose, None)];
let mut attachment_models = Vec::with_capacity(attachments.len());
for attachment in &attachments {
let Some(pose) = attachment.entry.skel.pose.as_ref() else {
continue;
};
key.push('+');
key.push_str(&attachment.entry.skel.name);
attachment_models.push(attachment.index);
dobj_models.push((
pose,
Some(assets::Attach {
parent_model: 0,
tag: attachment.tag.to_owned(),
}),
));
}
let dobj = assets::DObj::build(&dobj_models).ok()?;
Some(ItemComposition {
name: entry.skel.name.clone(),
model: assets::WorldWeaponIndex::from_order(model_index),
attachments: attachment_models,
key,
dobj,
})
}
fn prepare_item_compositions(
weapons: Option<Res<assets::PreparedWeapons>>,
world_weapons: Option<Res<assets::PreparedWorldWeapons>>,
mut prepared: ResMut<PreparedItemCompositions>,
) {
let (Some(weapons), Some(world)) = (weapons, world_weapons) else {
return;
};
if prepared.owned_by(&weapons, &world) {
return;
}
let started = std::time::Instant::now();
let mut by_weapon = std::collections::HashMap::new();
if !world.0.is_empty() {
for weapon in 1..=weapons.0.len() as u32 {
if let Some(composition) = compose_item(&weapons.0, &world.0, weapon) {
by_weapon.insert(weapon, std::sync::Arc::new(composition));
}
}
}
diag::info!(
World,
"dropped items: {} weapon compositions prepared in {:.1}ms",
by_weapon.len(),
started.elapsed().as_secs_f64() * 1000.0
);
*prepared = PreparedItemCompositions {
owner: Some((
std::sync::Arc::as_ptr(&weapons.0) as usize,
world.0.identity(),
)),
by_weapon,
};
}
#[derive(Resource, Default)]
struct ItemOccupancy {
rows: Vec<OccupiedItem>,
@@ -29,10 +127,7 @@ struct OccupiedItem {
entnum: i32,
origin: [f32; 3],
angles: [f32; 3],
name: String,
model: assets::WorldWeaponIndex,
attachments: Vec<(assets::WorldWeaponIndex, String)>,
key: String,
composition: std::sync::Arc<ItemComposition>,
lighting_origin: [f32; 3],
}
@@ -77,6 +172,11 @@ fn item_scene_slot(scene: &render_scene::GfxScene, entnum: u32) -> ItemSceneSlot
pub fn register_item_systems(app: &mut App) {
app.init_resource::<ItemDrawPlan>()
.init_resource::<ItemOccupancy>()
.init_resource::<PreparedItemCompositions>()
.add_systems(
Update,
prepare_item_compositions.in_set(net::ClientSet::Load),
)
.init_resource::<ItemPoseProduct>()
.add_systems(
Update,
@@ -147,6 +247,7 @@ fn occupy_item_scene_ents(
local_client: Option<Res<net::LocalPresentClient>>,
weapons: Option<Res<assets::PreparedWeapons>>,
world_weapons: Option<Res<assets::PreparedWorldWeapons>>,
compositions: Res<PreparedItemCompositions>,
mut occupancy: ResMut<ItemOccupancy>,
mut scene_skels: ResMut<AnimDObjSceneSkels>,
mut scene_submissions: MessageWriter<AnimDObjSceneSubmission>,
@@ -165,7 +266,13 @@ fn occupy_item_scene_ents(
.filter(|clock| clock.started())
.map(|clock| clock.time())
.unwrap_or_else(|| sim::level_time_ms(snapshot.tick));
let weapons_reg = weapons.as_ref().map(|w| &w.0).filter(|reg| !reg.is_empty());
let live = match (weapons.as_deref(), world_weapons.as_deref()) {
(Some(weapons), Some(world)) => compositions.owned_by(weapons, world),
_ => false,
};
if !live {
return;
}
let catalog = world_weapons
.as_ref()
.map(|prepared| &prepared.0)
@@ -190,34 +297,16 @@ fn occupy_item_scene_ents(
if hide_scavenger && item_is_scavenger(snapshot, es.number) {
continue;
}
let Some(entry) = weapons_reg.and_then(|reg| reg.world_model_entry(weapon, catalog)) else {
let Some(composition) = compositions.by_weapon.get(&weapon) else {
continue;
};
let Some(model_index) = weapons_reg
.and_then(|reg| reg.world_model_edge_of(weapon))
.and_then(|edge| edge.bound_index())
else {
let Some(entry) = catalog.get_at(composition.model.order()) else {
continue;
};
let name = entry.skel.name.as_str();
let Some(_pose) = entry.skel.pose.as_ref() else {
continue;
};
let attachments = weapons_reg
.map(|reg| crate::anim::remote_body::world_attachments(reg, catalog, weapon))
.unwrap_or_default();
let mut key = name.to_owned();
for attachment in &attachments {
key.push('+');
key.push_str(&attachment.entry.skel.name);
}
let mut models = vec![scene_skels.model(name, &entry.skel, 0)];
models.extend(attachments.iter().map(|attachment| {
scene_skels.model(
attachment.entry.skel.name.as_str(),
&attachment.entry.skel,
0,
)
let mut models = vec![scene_skels.shared(&composition.name, &entry.skel, 0)];
models.extend(composition.attachments.iter().filter_map(|index| {
let attachment = catalog.get_at(index.order())?;
Some(scene_skels.shared(attachment.skel.name.as_str(), &attachment.skel, 0))
}));
let origin = evaluate_origin(es, at_time);
let lighting_origin = item_lighting_origin(origin);
@@ -248,13 +337,7 @@ fn occupy_item_scene_ents(
entnum: es.number,
origin,
angles,
name: name.to_owned(),
model: assets::WorldWeaponIndex::from_order(model_index),
attachments: attachments
.iter()
.map(|attachment| (attachment.index, attachment.tag.to_owned()))
.collect(),
key,
composition: std::sync::Arc::clone(composition),
lighting_origin,
});
}
@@ -287,69 +370,58 @@ fn pose_items(
if slot.hidden {
continue;
}
let asset_index =
if let Some(index) = product.assets.iter().position(|asset| asset.key == row.key) {
index
} else {
let Some(entry) = catalog.get_at(row.model.order()) else {
continue;
let composition = &row.composition;
let asset_index = if let Some(index) = product
.assets
.iter()
.position(|asset| asset.key == composition.key)
{
index
} else {
let mut skels = Vec::with_capacity(1 + composition.attachments.len());
let mut model_indices = Vec::with_capacity(1 + composition.attachments.len());
for index in std::iter::once(&composition.model).chain(&composition.attachments) {
let Some(entry) = catalog.get_at(index.order()) else {
break;
};
let Some(pose) = entry.skel.pose.as_ref() else {
continue;
};
let mut skels = vec![&*entry.skel];
let mut model_indices = vec![row.model];
let mut dobj_models = vec![(pose, None)];
for (model, tag) in &row.attachments {
let Some(attachment) = catalog.get_at(model.order()) else {
continue;
};
let Some(pose) = attachment.skel.pose.as_ref() else {
continue;
};
skels.push(&*attachment.skel);
model_indices.push(*model);
dobj_models.push((
pose,
Some(assets::Attach {
parent_model: 0,
tag: tag.clone(),
}),
));
}
let Some(dobj) = assets::DObj::build(&dobj_models).ok() else {
continue;
};
let dobj_state = assets::dobj::DObjSemanticState::bind_pose(row.name.clone(), 1, 1);
let Ok(request) = dobj_state.resolve_request(|_| None) else {
continue;
};
let Some((surfaces, _)) = pose_script_dobj_with_materials(
None,
&skels,
&dobj,
&request,
None,
slot.skin_entries,
) else {
continue;
};
let index = product.assets.len();
product.assets.push(ItemPosedAsset {
key: row.key.clone(),
models: model_indices,
surfaces,
});
index
skels.push(&*entry.skel);
model_indices.push(*index);
}
if skels.len() != 1 + composition.attachments.len() {
continue;
}
let dobj = &composition.dobj;
let dobj_state =
assets::dobj::DObjSemanticState::bind_pose(composition.name.clone(), 1, 1);
let Ok(request) = dobj_state.resolve_request(|_| None) else {
continue;
};
let Some((surfaces, _)) = pose_script_dobj_with_materials(
None,
&skels,
dobj,
&request,
None,
slot.skin_entries,
) else {
continue;
};
let index = product.assets.len();
product.assets.push(ItemPosedAsset {
key: composition.key.clone(),
models: model_indices,
surfaces,
});
index
};
product.owners.push(ItemPosedOwner {
asset_index,
index: row.index,
entnum,
origin: row.origin,
angles: row.angles,
name: row.name.clone(),
model: row.model,
name: composition.name.clone(),
model: composition.model,
lighting_origin: row.lighting_origin,
});
}
@@ -364,8 +436,8 @@ fn append_item_draws(
facts: Res<WorldPresentFacts>,
mut plan: ResMut<ItemDrawPlan>,
mut lighting_requests: ResMut<ModelLightingRequests>,
model_materials: Res<crate::anim::model_materials::PreparedModelMaterials>,
mut last_material_generation: Local<Option<render_material::MaterialGenerationId>>,
mut material_cache: Local<HashMap<String, SmodelPassMaterial>>,
mut draws: Local<Vec<XModelSurfaceDraw>>,
mut owners: Local<Vec<ItemOwnerDraw>>,
) {
@@ -382,7 +454,6 @@ fn append_item_draws(
plan.revision = revision;
plan.generation = generation;
plan.publish_no_rows();
material_cache.clear();
*last_material_generation = None;
return;
}
@@ -391,7 +462,7 @@ fn append_item_draws(
.is_some_and(|value| value.is_changed());
let atlas_changed = atlas.as_ref().is_some_and(|value| value.is_changed());
let tess_changed = tess.as_ref().is_some_and(|value| value.is_changed());
let (Some(catalog), Some(atlas), Some(tess)) = (catalog, atlas_ref, tess.as_deref()) else {
let (Some(catalog), Some(_), Some(tess)) = (catalog, atlas_ref, tess.as_deref()) else {
unreachable!("required item draw resources checked above");
};
let material_generation = tess.catalog.generation_id;
@@ -406,7 +477,6 @@ fn append_item_draws(
plan.revision = revision;
plan.generation = generation;
plan.revisions.bump_admission();
material_cache.clear();
*last_material_generation = Some(material_generation);
}
for row in &product.owners {
@@ -425,12 +495,9 @@ fn append_item_draws(
let entry =
catalog.get_at(posed.models.get(usize::from(surface.model))?.order())?;
let present_name = entry.material_present_name(surface.surface_index)?;
if let Some(material) = material_cache.get(present_name) {
return Some(material.clone());
}
let material = body_lit_pass_material(atlas, &tess.catalog, present_name)?;
material_cache.insert(present_name.to_owned(), material.clone());
Some(material)
model_materials
.material(&tess.catalog, present_name)
.cloned()
})
.collect();
if materials.iter().all(Option::is_none) {
@@ -2,6 +2,7 @@ use bevy::prelude::*;
use frame::{MatchInstalled, MatchTornDown, SessionSwapApplied};
use net::ClientSet;
use crate::anim::fpv_prepared::PreparedFpv;
use crate::anim::scene_submission::AnimDObjSceneSkels;
use crate::occupancy::dyn_ent::{DynEntPhysClip, DynEntPhysWorld};
use crate::occupancy::fpv_present::{
@@ -17,6 +18,8 @@ pub fn reset_anim_for_match(
mut cursor: ResMut<FpvPresentCursor>,
mut pending: ResMut<PendingFpvSpawn>,
mut viewmodel: ResMut<SessionViewmodel>,
mut prepared_fpv: ResMut<PreparedFpv>,
mut model_materials: ResMut<crate::anim::model_materials::PreparedModelMaterials>,
mut settled: ResMut<FpvHeldSettled>,
mut held_life: ResMut<FpvHeldLife>,
mut scene_skels: ResMut<AnimDObjSceneSkels>,
@@ -32,6 +35,8 @@ pub fn reset_anim_for_match(
cursor.0.clear();
pending.0 = None;
*viewmodel = SessionViewmodel::default();
prepared_fpv.clear();
model_materials.clear();
settled.0 = None;
held_life.0 = None;
*scene_skels = AnimDObjSceneSkels::default();
+13 -33
View File
@@ -7,7 +7,7 @@ use crate::anim::xmodel_pose::PosedModelSurface;
use crate::occupancy::script_model::pose_script_dobj_with_materials;
use crate::{
MissileDrawPlan, MissileOwnerDraw, XMODEL_OBJECT_ID_MISSILE_BASE, append_missile_surfaces,
authored_lit_xmodel_pass_material, dobj_lighting_box_half,
dobj_lighting_box_half,
};
use anim_iw4::DOBJ_RADIUS_PARENT_ROOT;
use render_scene::{
@@ -187,7 +187,7 @@ fn occupy_missile_scene_ents(
entnum,
quat: Some(scene_quat_from_angles(angles)),
occupy_model_n: 1,
models: vec![scene_skels.model(name, &entry.skel, 0)],
models: vec![scene_skels.shared(name, &entry.skel, 0)],
hide_part_bits: [0; 6],
store_skin: true,
});
@@ -213,29 +213,19 @@ fn occupy_missile_scene_ents(
fn publish_missile_dobj_poses(
occupancy: Res<MissileOccupancy>,
projectile_meshes: Option<Res<assets::PreparedProjectileMeshes>>,
prepared: Res<crate::anim::model_materials::PreparedModelMaterials>,
mut dobj_poses: ResMut<crate::anim::dobj_pose::HostDObjPoseFrame>,
) {
let catalog = missile_pose_catalog(projectile_meshes.as_deref());
let Some(catalog) = catalog else {
return;
};
for row in &occupancy.rows {
let Some(id) = row.id else {
continue;
};
let Some(entry) = catalog.get(&row.name) else {
continue;
};
let Some(pose) = entry.skel.pose.as_ref() else {
continue;
};
let Ok(dobj) = assets::DObj::build(&[(pose, None)]) else {
let Some(dobj) = prepared.projectile_dobj(&row.name) else {
continue;
};
let entity_world = missile_world_from_local(row.origin, row.angles);
let Ok(local) = assets::dobj::pose_dobj_with_controller(
&dobj,
dobj,
&assets::dobj::DObjPoseRequest::bind_pose(),
Mat4::IDENTITY,
|_, _, _| {},
@@ -251,6 +241,7 @@ fn pose_missiles(
occupancy: Res<MissileOccupancy>,
projectile_meshes: Option<Res<assets::PreparedProjectileMeshes>>,
gfx: Res<HostGfxScene>,
prepared: Res<crate::anim::model_materials::PreparedModelMaterials>,
mut product: ResMut<MissilePoseProduct>,
) {
product.rows.clear();
@@ -267,10 +258,7 @@ fn pose_missiles(
let Some(entry) = catalog.get(&row.name) else {
continue;
};
let Some(pose) = entry.skel.pose.as_ref() else {
continue;
};
let Some(dobj) = assets::DObj::build(&[(pose, None)]).ok() else {
let Some(dobj) = prepared.projectile_dobj(&row.name) else {
continue;
};
let dobj_state = assets::dobj::DObjSemanticState::bind_pose(row.name.clone(), 1, 1);
@@ -280,7 +268,7 @@ fn pose_missiles(
let Some((surfaces, _)) = pose_script_dobj_with_materials(
None,
&[&entry.skel],
&dobj,
dobj,
&request,
None,
slot.skin_entries,
@@ -309,6 +297,7 @@ fn append_missile_draws(
facts: Res<WorldPresentFacts>,
mut plan: ResMut<MissileDrawPlan>,
mut lighting_requests: ResMut<ModelLightingRequests>,
prepared: Res<crate::anim::model_materials::PreparedModelMaterials>,
mut staged: Local<MissileDrawPlan>,
mut draws: Local<Vec<XModelSurfaceDraw>>,
mut owners: Local<Vec<MissileOwnerDraw>>,
@@ -326,10 +315,9 @@ fn append_missile_draws(
plan.publish_rebuild(staging, &mut draws, &mut owners);
return;
}
let (Some(catalog), Some(atlas), Some(tess)) = (catalog, atlas_ref, tess.as_deref()) else {
let (Some(catalog), Some(_), Some(tess)) = (catalog, atlas_ref, tess.as_deref()) else {
unreachable!("required missile draw resources checked above");
};
let mut material_cache: HashMap<assets::MaterialIndex, SmodelPassMaterial> = HashMap::new();
for row in &product.rows {
let entnum = row.id.unwrap_or(0);
let Some(entry) = catalog.get(&row.name) else {
@@ -340,17 +328,9 @@ fn append_missile_draws(
.iter()
.map(|surface| {
let authored = entry.material_index(surface.surface_index)?;
if let Some(material) = material_cache.get(&authored) {
return Some(material.clone());
}
let material = authored_lit_xmodel_pass_material(
atlas,
&tess.catalog,
tess.material_images.as_ref(),
authored,
)?;
material_cache.insert(authored, material.clone());
Some(material)
prepared
.projectile_material(&tess.catalog, authored)
.cloned()
})
.collect();
if materials.iter().all(Option::is_none) {
+106 -42
View File
@@ -1,21 +1,21 @@
use crate::anim::remote_body::{
AdvancedRemoteTree, CpuBodyGeom, CpuSurfMeta, PendingGunSkin, RemoteBodySkinnedItem,
RemoteBodySkinnedQueue, RemoteBodyTrees, RemoteModelLods, RemoteSkinModels,
RemoteSkinPoseHashes, SkinAfterPose, WorldGunGap, advance_remote_tree, bind_remote_skin_models,
clone_corpse_tree_from_victim, commit_assembled_body, dobj_radii, ensure_remote_dobj,
hash_skin_matrices, occupy_lod_byte, occupy_remote_kit_dobj, packed_anim, pose_remote_dobj,
push_cached_surfaces, 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,
AdvancedRemoteTree, CpuBodyGeom, CpuSurfMeta, PendingGunSkin, PreparedRemoteKits,
RemoteBodySkinnedItem, RemoteBodySkinnedQueue, RemoteBodyTrees, RemoteModelLods,
RemoteSkinModels, RemoteSkinPoseHashes, SkinAfterPose, WorldGunGap, advance_remote_tree,
bind_remote_skin_models, clone_corpse_tree_from_victim, commit_assembled_body, dobj_radii,
ensure_remote_dobj, hash_skin_matrices, occupy_lod_byte, occupy_remote_kit_dobj, packed_anim,
pose_remote_dobj, push_cached_surfaces, 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::xmodel_pose::{build_skin_layout, skin_packed_into, stream_lod_surface_rigid};
use crate::dobj_lighting_box_half;
use crate::gaps::{RenderGap, RenderGapCause, RenderPresentationGaps};
use crate::{
RemoteBodyDrawPlan, append_remote_body_cpu_blob, body_lit_pass_material,
finish_remote_body_draw_plan, install_body_packed_session, push_remote_body_cpu_draw,
take_body_packed_session, topology_fingerprint,
RemoteBodyDrawPlan, append_remote_body_cpu_blob, finish_remote_body_draw_plan,
install_body_packed_session, push_remote_body_cpu_draw, take_body_packed_session,
topology_fingerprint,
};
use anim_iw4::{PLAYER_ANIM_RAW_MASK, PlayerAnimValue};
use assets::{ModelSkel, PreparedBodies, PreparedWeapons, PreparedWorldWeapons};
@@ -71,6 +71,8 @@ pub fn register_remote_body_systems(app: &mut App) {
.init_resource::<RemoteSkinPoseHashes>()
.init_resource::<RemoteBodySkinnedQueue>()
.init_resource::<RemoteBodyLightingBinds>()
.init_resource::<PreparedRemoteKits>()
.add_systems(Update, prepare_remote_kits.in_set(ClientSet::Load))
.init_resource::<crate::anim::dobj_pose::HostDObjPoseFrame>()
.init_resource::<crate::anim::dobj_pose::PosedPlayerFrame>()
.add_systems(
@@ -131,6 +133,28 @@ pub fn register_remote_body_systems(app: &mut App) {
);
}
fn prepare_remote_kits(
bodies: Option<Res<PreparedBodies>>,
weapons: Option<Res<PreparedWeapons>>,
world_weapons: Option<Res<PreparedWorldWeapons>>,
mut kits: ResMut<PreparedRemoteKits>,
) {
let (Some(bodies), Some(weapons), Some(world)) = (bodies, weapons, world_weapons) else {
return;
};
if kits.owned_by(&bodies, &weapons, &world) {
return;
}
let started = std::time::Instant::now();
*kits = PreparedRemoteKits::prepare(&bodies, &weapons, &world);
diag::info!(
World,
"remote kits: prepared for {} weapons in {:.1}ms",
weapons.0.len(),
started.elapsed().as_secs_f64() * 1000.0
);
}
fn finish_body_draw_plan(mut plan: ResMut<RemoteBodyDrawPlan>) {
finish_remote_body_draw_plan(&mut plan);
}
@@ -148,6 +172,7 @@ fn occupy_remote_scene_ents(
bodies: Option<Res<PreparedBodies>>,
weapons: Option<Res<PreparedWeapons>>,
world_weapons: Option<Res<PreparedWorldWeapons>>,
prepared_kits: Res<PreparedRemoteKits>,
remotes: Query<
(&CEntity, &CEntityRuntime, &Transform),
(
@@ -160,6 +185,15 @@ fn occupy_remote_scene_ents(
let Some(bodies) = bodies.as_deref() else {
return;
};
if let Some(world) = world_weapons.as_deref() {
scene_skels.seed(bodies, world);
}
let live_kits = match (weapons.as_deref(), world_weapons.as_deref()) {
(Some(weapons), Some(world)) if prepared_kits.owned_by(bodies, weapons, world) => {
Some((&*prepared_kits, world))
}
_ => None,
};
let kits = bodies.0.kits();
if kits.allies.is_none() && kits.axis.is_none() {
return;
@@ -195,14 +229,31 @@ fn occupy_remote_scene_ents(
let ffa_team = meta.and_then(|m| m.ffa_team);
let client_state_team = meta.map(|m| m.client_state_team).unwrap_or(0);
let axis = assets::kit_assignment_is_axis(client_state_team, ffa_team);
let Some((kit_models, radius)) = occupy_remote_kit_dobj(
bodies,
weapons.as_deref(),
world_weapons.as_deref(),
axis,
remote_pose_sample(runtime).weapon,
) else {
continue;
let held = remote_pose_sample(runtime).weapon;
let (kit_models, radius, hide_part_bits) = match live_kits {
Some((kits, world)) => {
let Some(kit) = kits.get(axis, held) else {
continue;
};
let Some(models) = kit.models(bodies, world) else {
continue;
};
(models, kit.radius, kit.hide_part_bits)
}
None => {
let Some((models, radius)) = occupy_remote_kit_dobj(
bodies,
weapons.as_deref(),
world_weapons.as_deref(),
axis,
held,
true,
) else {
continue;
};
let bits = crate::anim::remote_body::kit_hide_part_bits(&models);
(models, radius, bits)
}
};
let origin = transform.translation.to_array();
let quat = [
@@ -218,19 +269,10 @@ fn occupy_remote_scene_ents(
.iter()
.map(|model| occupy_lod_byte(skel_camera_lod(model.skel, origin, eye, ramp)))
.collect();
let mut hide_part_bits = [0u32; 6];
let mut base = 0usize;
let models: Vec<_> = kit_models
.iter()
.enumerate()
.map(|(i, model)| {
render_scene::hide_part_bits_from_tags(
&mut hide_part_bits,
model.skel,
base,
&model.hide_tags,
);
base += model.skel.bones.len();
scene_skels.model(
model.name,
model.skel,
@@ -388,6 +430,7 @@ struct RemotePoseFrame<'a> {
bodies: &'a PreparedBodies,
weapons: Option<&'a PreparedWeapons>,
world_weapons: Option<&'a PreparedWorldWeapons>,
kits: Option<&'a PreparedRemoteKits>,
dt: f32,
eye: Option<Vec3>,
ramp: LodRampArgs,
@@ -436,6 +479,7 @@ fn pose_remote_bodies(
bodies: Option<Res<PreparedBodies>>,
weapons: Option<Res<PreparedWeapons>>,
world_weapons: Option<Res<PreparedWorldWeapons>>,
prepared_kits: Res<PreparedRemoteKits>,
mut trees: ResMut<RemoteBodyTrees>,
mut pose_hashes: ResMut<RemoteSkinPoseHashes>,
mut submit: ResMut<RemoteBodySkinnedQueue>,
@@ -555,6 +599,12 @@ fn pose_remote_bodies(
bodies,
weapons: weapons.as_deref(),
world_weapons: world_weapons.as_deref(),
kits: match (weapons.as_deref(), world_weapons.as_deref()) {
(Some(weapons), Some(world)) if prepared_kits.owned_by(bodies, weapons, world) => {
Some(&*prepared_kits)
}
_ => None,
},
dt: cg_clock
.as_ref()
.map(|clock| clock.frametime_secs())
@@ -687,6 +737,7 @@ impl<'a> RemotePoseFrame<'a> {
let bodies = self.bodies;
let weapons = self.weapons;
let world_weapons = self.world_weapons;
let kits = self.kits;
let weapon = sample.weapon;
let view_pitch_deg = sample.view_pitch_deg;
let prone = sample.prone;
@@ -750,10 +801,14 @@ impl<'a> RemotePoseFrame<'a> {
push_cached_surfaces(persist_key, transform, pose_hashes, submit);
return Ok(PoseOneOutcome::Posed);
}
ensure_remote_dobj(&model_set, e_type, persist_key, trees)?;
let kit = kits
.and_then(|kits| kits.get(axis, weapon))
.ok_or_else(|| format!("remote kit not prepared: axis={axis} weapon={weapon}"))?;
let prepared_dobj = kit.dobj.as_ref();
ensure_remote_dobj(&model_set, e_type, persist_key, trees, prepared_dobj)?;
let dobj = trees
.get(persist_key)
.and_then(|slot| slot.dobj.as_ref())
.and_then(|slot| slot.dobj.as_deref())
.expect("composed");
validate_remote_tracks(dobj, clips.as_ref(), body, &model_set.body_name)?;
@@ -764,6 +819,7 @@ impl<'a> RemotePoseFrame<'a> {
)?;
let skin = publish_remote_dobj(
dobj,
kit.bolt_bones,
&world,
persist_key,
runtime,
@@ -772,7 +828,13 @@ impl<'a> RemotePoseFrame<'a> {
dobj_poses,
)?;
if !is_corpse {
publish_posed_player_head(dobj, &world, identity, transform, posed_players)?;
publish_posed_player_head(
kit.head_bone,
&world,
identity,
transform,
posed_players,
)?;
}
let hash = hash_skin_matrices(&skin);
let pose_same = pose_hashes.remember_pose_hash(persist_key, hash);
@@ -848,6 +910,7 @@ fn remote_skin_action<'a>(
fn publish_remote_dobj(
dobj: &assets::DObj,
bolt_bones: [Option<u16>; 4],
world: &[Mat4],
persist_key: u32,
runtime: &CEntityRuntime,
@@ -864,8 +927,8 @@ fn publish_remote_dobj(
world,
)
.map_err(|error| format!("DObj bone publication failed: {error:?}"))?;
let target = |tag: &str| -> Option<fx::FxBoltTarget> {
let bone = u16::try_from(dobj.find(tag)?).ok()?;
let target = |bone: Option<u16>| -> Option<fx::FxBoltTarget> {
let bone = bone?;
let orientation = dobj_poses.resolve(persist_key, i32::from(bone)).ok()?;
Some(fx::FxBoltTarget {
dobj: persist_key,
@@ -877,21 +940,21 @@ fn publish_remote_dobj(
orientation,
})
};
bolts.flash = target("tag_flash");
bolts.brass = target("tag_brass");
bolts.knife = target("tag_knife_fx");
bolts.laser = target(fx_iw4::FX_LASER_TAG);
bolts.flash = target(bolt_bones[0]);
bolts.brass = target(bolt_bones[1]);
bolts.knife = target(bolt_bones[2]);
bolts.laser = target(bolt_bones[3]);
Ok(dobj.skin_matrices(world))
}
fn publish_posed_player_head(
dobj: &assets::DObj,
head_bone: Option<usize>,
world: &[Mat4],
identity: &CEntity,
transform: &Transform,
frame: &mut crate::anim::dobj_pose::PosedPlayerFrame,
) -> Result<(), String> {
let head = match dobj.find("j_head") {
let head = match head_bone {
None => crate::anim::dobj_pose::PosedPlayerHead::NoDObjOrHead,
Some(index) => {
let bone = world.get(index).ok_or_else(|| {
@@ -1165,6 +1228,7 @@ fn submit_remote_bodies(
resolved: Res<ResolvedModelLightingTable>,
atlas: Option<Res<WorldModelLightingAtlas>>,
tess: Option<Res<render_scene::TessMaterials>>,
model_materials: Res<crate::anim::model_materials::PreparedModelMaterials>,
mut last_catalog: Local<Option<Arc<render_material::RuntimeMaterialCatalog>>>,
) {
let items = submit.take();
@@ -1176,12 +1240,12 @@ fn submit_remote_bodies(
*last_catalog = None;
return;
}
let Some(atlas) = atlas else {
if atlas.is_none() {
plan.clear_geometry();
*last_catalog = None;
gaps.raise(RenderGapCause::RemoteBodyLightingAllocFailed);
return;
};
}
let Some(tess) = tess else {
plan.clear_geometry();
*last_catalog = None;
@@ -1276,7 +1340,7 @@ fn submit_remote_bodies(
let mat_idx = if let Some(&idx) = mat_by_name.get(name) {
idx
} else {
let Some(material) = body_lit_pass_material(&atlas, &tess.catalog, name) else {
let Some(material) = model_materials.material(&tess.catalog, name).cloned() else {
any_missing_material = true;
last_cause = Some(RenderGapCause::RemoteBodyMaterialMissing {
name: name.to_owned(),
@@ -29,7 +29,7 @@ struct ScriptModelDobjs {
}
struct PersistentScriptDobj {
dobj: assets::DObj,
dobj: std::sync::Arc<assets::DObj>,
reuse_key: assets::dobj::DObjReuseKey,
}
@@ -74,12 +74,11 @@ struct ScriptModelPoseLocals<'w, 's> {
struct ScriptModelTessLocals<'w, 's> {
last_material_generation: Local<'s, Option<render_material::MaterialGenerationId>>,
last_unbound_materials: Local<'s, usize>,
material_cache: Local<'s, HashMap<assets::MaterialIndex, SmodelPassMaterial>>,
model_materials: Res<'w, crate::anim::model_materials::PreparedModelMaterials>,
live_plan: Local<'s, Vec<usize>>,
product_to_plan: Local<'s, Vec<usize>>,
keep: Local<'s, Vec<bool>>,
remap: Local<'s, Vec<Option<usize>>>,
_marker: std::marker::PhantomData<&'w ()>,
}
struct ScriptOwnerRow {
@@ -508,6 +507,7 @@ fn pose_script_models(
cameras: Query<(&GlobalTransform, &Projection, &Camera), With<FpvLens>>,
owners: Query<(Entity, &WorldScriptModelInstance, &Transform, &Visibility)>,
mut persist: ResMut<ScriptModelDobjs>,
prepared: Res<crate::anim::model_materials::PreparedModelMaterials>,
mut product: ResMut<ScriptModelPoseProduct>,
select: Res<RenderFocusSelect>,
mut focus: ResMut<RenderFocus>,
@@ -599,7 +599,8 @@ fn pose_script_models(
.collect();
let asset_index = if let Some(id) = owner_id {
if compose_or_reuse_script_dobj(&mut persist, id, &assets, &owner.dobj_state).is_none()
if compose_or_reuse_script_dobj(&mut persist, id, &assets, &owner.dobj_state, &prepared)
.is_none()
{
persist.by_id.remove(&id);
continue;
@@ -732,12 +733,11 @@ fn commit_script_model_draw_plan(
let ScriptModelTessLocals {
mut last_material_generation,
mut last_unbound_materials,
mut material_cache,
model_materials,
mut live_plan,
mut product_to_plan,
mut keep,
mut remap,
_marker: _,
} = locals;
if product.producer_unavailable {
plan.publish_no_rows();
@@ -764,7 +764,6 @@ fn commit_script_model_draw_plan(
plan.revision = revision;
plan.generation = generation;
plan.revisions = revisions;
material_cache.clear();
*last_material_generation = Some(material_generation);
}
draws.clear();
@@ -783,45 +782,7 @@ fn commit_script_model_draw_plan(
.iter()
.zip(posed.authored.iter().copied())
.map(|(_surface, authored)| {
let authored = authored?;
if let Some(material) = material_cache.get(&authored) {
return Some(material.clone());
}
let world_material = tess.catalog.derived(authored)?;
let ordinal = tess
.catalog
.sorted_materials
.ordinal_for_asset_id(authored.order())?;
let maps = render_scene::runtime_maps(
Some(authored),
&tess.catalog,
tess.material_images.as_ref(),
);
let inv_h =
lighting_iw4::model_lighting_inv_image_height(atlas.dims.image_height)?;
let scale = lighting_iw4::model_lighting_lookup_scale(inv_h);
let material = SmodelPassMaterial {
model_lighting_required: true,
color: maps.color,
specular: maps.specular,
probe: None,
atlas: Some(atlas.image.clone()),
alpha_mode: maps.alpha_mode,
draw_mode: maps.draw_mode,
cull_mode: maps.cull_mode,
env_map_parms: maps.env_map_parms,
lighting_lookup_scale: [scale.u, scale.v, scale.w, scale.q],
atlas_lookup: [
lighting_iw4::MODEL_LIGHTING_INV_ATLAS_WIDTH as f32,
inv_h,
lighting_iw4::MODEL_LIGHTING_VOLUME_W,
0.0,
],
sort_key: world_material.sort_key,
material_sorted_index: Some(ordinal.get()),
};
material_cache.insert(authored, material.clone());
Some(material)
model_materials.authored(&tess.catalog, authored?).cloned()
})
.collect::<Vec<_>>();
append_or_overwrite_script_model(
@@ -1108,6 +1069,7 @@ fn compose_or_reuse_script_dobj(
id: u32,
catalog: &assets::MapXModelSceneCatalog,
state: &assets::dobj::DObjSemanticState,
prepared: &crate::anim::model_materials::PreparedModelMaterials,
) -> Option<()> {
let key = script_dobj_reuse_key(state);
let reuse = persist
@@ -1118,7 +1080,14 @@ fn compose_or_reuse_script_dobj(
return Some(());
}
let (specs, _) = collect_presented_models(catalog, state)?;
let dobj = assets::DObj::build(&specs).ok()?;
let single = match state.composition.models.as_slice() {
[only] => prepared.scene_dobj(&only.model).cloned(),
_ => None,
};
let dobj = match single {
Some(dobj) => dobj,
None => std::sync::Arc::new(assets::DObj::build(&specs).ok()?),
};
persist.by_id.insert(
id,
PersistentScriptDobj {
+40 -6
View File
@@ -135,6 +135,7 @@ impl ShellInternKey {
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
enum ExecutionShareExtra {
Identity,
ViewmodelCodeMesh,
LightingHandle(u32),
Instance { depth_hack: bool },
@@ -151,6 +152,7 @@ struct OverlayKeepKey {
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
enum OverlayKeepExtra {
World,
ViewmodelCodeMesh,
Lighting {
handle: u32,
packed: Option<[u8; 4]>,
@@ -188,6 +190,9 @@ fn execution_share_extra(draw: &RetainedDrawItem) -> ExecutionShareExtra {
RetainedDrawKind::CodeMesh {
draw, arg_count, ..
} if arg_count != 0 => ExecutionShareExtra::Unique(draw),
RetainedDrawKind::CodeMesh {
viewmodel: true, ..
} => ExecutionShareExtra::ViewmodelCodeMesh,
RetainedDrawKind::CodeMesh { .. } => ExecutionShareExtra::Identity,
}
}
@@ -244,6 +249,9 @@ fn smodel_code_world_from_local(kind: &RetainedDrawKind) -> Mat4 {
fn overlay_keep_key(draw: &RetainedDrawItem) -> OverlayKeepKey {
let scene_light = dpvs_iw4::GfxDrawSurf { packed: draw.key }.scene_light_index();
let extra = match draw.kind {
RetainedDrawKind::CodeMesh {
viewmodel: true, ..
} => OverlayKeepExtra::ViewmodelCodeMesh,
RetainedDrawKind::World { .. } | RetainedDrawKind::CodeMesh { .. } => {
OverlayKeepExtra::World
}
@@ -371,9 +379,22 @@ fn overlay_draw_material(
);
}
RetainedDrawKind::CodeMesh {
args, arg_count, ..
} if arg_count != 0 => {
overlay_code_mesh_tess_code_constants(scratch, &args[..usize::from(arg_count)]);
args,
arg_count,
viewmodel,
..
} => {
if arg_count != 0 {
overlay_code_mesh_tess_code_constants(scratch, &args[..usize::from(arg_count)]);
}
if viewmodel && let Some(clip) = runtime.frame.viewmodel_clip_from_world {
overlay_viewmodel_depth_hack(
scratch,
clip,
runtime.frame.view_origin,
Mat4::IDENTITY,
);
}
}
RetainedDrawKind::Glass {
lighting_handle, ..
@@ -458,9 +479,22 @@ fn overlay_draw_obj_only(
);
}
RetainedDrawKind::CodeMesh {
args, arg_count, ..
} if arg_count != 0 => {
overlay_code_mesh_tess_code_constants(scratch, &args[..usize::from(arg_count)]);
args,
arg_count,
viewmodel,
..
} => {
if arg_count != 0 {
overlay_code_mesh_tess_code_constants(scratch, &args[..usize::from(arg_count)]);
}
if viewmodel && let Some(clip) = runtime.frame.viewmodel_clip_from_world {
overlay_viewmodel_depth_hack(
scratch,
clip,
runtime.frame.view_origin,
Mat4::IDENTITY,
);
}
}
_ => {}
}
+1
View File
@@ -59,6 +59,7 @@ pub enum RetainedDrawKind {
},
CodeMesh {
viewmodel: bool,
draw: u32,
material: u32,
arg_count: u8,
@@ -98,6 +98,7 @@ impl FxSceneAccess<'_> {
pub(crate) fn register_combat_fx_systems(app: &mut App) {
app.add_message::<BulletHitFx>()
.init_resource::<render_fx::FxModelStaging>()
.init_resource::<crate::assemble::drawsurf::GfxGlassMeshPlan>()
.init_resource::<crate::assemble::drawsurf::CgGlassTable>()
.add_systems(Update, latch_authority_load_hold.in_set(ClientSet::Load))
@@ -120,11 +121,15 @@ pub(crate) fn register_combat_fx_systems(app: &mut App) {
)
.add_systems(
crate::assemble::StaticSunAndFx,
generate_fx_transaction.pipe(commit_fx_transaction),
(
generate_fx_transaction.pipe(commit_fx_transaction),
super::super::motion_tracker::draw_motion_tracker,
)
.chain(),
)
.add_systems(
Update,
(drain_bullet_hit_fx, drain_pellet_fx)
(drain_bullet_hit_fx, drain_pellet_fx, present_tracker_light)
.chain()
.after(render_anim::occupancy::fpv_present::publish_fpv_dobj_pose)
.after(frame::WorkerCmdSet::SkinModel)
@@ -143,6 +148,78 @@ pub(crate) fn register_combat_fx_systems(app: &mut App) {
.add_observer(cg_melee_blood);
}
#[derive(Default)]
struct TrackerLight {
owner: Option<(
frame::WorldGeneration,
sim::ClientId,
sim::LifeSequence,
u32,
)>,
bolt: Option<(u32, u16)>,
}
#[allow(clippy::too_many_arguments)]
fn present_tracker_light(
presented: Res<PresentedSnapshot>,
local: Res<LocalPresentClient>,
generation: Res<frame::WorldGeneration>,
prepared: Res<render_anim::PreparedFpv>,
bolts: Res<render_anim::FpvBoltTargets>,
catalog: Option<Res<PreparedFxCatalog>>,
mut elem_infos: ResMut<PreparedFxElemInfos>,
mut host: ResMut<HostFxSystem>,
fx_world: FxSceneAccess,
mut light: Local<TrackerLight>,
) {
const EFFECT: &str = "misc/light_motion_tracker";
let owner = presented.snapshot().and_then(|snapshot| {
let meta = snapshot.meta.for_client(local.0)?;
let ps = presented.player(local.0)?;
let weapon = weapon_iw4::bg_get_viewmodel_weapon_index(ps);
(meta.lifecycle == sim::ClientLifecycle::Alive
&& ps.other_flags & 0x400 == 0
&& prepared.table()?.facts_of(weapon).is_some_and(|facts| {
facts.motion_tracker
|| (facts.inventory_type == 3
&& prepared
.table()
.and_then(|t| t.facts_of(ps.weapon_primary))
.is_some_and(|parent| parent.motion_tracker))
}))
.then_some((*generation, local.0, meta.life_sequence, weapon))
});
if owner != light.owner {
if light.owner.is_some_and(|old| old.0 == *generation) {
if let Some((dobj, bone)) = light.bolt {
host.0.stop_bolted(EFFECT, dobj, bone);
}
}
light.owner = owner;
light.bolt = None;
}
if owner.is_none() || light.bolt.is_some() {
return;
}
let (Some(target), Some(catalog)) = (bolts.tracker_light, catalog) else {
return;
};
elem_infos.0.sync(&catalog.0);
if render_fx::present::play_named_bolted_in_world(
&mut host.0,
&catalog.0,
&elem_infos.0,
EFFECT,
target,
fx_world.view().as_ref().map(|scene| scene as &dyn FxScene),
)
.and_then(PlayResult::handle)
.is_some()
{
light.bolt = Some((target.dobj, target.bone));
}
}
fn queue_tag_lasers(
post_lights: &mut HostFxPostLights,
fpv_bolts: &crate::adapters::anim::fpv_present::FpvBoltTargets,
@@ -418,16 +495,16 @@ fn tick_fx_remaining_update(
}
#[derive(SystemParam)]
struct FxPresentEnv<'w, 's> {
struct FxPresentEnv<'w> {
outdoor: Option<Res<'w, MapOutdoor>>,
dlights: ResMut<'w, HostFxDlights>,
post_lights: ResMut<'w, HostFxPostLights>,
models: Option<Res<'w, PreparedFxModels>>,
model_geometry: Option<Res<'w, render_fx::PreparedFxModelGeometry>>,
atlas: Option<Res<'w, WorldModelLightingAtlas>>,
atpoint: Res<'w, render_scene::DynAtPointLookup>,
lod_skinned: Res<'w, LodRampSkinnedDvar>,
/// This frame's fx model rebuild, before the plan publishes it.
staged_models: Local<'s, FxModelDrawPlan>,
staged_models: ResMut<'w, render_fx::FxModelStaging>,
}
#[allow(clippy::too_many_arguments)]
@@ -553,9 +630,12 @@ fn commit_fx_transaction(
_present_span,
} = transaction;
clear_fx_owned_plans(&mut plan, &mut spark_plan, &mut mark_plan);
// The fx model rows are rebuilt from nothing every frame, so the rebuild
// goes to staging and `model_plan` only moves when the rows differ.
env.staged_models.clear();
env.staged_models.0.clear();
if let Some(prepared) = &env.model_geometry {
env.staged_models.0.use_prepared(&prepared.0);
} else {
env.staged_models.0 = FxModelDrawPlan::default();
}
env.dlights.scene.clear();
env.dlights.cap_full = 0;
env.post_lights.queued.clear();
@@ -604,12 +684,11 @@ fn commit_fx_transaction(
cam_tf.translation,
planes,
env.lod_skinned.args(),
&runtime,
lighting_cache.as_deref(),
&mut lighting_requests,
&mut env.staged_models,
&mut env.staged_models.0,
);
model_plan.publish_rebuild(&mut env.staged_models);
model_plan.publish_rebuild(&mut env.staged_models.0);
let spot_cone = lighting_iw4::SpotLightConeDvars::register_defaults();
for light in &out.spot_lights {
match lighting_iw4::r_add_omni_light_to_scene_allows(
@@ -1086,7 +1165,6 @@ fn fill_fx_model_plan(
camera_origin: Vec3,
frustum_planes: &[[f32; 4]],
lod_ramp: crate::assemble::drawsurf::tess::smodel::LodRampArgs,
runtime: &MaterialGeneration,
cache: Option<&WorldModelLightingCache>,
lighting_requests: &mut ModelLightingRequests,
plan: &mut FxModelDrawPlan,
@@ -1105,7 +1183,7 @@ fn fill_fx_model_plan(
dims: world.model_lighting_dims?,
})
});
let Some(atlas) = atlas.or(scene_atlas.as_ref()) else {
let Some(_) = atlas.or(scene_atlas.as_ref()) else {
plan.skipped_no_lighting = plan.generated;
return;
};
@@ -1147,71 +1225,11 @@ fn fill_fx_model_plan(
plan.skipped_culled = plan.skipped_culled.saturating_add(1);
continue;
}
let asset_surfaces = if let Some(asset) = plan.asset(instance.model_index, lod) {
asset.surfaces.clone()
} else {
let Some(pose) = entry.skel.pose.as_ref() else {
plan.skipped_no_pose = plan.skipped_no_pose.saturating_add(1);
continue;
};
let Ok(dobj) = assets::DObj::build(&[(pose, None)]) else {
plan.skipped_no_pose = plan.skipped_no_pose.saturating_add(1);
continue;
};
let state = assets::dobj::DObjSemanticState::bind_pose(
models
.name_at(instance.model_index)
.unwrap_or(instance.def_name.as_str())
.to_owned(),
1,
1,
);
let Ok(request) = state.resolve_request(|_| None) else {
plan.skipped_no_pose = plan.skipped_no_pose.saturating_add(1);
continue;
};
let Some((surfaces, _)) =
crate::adapters::anim::script_model::pose_script_dobj_with_materials(
None,
&[entry.skel.as_ref()],
&dobj,
&request,
None,
&[],
)
else {
plan.skipped_no_pose = plan.skipped_no_pose.saturating_add(1);
continue;
};
let lod_surfaces = entry.skel.surfaces_for_lod(lod);
let surfaces: Vec<_> = surfaces
.into_iter()
.filter(|surface| lod_surfaces.contains(&surface.surface_index))
.collect();
let materials: Vec<_> = surfaces
.iter()
.map(|surface| {
let material = entry.material_index(surface.surface_index)?;
crate::assemble::drawsurf::tess::xmodel::bound_lit_xmodel_pass_material(
atlas,
&runtime.catalog,
material,
)
})
.collect();
let appended = crate::assemble::drawsurf::tess::xmodel::append_fx_model_asset(
plan,
instance.model_index,
lod,
&surfaces,
&materials,
);
if appended.is_empty() {
plan.skipped_no_material = plan.skipped_no_material.saturating_add(1);
continue;
}
appended
let Some(asset) = plan.asset(instance.model_index, lod) else {
plan.skipped_no_material = plan.skipped_no_material.saturating_add(1);
continue;
};
let asset_surfaces = asset.surfaces.clone();
let box_half = entry.skel.radius.and_then(|radius| {
crate::prepare::scene::model_lighting_cache::dobj_lighting_box_half(
@@ -2,12 +2,14 @@ use bevy::prelude::*;
pub mod anim;
pub mod fx;
pub(crate) mod motion_tracker;
pub struct RenderAdaptersPlugin;
impl Plugin for RenderAdaptersPlugin {
fn build(&self, app: &mut App) {
crate::adapters::fx::system::register_combat_fx_systems(app);
motion_tracker::register(app);
crate::adapters::anim::dyn_ent::register_dyn_ent_frontend(app);
crate::adapters::anim::dyn_ent_brush::register_dyn_ent_brush_systems(app);
}
@@ -0,0 +1,337 @@
use std::collections::HashMap;
use bevy::prelude::*;
use net::{CgFrameClock, LocalPresentClient, PresentedSnapshot};
use render_anim::{FpvBoltTargets, PreparedFpv};
use render_fx::{FxCodeMeshPlan, FxWorldColorImages};
pub(crate) const MATERIALS: [&str; 4] = [
"motiontracker3d_bg",
"motiontracker3d_sweep",
"motiontracker3d_ping_enemy_mp",
"motiontracker3d_ping_friendly_mp",
];
const RANGE: f32 = 1600.0;
const SWEEP_MS: i32 = 3000;
const SWEEP_SPEED: f32 = 2000.0;
const FADE_MS: i32 = 3000;
const CENTER_Y: f32 = -0.37;
const PING_SIZE: f32 = 0.2;
#[derive(Clone, Copy)]
struct Ping {
distance: f32,
yaw: f32,
start_angle: f32,
end_angle: f32,
time: i32,
enemy: bool,
}
#[derive(Resource, Default)]
pub(crate) struct MotionTracker {
owner: Option<(u64, sim::ClientId, sim::LifeSequence)>,
last_time: Option<i32>,
previous_origin: Vec2,
phase_ms: i32,
active: bool,
ping_played: bool,
changed_at: i32,
contacts: HashMap<sim::ClientId, [Option<Ping>; 2]>,
}
pub(crate) fn register(app: &mut App) {
app.init_resource::<MotionTracker>();
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn draw_motion_tracker(
mut tracker: ResMut<MotionTracker>,
presented: Res<PresentedSnapshot>,
local: Res<LocalPresentClient>,
clock: Res<CgFrameClock>,
mut sound: MessageWriter<audio::AliasCommand>,
prepared: Res<PreparedFpv>,
bolts: Res<FpvBoltTargets>,
colors: Res<FxWorldColorImages>,
mut plan: ResMut<FxCodeMeshPlan>,
) {
let Some(snapshot) = presented.snapshot() else {
*tracker = MotionTracker::default();
return;
};
let Some(ps) = presented.player(local.0) else {
*tracker = MotionTracker::default();
return;
};
let Some(meta) = snapshot
.meta
.clients
.iter()
.find(|(id, _)| *id == local.0)
.map(|(_, m)| m)
else {
return;
};
let Some(table) = prepared.table() else {
*tracker = MotionTracker::default();
return;
};
let now = clock.time();
let owner = (table.catalog_id(), local.0, meta.life_sequence);
if tracker.owner != Some(owner) || tracker.last_time.is_some_and(|t| now < t) {
*tracker = MotionTracker {
owner: Some(owner),
..Default::default()
};
}
let weapon = weapon_iw4::bg_get_viewmodel_weapon_index(ps);
let active = table.facts_of(weapon).is_some_and(|f| {
f.motion_tracker
|| (f.inventory_type == 3
&& table
.facts_of(ps.weapon_primary)
.is_some_and(|parent| parent.motion_tracker))
}) && ps.other_flags & (1 << 10) == 0
&& meta.lifecycle == sim::ClientLifecycle::Alive;
let origin = Vec2::new(ps.origin[0], ps.origin[1]);
let dt = tracker.last_time.map_or(0, |t| now.saturating_sub(t));
tracker.last_time = Some(now);
if active != tracker.active {
tracker.active = active;
tracker.changed_at = now;
if active {
tracker.phase_ms = 0;
tracker.ping_played = false;
tracker.previous_origin = origin;
}
}
let mut play = |alias: &str, pitch: f32| {
sound.write(audio::AliasCommand::PlayPitched {
sound: audio::PlayAlias {
namespace: assets::AssetNamespace::Iw4,
alias: alias.to_owned(),
fallback: None,
origin_inches: None,
snd_ent: Some(audio::SND_ENT_LOCAL),
},
pitch,
});
};
if dt > 0 && (active || now - tracker.changed_at < 500) {
let view_yaw = ps.viewangles[1].to_radians();
for history in tracker.contacts.values_mut() {
if let Some(ping) = &mut history[0] {
let current = (ping.yaw - view_yaw).rem_euclid(std::f32::consts::TAU);
let span = (ping.end_angle - ping.start_angle).rem_euclid(std::f32::consts::TAU);
if span <= (current - ping.start_angle).rem_euclid(std::f32::consts::TAU) {
let backwards = (ping.start_angle - current).rem_euclid(std::f32::consts::TAU);
let forwards = (current - ping.end_angle).rem_euclid(std::f32::consts::TAU);
if forwards <= backwards {
ping.end_angle += forwards;
} else {
ping.start_angle -= backwards;
}
}
}
}
let previous = tracker.phase_ms;
let elapsed = previous.saturating_add(dt);
tracker.phase_ms = elapsed.rem_euclid(SWEEP_MS);
if elapsed >= SWEEP_MS {
tracker.ping_played = false;
if active {
play("motiontracker_ping", 1.0);
} else {
tracker.phase_ms = 0;
}
}
let radius = tracker.phase_ms as f32 * SWEEP_SPEED / 1000.0;
let previous_radius = previous as f32 * SWEEP_SPEED / 1000.0;
let yaw = ps.viewangles[1].to_radians();
let forward = Vec2::new(yaw.cos(), yaw.sin());
for (id, state) in snapshot
.players
.iter()
.filter(|_| active || elapsed < SWEEP_MS)
{
if now.saturating_sub(sim::level_time_ms(snapshot.tick)) > 500
|| *id == local.0
|| state.health <= 0
|| state.perks[0] & (1 << 28) != 0
{
continue;
}
let Some(actor) = snapshot
.meta
.clients
.iter()
.find(|(c, _)| c == id)
.map(|(_, m)| m)
else {
continue;
};
if actor.lifecycle != sim::ClientLifecycle::Alive || actor.client_state_team == 3 {
continue;
}
let position = Vec2::new(state.origin[0], state.origin[1]);
let delta = position - origin;
if delta.length_squared() > radius * radius
|| (elapsed < SWEEP_MS
&& (position - tracker.previous_origin).length_squared()
<= previous_radius * previous_radius)
|| delta.dot(forward) < 0.0
{
continue;
}
let ping = Ping {
distance: delta.length(),
yaw: delta.y.atan2(delta.x),
start_angle: (delta.y.atan2(delta.x) - yaw).rem_euclid(std::f32::consts::TAU),
end_angle: (delta.y.atan2(delta.x) - yaw).rem_euclid(std::f32::consts::TAU),
time: now,
enemy: !matches!(meta.client_state_team, 1 | 2)
|| meta.client_state_team != actor.client_state_team,
};
if ping.enemy && !tracker.ping_played {
if let Some([tl, bl, br]) = bolts.tracker_screen {
let aspect = (br - bl).length() / (tl - bl).length();
let angle = ping.yaw - yaw;
let x = -angle.sin() * ping.distance / RANGE / aspect;
let y = angle.cos() * ping.distance / RANGE + CENTER_Y;
if x.abs() <= 0.5 && y.abs() <= 0.5 {
let max_distance = RANGE * Vec2::new(0.5 * aspect, 0.5 - CENTER_Y).length();
play("motiontracker_pong", 2.0 - ping.distance / max_distance);
tracker.ping_played = true;
}
}
}
let history = tracker.contacts.entry(*id).or_insert([None, None]);
history[1] = history[0];
history[0] = Some(ping);
}
tracker.previous_origin = origin;
}
tracker
.contacts
.retain(|_, history| history.iter().flatten().any(|p| now - p.time < FADE_MS));
let fade = ((now - tracker.changed_at) as f32 / 500.0).clamp(0.0, 1.0);
let alpha = if active { fade } else { 1.0 - fade };
if alpha <= 0.0 {
return;
}
let Some([tl, bl, br]) = bolts.tracker_screen else {
return;
};
let right = br - bl;
let up = tl - bl;
let width = right.length();
let height = up.length();
if width <= 0.0 || height <= 0.0 {
return;
}
let right = right / width;
let up = up / height;
let normal = right.cross(up).normalize_or_zero();
let rotation = Quat::from_mat3(&Mat3::from_cols(right, up, normal));
let center = (tl + br) * 0.5;
let mut quad = |material: usize, offset: Vec2, size: Vec2, opacity: f32| {
let Some(binding) = &colors.motion_tracker[material] else {
return;
};
let slot = plan.begin_material_draw(
binding.color.clone(),
binding.sort_key,
binding.material_sorted_index,
);
plan.draws[slot as usize].viewmodel = true;
plan.push_quad(
Transform {
translation: center + right * offset.x + up * offset.y,
rotation,
scale: Vec3::new(size.x, size.y, 1.0),
},
[255, 255, 255, (opacity.clamp(0.0, 1.0) * 255.0) as u8],
fx_iw4::FxSpriteAtlasUv::FULL,
);
plan.end_material_draw(slot);
};
quad(0, Vec2::ZERO, Vec2::new(width, height), alpha);
let visible = tracker
.contacts
.values()
.flatten()
.flatten()
.filter(|ping| (0..FADE_MS).contains(&(now - ping.time)))
.count();
let max_points = if visible == 0 {
32
} else {
(634 / visible).min(32).max(1)
};
for history in tracker.contacts.values() {
for ping in history.iter().flatten() {
let age = now - ping.time;
if !(0..FADE_MS).contains(&age) {
continue;
}
let span = (ping.end_angle - ping.start_angle).rem_euclid(std::f32::consts::TAU);
let length = ping.distance / RANGE * span;
let mut points = (length / (PING_SIZE * 0.25) + 0.5).floor().max(1.0) as usize;
if length > 0.0 {
points = points.max(3);
}
points = points.min(max_points);
let edge = if length < 0.15 { length * 2.0 } else { 0.3 };
let opacity =
(alpha * (1.0 - age as f32 / FADE_MS as f32) * (age as f32 / 100.0).min(1.0)
/ points as f32
/ (1.0 - edge))
.min(1.0);
for point in 0..points {
let fraction = (point as f32 + 0.5) / points as f32;
let angle = ping.start_angle + fraction * span;
let position = Vec2::new(
-angle.sin() * ping.distance / RANGE / (width / height),
angle.cos() * ping.distance / RANGE + CENTER_Y,
);
if position.x.abs() > 0.5 + PING_SIZE || position.y.abs() > 0.5 + PING_SIZE {
continue;
}
let taper = if fraction < edge {
fraction / edge
} else if fraction > 1.0 - edge {
(1.0 - fraction) / edge
} else {
1.0
};
quad(
if ping.enemy { 2 } else { 3 },
position * Vec2::new(width, height),
Vec2::splat(PING_SIZE * height),
opacity * taper,
);
}
}
}
let phase = tracker.phase_ms as f32 / SWEEP_MS as f32;
if phase < 0.25 {
let prior = SWEEP_SPEED * (phase + 1.0) * SWEEP_MS as f32 / 1000.0 / RANGE;
quad(
1,
Vec2::new(0.0, (CENTER_Y + 0.5 * prior) * height),
Vec2::new(2.0 * prior * height, prior * height),
alpha * (1.0 - 4.0 * phase),
);
}
let sweep_range = SWEEP_SPEED * phase * SWEEP_MS as f32 / 1000.0 / RANGE;
quad(
1,
Vec2::new(0.0, (CENTER_Y + 0.5 * sweep_range) * height),
Vec2::new(2.0 * sweep_range * height, sweep_range * height),
alpha,
);
plan.bump();
plan.publish_share();
}
@@ -40,6 +40,9 @@ pub fn depth_range_type_for_draw(kind: &RetainedDrawKind, key: u64) -> i32 {
let object_id = dpvs_iw4::GfxDrawSurf { packed: key }.object_id();
depth_range_type_from_hack_flags(host_viewmodel_render_fx_flags(object_id))
}
RetainedDrawKind::CodeMesh {
viewmodel: true, ..
} => GFX_DEPTH_RANGE_VIEWMODEL,
_ => GFX_DEPTH_RANGE_SCENE,
}
}
@@ -483,8 +483,11 @@ fn logical_input_key(draw: &RetainedDrawItem, tech: TechType) -> LogicalInputKey
surface, object_id, ..
} => (2, surface, u32::from(object_id)),
RetainedDrawKind::CodeMesh {
draw, arg_count, ..
} => (3, draw, u32::from(arg_count)),
draw,
arg_count,
viewmodel,
..
} => (3, draw, u32::from(arg_count) | (u32::from(viewmodel) << 8)),
RetainedDrawKind::ParticleCloud { draw, .. } => (4, draw, 0),
RetainedDrawKind::MarkMesh { draw, .. } => (5, draw, 0),
RetainedDrawKind::Glass {
@@ -491,6 +491,11 @@ impl MaterialProgramCompile {
techs.extend(lighting_iw4::LIT_TECH_NO_SHADOW_LOCAL_SLOTS);
techs.extend(lighting_iw4::LIT_TECH_SHADOW_DIR_SLOTS);
techs.extend(lighting_iw4::LIT_TECH_SHADOW_SPOT_SLOTS);
techs.extend([
lighting_iw4::TECHNIQUE_LIGHT_SPOT,
lighting_iw4::TECHNIQUE_LIGHT_OMNI,
lighting_iw4::TECHNIQUE_LIGHT_SPOT_SHADOW,
]);
techs.push(SUN_SHADOW_CASTER_TECH);
techs.push(EMISSIVE_TECH_TYPE);
let reachable: HashSet<usize> = catalog
@@ -411,8 +411,16 @@ pub(crate) fn mix_draw_membership(id: &mut u64, item: &RetainedDrawItem) {
surface, object_id, ..
} => (2, u64::from(surface), u64::from(object_id), 0),
RetainedDrawKind::CodeMesh {
draw, arg_count, ..
} => (3, u64::from(draw), u64::from(arg_count), 0),
draw,
arg_count,
viewmodel,
..
} => (
3,
u64::from(draw),
u64::from(arg_count),
u64::from(viewmodel),
),
RetainedDrawKind::ParticleCloud { draw, .. } => (4, u64::from(draw), 0, 0),
RetainedDrawKind::MarkMesh { draw, .. } => (5, u64::from(draw), 0, 0),
RetainedDrawKind::Glass {
@@ -477,6 +485,7 @@ fn fx_lane_layout_hash(
}
super::list::mix_content_id(&mut id, i as u64);
super::list::mix_content_id(&mut id, u64::from(draw.material));
super::list::mix_content_id(&mut id, u64::from(draw.viewmodel));
let mat = plan.materials.get(draw.material as usize);
super::list::mix_content_id(&mut id, u64::from(mat.map(|m| m.sort_key).unwrap_or(0)));
super::list::mix_content_id(
@@ -1969,6 +1978,7 @@ pub(crate) fn rebuild_fx_draw_lane(
key,
material_sorted_index,
RetainedDrawKind::CodeMesh {
viewmodel: draw.viewmodel,
draw: i as u32,
material: draw.material,
arg_count: n as u8,
@@ -182,6 +182,7 @@ impl ChangeState0Host {
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct ChangeState1Host {
pub stencil: u32,
pub depth_write: bool,
pub depth_test_enable: bool,
@@ -303,13 +304,14 @@ impl GfxPassState {
|| !colour.dst.is_known()
|| (alpha_blend_op != 0 && (!alpha.src.is_known() || !alpha.dst.is_known()))),
unknown_blend_operation: blend_op > 5 || (blend_op != 0 && alpha_blend_op > 5),
stencil: self.word1 & 0xc0 != 0,
stencil: false,
};
fields.any().then_some(fields)
}
pub fn apply_change_state_1_host(self) -> ChangeState1Host {
ChangeState1Host {
stencil: self.word1 & 0xffff_ffc0,
depth_write: assets::depth_write_enable(self.word1),
depth_test_enable: assets::depth_test_enable(self.word1),
depth_func: ((self.word1 >> 2) & 3) as u8,
@@ -92,6 +92,8 @@ impl Plugin for RenderPreparePlugin {
arm_world_spawn_on_install.before(spawn_world),
spawn_world,
spawn_world_finish.after(spawn_world),
render_anim::prepare_fpv_compositions.after(spawn_world_finish),
render_anim::prepare_model_materials.after(spawn_world_finish),
crate::assemble::drawsurf::tess::glass::apply_cg_glass_tess
.after(spawn_world)
.after(WorkerCmdSet::FxNonDependent),
@@ -182,7 +182,13 @@ pub(crate) fn spawn_world(
fx_catalog: Option<Res<PreparedFxCatalog>>,
report: Option<Res<LaunchReport>>,
present_ack: Res<WorldPresentAck>,
prepared: (
Res<render_anim::PreparedFpv>,
Res<render_anim::PreparedModelMaterials>,
Option<Res<render_fx::PreparedFxModels>>,
),
) {
let (fpv, model_materials, fx_models) = prepared;
// Pacing belongs to the load that is still running, not to the screen that
// happens to be drawing it: a run without an overlay must spawn the world
// the same way this one does.
@@ -199,7 +205,15 @@ pub(crate) fn spawn_world(
let gap_ms = job.slice_gap_ms(std::time::Instant::now());
let spawn = job.spawn;
let gpu_ready = gpu.as_deref();
if super::world_gpu::poll(&mut job.gpu_wait, spawn, gpu_ready, gap_ms) {
let gpu_done = super::world_gpu::poll(&mut job.gpu_wait, spawn, gpu_ready, gap_ms);
let fpv_done = fpv.settled_for(&tess.catalog) && model_materials.settled_for(&tess.catalog);
if gpu_done && !fpv_done {
diag::info!(
World,
"world spawn: GPU ready; overlay holds for model and first-person preparation"
);
}
if gpu_done && fpv_done {
let quiet = job.gpu_wait.quiet();
let elapsed_ms = job.gpu_wait.elapsed().as_secs_f32() * 1000.0;
finish_world_spawn(&mut scene, &mut job, &mut commands);
@@ -211,6 +225,7 @@ pub(crate) fn spawn_world(
gpu_ready.map(|g| i32::from(g.pipelines)).unwrap_or(-1),
elapsed_ms,
);
log_load_ledger(&job, gpu_ready, &fpv, elapsed_ms);
}
return;
}
@@ -675,6 +690,10 @@ pub(crate) fn spawn_world(
tracers.as_deref(),
fx_catalog.as_deref(),
);
let fx_geometry =
super::world_plan::prepare_fx_model_geometry(&scene, fx_models.as_deref());
commands.insert_resource(fx_geometry.0.clone());
commands.insert_resource(fx_geometry);
tess.material_images = std::sync::Arc::new(job.images.exact_handles().to_vec());
if let Some(host) = fx_host.as_mut() {
match scene.fx_glass.as_ref() {
@@ -794,6 +813,37 @@ impl WorldPresentAck {
#[derive(Resource, Default)]
struct ExtractedWorldPresent(Option<WorldGeneration>);
fn log_load_ledger(
job: &WorldSpawnJob,
gpu: Option<&WorldGpuReady>,
fpv: &render_anim::PreparedFpv,
gpu_wait_ms: f32,
) {
let images = &job.images;
let pipelines_added = gpu.and_then(|gpu| job.gpu_wait.pipelines_added(gpu.pipeline_n));
let fpv_census = fpv.table().map(|table| table.census());
diag::info!(
World,
"load ledger: images handed={} handed_bytes={} reused_slot={} reused_slot_bytes={} reused_common={} reused_common_bytes={} resident_map={} | pipelines cached={} compiled_this_load={} | fpv models={} rigs={} materials={} built_ms={} | gpu_wait_ms={gpu_wait_ms:.1}",
images
.done
.saturating_sub(images.skipped)
.saturating_sub(images.reused_handles),
images.handed_bytes,
images.reused_handles,
images.reused_handle_bytes,
images.reused_common_handles,
images.reused_common_bytes,
images.resident,
gpu.map_or_else(|| "?".to_owned(), |gpu| gpu.pipeline_n.to_string()),
pipelines_added.map_or_else(|| "?".to_owned(), |n| n.to_string()),
fpv_census.map_or_else(|| "?".to_owned(), |c| c.models.to_string()),
fpv_census.map_or_else(|| "?".to_owned(), |c| c.rigs.to_string()),
fpv_census.map_or_else(|| "?".to_owned(), |c| c.materials.to_string()),
fpv_census.map_or_else(|| "?".to_owned(), |c| format!("{:.1}", c.elapsed_ms)),
);
}
fn finish_world_spawn(scene: &mut WorldScene, job: &mut WorldSpawnJob, commands: &mut Commands) {
job.phase = WorldSpawnPhase::Done;
scene.spawned = true;
@@ -15,6 +15,7 @@ pub(crate) fn overlay_is_quiet(ready: bool, quiet: u32) -> bool {
pub struct WorldGpuWait {
started: Option<std::time::Instant>,
quiet: u32,
pipelines_at_arm: Option<u32>,
stage: Option<assets::StageHandle>,
images_stage: Option<assets::StageHandle>,
pipelines_stage: Option<assets::StageHandle>,
@@ -24,6 +25,7 @@ impl WorldGpuWait {
pub fn arm(&mut self, progress: Option<&assets::LoadProgress>) {
self.started = Some(std::time::Instant::now());
self.quiet = 0;
self.pipelines_at_arm = None;
// A wait that never reached its gate before the world changed under it
// was interrupted; it did not quietly succeed.
for stage in [
@@ -50,6 +52,10 @@ impl WorldGpuWait {
self.quiet
}
pub(crate) fn pipelines_added(&self, now: u32) -> Option<u32> {
self.pipelines_at_arm.map(|at| now.saturating_sub(at))
}
pub(crate) fn elapsed(&self) -> std::time::Duration {
self.started
.map(|at| at.elapsed())
@@ -215,6 +221,9 @@ pub(crate) fn poll(
gap_ms: f32,
) -> bool {
if let Some(gpu) = gpu {
if wait.pipelines_at_arm.is_none() {
wait.pipelines_at_arm = Some(gpu.pipeline_n);
}
// Residency and warmth are gauges: what is on the GPU right now. They
// may fall between frames, and it is the existing gate — `gpu.images`,
// `gpu.pipelines` — that says the wait is over, never count equality.
@@ -123,6 +123,18 @@ pub(crate) fn fx_world_color_images(
}
}
for (asset_id, material) in scene.runtime_material_catalog.materials.iter().enumerate() {
if crate::adapters::motion_tracker::MATERIALS.contains(&material.name.as_str()) {
stitch_fx_color_by_asset(
&mut colors_by_asset,
&mut keys_by_asset,
exact_material_handles,
scene,
asset_id,
);
}
}
for (asset_id, handle) in colors_by_asset.iter().chain(mark_colors_by_asset.iter()) {
let Some(material) = scene
.runtime_material_catalog
@@ -189,7 +201,23 @@ pub(crate) fn fx_world_color_images(
);
}
}
let motion_tracker = crate::adapters::motion_tracker::MATERIALS.map(|name| {
let catalog = &scene.runtime_material_catalog;
let ordinal = catalog.ordinal_for_material_name(name)?;
let material = catalog
.materials
.iter()
.find(|material| material.name == name)?;
Some(render_fx::drawsurf::tess::fx::FxPassMaterial {
color: fx_color_by_name.get(name).cloned(),
sort_key: material
.baked_draw_surf
.map(|key| dpvs_iw4::GfxDrawSurf { packed: key }.primary_sort_key())?,
material_sorted_index: Some(ordinal.get()),
})
});
FxWorldColorImages {
motion_tracker,
colors: fx_color_by_name,
keys: fx_keys,
colors_by_asset,
@@ -199,6 +227,98 @@ pub(crate) fn fx_world_color_images(
}
}
pub(crate) fn prepare_fx_model_geometry(
scene: &WorldScene,
models: Option<&render_fx::PreparedFxModels>,
) -> render_fx::PreparedFxModelGeometry {
let mut plan = render_fx::FxModelDrawPlan::default();
plan.generation = scene.runtime_material_catalog.generation_id.0;
let Some(models) = models else {
return render_fx::PreparedFxModelGeometry(plan);
};
let Some((image, dims)) = scene
.model_lighting_image
.clone()
.zip(scene.model_lighting_dims)
else {
return render_fx::PreparedFxModelGeometry(plan);
};
let atlas = super::model_lighting_atlas::WorldModelLightingAtlas { image, dims };
let models = &models.0;
for index in 0..models.len() {
let Some(entry) = models.get_at(index) else {
continue;
};
let name = models.name_at(index).unwrap_or_default();
let binding = (|| {
let pose = entry.skel.pose.as_ref()?;
let dobj = assets::DObj::build(&[(pose, None)]).ok()?;
let state = assets::dobj::DObjSemanticState::bind_pose(name.to_owned(), 1, 1);
let request = state.resolve_request(|_| None).ok()?;
let world = assets::dobj::pose_dobj(&dobj, &request, Mat4::IDENTITY).ok()?;
let skin = dobj.skin_matrices(&world);
Some((dobj, request, skin))
})();
let Some((dobj, request, skin)) = binding else {
diag::warn!(World, "FX model `{name}` refused: bind pose unavailable");
continue;
};
for lod in 0..entry
.skel
.lod
.map_or(1, |selector| selector.num_lods().clamp(0, 4) as u8)
{
let Some(lod_surfaces) = render_anim::anim::xmodel_pose::skin_model_filtered(
&entry.skel,
|bone| skin[bone],
&[],
|surface| {
dobj.surface_visible(
0,
&entry.skel.surface_part_bits[surface],
&request.hide_part_bits,
)
},
|surface| {
render_anim::anim::xmodel_pose::stream_lod_surface_rigid(
&entry.skel,
lod,
&[],
0,
surface,
)
},
lod,
) else {
diag::warn!(
World,
"FX model `{name}` LOD {lod} refused: geometry unavailable"
);
continue;
};
let materials: Vec<_> = lod_surfaces
.iter()
.map(|surface| {
crate::assemble::drawsurf::tess::xmodel::bound_lit_xmodel_pass_material(
&atlas,
&scene.runtime_material_catalog,
entry.material_index(surface.surface_index)?,
)
})
.collect();
if materials.iter().any(Option::is_none) {
diag::warn!(
World,
"FX model `{name}` LOD {lod} refused: material binding unavailable"
);
continue;
}
render_fx::append_fx_model_asset(&mut plan, index, lod, &lod_surfaces, &materials);
}
}
render_fx::PreparedFxModelGeometry(plan)
}
pub fn install(
commands: &mut Commands,
scene: &WorldScene,
+2
View File
@@ -25,6 +25,7 @@ pub struct FxPassMaterial {
#[derive(Clone, Copy, Debug)]
pub struct FxSurfaceDraw {
pub viewmodel: bool,
pub material: u32,
pub index_start: u32,
pub index_count: u32,
@@ -220,6 +221,7 @@ impl FxCodeMeshPlan {
});
let index_start = self.indices.len() as u32;
self.draws.push(FxSurfaceDraw {
viewmodel: false,
material,
index_start,
index_count: 0,
+1
View File
@@ -21,6 +21,7 @@ pub struct PreparedImpactFx(pub Option<assets::OwnedFxImpactTable>);
#[derive(Resource, Default)]
pub struct FxWorldColorImages {
pub motion_tracker: [Option<crate::drawsurf::tess::fx::FxPassMaterial>; 4],
pub colors: HashMap<String, Handle<Image>>,
pub keys: HashMap<String, (u8, u16)>,
pub colors_by_asset: HashMap<usize, Handle<Image>>,
+2
View File
@@ -31,3 +31,5 @@ pub use tracer::{
PreparedTracers, QueuedBeam, TracerDrawGate, TracerSpawnSkip, TracerWorld, tick_tracer_beams,
try_spawn_tracer,
};
pub use model_draw::{FxModelStaging, PreparedFxModelGeometry};
+19 -14
View File
@@ -102,15 +102,16 @@ pub fn append_fx_model_asset(
surfaces: &[render_anim::fpv_pose::PosedModelSurface],
materials: &[Option<SmodelPassMaterial>],
) -> Vec<(u32, u32)> {
let geometry = std::sync::Arc::make_mut(&mut plan.geometry);
let mut asset_surfaces = Vec::new();
let vertices_empty = plan.vertices.is_empty();
let vertices_empty = geometry.vertices.is_empty();
let mut packed_ok = vertices_empty
|| matches!(
plan.packed_vertices,
geometry.packed_vertices,
assets::RetailPackedVertexPayload::Iw4(_)
);
let mut packed = match std::mem::replace(
&mut plan.packed_vertices,
&mut geometry.packed_vertices,
assets::RetailPackedVertexPayload::Unavailable {
source_layout: XMODEL_PACKED_UNAVAILABLE,
},
@@ -120,40 +121,44 @@ pub fn append_fx_model_asset(
};
for (surface, material) in surfaces.iter().zip(materials) {
let Some(material) = material else { continue };
let before = plan.vertices.len();
let before = geometry.vertices.len();
let Some((start, count)) =
append_mesh(&surface.mesh, &mut plan.vertices, &mut plan.indices)
append_mesh(&surface.mesh, &mut geometry.vertices, &mut geometry.indices)
else {
continue;
};
let decoded = plan.vertices.len() - before;
let decoded = geometry.vertices.len() - before;
if packed_ok && surface.packed_vertices.len() == decoded {
packed.extend_from_slice(&surface.packed_vertices);
} else {
packed_ok = false;
packed.clear();
}
let surface_index = plan.surface_ranges.len() as u32;
plan.surface_ranges.push((start, count));
let material_index = plan.materials.len() as u32;
plan.materials.push(material.clone());
let surface_index = geometry.surface_ranges.len() as u32;
geometry.surface_ranges.push((start, count));
let material_index = geometry.materials.len() as u32;
geometry.materials.push(material.clone());
asset_surfaces.push((surface_index, material_index));
}
plan.packed_vertices = install_retained_packed(
geometry.packed_vertices = install_retained_packed(
packed_ok,
packed,
plan.vertices.len(),
geometry.vertices.len(),
XMODEL_PACKED_EMPTY_PLAN,
XMODEL_PACKED_UNAVAILABLE,
);
plan.assets.push(FxModelAssetDraw {
geometry.assets.push(FxModelAssetDraw {
model_index,
lod,
surfaces: asset_surfaces.clone(),
});
let topology = {
let mut revisions = render_frame::SourceRevisions::default();
revisions.set_topology_from(&plan.indices, &plan.surface_ranges, plan.vertices.len());
revisions.set_topology_from(
&geometry.indices,
&geometry.surface_ranges,
geometry.vertices.len(),
);
revisions.topology
};
let mut rev = plan.revision;
+33 -41
View File
@@ -1,6 +1,7 @@
use bevy::prelude::*;
use render_frame::{SmodelVertex, SourceRevisions};
use render_scene::{SmodelPassMaterial, XModelSurfaceDraw};
use std::sync::Arc;
pub const XMODEL_OBJECT_ID_FX_BASE: u16 = 0x700;
@@ -11,13 +12,25 @@ pub struct FxModelAssetDraw {
pub surfaces: Vec<(u32, u32)>,
}
#[derive(Resource, Clone, Debug, Default)]
pub struct FxModelDrawPlan {
#[derive(Clone, Debug, Default)]
pub(crate) struct FxModelGeometry {
pub(crate) vertices: Vec<SmodelVertex>,
pub(crate) indices: Vec<u32>,
pub(crate) surface_ranges: Vec<(u32, u32)>,
pub(crate) materials: Vec<SmodelPassMaterial>,
pub(crate) assets: Vec<FxModelAssetDraw>,
pub(crate) packed_vertices: assets::RetailPackedVertexPayload,
}
#[derive(Resource, Default)]
pub struct FxModelStaging(pub FxModelDrawPlan);
#[derive(Resource, Clone, Debug, Default)]
pub struct PreparedFxModelGeometry(pub FxModelDrawPlan);
#[derive(Resource, Clone, Debug, Default)]
pub struct FxModelDrawPlan {
pub(crate) geometry: Arc<FxModelGeometry>,
pub(crate) draws: Vec<XModelSurfaceDraw>,
pub generated: u32,
pub skipped_no_catalog: u32,
@@ -30,7 +43,6 @@ pub struct FxModelDrawPlan {
pub revision: u64,
pub generation: u64,
pub revisions: SourceRevisions,
pub(crate) packed_vertices: assets::RetailPackedVertexPayload,
}
impl FxModelDrawPlan {
@@ -38,42 +50,31 @@ impl FxModelDrawPlan {
&self.draws
}
/// Adds one row to a rebuild in progress. The revision does not move here:
/// it moves once, in `publish_rebuild`, and only if the finished rebuild
/// differs from what this plan already published.
pub fn push_draw(&mut self, draw: XModelSurfaceDraw) {
self.draws.push(draw);
}
pub fn materials(&self) -> &[SmodelPassMaterial] {
&self.materials
&self.geometry.materials
}
pub fn vertices(&self) -> &[SmodelVertex] {
&self.vertices
&self.geometry.vertices
}
pub fn indices(&self) -> &[u32] {
&self.indices
&self.geometry.indices
}
pub fn surface_ranges(&self) -> &[(u32, u32)] {
&self.surface_ranges
&self.geometry.surface_ranges
}
pub fn packed_vertices(&self) -> &assets::RetailPackedVertexPayload {
&self.packed_vertices
&self.geometry.packed_vertices
}
/// Empties a staging plan for this frame's rebuild, keeping its allocations
/// and its revisions. This is not a publication: the revision moves when the
/// rebuild that follows changes something, never because a frame passed.
pub fn clear(&mut self) {
self.vertices.clear();
self.indices.clear();
self.surface_ranges.clear();
self.materials.clear();
self.assets.clear();
self.draws.clear();
self.generated = 0;
self.skipped_no_catalog = 0;
@@ -83,35 +84,25 @@ impl FxModelDrawPlan {
self.skipped_no_material = 0;
self.skipped_no_lighting = 0;
self.skipped_render_fx_flags = 0;
match &mut self.packed_vertices {
assets::RetailPackedVertexPayload::Iw4(rows) => rows.clear(),
unavailable @ assets::RetailPackedVertexPayload::Unavailable { .. } => {
*unavailable = assets::RetailPackedVertexPayload::default();
}
}
}
/// Takes this frame's rebuild and reports what actually moved. Counters are
/// this frame's either way; the revisions move only when the rows do.
pub fn use_prepared(&mut self, prepared: &Self) {
self.geometry = Arc::clone(&prepared.geometry);
self.generation = prepared.generation;
}
pub fn publish_rebuild(&mut self, staged: &mut Self) {
let mut geometry = render_frame::publish_rows(&mut self.indices, &mut staged.indices);
geometry |=
render_frame::publish_rows(&mut self.surface_ranges, &mut staged.surface_ranges);
geometry |= render_frame::publish_rows(&mut self.materials, &mut staged.materials);
geometry |= render_frame::publish_rows(&mut self.assets, &mut staged.assets);
geometry |= self.vertices.len() != staged.vertices.len();
self.generation = staged.generation;
let geometry = !Arc::ptr_eq(&self.geometry, &staged.geometry);
if geometry {
std::mem::swap(&mut self.vertices, &mut staged.vertices);
self.packed_vertices =
std::mem::replace(&mut staged.packed_vertices, Default::default());
self.geometry = Arc::clone(&staged.geometry);
self.revisions.set_topology_from(
&self.indices,
&self.surface_ranges,
self.vertices.len(),
&self.geometry.indices,
&self.geometry.surface_ranges,
self.geometry.vertices.len(),
);
self.revisions.bump_packed_write(&mut self.revision);
}
staged.vertices.clear();
if render_frame::publish_rows(&mut self.draws, &mut staged.draws) || geometry {
self.revisions.bump_draws();
}
@@ -126,7 +117,8 @@ impl FxModelDrawPlan {
}
pub fn asset(&self, model_index: usize, lod: u8) -> Option<&FxModelAssetDraw> {
self.assets
self.geometry
.assets
.iter()
.find(|asset| asset.model_index == model_index && asset.lod == lod)
}
+3
View File
@@ -83,6 +83,9 @@ fn kill_fx_on_match_torn_down(
*entity_marks = EntityMarks::default();
commands.remove_resource::<PreparedFxCatalog>();
commands.remove_resource::<PreparedFxModels>();
commands.remove_resource::<crate::PreparedFxModelGeometry>();
commands.insert_resource(crate::FxModelDrawPlan::default());
commands.insert_resource(crate::FxModelStaging::default());
commands.remove_resource::<PreparedImpactFx>();
commands.remove_resource::<PreparedTracers>();
}
@@ -1,9 +1,10 @@
use crate::drawsurf::scene_depth::{SCENE_DEPTH_FORMAT, SceneDepthTexture};
use std::collections::{BTreeMap, BTreeSet};
use std::num::NonZeroU64;
use std::sync::Arc;
use std::time::Instant;
use bevy::core_pipeline::core_3d::{CORE_3D_DEPTH_FORMAT, main_opaque_pass_3d};
use bevy::core_pipeline::core_3d::main_opaque_pass_3d;
use bevy::core_pipeline::upscaling::ViewUpscalingPipeline;
use bevy::core_pipeline::{Core3d, Core3dSystems};
use bevy::mesh::VertexBufferLayout;
@@ -25,7 +26,7 @@ use bevy::render::render_resource::{
VertexStepMode,
};
use bevy::render::renderer::{RenderContext, RenderDevice, RenderQueue, ViewQuery};
use bevy::render::view::{ExtractedView, Msaa, ViewDepthTexture, ViewTarget};
use bevy::render::view::{ExtractedView, Msaa, ViewTarget};
use bevy::render::{Render, RenderSystems};
use super::ExtractedRenderFrameProducts;
@@ -587,7 +588,7 @@ fn exact_pipeline_plan(
format: key.depth_format,
depth_write_enabled: Some(key.state1.depth_write),
depth_compare,
stencil: Default::default(),
stencil: super::scene_depth::stencil_state(key.state1.stencil),
bias: if key.forward_z {
polygon_offset_bias_forward_z(key.state1.polyoffset_level)
} else {
@@ -3202,7 +3203,7 @@ fn submit_exact_draws<'a>(
encoder: &mut CommandEncoder,
device: &RenderDevice,
target: &ViewTarget,
depth: &ViewDepthTexture,
depth: &SceneDepthTexture,
extracted_view: &ExtractedView,
geometry: &ExactColourGeometry,
smodel_cache_gpu: &SmodelCacheGpu,
@@ -3309,7 +3310,7 @@ fn submit_exact_draw_run<'a>(
encoder: &mut CommandEncoder,
device: &RenderDevice,
attachment: RenderPassColorAttachment<'_>,
depth: &ViewDepthTexture,
depth: &SceneDepthTexture,
extracted_view: &ExtractedView,
geometry: &ExactColourGeometry,
smodel_cache_gpu: &SmodelCacheGpu,
@@ -4329,7 +4330,14 @@ fn record_shadowmap_draws<'a>(
},
store: StoreOp::Store,
}),
stencil_ops: None,
stencil_ops: Some(Operations {
load: if cleared {
LoadOp::Clear(0)
} else {
LoadOp::Load
},
store: StoreOp::Store,
}),
}),
timestamp_writes: None,
occlusion_query_set: None,
@@ -4460,7 +4468,10 @@ fn clear_empty_shadowmap_sun_partition_zero(
load: LoadOp::Clear(super::backend::SHADOWMAP_CLEAR_Z),
store: StoreOp::Store,
}),
stencil_ops: None,
stencil_ops: Some(Operations {
load: LoadOp::Clear(0),
store: StoreOp::Store,
}),
}),
timestamp_writes: None,
occlusion_query_set: None,
@@ -6870,6 +6881,7 @@ pub(super) fn register(app: &mut App) {
prepare_camera::install_shared_colour_pass
.in_set(RenderSystems::Prepare)
.after(geometry::upload_exact_geometry)
.after(super::scene_depth::prepare_scene_depth)
.after(pipeline::kick_extracted_colour_pipelines)
.after(super::gpu_resources::prepare_uploaded_image_registry),
prepare_camera::prepare_colour_lanes
@@ -256,7 +256,7 @@ fn schedule_admitted_pipelines(
depth_format: if shadow {
SHADOWMAP_SUN_DEPTH_FORMAT
} else {
CORE_3D_DEPTH_FORMAT
SCENE_DEPTH_FORMAT
},
samples: if shadow { 1 } else { samples },
state0,
@@ -15,7 +15,7 @@ pub(super) fn prepare_colour_lanes(
views: Query<
(
&ViewTarget,
&ViewDepthTexture,
&SceneDepthTexture,
&ExtractedView,
Option<&Msaa>,
),
@@ -408,7 +408,7 @@ pub(super) fn prepare_camera_colour(lane: CameraLane<'_>) {
let prepare_target = ExactPrepareTarget {
color: target.main_texture_format(),
samples,
depth: CORE_3D_DEPTH_FORMAT,
depth: SCENE_DEPTH_FORMAT,
forward_z: false,
use_world_pretess: true,
};
@@ -902,7 +902,7 @@ pub(super) fn install_shared_colour_pass(
views: Query<
(
&ViewTarget,
&ViewDepthTexture,
&SceneDepthTexture,
&ExtractedView,
Option<&Msaa>,
),
@@ -1010,7 +1010,7 @@ pub(super) fn install_shared_colour_pass(
type CameraTargetView<'a> = (
&'a ViewTarget,
&'a ViewDepthTexture,
&'a SceneDepthTexture,
&'a ExtractedView,
Option<&'a Msaa>,
);

Some files were not shown because too many files have changed in this diff Show More