mirror of
https://github.com/vladtrc/iw4L
synced 2026-10-25 18:05:19 -04:00
Prepare weapon presentation and add alternate modes, device feedback and collision fixes
This commit is contained in:
Generated
+105
@@ -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"
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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]),
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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(¬e.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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 => {}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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] {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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))?,
|
||||
|
||||
@@ -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>,
|
||||
|
||||
|
||||
@@ -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()?,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()?,
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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(®istry, id, right_edges, xanims)
|
||||
} else {
|
||||
WeaponAnimations::from_registry(®istry, 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(®istry, 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)));
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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(®istry.0, weapon))
|
||||
.unwrap_or_default(),
|
||||
.unwrap_or_else(Vec::new),
|
||||
source: KitSource::World(gun_index),
|
||||
});
|
||||
for attachment in world_attachments(®istry.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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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(®.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(
|
||||
®.0,
|
||||
request.weapon_id,
|
||||
reg.0.sz_xanim_right_edges_of(request.weapon_id),
|
||||
&cat.0,
|
||||
)
|
||||
} else {
|
||||
WeaponAnimations::from_registry(®.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(
|
||||
®.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(®.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(®.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>()
|
||||
|
||||
@@ -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 })
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>>,
|
||||
|
||||
@@ -31,3 +31,5 @@ pub use tracer::{
|
||||
PreparedTracers, QueuedBeam, TracerDrawGate, TracerSpawnSkip, TracerWorld, tick_tracer_beams,
|
||||
try_spawn_tracer,
|
||||
};
|
||||
|
||||
pub use model_draw::{FxModelStaging, PreparedFxModelGeometry};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user