mirror of
https://github.com/vladtrc/iw4L
synced 2026-10-30 03:29:55 -04:00
Run IW4 match flow, menus and killstreaks from the game scripts
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Generated
+71
-9
@@ -331,6 +331,8 @@ dependencies = [
|
||||
"fastfile_iw5",
|
||||
"fastfile_t5",
|
||||
"fx_iw4",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"weapon_iw4",
|
||||
"xmodel_runtime",
|
||||
]
|
||||
@@ -563,10 +565,15 @@ dependencies = [
|
||||
name = "audio"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"asset_audio",
|
||||
"asset_core",
|
||||
"asset_game",
|
||||
"asset_iw4",
|
||||
"asset_transport",
|
||||
"assets",
|
||||
"bevy",
|
||||
"diag",
|
||||
"entity_iw4",
|
||||
"frame",
|
||||
"gamemode_iw4",
|
||||
"hud_iw4",
|
||||
@@ -576,7 +583,6 @@ dependencies = [
|
||||
"perf",
|
||||
"playerstate_iw4",
|
||||
"sim",
|
||||
"sound_iw4",
|
||||
"symphonia",
|
||||
"weapon_iw4",
|
||||
]
|
||||
@@ -1933,18 +1939,26 @@ dependencies = [
|
||||
name = "bootstrap"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"asset_audio",
|
||||
"asset_core",
|
||||
"asset_game",
|
||||
"asset_material",
|
||||
"asset_transport",
|
||||
"assets",
|
||||
"audio",
|
||||
"bevy",
|
||||
"bots",
|
||||
"console",
|
||||
"diag",
|
||||
"frame",
|
||||
"hud",
|
||||
"libc",
|
||||
"master_protocol",
|
||||
"net",
|
||||
"perf",
|
||||
"render",
|
||||
"render_frontend",
|
||||
"render_gpu",
|
||||
"replay",
|
||||
"serde_json",
|
||||
"session",
|
||||
@@ -1957,6 +1971,7 @@ dependencies = [
|
||||
name = "bots"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"asset_transport",
|
||||
"assets",
|
||||
"bevy",
|
||||
"diag",
|
||||
@@ -1969,6 +1984,7 @@ dependencies = [
|
||||
"net",
|
||||
"playerstate_iw4",
|
||||
"sim",
|
||||
"trace_iw4",
|
||||
"weapon_iw4",
|
||||
]
|
||||
|
||||
@@ -2160,6 +2176,10 @@ dependencies = [
|
||||
name = "console"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"asset_core",
|
||||
"asset_game",
|
||||
"asset_transport",
|
||||
"asset_world",
|
||||
"assets",
|
||||
"audio",
|
||||
"bevy",
|
||||
@@ -3221,6 +3241,10 @@ checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df"
|
||||
name = "hud"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"asset_core",
|
||||
"asset_game",
|
||||
"asset_material",
|
||||
"asset_transport",
|
||||
"assets",
|
||||
"bevy",
|
||||
"diag",
|
||||
@@ -3587,6 +3611,7 @@ version = "0.1.0"
|
||||
name = "launcher"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"asset_transport",
|
||||
"assets",
|
||||
"bootstrap",
|
||||
"diag",
|
||||
@@ -3914,6 +3939,7 @@ dependencies = [
|
||||
name = "net"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anim_iw4",
|
||||
"bevy",
|
||||
"diag",
|
||||
"entity_iw4",
|
||||
@@ -3936,6 +3962,7 @@ dependencies = [
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"weapon_iw4",
|
||||
"xmodel_runtime",
|
||||
"zstd",
|
||||
]
|
||||
|
||||
@@ -5021,7 +5048,14 @@ name = "render_anim"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anim_iw4",
|
||||
"asset_anim",
|
||||
"asset_core",
|
||||
"asset_game",
|
||||
"asset_iw4",
|
||||
"asset_material",
|
||||
"asset_model",
|
||||
"asset_transport",
|
||||
"asset_world",
|
||||
"assets",
|
||||
"audio",
|
||||
"bevy",
|
||||
@@ -5079,7 +5113,13 @@ name = "render_frontend"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anim_iw4",
|
||||
"asset_audio",
|
||||
"asset_core",
|
||||
"asset_game",
|
||||
"asset_iw4",
|
||||
"asset_material",
|
||||
"asset_model",
|
||||
"asset_transport",
|
||||
"asset_world",
|
||||
"assets",
|
||||
"audio",
|
||||
@@ -5111,13 +5151,19 @@ dependencies = [
|
||||
"render_scene",
|
||||
"sim",
|
||||
"weapon_iw4",
|
||||
"xmodel_runtime",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "render_fx"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"asset_audio",
|
||||
"asset_core",
|
||||
"asset_game",
|
||||
"asset_iw4",
|
||||
"asset_model",
|
||||
"asset_world",
|
||||
"assets",
|
||||
"audio",
|
||||
"bevy",
|
||||
@@ -5141,7 +5187,10 @@ dependencies = [
|
||||
name = "render_gpu"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"asset_core",
|
||||
"asset_iw4",
|
||||
"asset_material",
|
||||
"asset_world",
|
||||
"assets",
|
||||
"bevy",
|
||||
"bytemuck",
|
||||
@@ -5178,6 +5227,10 @@ dependencies = [
|
||||
name = "render_scene"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"asset_game",
|
||||
"asset_material",
|
||||
"asset_model",
|
||||
"asset_world",
|
||||
"assets",
|
||||
"bevy",
|
||||
"diag",
|
||||
@@ -5188,6 +5241,7 @@ dependencies = [
|
||||
"math_iw4",
|
||||
"render_material",
|
||||
"sim",
|
||||
"xmodel_runtime",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5513,19 +5567,30 @@ dependencies = [
|
||||
name = "session"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"asset_anim",
|
||||
"asset_audio",
|
||||
"asset_core",
|
||||
"asset_game",
|
||||
"asset_model",
|
||||
"asset_transport",
|
||||
"asset_world",
|
||||
"assets",
|
||||
"audio",
|
||||
"bevy",
|
||||
"bots",
|
||||
"clipmap_iw4",
|
||||
"diag",
|
||||
"frame",
|
||||
"gamemode_iw4",
|
||||
"hud_iw4",
|
||||
"net",
|
||||
"perf",
|
||||
"render",
|
||||
"render_frontend",
|
||||
"render_fx",
|
||||
"sim",
|
||||
"weapon_iw4",
|
||||
"xmodel_runtime",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5571,7 +5636,7 @@ dependencies = [
|
||||
"movement_iw4",
|
||||
"perf",
|
||||
"playerstate_iw4",
|
||||
"sound_iw4",
|
||||
"sha2",
|
||||
"trace_iw4",
|
||||
"weapon_iw4",
|
||||
"xmodel_runtime",
|
||||
@@ -5684,13 +5749,6 @@ dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sound_iw4"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"killcam_iw4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "spin"
|
||||
version = "0.10.1"
|
||||
@@ -6232,6 +6290,10 @@ checksum = "214ca0b2191785cbc06209b9ca1861e048e39b5ba33574b3cedd58363d5bb5f6"
|
||||
name = "ui"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"asset_core",
|
||||
"asset_game",
|
||||
"asset_material",
|
||||
"asset_transport",
|
||||
"assets",
|
||||
"bevy",
|
||||
"diag",
|
||||
|
||||
@@ -40,7 +40,6 @@ members = [
|
||||
"crates/hud",
|
||||
"crates/hud_iw4",
|
||||
"crates/killcam_iw4",
|
||||
"crates/sound_iw4",
|
||||
"crates/input_iw4",
|
||||
"crates/launcher",
|
||||
"crates/lighting_iw4",
|
||||
@@ -141,7 +140,6 @@ gamemode_iw4 = { path = "crates/gamemode_iw4" }
|
||||
hud = { path = "crates/hud" }
|
||||
hud_iw4 = { path = "crates/hud_iw4" }
|
||||
killcam_iw4 = { path = "crates/killcam_iw4" }
|
||||
sound_iw4 = { path = "crates/sound_iw4" }
|
||||
input_iw4 = { path = "crates/input_iw4" }
|
||||
launcher = { path = "crates/launcher" }
|
||||
lighting_iw4 = { path = "crates/lighting_iw4" }
|
||||
|
||||
@@ -12,7 +12,7 @@ ARGS := $(wordlist 2,$(words $(MAKECMDGOALS)),$(MAKECMDGOALS))
|
||||
|
||||
.PHONY: map export-gltf play bench bench-load-session bench-live bench-overhead bench-perf menu menu-shots scenario chaos lifecycle-all lifecycle-swap lifecycle-replace lifecycle-play-in lifecycle-demo-out lifecycle-demo-map launcher deploy logs loc clean help
|
||||
.PHONY: build-windows setup-windows release publish provision
|
||||
.PHONY: mr publish-check approved
|
||||
.PHONY: mr publish-check approved duo
|
||||
.PHONY: $(ARGS)
|
||||
|
||||
$(ARGS):
|
||||
@@ -218,6 +218,15 @@ lifecycle-demo-map: require-games
|
||||
cd $(ROOT) && IW4L_PERF=1 $(CARGO) run $(PROFILE_ARG) -p launcher -- map mp_boneyard --cmds '$(LIFECYCLE_DEMO_MAP_CMDS)'
|
||||
cd $(ROOT) && $(CARGO) run -p xtask -- live demo-map
|
||||
|
||||
HOST_CMDS ?= spawn assault
|
||||
CLIENT_CMDS ?= spawn assault
|
||||
MODE ?= dm
|
||||
export HOST_CMDS CLIENT_CMDS MODE ZONE PROFILE
|
||||
|
||||
duo: require-games
|
||||
cd $(ROOT) && $(CARGO) build $(PROFILE_ARG) -p launcher
|
||||
cd $(ROOT) && $(CARGO) run --quiet -p xtask -- duo
|
||||
|
||||
menu: require-games
|
||||
cd $(ROOT) && $(CARGO) run $(PROFILE_ARG) -p launcher -- menu $(CMDS_ARG)
|
||||
|
||||
@@ -345,6 +354,7 @@ help:
|
||||
@echo "make lifecycle-demo-out demo → disconnect → menu"
|
||||
@echo "make lifecycle-demo-map demo → map mp_rust"
|
||||
@echo "make menu run the main-menu shell (Maps / Settings / Quit)"
|
||||
@echo "make duo two windows through the master; HOST_CMDS / CLIENT_CMDS, ZONE / MODE"
|
||||
@echo " add CMDS='wait 2s; quit' to script it"
|
||||
@echo "make menu-shots 2D UI pack under iw4l-artifacts/menu-shots (no map)"
|
||||
@echo "make launcher windows build password-protected dev + prod portable ZIPs"
|
||||
|
||||
@@ -2,7 +2,7 @@ pub const DOBJ_RADIUS_PARENT_ROOT: u8 = 0xff;
|
||||
|
||||
pub const DOBJ_COMPUTE_BOUNDS_MODEL_LIMIT: usize = 32;
|
||||
|
||||
pub fn dobj_compute_bounds_radius(xmodel_radii: &[f32], parents: &[u8]) -> f32 {
|
||||
pub fn compute_bounds_radius(xmodel_radii: &[f32], parents: &[u8]) -> f32 {
|
||||
let n = xmodel_radii
|
||||
.len()
|
||||
.min(parents.len())
|
||||
|
||||
@@ -12,7 +12,7 @@ pub const XANIM_CLIENT_ANIM_BLEND_FLOOR_OLD_DURATION_MS: i32 = 0xfa;
|
||||
|
||||
pub const XANIM_LEGS_PARENT_WEIGHT_WHEN_TORSO: f32 = 0.01;
|
||||
|
||||
pub fn xanim_sanitize_goal_weight(goal_weight: f32, notify_a: i32, notify_b: i32) -> f32 {
|
||||
pub fn sanitize_goal_weight(goal_weight: f32, notify_a: i32, notify_b: i32) -> f32 {
|
||||
if goal_weight < XANIM_GOAL_WEIGHT_SNAP_EPS {
|
||||
if notify_a == 0 && notify_b == 0 {
|
||||
0.0
|
||||
@@ -24,11 +24,11 @@ pub fn xanim_sanitize_goal_weight(goal_weight: f32, notify_a: i32, notify_b: i32
|
||||
}
|
||||
}
|
||||
|
||||
pub fn xanim_goal_time_from_blend_ms(blend_ms: i32) -> f32 {
|
||||
pub fn goal_time_from_blend_ms(blend_ms: i32) -> f32 {
|
||||
blend_ms as f32 * XANIM_WEIGHT_FLOOR_SCALE
|
||||
}
|
||||
|
||||
pub fn xanim_client_anim_blend_ms(
|
||||
pub fn client_anim_blend_ms(
|
||||
new_index: u16,
|
||||
authored_blend_ms: i32,
|
||||
old_xanim_exists: bool,
|
||||
@@ -85,14 +85,14 @@ pub const XANIM_CLIENT_ANIM_RATE_LERP_MAX_SPEED: f32 = 150.0;
|
||||
|
||||
pub const XANIM_CLIENT_ANIM_RATE_LERP_SPAN: f32 = 130.0;
|
||||
|
||||
pub fn xanim_vec3_distance(a: [f32; 3], b: [f32; 3]) -> f32 {
|
||||
pub fn vec3_distance(a: [f32; 3], b: [f32; 3]) -> f32 {
|
||||
let dx = b[0] - a[0];
|
||||
let dy = b[1] - a[1];
|
||||
let dz = b[2] - a[2];
|
||||
libm::sqrtf(dx * dx + dy * dy + dz * dz)
|
||||
}
|
||||
|
||||
pub fn xanim_client_anim_playback_rate(
|
||||
pub fn client_anim_playback_rate(
|
||||
origin: [f32; 3],
|
||||
last_origin: [f32; 3],
|
||||
now_ms: i32,
|
||||
@@ -110,7 +110,7 @@ pub fn xanim_client_anim_playback_rate(
|
||||
let dist = if ladder {
|
||||
libm::fabsf(origin[2] - last_origin[2])
|
||||
} else {
|
||||
xanim_vec3_distance(origin, last_origin)
|
||||
vec3_distance(origin, last_origin)
|
||||
};
|
||||
let rate = dist / dt / move_speed;
|
||||
if rate < XANIM_CLIENT_ANIM_RATE_MIN {
|
||||
|
||||
@@ -40,7 +40,7 @@ pub fn or_shift_part_bits(dst: &mut [u32; 6], src: [u32; 6], bone_base: u32) {
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn dobj_surface_hidden(part_bits: &[u32; 6], hide: &[u32; 6], bone_base: u32) -> bool {
|
||||
pub fn surface_hidden(part_bits: &[u32; 6], hide: &[u32; 6], bone_base: u32) -> bool {
|
||||
let mut lifted = [0u32; 6];
|
||||
or_shift_part_bits(&mut lifted, *part_bits, bone_base);
|
||||
lifted.iter().zip(hide.iter()).any(|(a, b)| a & b != 0)
|
||||
|
||||
+11
-11
@@ -20,7 +20,7 @@ pub use channel::{full_quat, half_quat, quantized_trans, quat16};
|
||||
pub use compose::{Local, compose_rotation, compose_translation};
|
||||
pub use dobj::DObj;
|
||||
pub use dobj_bounds::{
|
||||
DOBJ_COMPUTE_BOUNDS_MODEL_LIMIT, DOBJ_RADIUS_PARENT_ROOT, dobj_compute_bounds_radius,
|
||||
DOBJ_COMPUTE_BOUNDS_MODEL_LIMIT, DOBJ_RADIUS_PARENT_ROOT, compute_bounds_radius,
|
||||
};
|
||||
pub use goal_weight::{
|
||||
XANIM_CLIENT_ANIM_BLEND_FLOOR_NEW_MS, XANIM_CLIENT_ANIM_BLEND_FLOOR_NO_DURATION_MS,
|
||||
@@ -29,11 +29,10 @@ pub use goal_weight::{
|
||||
XANIM_CLIENT_ANIM_RATE_LERP_MAX_SPEED, XANIM_CLIENT_ANIM_RATE_LERP_MIN_SPEED,
|
||||
XANIM_CLIENT_ANIM_RATE_LERP_SPAN, XANIM_CLIENT_ANIM_RATE_MIN, XANIM_CLIENT_ANIM_RATE_SHORT_CAP,
|
||||
XANIM_CLIENT_ANIM_RATE_ZERO_EPS, XANIM_GOAL_WEIGHT_SNAP_EPS,
|
||||
XANIM_LEGS_PARENT_WEIGHT_WHEN_TORSO, xanim_client_anim_blend_ms,
|
||||
xanim_client_anim_playback_rate, xanim_goal_time_from_blend_ms, xanim_sanitize_goal_weight,
|
||||
xanim_vec3_distance,
|
||||
XANIM_LEGS_PARENT_WEIGHT_WHEN_TORSO, client_anim_blend_ms, client_anim_playback_rate,
|
||||
goal_time_from_blend_ms, sanitize_goal_weight, vec3_distance,
|
||||
};
|
||||
pub use hide::{dobj_surface_hidden, hide_part_bit, or_shift_part_bits, set_hide_part_bit};
|
||||
pub use hide::{hide_part_bit, or_shift_part_bits, set_hide_part_bit, surface_hidden};
|
||||
pub use part_bits::{PartBits, xmodel_no_scale_bit};
|
||||
pub use player_anim::{
|
||||
ANIM_BODY_PART_NAMES, ANIM_COND_AKIMBO, ANIM_COND_CROUCHING, ANIM_COND_DAMAGETYPE,
|
||||
@@ -41,7 +40,8 @@ pub use player_anim::{
|
||||
ANIM_COND_MOUNTED, ANIM_COND_MOVETYPE, ANIM_COND_NAMES, ANIM_COND_PERK,
|
||||
ANIM_COND_PLAYERANIMTYPE, ANIM_COND_PLAYERANIMTYPEPRIMARY, ANIM_COND_STRAFING,
|
||||
ANIM_COND_WEAPON_POSITION, ANIM_COND_WEAPONCLASS, ANIM_DAMAGE_EXPLOSION_NEAR_DIST_SQ,
|
||||
ANIM_DAMAGETYPE_NAMES, ANIM_ET_DEATH, ANIM_ET_FIREWEAPON, ANIM_ET_NAMES, ANIM_ET_RELOAD,
|
||||
ANIM_DAMAGETYPE_NAMES, ANIM_ET_DEATH, ANIM_ET_FIREWEAPON, ANIM_ET_KNIFE_MELEE, ANIM_ET_KNIFE_MELEE_CHARGE,
|
||||
ANIM_ET_MELEEATTACK, ANIM_ET_NAMES, ANIM_ET_RELOAD,
|
||||
ANIM_HITDIR_FRONT_BACK_DOT_SQ, ANIM_HITDIRECTION_NAMES, ANIM_HITLOCATION_NAMES, ANIM_MT_IDLE,
|
||||
ANIM_MT_IDLECR, ANIM_MT_IDLELASTSTAND, ANIM_MT_IDLEPRONE, ANIM_MT_NAMES, ANIM_PARSE_MODES,
|
||||
ANIM_PERK_NAMES, ANIM_PLAYERANIMTYPE_NAMES, ANIM_STATE_NAMES, ANIM_STRAFING_NAMES,
|
||||
@@ -49,17 +49,17 @@ pub use player_anim::{
|
||||
ANIMFLAG_LOOPSYNC, ANIMFLAG_NONLOOPSYNC, ANIMTREE_PROPERTY_NAMES, PLAYER_ANIM_INDEX_MASK,
|
||||
PLAYER_ANIM_RAW_MASK, PLAYER_ANIM_RESTART_TOGGLE, PlayerAnimValue, anim_cond_evaluable,
|
||||
anim_cond_null_value_defaults_to_one, anim_cond_value_names, anim_script_hit_direction,
|
||||
anim_script_hit_location, anim_weapon_position_from_pm_flags, bg_random,
|
||||
anim_script_hit_location, anim_weapon_position_from_pm_flags, random,
|
||||
};
|
||||
pub use quat::{
|
||||
QUAT_IDENTITY, Quat, VEC3_ZERO, Vec3, normalize, quat_add_weighted, quat_dot, quat_mul,
|
||||
quat_neg, vec3_add_scaled, xanim_apply_additive,
|
||||
QUAT_IDENTITY, Quat, VEC3_ZERO, Vec3, apply_additive, normalize, quat_add_weighted, quat_dot,
|
||||
quat_mul, quat_neg, vec3_add_scaled,
|
||||
};
|
||||
pub use sample::{FrameKind, sample_quat, sample_vec3, span, time_to_frame};
|
||||
pub use skel::{DuplicatePart, Skel};
|
||||
pub use xanim::XAnimParts;
|
||||
pub use xanim_time::{
|
||||
XANIM_NONLOOP_END_PARK, XANIM_WEIGHT_FLOOR, XANIM_WEIGHT_FLOOR_SCALE,
|
||||
xanim_advance_goal_weight, xanim_advance_leaf_time,
|
||||
XANIM_NONLOOP_END_PARK, XANIM_WEIGHT_FLOOR, XANIM_WEIGHT_FLOOR_SCALE, advance_goal_weight,
|
||||
advance_leaf_time,
|
||||
};
|
||||
pub use xbone::XBoneInfo;
|
||||
|
||||
@@ -142,6 +142,9 @@ pub const ANIM_ET_NAMES: &[&str] = &[
|
||||
pub const ANIM_ET_DEATH: u8 = 1;
|
||||
pub const ANIM_ET_FIREWEAPON: u8 = 2;
|
||||
pub const ANIM_ET_RELOAD: u8 = 10;
|
||||
pub const ANIM_ET_MELEEATTACK: u8 = 17;
|
||||
pub const ANIM_ET_KNIFE_MELEE: u8 = 18;
|
||||
pub const ANIM_ET_KNIFE_MELEE_CHARGE: u8 = 19;
|
||||
|
||||
pub const ANIM_COND_NAMES: &[&str] = &[
|
||||
"PLAYERANIMTYPE",
|
||||
@@ -283,7 +286,7 @@ pub fn anim_cond_value_names(index: u8) -> &'static [&'static str] {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn bg_random(seed: &mut u32) -> u32 {
|
||||
pub fn random(seed: &mut u32) -> u32 {
|
||||
let next = seed.wrapping_mul(0x343fd).wrapping_add(0x269ec3);
|
||||
*seed = next;
|
||||
next >> 17
|
||||
|
||||
@@ -64,7 +64,7 @@ pub fn quat_mul(a: Quat, b: Quat) -> Quat {
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn xanim_apply_additive(
|
||||
pub fn apply_additive(
|
||||
dest_rot: Quat,
|
||||
dest_trans: Vec3,
|
||||
add_rot: Quat,
|
||||
|
||||
@@ -4,7 +4,7 @@ pub const XANIM_WEIGHT_FLOOR: f32 = f32::from_bits(0x358637be);
|
||||
|
||||
pub const XANIM_WEIGHT_FLOOR_SCALE: f32 = 0.001;
|
||||
|
||||
pub fn xanim_advance_leaf_time(
|
||||
pub fn advance_leaf_time(
|
||||
old_time: f32,
|
||||
cycle_count: i16,
|
||||
rate: f32,
|
||||
@@ -33,7 +33,7 @@ pub fn xanim_advance_leaf_time(
|
||||
(time, cycle)
|
||||
}
|
||||
|
||||
pub fn xanim_advance_goal_weight(
|
||||
pub fn advance_goal_weight(
|
||||
weight: f32,
|
||||
goal_weight: f32,
|
||||
goal_time: f32,
|
||||
|
||||
@@ -42,7 +42,9 @@ The runner builds nothing; `make approved` builds `target/play/iw4l` first.
|
||||
Each run owns one directory, `iw4l-artifacts/approved-tests/<run-id>/`. The
|
||||
game is started with that directory as its working directory, so everything
|
||||
IW4L writes (`iw4l-artifacts/cache`, logs, perf run, dumps, screenshots) lands
|
||||
under it. With `--cache cold` that cache starts empty; with `--cache shared`
|
||||
under it. On Windows the launcher enters the folder holding its executable, so
|
||||
the runner hard-links (or copies) the binary into the run directory and starts
|
||||
that one. With `--cache cold` that cache starts empty; with `--cache shared`
|
||||
it is a symlink to the repository's cache.
|
||||
|
||||
A seed resolves to maps and spawn picks once. `run.json` keeps the resolved
|
||||
@@ -54,7 +56,8 @@ selection cannot move a replayed scene.
|
||||
|
||||
- `run.json` — build, platform, render, content, maps, seed, the resolved
|
||||
scenes and the exact command script, cache mode, bot target and actual,
|
||||
lifecycle timestamps, phases with start/end or a failure reason, liveness
|
||||
lifecycle timestamps, phases with start/end or a failure reason, the GSC
|
||||
program each map installed (or the refusal that stopped it), liveness
|
||||
assertions, and paths to the large artifacts.
|
||||
- `00_cold_load.png`, `01_overgrown_gameplay.{png,dump.txt}`,
|
||||
`02_after_disconnect.{png,dump.txt}`, `03_second_map_gameplay.{png,dump.txt}`
|
||||
@@ -62,10 +65,21 @@ selection cannot move a replayed scene.
|
||||
- `child_stdout.txt`, `child_stderr.txt`, and the game's own
|
||||
`iw4l-artifacts/logs/latest.log` and `iw4l-artifacts/runs/<id>/` perf run.
|
||||
|
||||
Assertions are liveness only: the map loaded, the players exist, simulation
|
||||
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.
|
||||
Assertions are liveness only: the map loaded, its GSC program installed, the
|
||||
players exist, simulation 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, no script fault ended the
|
||||
process, and `quit` ended the process on its own.
|
||||
|
||||
Every match runs a GSC program, so the game announces it on stdout the way it
|
||||
announces lifecycle boundaries: `gsc: installed map=… gametype=… fingerprint=…
|
||||
modules=… functions=… natives=… entries=…` when the world is published, or
|
||||
`gsc: refused map=… stage=compile|install|entry fault="…"` when the match load
|
||||
stopped at the script. `run.json` keeps both under `gsc.a` / `gsc.b` and asserts
|
||||
`a.gsc_installed`, `b.gsc_installed` and `gsc.no_execution_fault`. A phase that
|
||||
fails after a refusal carries the refusal as its `failure_reason`, and a script
|
||||
fault that ends the process (`GSC execution failed: …` in `child_stderr.txt`)
|
||||
becomes the run's `failure`.
|
||||
|
||||
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
|
||||
|
||||
@@ -297,7 +297,8 @@ fn run(root: &Path, args: &Args) -> Result<bool, String> {
|
||||
|
||||
let cache = prepare_cache(root, &run_dir, args.cache)?;
|
||||
manifest["cache"] = cache;
|
||||
manifest["command_line"] = json!([args.bin.display().to_string(), child_args]);
|
||||
let bin = stage_binary(&args.bin, &run_dir)?;
|
||||
manifest["command_line"] = json!([bin.display().to_string(), child_args]);
|
||||
manifest["script"] = script.into();
|
||||
manifest["phases_declared"] = phases
|
||||
.iter()
|
||||
@@ -314,7 +315,7 @@ fn run(root: &Path, args: &Args) -> Result<bool, String> {
|
||||
println!("run: {}", run_dir.display());
|
||||
|
||||
let outcome = runner::run(runner::Launch {
|
||||
bin: &args.bin,
|
||||
bin: &bin,
|
||||
cwd: &run_dir,
|
||||
args: child_args,
|
||||
env: vec![
|
||||
@@ -335,6 +336,23 @@ fn run(root: &Path, args: &Args) -> Result<bool, String> {
|
||||
Ok(passed)
|
||||
}
|
||||
|
||||
/// On Windows the launcher enters the folder holding its executable, so the binary must sit in the run directory.
|
||||
fn stage_binary(bin: &Path, run_dir: &Path) -> Result<PathBuf, String> {
|
||||
if !cfg!(windows) {
|
||||
return Ok(bin.to_path_buf());
|
||||
}
|
||||
let exe = bin.with_extension("exe");
|
||||
let bin = if bin.exists() { bin } else { exe.as_path() };
|
||||
let name = bin
|
||||
.file_name()
|
||||
.ok_or_else(|| format!("--bin {} names no file", bin.display()))?;
|
||||
let staged = run_dir.join(name);
|
||||
std::fs::hard_link(bin, &staged)
|
||||
.or_else(|_| std::fs::copy(bin, &staged).map(drop))
|
||||
.map_err(|e| format!("stage {} into {}: {e}", bin.display(), run_dir.display()))?;
|
||||
Ok(staged)
|
||||
}
|
||||
|
||||
fn prepare_cache(root: &Path, run_dir: &Path, mode: CacheMode) -> Result<Value, String> {
|
||||
let child_artifacts = run_dir.join("iw4l-artifacts");
|
||||
let child_cache = child_artifacts.join("cache");
|
||||
@@ -359,8 +377,11 @@ fn prepare_cache(root: &Path, run_dir: &Path, mode: CacheMode) -> Result<Value,
|
||||
let shared = root.join("iw4l-artifacts/cache");
|
||||
std::fs::create_dir_all(&child_artifacts)
|
||||
.map_err(|e| format!("create {}: {e}", child_artifacts.display()))?;
|
||||
std::os::unix::fs::symlink(&shared, &child_cache)
|
||||
.map_err(|e| format!("link {}: {e}", child_cache.display()))?;
|
||||
#[cfg(unix)]
|
||||
let linked = std::os::unix::fs::symlink(&shared, &child_cache);
|
||||
#[cfg(windows)]
|
||||
let linked = std::os::windows::fs::symlink_dir(&shared, &child_cache);
|
||||
linked.map_err(|e| format!("link {}: {e}", child_cache.display()))?;
|
||||
Ok(json!({
|
||||
"mode": "shared",
|
||||
"cold_load": false,
|
||||
|
||||
@@ -229,6 +229,109 @@ fn facts(event: &Event) -> Value {
|
||||
})
|
||||
}
|
||||
|
||||
fn event_json(event: &Event) -> Value {
|
||||
let mut v = json!({ "ns": event.ns(), "controller_ms": event.at_ms });
|
||||
for (k, val) in &event.fields {
|
||||
if k != "ns" && k != "pid" {
|
||||
v[k] = val.clone().into();
|
||||
}
|
||||
}
|
||||
v
|
||||
}
|
||||
|
||||
struct ScriptMap {
|
||||
map: String,
|
||||
installed: Option<Event>,
|
||||
refused: Option<Event>,
|
||||
}
|
||||
|
||||
impl ScriptMap {
|
||||
fn gather(events: &[Event], map: &str) -> Self {
|
||||
let named = |name: &str| {
|
||||
events
|
||||
.iter()
|
||||
.find(|e| e.name == name && e.get("map") == Some(map))
|
||||
.cloned()
|
||||
};
|
||||
Self {
|
||||
map: map.to_owned(),
|
||||
installed: named("installed"),
|
||||
refused: named("refused"),
|
||||
}
|
||||
}
|
||||
|
||||
fn to_json(&self) -> Value {
|
||||
json!({
|
||||
"map": self.map,
|
||||
"installed": self.installed.as_ref().map(event_json),
|
||||
"refused": self.refused.as_ref().map(event_json),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
struct ScriptEvidence {
|
||||
a: ScriptMap,
|
||||
b: ScriptMap,
|
||||
execution_fault: Option<String>,
|
||||
}
|
||||
|
||||
impl ScriptEvidence {
|
||||
fn gather(run_dir: &Path, events: &[Event], map_a: &str, map_b: &str) -> Self {
|
||||
let execution_fault = std::fs::read_to_string(run_dir.join("child_stderr.txt"))
|
||||
.ok()
|
||||
.and_then(|text| {
|
||||
text.lines().find_map(|line| {
|
||||
line.find("GSC execution failed:")
|
||||
.map(|at| line[at..].to_owned())
|
||||
})
|
||||
})
|
||||
.or_else(|| {
|
||||
events
|
||||
.iter()
|
||||
.find(|e| e.name == "fault")
|
||||
.map(|e| format!("GSC execution failed: {}", e.get("fault").unwrap_or("")))
|
||||
});
|
||||
Self {
|
||||
a: ScriptMap::gather(events, map_a),
|
||||
b: ScriptMap::gather(events, map_b),
|
||||
execution_fault,
|
||||
}
|
||||
}
|
||||
|
||||
fn failure(&self, events: &[Event]) -> Option<String> {
|
||||
refused_since(events, 0).or_else(|| self.execution_fault.clone())
|
||||
}
|
||||
|
||||
fn to_json(&self, events: &[Event]) -> Value {
|
||||
json!({
|
||||
"a": self.a.to_json(),
|
||||
"b": self.b.to_json(),
|
||||
"execution_fault": self.execution_fault,
|
||||
"events": events.iter().map(|e| {
|
||||
let mut v = event_json(e);
|
||||
v["event"] = e.name.clone().into();
|
||||
v
|
||||
}).collect::<Vec<_>>(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn refused_since(events: &[Event], from: u128) -> Option<String> {
|
||||
events
|
||||
.iter()
|
||||
.find(|e| e.name == "refused" && e.ns().unwrap_or(0) >= from)
|
||||
.map(refusal_text)
|
||||
}
|
||||
|
||||
fn refusal_text(event: &Event) -> String {
|
||||
format!(
|
||||
"GSC {} refused on {}: {}",
|
||||
event.get("stage").unwrap_or("?"),
|
||||
event.get("map").unwrap_or("?"),
|
||||
event.get("fault").unwrap_or("")
|
||||
)
|
||||
}
|
||||
|
||||
struct Assertions(Vec<Value>);
|
||||
|
||||
impl Assertions {
|
||||
@@ -246,6 +349,8 @@ pub fn finish(run_dir: &Path, manifest: &mut Value, phases: &[Phase], outcome: &
|
||||
let (log, unphased) = read_log(&child.join("logs/latest.log"));
|
||||
let marks = &outcome.marks;
|
||||
let lifecycle = &outcome.lifecycle;
|
||||
let map_b = manifest["maps"]["b"].as_str().unwrap_or("").to_owned();
|
||||
let script = ScriptEvidence::gather(run_dir, &outcome.script, SCENARIO.map_a, &map_b);
|
||||
|
||||
let (end_json, exit_ms, exited_clean) = match &outcome.end {
|
||||
End::Exited { code, at_ms } => (
|
||||
@@ -265,12 +370,19 @@ pub fn finish(run_dir: &Path, manifest: &mut Value, phases: &[Phase], outcome: &
|
||||
),
|
||||
};
|
||||
manifest["process"] = end_json;
|
||||
let failure_reason = match &outcome.end {
|
||||
End::Exited { code, .. } => {
|
||||
format!("process exited (code {code:?}) before this phase ended")
|
||||
let failure_reason = |begin: Option<&Event>| {
|
||||
let from = begin.and_then(Event::ns).unwrap_or(0);
|
||||
if let Some(refused) = refused_since(&outcome.script, from) {
|
||||
return refused;
|
||||
}
|
||||
match &outcome.end {
|
||||
End::Exited { code, .. } => match &script.execution_fault {
|
||||
Some(fault) => format!("process exited (code {code:?}): {fault}"),
|
||||
None => format!("process exited (code {code:?}) before this phase ended"),
|
||||
},
|
||||
End::Timeout { waiting_for, .. } => format!("timeout waiting for {waiting_for}"),
|
||||
End::SpawnFailed(reason) => reason.clone(),
|
||||
}
|
||||
End::Timeout { waiting_for, .. } => format!("timeout waiting for {waiting_for}"),
|
||||
End::SpawnFailed(reason) => reason.clone(),
|
||||
};
|
||||
|
||||
manifest["render"] = match &outcome.runtime {
|
||||
@@ -295,11 +407,11 @@ pub fn finish(run_dir: &Path, manifest: &mut Value, phases: &[Phase], outcome: &
|
||||
let (status, reason, completed_ms) = match (begin, end, phase.name.as_str()) {
|
||||
(Some(_), _, "quit") => match exit_ms {
|
||||
Some(ms) if exited_clean => ("completed", None, Some(ms)),
|
||||
Some(ms) => ("failed", Some(failure_reason.clone()), Some(ms)),
|
||||
None => ("failed", Some(failure_reason.clone()), None),
|
||||
Some(ms) => ("failed", Some(failure_reason(begin)), Some(ms)),
|
||||
None => ("failed", Some(failure_reason(begin)), None),
|
||||
},
|
||||
(Some(_), Some(e), _) => ("completed", None, Some(e.at_ms)),
|
||||
(Some(_), None, _) => ("failed", Some(failure_reason.clone()), None),
|
||||
(Some(_), None, _) => ("failed", Some(failure_reason(begin)), None),
|
||||
(None, _, _) => ("not_reached", None, None),
|
||||
};
|
||||
let warnings = log
|
||||
@@ -334,17 +446,7 @@ pub fn finish(run_dir: &Path, manifest: &mut Value, phases: &[Phase], outcome: &
|
||||
.iter()
|
||||
.find(|e| e.name == name && e.ns().unwrap_or(0) >= from)
|
||||
};
|
||||
let boundary = |e: Option<&Event>| {
|
||||
e.map(|e| {
|
||||
let mut v = json!({ "ns": e.ns(), "controller_ms": e.at_ms });
|
||||
for (k, val) in &e.fields {
|
||||
if k != "ns" && k != "pid" {
|
||||
v[k] = val.clone().into();
|
||||
}
|
||||
}
|
||||
v
|
||||
})
|
||||
};
|
||||
let boundary = |e: Option<&Event>| e.map(event_json);
|
||||
let revoked = after("local_session_revoked");
|
||||
let menu = after("menu_interactive");
|
||||
let teardown = after("old_runtime_teardown_complete");
|
||||
@@ -509,6 +611,11 @@ pub fn finish(run_dir: &Path, manifest: &mut Value, phases: &[Phase], outcome: &
|
||||
mark(marks, "a.cold_load.end").is_some(),
|
||||
"a.cold_load.end mark",
|
||||
);
|
||||
asserts.check(
|
||||
"a.gsc_installed",
|
||||
script.a.installed.is_some(),
|
||||
script.a.to_json(),
|
||||
);
|
||||
asserts.check(
|
||||
"a.players",
|
||||
a_clients.is_some_and(|c| c >= players),
|
||||
@@ -539,11 +646,21 @@ pub fn finish(run_dir: &Path, manifest: &mut Value, phases: &[Phase], outcome: &
|
||||
mark(marks, "b.load.end").is_some(),
|
||||
"b.load.end mark",
|
||||
);
|
||||
asserts.check(
|
||||
"b.gsc_installed",
|
||||
script.b.installed.is_some(),
|
||||
script.b.to_json(),
|
||||
);
|
||||
asserts.check(
|
||||
"b.players",
|
||||
b_clients.is_some_and(|c| c >= players),
|
||||
json!({ "clients": b_clients, "target": players }),
|
||||
);
|
||||
asserts.check(
|
||||
"gsc.no_execution_fault",
|
||||
script.execution_fault.is_none(),
|
||||
json!(script.execution_fault),
|
||||
);
|
||||
asserts.check("quit.requested", quit.is_some(), "lifecycle quit_requested");
|
||||
asserts.check(
|
||||
"quit.process_exited_by_itself",
|
||||
@@ -583,8 +700,12 @@ pub fn finish(run_dir: &Path, manifest: &mut Value, phases: &[Phase], outcome: &
|
||||
"child_stderr": run_dir.join("child_stderr.txt").display().to_string(),
|
||||
});
|
||||
manifest["log_outside_phases"] = unphased.to_json();
|
||||
manifest["gsc"] = script.to_json(&outcome.script);
|
||||
|
||||
let passed = asserts.all_passed() && exited_clean;
|
||||
if !passed && let Some(failure) = script.failure(&outcome.script) {
|
||||
manifest["failure"] = failure.into();
|
||||
}
|
||||
manifest["assertions"] = asserts.0.into();
|
||||
manifest["result"] = match (&outcome.end, passed) {
|
||||
(_, true) => "passed",
|
||||
|
||||
@@ -43,6 +43,7 @@ pub struct Outcome {
|
||||
pub end: End,
|
||||
pub marks: Vec<Event>,
|
||||
pub lifecycle: Vec<Event>,
|
||||
pub script: Vec<Event>,
|
||||
pub runtime: Option<Event>,
|
||||
}
|
||||
|
||||
@@ -51,6 +52,7 @@ pub fn run(launch: Launch<'_>) -> Outcome {
|
||||
end: End::SpawnFailed(String::new()),
|
||||
marks: Vec::new(),
|
||||
lifecycle: Vec::new(),
|
||||
script: Vec::new(),
|
||||
runtime: None,
|
||||
};
|
||||
let stdout_file = std::fs::File::create(launch.cwd.join("child_stdout.txt"));
|
||||
@@ -113,6 +115,10 @@ pub fn run(launch: Launch<'_>) -> Outcome {
|
||||
}
|
||||
println!("[{:>7.1}s] {}", at_ms as f64 / 1000.0, line);
|
||||
outcome.lifecycle.push(event);
|
||||
} else if let Some(event) = parse(&line, "gsc:", at_ms) {
|
||||
last_progress = Instant::now();
|
||||
println!("[{:>7.1}s] {}", at_ms as f64 / 1000.0, line);
|
||||
outcome.script.push(event);
|
||||
} else if let Some(event) = parse(&line, "runtime:", at_ms) {
|
||||
println!("[{:>7.1}s] {}", at_ms as f64 / 1000.0, line);
|
||||
outcome.runtime.get_or_insert(event);
|
||||
@@ -130,6 +136,8 @@ pub fn run(launch: Launch<'_>) -> Outcome {
|
||||
outcome.marks.push(event);
|
||||
} else if let Some(event) = parse(&line, "lifecycle:", at_ms) {
|
||||
outcome.lifecycle.push(event);
|
||||
} else if let Some(event) = parse(&line, "gsc:", at_ms) {
|
||||
outcome.script.push(event);
|
||||
}
|
||||
}
|
||||
outcome.end = end;
|
||||
|
||||
@@ -36,7 +36,7 @@ pub(crate) fn compile_multiplayer(
|
||||
}
|
||||
|
||||
fn harvest_script_anim_names(script: &[u8], names: &mut HashSet<String>) {
|
||||
let mut parser = ComParser::new(script);
|
||||
let mut parser = AtrParser::new(script);
|
||||
loop {
|
||||
let token = parser.parse(true);
|
||||
if token.is_empty() {
|
||||
@@ -46,7 +46,7 @@ fn harvest_script_anim_names(script: &[u8], names: &mut HashSet<String>) {
|
||||
continue;
|
||||
}
|
||||
let anim = parser.parse(false);
|
||||
if anim.is_empty() || !scr_is_identifier(&anim) {
|
||||
if anim.is_empty() || !is_identifier(&anim) {
|
||||
continue;
|
||||
}
|
||||
names.insert(ascii_lower(&anim));
|
||||
@@ -65,7 +65,7 @@ fn parse_atr(
|
||||
names: &HashSet<String>,
|
||||
ignored: &mut usize,
|
||||
) -> Result<Vec<ParsedAnim>, AtrCompileError> {
|
||||
let mut parser = ComParser::new(src);
|
||||
let mut parser = AtrParser::new(src);
|
||||
let (children, eof) = parse_internal(&mut parser, names, ignored, true, false, false)?;
|
||||
if !eof {
|
||||
return Err(parser.bad_token("bad token"));
|
||||
@@ -81,7 +81,7 @@ struct ParsedAnim {
|
||||
|
||||
#[allow(clippy::never_loop)]
|
||||
fn parse_internal(
|
||||
parser: &mut ComParser<'_>,
|
||||
parser: &mut AtrParser<'_>,
|
||||
names: &HashSet<String>,
|
||||
ignored: &mut usize,
|
||||
include_parent: bool,
|
||||
@@ -99,7 +99,7 @@ fn parse_internal(
|
||||
finish_parent(&mut children, include_parent, loop_sync);
|
||||
return Ok((children, true));
|
||||
}
|
||||
if !scr_is_identifier(&token) {
|
||||
if !is_identifier(&token) {
|
||||
break;
|
||||
}
|
||||
if let Some(work) = current.take() {
|
||||
@@ -124,7 +124,7 @@ fn parse_internal(
|
||||
});
|
||||
continue;
|
||||
}
|
||||
if scr_is_identifier(&on_line) {
|
||||
if is_identifier(&on_line) {
|
||||
return Err(parser.bad_token("FIXME: aliases not yet implemented"));
|
||||
}
|
||||
if on_line != ":" {
|
||||
@@ -239,7 +239,7 @@ fn finish_parent(children: &mut Vec<ParsedAnim>, include_parent: bool, loop_sync
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_properties(parser: &mut ComParser<'_>) -> Result<u16, AtrCompileError> {
|
||||
fn parse_properties(parser: &mut AtrParser<'_>) -> Result<u16, AtrCompileError> {
|
||||
let mut flags = 0u16;
|
||||
loop {
|
||||
let token = parser.parse(false);
|
||||
@@ -351,7 +351,7 @@ fn ascii_lower(token: &str) -> String {
|
||||
token.chars().map(|c| c.to_ascii_lowercase()).collect()
|
||||
}
|
||||
|
||||
fn scr_is_identifier(token: &str) -> bool {
|
||||
fn is_identifier(token: &str) -> bool {
|
||||
!token.is_empty() && token.bytes().all(is_c_sym)
|
||||
}
|
||||
|
||||
@@ -359,7 +359,7 @@ fn is_c_sym(c: u8) -> bool {
|
||||
c.is_ascii_alphanumeric() || c == b'_'
|
||||
}
|
||||
|
||||
pub(crate) struct ComParser<'a> {
|
||||
pub(crate) struct AtrParser<'a> {
|
||||
src: &'a [u8],
|
||||
pos: usize,
|
||||
hit_eof: bool,
|
||||
@@ -367,7 +367,7 @@ pub(crate) struct ComParser<'a> {
|
||||
last_offset: usize,
|
||||
}
|
||||
|
||||
impl<'a> ComParser<'a> {
|
||||
impl<'a> AtrParser<'a> {
|
||||
pub(crate) fn new(src: &'a [u8]) -> Self {
|
||||
Self {
|
||||
src,
|
||||
|
||||
@@ -9,7 +9,7 @@ use anim_iw4::{
|
||||
};
|
||||
|
||||
use crate::animtree::CompiledAnimTreeDefinition;
|
||||
use crate::atr_compile::ComParser;
|
||||
use crate::atr_compile::AtrParser;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct ParsedAnimCommand {
|
||||
@@ -139,7 +139,7 @@ pub(crate) fn parse_player_anim_script(
|
||||
script: &[u8],
|
||||
tree: &CompiledAnimTreeDefinition,
|
||||
) -> Result<Arc<ParsedPlayerAnimScript>, PlayerAnimParseError> {
|
||||
let mut parser = ComParser::new(script);
|
||||
let mut parser = AtrParser::new(script);
|
||||
let mut parsed = ParsedPlayerAnimScript {
|
||||
slots: Vec::new(),
|
||||
events: Vec::new(),
|
||||
@@ -294,7 +294,7 @@ fn flush_event_slot(
|
||||
}
|
||||
|
||||
fn parse_item(
|
||||
parser: &mut ComParser<'_>,
|
||||
parser: &mut AtrParser<'_>,
|
||||
tree: &CompiledAnimTreeDefinition,
|
||||
parsed: &mut ParsedPlayerAnimScript,
|
||||
aliases: &HashMap<(u8, String), u64>,
|
||||
@@ -372,7 +372,7 @@ fn parse_item(
|
||||
}
|
||||
|
||||
fn parse_define(
|
||||
parser: &mut ComParser<'_>,
|
||||
parser: &mut AtrParser<'_>,
|
||||
aliases: &mut HashMap<(u8, String), u64>,
|
||||
) -> Result<(), PlayerAnimParseError> {
|
||||
let cond = parser.parse(false);
|
||||
@@ -429,7 +429,7 @@ fn resolve_cond_value(
|
||||
}
|
||||
|
||||
fn parse_conditions(
|
||||
parser: &mut ComParser<'_>,
|
||||
parser: &mut AtrParser<'_>,
|
||||
aliases: &HashMap<(u8, String), u64>,
|
||||
) -> Result<(bool, Vec<ParsedAnimCondition>, String), PlayerAnimParseError> {
|
||||
let mut skip = false;
|
||||
@@ -516,7 +516,7 @@ fn parse_conditions(
|
||||
Ok((skip, conditions, raw.join(" ")))
|
||||
}
|
||||
|
||||
fn skip_unknown_block(parser: &mut ComParser<'_>) {
|
||||
fn skip_unknown_block(parser: &mut AtrParser<'_>) {
|
||||
let mut depth = 0i32;
|
||||
loop {
|
||||
let token = parser.parse(true);
|
||||
@@ -546,7 +546,7 @@ fn ascii_lower(token: &str) -> String {
|
||||
token.chars().map(|c| c.to_ascii_lowercase()).collect()
|
||||
}
|
||||
|
||||
fn bad(parser: &ComParser<'_>, message: &'static str) -> PlayerAnimParseError {
|
||||
fn bad(parser: &AtrParser<'_>, message: &'static str) -> PlayerAnimParseError {
|
||||
PlayerAnimParseError::BadToken {
|
||||
offset: parser.last_offset(),
|
||||
message: message.to_owned(),
|
||||
|
||||
@@ -126,6 +126,20 @@ impl XAnimCatalog {
|
||||
self.entries.get(self.index_by_name(ns, name)?)
|
||||
}
|
||||
|
||||
pub fn hint_edge(
|
||||
&self,
|
||||
hint: Option<&str>,
|
||||
ns: AssetNamespace,
|
||||
) -> asset_core::AssetEdge<asset_core::XAnimSpace> {
|
||||
match hint.filter(|name| !name.is_empty()) {
|
||||
None => asset_core::AssetEdge::Absent,
|
||||
Some(name) => match self.index_by_name(ns, name) {
|
||||
Some(index) => asset_core::AssetEdge::bind_order(index, self.zone_of(index)),
|
||||
None => asset_core::AssetEdge::Unresolved(asset_core::AssetEdgeReason::CatalogMiss),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn has_key(&self, key: &XAnimKey) -> bool {
|
||||
self.indices.contains_key(key)
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ use crate::asset_graph::{AssetEdge, AssetEdgeCensus, AssetEdgeReason, ZoneOwner}
|
||||
use crate::ent_channel::{EntChannel, parse_ent_channel_file};
|
||||
use crate::{AssetNamespace, ZoneGame};
|
||||
|
||||
pub use asset_iw4::{lerp_range, pick_weighted_variant_index, snd_advance_lcg, snd_unit_random};
|
||||
pub use asset_iw4::{advance_lcg, lerp_range, pick_weighted_variant_index, unit_random};
|
||||
|
||||
pub type LoadedSoundEdge = crate::asset_graph::AssetEdge<crate::asset_graph::LoadedSoundSpace>;
|
||||
|
||||
@@ -1159,8 +1159,8 @@ impl SoundCatalog {
|
||||
picked: None,
|
||||
});
|
||||
};
|
||||
let t_vol = snd_unit_random(rng);
|
||||
let t_pitch = snd_unit_random(rng);
|
||||
let t_vol = unit_random(rng);
|
||||
let t_pitch = unit_random(rng);
|
||||
let volume = if row.vol_min == 0.0 && row.vol_max == 0.0 {
|
||||
1.0
|
||||
} else {
|
||||
|
||||
@@ -23,6 +23,8 @@ xmodel_runtime.workspace = true
|
||||
fx_iw4.workspace = true
|
||||
diag.workspace = true
|
||||
bevy.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -23,6 +23,134 @@ pub struct MapTeamSettings {
|
||||
pub defenders: Option<String>,
|
||||
pub allies: Option<asset_core::AssetKey>,
|
||||
pub axis: Option<asset_core::AssetKey>,
|
||||
pub allies_charset: Option<String>,
|
||||
pub axis_charset: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub struct ObjectiveVisuals {
|
||||
pub neutral_flag: Option<String>,
|
||||
pub bomb: Option<String>,
|
||||
pub bomb_explosion_fx: Option<String>,
|
||||
pub plant_weapon: Option<String>,
|
||||
pub defuse_weapon: Option<String>,
|
||||
pub flag: [Option<String>; 2],
|
||||
pub flag_carry: [Option<String>; 2],
|
||||
pub flag_base_fx: [Option<String>; 2],
|
||||
pub crate_model: [Option<String>; 2],
|
||||
pub crate_overlay: [Option<String>; 2],
|
||||
}
|
||||
|
||||
impl ObjectiveVisuals {
|
||||
#[must_use]
|
||||
pub fn from_faction_table(
|
||||
table: &CapturedStringTable,
|
||||
allieschar: Option<&str>,
|
||||
axischar: Option<&str>,
|
||||
) -> Self {
|
||||
let cell = |charset: Option<&str>, column| {
|
||||
let charset = charset.filter(|s| !s.is_empty())?;
|
||||
let value = table.lookup_col(charset, column);
|
||||
(!value.is_empty()).then(|| value.to_owned())
|
||||
};
|
||||
let pair = |column| [cell(allieschar, column), cell(axischar, column)];
|
||||
Self {
|
||||
neutral_flag: Some("prop_flag_neutral".to_owned()),
|
||||
bomb: Some("prop_suitcase_bomb".to_owned()),
|
||||
plant_weapon: Some("briefcase_bomb_mp".to_owned()),
|
||||
defuse_weapon: Some("briefcase_bomb_defuse_mp".to_owned()),
|
||||
bomb_explosion_fx: Some("explosions/tanker_explosion".to_owned()),
|
||||
flag: pair(10),
|
||||
flag_carry: pair(11),
|
||||
flag_base_fx: pair(13),
|
||||
crate_model: pair(18),
|
||||
crate_overlay: [
|
||||
Some("com_plasticcase_friendly".to_owned()),
|
||||
Some("com_plasticcase_enemy".to_owned()),
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn t5(allies: Option<&str>, axis: Option<&str>) -> Self {
|
||||
let allies_flag = match allies {
|
||||
Some("marines") => "mp_flag_allies_1",
|
||||
Some("rebels") => "mp_flag_allies_3",
|
||||
_ => "mp_flag_allies_2",
|
||||
};
|
||||
let axis_flag = match axis {
|
||||
Some("russian") => "mp_flag_axis_1",
|
||||
Some("tropas") => "mp_flag_axis_3",
|
||||
_ => "mp_flag_axis_2",
|
||||
};
|
||||
let crates = [
|
||||
Some("mp_supplydrop_ally".to_owned()),
|
||||
Some("mp_supplydrop_axis".to_owned()),
|
||||
];
|
||||
Self {
|
||||
neutral_flag: Some("mp_flag_neutral".to_owned()),
|
||||
bomb: Some("prop_suitcase_bomb".to_owned()),
|
||||
plant_weapon: Some("briefcase_bomb_mp".to_owned()),
|
||||
defuse_weapon: Some("briefcase_bomb_defuse_mp".to_owned()),
|
||||
bomb_explosion_fx: Some("maps/mp_maps/fx_mp_exp_bomb".to_owned()),
|
||||
flag: [Some(allies_flag.to_owned()), Some(axis_flag.to_owned())],
|
||||
flag_carry: [
|
||||
Some(format!("{allies_flag}_carry")),
|
||||
Some(format!("{axis_flag}_carry")),
|
||||
],
|
||||
flag_base_fx: std::array::from_fn(|_| Some("misc/fx_ui_flagbase_gold_t5".to_owned())),
|
||||
crate_model: crates.clone(),
|
||||
crate_overlay: crates,
|
||||
}
|
||||
}
|
||||
|
||||
fn models(&self) -> impl Iterator<Item = &Option<String>> {
|
||||
[&self.neutral_flag, &self.bomb]
|
||||
.into_iter()
|
||||
.chain(&self.flag)
|
||||
.chain(&self.flag_carry)
|
||||
.chain(&self.crate_model)
|
||||
.chain(&self.crate_overlay)
|
||||
}
|
||||
|
||||
pub fn model_names(&self) -> impl Iterator<Item = &str> {
|
||||
self.models().filter_map(|name| name.as_deref())
|
||||
}
|
||||
|
||||
pub fn is_complete(&self) -> bool {
|
||||
self.models()
|
||||
.chain(&self.flag_base_fx)
|
||||
.chain([
|
||||
&self.bomb_explosion_fx,
|
||||
&self.plant_weapon,
|
||||
&self.defuse_weapon,
|
||||
])
|
||||
.all(|name| name.as_deref().is_some_and(|s| !s.is_empty()))
|
||||
}
|
||||
|
||||
pub fn model_pairs<'a>(&'a self, other: &'a Self) -> impl Iterator<Item = (&'a str, &'a str)> {
|
||||
self.models()
|
||||
.zip(other.models())
|
||||
.filter_map(|(a, b)| Some((a.as_deref()?, b.as_deref()?)))
|
||||
}
|
||||
|
||||
pub fn weapon_pairs<'a>(&'a self, other: &'a Self) -> impl Iterator<Item = (&'a str, &'a str)> {
|
||||
[&self.plant_weapon, &self.defuse_weapon]
|
||||
.into_iter()
|
||||
.zip([&other.plant_weapon, &other.defuse_weapon])
|
||||
.filter_map(|(a, b)| Some((a.as_deref()?, b.as_deref()?)))
|
||||
}
|
||||
|
||||
pub fn fx_pairs<'a>(&'a self, other: &'a Self) -> impl Iterator<Item = (&'a str, &'a str)> {
|
||||
self.flag_base_fx
|
||||
.iter()
|
||||
.zip(&other.flag_base_fx)
|
||||
.chain(std::iter::once((
|
||||
&self.bomb_explosion_fx,
|
||||
&other.bomb_explosion_fx,
|
||||
)))
|
||||
.filter_map(|(a, b)| Some((a.as_deref()?, b.as_deref()?)))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Resource)]
|
||||
@@ -71,6 +199,8 @@ pub fn team_settings(
|
||||
)
|
||||
.ok()
|
||||
}),
|
||||
allies_charset: allieschar.map(str::to_owned),
|
||||
axis_charset: axischar.map(str::to_owned),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
@@ -234,6 +364,33 @@ pub fn arena_charsets(text: &str, map: &str) -> Option<ArenaCharsets> {
|
||||
.find(|row| row.map.eq_ignore_ascii_case(map))
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn arena_entry(text: &str, map: &str) -> Option<std::collections::BTreeMap<String, String>> {
|
||||
for block in text.split('{').skip(1) {
|
||||
let body = block.split('}').next().unwrap_or(block);
|
||||
let mut entry = std::collections::BTreeMap::new();
|
||||
for raw in body.lines() {
|
||||
let line = raw.trim();
|
||||
if line.is_empty() || line.starts_with("//") {
|
||||
continue;
|
||||
}
|
||||
let mut parts = line.split_whitespace();
|
||||
let Some(key) = parts.next() else {
|
||||
continue;
|
||||
};
|
||||
let value = unquote(parts.collect::<Vec<_>>().join(" ").as_str());
|
||||
entry.insert(key.to_ascii_lowercase(), value);
|
||||
}
|
||||
if entry
|
||||
.get("map")
|
||||
.is_some_and(|name| name.eq_ignore_ascii_case(map))
|
||||
{
|
||||
return Some(entry);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn parse_arena(text: &str) -> Vec<ArenaCharsets> {
|
||||
let mut rows = Vec::new();
|
||||
@@ -388,6 +545,8 @@ pub fn t5_settings_from_teamset_gsc(script: &str) -> MapTeamSettings {
|
||||
)
|
||||
.ok()
|
||||
}),
|
||||
allies_charset: asset_audio::game_string_assignment(script, "allies").map(str::to_owned),
|
||||
axis_charset: asset_audio::game_string_assignment(script, "axis").map(str::to_owned),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,23 +6,22 @@ use fastfile_iw4::{
|
||||
};
|
||||
use fx_iw4::{
|
||||
FX_EFFECT_DEF_SIZE, FX_ELEM_DEF_STRIDE, FX_SPARK_FOUNTAIN_DEF_OFF_SPARK_COUNT,
|
||||
FX_SPARK_FOUNTAIN_DEF_SIZE, FxEffectDefView, FxElemDefView, FxTrailVertex, fx_effect_def_view,
|
||||
fx_elem_def_view,
|
||||
FX_SPARK_FOUNTAIN_DEF_SIZE, FxEffectDefView, FxElemDefView, FxTrailVertex, effect_def_view,
|
||||
elem_def_view,
|
||||
};
|
||||
|
||||
use crate::asset_graph::{
|
||||
AssetEdge, AssetEdgeCensus, AssetEdgeFromPtrs, AssetEdgeReason, ZoneOwner,
|
||||
};
|
||||
use crate::asset_graph::AssetEdgeFromPtrs;
|
||||
use crate::graph_support::AuthoredRef;
|
||||
use crate::material_catalog::{MaterialCatalog, MaterialDefinitions, TS_2D, TS_COLOR_MAP};
|
||||
use asset_core::{AssetEdge, AssetEdgeCensus, AssetEdgeReason, ZoneOwner};
|
||||
use asset_material::{MaterialCatalog, MaterialDefinitions, TS_2D, TS_COLOR_MAP};
|
||||
|
||||
#[inline]
|
||||
pub fn fx_material_bind_name(name: &str) -> &str {
|
||||
pub fn material_bind_name(name: &str) -> &str {
|
||||
crate::AssetRef::bare_name(name)
|
||||
}
|
||||
|
||||
pub fn insert_fx_color_image<V>(map: &mut HashMap<String, V>, authored_name: &str, image: V) {
|
||||
let bind = fx_material_bind_name(authored_name);
|
||||
let bind = material_bind_name(authored_name);
|
||||
if bind.is_empty() {
|
||||
return;
|
||||
}
|
||||
@@ -33,7 +32,7 @@ pub fn alias_fx_color_map_stubs<V: Clone>(map: &mut HashMap<String, V>) -> usize
|
||||
let pairs: Vec<(String, String)> = map
|
||||
.keys()
|
||||
.filter_map(|name| {
|
||||
let bind = fx_material_bind_name(name);
|
||||
let bind = material_bind_name(name);
|
||||
(bind != name.as_str() && !map.contains_key(bind))
|
||||
.then(|| (bind.to_owned(), name.clone()))
|
||||
})
|
||||
@@ -52,7 +51,7 @@ pub fn lookup_fx_color_image<'a, V>(
|
||||
map: &'a HashMap<String, V>,
|
||||
authored_name: &str,
|
||||
) -> Option<&'a V> {
|
||||
map.get(fx_material_bind_name(authored_name))
|
||||
map.get(material_bind_name(authored_name))
|
||||
}
|
||||
|
||||
pub mod elem_type {
|
||||
@@ -93,11 +92,11 @@ pub mod elem_type {
|
||||
}
|
||||
|
||||
pub type FxElemMaterialReason = AssetEdgeReason;
|
||||
pub type FxElemMaterial = AssetEdge<crate::asset_graph::MaterialSpace>;
|
||||
pub type FxElemMaterial = AssetEdge<asset_core::MaterialSpace>;
|
||||
|
||||
pub type FxChildEdge = AssetEdge<crate::asset_graph::FxSpace>;
|
||||
pub type FxChildEdge = AssetEdge<asset_core::FxSpace>;
|
||||
|
||||
pub type FxElemModelEdge = AssetEdge<crate::asset_graph::FxModelSpace>;
|
||||
pub type FxElemModelEdge = AssetEdge<asset_core::FxModelSpace>;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum FxBankSound<'a> {
|
||||
@@ -286,7 +285,7 @@ impl OwnedFxElemDef {
|
||||
if self.view.elem_type != elem_type::RUNNER {
|
||||
return None;
|
||||
}
|
||||
let idx = fx_iw4::fx_elem_visual_index(self.view.visual_count, random_seed);
|
||||
let idx = fx_iw4::elem_visual_index(self.view.visual_count, random_seed);
|
||||
match self.visuals.get(idx)? {
|
||||
OwnedFxVisual::Runner { edge, .. } => Some(*edge),
|
||||
_ => None,
|
||||
@@ -301,7 +300,7 @@ impl OwnedFxElemDef {
|
||||
if self.view.elem_type != elem_type::SOUND {
|
||||
return FxBankSound::Gap;
|
||||
}
|
||||
let idx = fx_iw4::fx_elem_visual_index(self.view.visual_count, random_seed);
|
||||
let idx = fx_iw4::elem_visual_index(self.view.visual_count, random_seed);
|
||||
let hint = match self.visuals.get(idx) {
|
||||
None => return FxBankSound::Gap,
|
||||
Some(OwnedFxVisual::None) => return FxBankSound::Silent,
|
||||
@@ -328,7 +327,7 @@ impl OwnedFxElemDef {
|
||||
if self.view.elem_type != elem_type::MODEL {
|
||||
return None;
|
||||
}
|
||||
let idx = fx_iw4::fx_elem_visual_index(self.view.visual_count, random_seed);
|
||||
let idx = fx_iw4::elem_visual_index(self.view.visual_count, random_seed);
|
||||
match self.visuals.get(idx)? {
|
||||
OwnedFxVisual::Model { edge, .. } => Some(*edge),
|
||||
OwnedFxVisual::None => Some(AssetEdge::Absent),
|
||||
@@ -340,7 +339,7 @@ impl OwnedFxElemDef {
|
||||
if self.view.elem_type != elem_type::DECAL {
|
||||
return None;
|
||||
}
|
||||
let idx = fx_iw4::fx_elem_visual_index(self.view.visual_count, random_seed);
|
||||
let idx = fx_iw4::elem_visual_index(self.view.visual_count, random_seed);
|
||||
self.visuals.get(idx)
|
||||
}
|
||||
|
||||
@@ -403,6 +402,23 @@ impl std::ops::DerefMut for FxCatalog {
|
||||
}
|
||||
|
||||
impl FxCatalog {
|
||||
pub fn hint_edge(
|
||||
&self,
|
||||
authored_slot: bool,
|
||||
hint: Option<&str>,
|
||||
ns: crate::AssetNamespace,
|
||||
) -> AssetEdge<asset_core::FxSpace> {
|
||||
let hint = hint.filter(|name| !name.is_empty());
|
||||
if !authored_slot && hint.is_none() {
|
||||
return AssetEdge::Absent;
|
||||
}
|
||||
let ns = crate::body_namespace(ns);
|
||||
match hint.and_then(|name| self.index_in(ns, name)) {
|
||||
Some(index) => AssetEdge::bind_order(index, self.zone_of(index)),
|
||||
None => AssetEdge::Unresolved(AssetEdgeReason::CatalogMiss),
|
||||
}
|
||||
}
|
||||
|
||||
/// Ends the build: the effect definitions travel on, the zone pointer map
|
||||
/// that produced them does not.
|
||||
pub fn publish(self) -> FxDefinitions {
|
||||
@@ -506,7 +522,7 @@ impl FxCatalog {
|
||||
emission_count: geometry.emission_count,
|
||||
})
|
||||
} else {
|
||||
fx_effect_def_view(header_raw)
|
||||
effect_def_view(header_raw)
|
||||
};
|
||||
let Some(view) = view else {
|
||||
self.capture_gaps += 1;
|
||||
@@ -726,7 +742,7 @@ impl FxDefinitions {
|
||||
|
||||
pub fn resolve_createfx_id(&self, fxid: &str) -> Option<&OwnedFxEffectDef> {
|
||||
self.resolve_def_for_map(fxid)
|
||||
.or_else(|| self.get_in(fx_body_namespace(self.map_namespace), fxid))
|
||||
.or_else(|| self.get_in(body_namespace(self.map_namespace), fxid))
|
||||
}
|
||||
|
||||
pub fn map_fx_name<'a>(&self, name: &'a str) -> FxName<'a> {
|
||||
@@ -744,15 +760,27 @@ impl FxDefinitions {
|
||||
if let Some(def) = self.get_in(ns, name).filter(|d| !d.elems.is_empty()) {
|
||||
return Some(def);
|
||||
}
|
||||
let bind = fx_material_bind_name(name);
|
||||
let bind = material_bind_name(name);
|
||||
if bind == name {
|
||||
return None;
|
||||
}
|
||||
self.get_in(ns, bind).filter(|d| !d.elems.is_empty())
|
||||
}
|
||||
|
||||
pub fn alias_for_map(&mut self, from: &str, to: &str) -> bool {
|
||||
let map_ns = body_namespace(self.map_namespace);
|
||||
let Some(def) = self.resolve_def_in(map_ns, to) else {
|
||||
return false;
|
||||
};
|
||||
let Some(index) = self.index_in(map_ns, &def.name) else {
|
||||
return false;
|
||||
};
|
||||
self.by_key.insert((map_ns, ascii_lower(from)), index);
|
||||
true
|
||||
}
|
||||
|
||||
pub fn resolve_def_for_map(&self, name: &str) -> Option<&OwnedFxEffectDef> {
|
||||
let map_ns = fx_body_namespace(self.map_namespace);
|
||||
let map_ns = body_namespace(self.map_namespace);
|
||||
self.resolve_def_in(map_ns, name).or_else(|| {
|
||||
(map_ns != crate::AssetNamespace::Iw4)
|
||||
.then(|| self.resolve_def_in(crate::AssetNamespace::Iw4, name))
|
||||
@@ -1283,9 +1311,9 @@ fn capture_elem(
|
||||
.ok()?
|
||||
.to_vec();
|
||||
let view = if s.wire_format() == fastfile_iw4::Iw4WireFormat::X64 {
|
||||
fx_iw4::fx_elem_def_view_x64(&raw)?
|
||||
fx_iw4::elem_def_view_x64(&raw)?
|
||||
} else {
|
||||
fx_elem_def_view(&raw)?
|
||||
elem_def_view(&raw)?
|
||||
};
|
||||
let vel_count = view.vel_interval_count as usize + 1;
|
||||
let vis_count = view.vis_state_interval_count as usize + 1;
|
||||
@@ -1682,7 +1710,7 @@ fn material_has_decoded_color(materials: &MaterialDefinitions, mat_i: usize) ->
|
||||
.is_some_and(|img| img.decoded.is_some())
|
||||
}
|
||||
|
||||
pub fn fx_color_decoded_in_catalog(materials: &MaterialDefinitions, material: usize) -> bool {
|
||||
pub fn color_decoded_in_catalog(materials: &MaterialDefinitions, material: usize) -> bool {
|
||||
material_has_decoded_color(materials, material)
|
||||
}
|
||||
|
||||
@@ -1739,14 +1767,14 @@ fn lookup_playable_fx(
|
||||
name: &str,
|
||||
) -> Option<(usize, ZoneOwner)> {
|
||||
let key = ascii_lower(name);
|
||||
let bind = ascii_lower(fx_material_bind_name(name));
|
||||
let bind = ascii_lower(material_bind_name(name));
|
||||
playable
|
||||
.get(&(ns, key))
|
||||
.or_else(|| playable.get(&(ns, bind)))
|
||||
.copied()
|
||||
}
|
||||
|
||||
pub fn fx_body_namespace(ns: crate::AssetNamespace) -> crate::AssetNamespace {
|
||||
pub fn body_namespace(ns: crate::AssetNamespace) -> crate::AssetNamespace {
|
||||
match ns {
|
||||
crate::AssetNamespace::Iw5 => crate::AssetNamespace::Iw4,
|
||||
other => other,
|
||||
@@ -1762,7 +1790,7 @@ pub struct FxName<'a> {
|
||||
impl<'a> FxName<'a> {
|
||||
pub fn new(namespace: crate::AssetNamespace, name: &'a str) -> Self {
|
||||
Self {
|
||||
namespace: fx_body_namespace(namespace),
|
||||
namespace: body_namespace(namespace),
|
||||
name,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,11 +6,10 @@ use std::{
|
||||
use asset_material::MaterialDefinitions;
|
||||
use fastfile_iw4::{ScriptStrings, ZoneStream};
|
||||
|
||||
use asset_model::link::{capture_xmodel_material_slots, stamp_xmodel_material_edges};
|
||||
use crate::{
|
||||
AssetEdge, AssetEdgeCensus, MaterialCatalog, MaterialIndex, MaterialSpace, ModelSkel,
|
||||
ZoneOwner,
|
||||
asset_graph::{capture_xmodel_material_slots, stamp_xmodel_material_edges},
|
||||
capture_xmodel_skel,
|
||||
ZoneOwner, capture_xmodel_skel,
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
|
||||
@@ -90,13 +90,13 @@ impl ImpactFxCatalog {
|
||||
let mut nonflesh = std::array::from_fn(|_| String::new());
|
||||
let mut flesh = std::array::from_fn(|_| String::new());
|
||||
for (j, slot) in nonflesh.iter_mut().enumerate() {
|
||||
match fx_name_at_cell(s, fx, e.at(j * s.pointer_bytes())) {
|
||||
match name_at_cell(s, fx, e.at(j * s.pointer_bytes())) {
|
||||
Ok(n) => *slot = n,
|
||||
Err(()) => gaps += 1,
|
||||
}
|
||||
}
|
||||
for (j, slot) in flesh.iter_mut().enumerate() {
|
||||
match fx_name_at_cell(s, fx, e.at(s.layout(124, 248) + j * s.pointer_bytes())) {
|
||||
match name_at_cell(s, fx, e.at(s.layout(124, 248) + j * s.pointer_bytes())) {
|
||||
Ok(n) => *slot = n,
|
||||
Err(()) => gaps += 1,
|
||||
}
|
||||
@@ -114,7 +114,7 @@ impl ImpactFxCatalog {
|
||||
}
|
||||
}
|
||||
|
||||
fn fx_name_at_cell(
|
||||
fn name_at_cell(
|
||||
s: &ZoneStream<'_>,
|
||||
fx: &crate::FxCatalog,
|
||||
cell: fastfile_iw4::Ptr,
|
||||
@@ -176,7 +176,7 @@ impl ImpactFxCatalog {
|
||||
impl OwnedFxImpactTable {
|
||||
pub fn impact_row(&self, impact_type: i32, exit: bool) -> Option<usize> {
|
||||
if !self.t5 {
|
||||
return fx_iw4::fx_impact_table_row(impact_type, exit);
|
||||
return fx_iw4::impact_table_row(impact_type, exit);
|
||||
}
|
||||
let exit = usize::from(exit);
|
||||
Some(match impact_type {
|
||||
|
||||
@@ -22,6 +22,7 @@ mod impact_fx_catalog;
|
||||
mod localize;
|
||||
mod lochit;
|
||||
mod menu_catalog;
|
||||
mod menu_source;
|
||||
mod penetration;
|
||||
mod tracer_catalog;
|
||||
mod weapon_anim_dispatch;
|
||||
@@ -49,21 +50,4 @@ pub use weapon_families::*;
|
||||
|
||||
pub mod asset_graph {
|
||||
pub(crate) use crate::graph_support::*;
|
||||
pub use asset_core::*;
|
||||
pub use asset_model::link::*;
|
||||
}
|
||||
pub mod discover {
|
||||
pub use asset_transport::*;
|
||||
}
|
||||
pub mod material_catalog {
|
||||
pub use asset_material::*;
|
||||
}
|
||||
pub mod material_images {
|
||||
pub use asset_material::*;
|
||||
}
|
||||
pub mod model_skel {
|
||||
pub use asset_model::*;
|
||||
}
|
||||
pub mod zone {
|
||||
pub use asset_transport::*;
|
||||
}
|
||||
|
||||
@@ -1,31 +1,8 @@
|
||||
use weapon_iw4::{HITLOC_COUNT, LOCATION_DAMAGE_IDENTITY, location_damage_is_valid};
|
||||
use weapon_iw4::{HITLOC_COUNT, HITLOC_NAMES, LOCATION_DAMAGE_IDENTITY, location_damage_is_valid};
|
||||
|
||||
const LOCHIT_MAGIC: &str = "LOCDMGTABLE";
|
||||
const LOCHIT_RAWFILE: &str = "info/mp_lochit_dmgtable";
|
||||
|
||||
const HITLOC_NAMES: [&str; HITLOC_COUNT] = [
|
||||
"none",
|
||||
"helmet",
|
||||
"head",
|
||||
"neck",
|
||||
"torso_upper",
|
||||
"torso_lower",
|
||||
"right_arm_upper",
|
||||
"left_arm_upper",
|
||||
"right_arm_lower",
|
||||
"left_arm_lower",
|
||||
"right_hand",
|
||||
"left_hand",
|
||||
"right_leg_upper",
|
||||
"left_leg_upper",
|
||||
"right_leg_lower",
|
||||
"left_leg_lower",
|
||||
"right_foot",
|
||||
"left_foot",
|
||||
"gun",
|
||||
"shield",
|
||||
];
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum LochitTableError {
|
||||
MissingMagic,
|
||||
|
||||
@@ -9,19 +9,18 @@ use fastfile_iw4::{
|
||||
load_asset_at_observed, load_zone,
|
||||
};
|
||||
|
||||
use crate::asset_graph::AssetRef;
|
||||
use crate::discover::{GamesRoot, find_zone_file, game_root_for_zone};
|
||||
use crate::localize::decode_localized_text;
|
||||
use crate::material_catalog::{TS_2D, TS_COLOR_MAP};
|
||||
use crate::material_images::decode_zone_image_rgba;
|
||||
use crate::zone::{ZoneMemory, open_zone};
|
||||
use asset_core::AssetRef;
|
||||
use asset_iw4::size as sz;
|
||||
use asset_material::{TS_2D, TS_COLOR_MAP, decode_zone_image_rgba};
|
||||
use asset_transport::{GamesRoot, ZoneMemory, find_zone_file, game_root_for_zone, open_zone};
|
||||
|
||||
pub const ITEM_TYPE_BUTTON: i32 = 1;
|
||||
|
||||
pub const ITEM_TYPE_TEXT: i32 = 0;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
#[derive(Clone, Copy, Debug, Default, serde::Deserialize, serde::Serialize)]
|
||||
#[serde(default, deny_unknown_fields)]
|
||||
pub struct MenuRect {
|
||||
pub x: f32,
|
||||
pub y: f32,
|
||||
@@ -44,11 +43,73 @@ impl From<MenuRectCapture> for MenuRect {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||
pub enum MenuEvent {
|
||||
Script(String),
|
||||
If {
|
||||
condition: String,
|
||||
then: Vec<MenuEvent>,
|
||||
},
|
||||
Else(Vec<MenuEvent>),
|
||||
SetLocalVar {
|
||||
kind: i32,
|
||||
name: String,
|
||||
expr: String,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize)]
|
||||
#[serde(default, deny_unknown_fields)]
|
||||
pub struct MenuHandlers {
|
||||
pub open: Vec<MenuEvent>,
|
||||
pub close: Vec<MenuEvent>,
|
||||
pub close_request: Vec<MenuEvent>,
|
||||
pub esc: Vec<MenuEvent>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize)]
|
||||
#[serde(default, deny_unknown_fields)]
|
||||
pub struct ItemHandlers {
|
||||
pub action: Vec<MenuEvent>,
|
||||
pub accept: Vec<MenuEvent>,
|
||||
pub mouse_enter: Vec<MenuEvent>,
|
||||
pub mouse_exit: Vec<MenuEvent>,
|
||||
pub focus: Vec<MenuEvent>,
|
||||
pub leave_focus: Vec<MenuEvent>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
|
||||
#[serde(default, deny_unknown_fields)]
|
||||
pub struct MenuEditField {
|
||||
pub max_chars: usize,
|
||||
}
|
||||
|
||||
impl Default for MenuEditField {
|
||||
fn default() -> Self {
|
||||
Self { max_chars: 32 }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct MenuSlider {
|
||||
pub min: f32,
|
||||
pub max: f32,
|
||||
pub step: f32,
|
||||
#[serde(default)]
|
||||
pub display_range: Option<[f32; 2]>,
|
||||
#[serde(default)]
|
||||
pub suffix: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize)]
|
||||
#[serde(default, deny_unknown_fields)]
|
||||
pub struct MenuItem {
|
||||
pub name: String,
|
||||
pub handlers: ItemHandlers,
|
||||
|
||||
pub text_key: String,
|
||||
pub text_literal: bool,
|
||||
pub item_type: i32,
|
||||
|
||||
pub style: i32,
|
||||
@@ -72,6 +133,9 @@ pub struct MenuItem {
|
||||
pub dvar_test: String,
|
||||
pub enable_dvar: String,
|
||||
pub local_var: String,
|
||||
pub edit_field: Option<MenuEditField>,
|
||||
pub choices: Vec<(String, String)>,
|
||||
pub slider: Option<MenuSlider>,
|
||||
pub bg_ptr: u8,
|
||||
pub vis_ptr: u8,
|
||||
pub mat_ptr: u8,
|
||||
@@ -85,18 +149,21 @@ pub struct MenuItem {
|
||||
pub mouse_enter_ptr: u8,
|
||||
pub on_focus_ptr: u8,
|
||||
pub static_flags: i32,
|
||||
pub dvar_flags: i32,
|
||||
pub action: Vec<String>,
|
||||
pub mouse_enter: Vec<String>,
|
||||
pub on_focus: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
#[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize)]
|
||||
#[serde(default, deny_unknown_fields)]
|
||||
pub struct MenuDef {
|
||||
pub name: String,
|
||||
pub sound_name: String,
|
||||
pub window_background: String,
|
||||
pub expr_dvars: String,
|
||||
pub fullscreen: i32,
|
||||
pub focus_color: Option<[f32; 4]>,
|
||||
|
||||
pub rect: MenuRect,
|
||||
pub items: Vec<MenuItem>,
|
||||
@@ -106,6 +173,7 @@ pub struct MenuDef {
|
||||
|
||||
pub on_close_request: Vec<String>,
|
||||
pub on_esc: Vec<String>,
|
||||
pub handlers: MenuHandlers,
|
||||
|
||||
pub vis_exp: String,
|
||||
|
||||
@@ -113,6 +181,7 @@ pub struct MenuDef {
|
||||
|
||||
pub on_open_local_vars: Vec<MenuSetLocalVar>,
|
||||
|
||||
#[serde(skip)]
|
||||
expr_dvar_rows: std::sync::OnceLock<Vec<(i32, std::ops::Range<usize>)>>,
|
||||
}
|
||||
|
||||
@@ -144,7 +213,8 @@ fn expr_dvar_rows(list: &str) -> Vec<(i32, std::ops::Range<usize>)> {
|
||||
rows
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
#[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize)]
|
||||
#[serde(default, deny_unknown_fields)]
|
||||
pub struct MenuSetLocalVar {
|
||||
pub kind: i32,
|
||||
pub name: String,
|
||||
@@ -269,6 +339,7 @@ pub struct MenuCatalog {
|
||||
pub material_2d_plans: BTreeMap<String, HudMaterialPlan>,
|
||||
|
||||
pub material_images: BTreeMap<String, String>,
|
||||
pub material_srgb_reads: BTreeMap<String, bool>,
|
||||
pub lists: Vec<(String, i32)>,
|
||||
pub walked: usize,
|
||||
|
||||
@@ -277,6 +348,7 @@ pub struct MenuCatalog {
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
||||
pub struct HudMaterialPlan {
|
||||
pub texture_atlas: [u8; 2],
|
||||
pub technique_slots: Option<u64>,
|
||||
pub state_bits_entry: Option<[u8; asset_iw4::size::TECHNIQUE_SLOT_COUNT]>,
|
||||
pub state_rows: Vec<[u32; 2]>,
|
||||
@@ -296,8 +368,20 @@ pub struct HudMaterialTextureBinding {
|
||||
}
|
||||
|
||||
impl MenuCatalog {
|
||||
pub fn load_definitions(&mut self, source: &str) -> Result<(), String> {
|
||||
let definitions = crate::menu_source::load(source, self)?;
|
||||
self.menus
|
||||
.extend(definitions.into_iter().map(|def| (def.name.clone(), def)));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get(&self, name: &str) -> Option<&MenuDef> {
|
||||
self.menus.get(name)
|
||||
self.menus.get(name).or_else(|| {
|
||||
self.menus
|
||||
.iter()
|
||||
.find(|(key, _)| key.eq_ignore_ascii_case(name))
|
||||
.map(|(_, def)| def)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn played_sound_aliases(&self) -> std::collections::BTreeSet<&str> {
|
||||
@@ -407,6 +491,7 @@ impl MenuCatalog {
|
||||
self.rawfiles.insert(name, text);
|
||||
}
|
||||
self.material_state_bits.extend(other.material_state_bits);
|
||||
self.material_srgb_reads.extend(other.material_srgb_reads);
|
||||
self.material_2d_plans.extend(other.material_2d_plans);
|
||||
for (name, image) in other.material_images {
|
||||
self.material_images.insert(name, image);
|
||||
@@ -434,6 +519,7 @@ pub const HUD_CHROME_MENUS: &[&str] = &[
|
||||
"hud_fullscreen",
|
||||
"dpad_hd",
|
||||
"javelin_overlay_hd",
|
||||
"missilecam_hud_hd",
|
||||
"dpad_sd",
|
||||
"splash",
|
||||
"challenge",
|
||||
@@ -503,9 +589,14 @@ fn load_menu_catalog_with_iwd(path: &Path, games: Option<&Path>) -> Result<MenuC
|
||||
let mut stream = memory.stream(&image.bytes).map_err(|e| e.to_string())?;
|
||||
let mut sink = MenuSink {
|
||||
catalog: MenuCatalog::default(),
|
||||
russian_fonts: path
|
||||
.parent()
|
||||
.and_then(Path::file_name)
|
||||
.is_some_and(|name| name.eq_ignore_ascii_case("russian")),
|
||||
names: HashMap::new(),
|
||||
script_sets: HashMap::new(),
|
||||
script_set_stack: Vec::new(),
|
||||
branches: Vec::new(),
|
||||
image_links: HashMap::new(),
|
||||
images: Vec::new(),
|
||||
technique_links: HashMap::new(),
|
||||
@@ -533,17 +624,28 @@ enum TechniqueLink {
|
||||
|
||||
struct CapturedZoneImage {
|
||||
name: String,
|
||||
use_srgb_reads: bool,
|
||||
width: u16,
|
||||
height: u16,
|
||||
format: u32,
|
||||
payload: Vec<u8>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
enum RecordedEvent {
|
||||
Script(String),
|
||||
Begin(Option<String>),
|
||||
End,
|
||||
SetLocalVar(i32, String, String),
|
||||
}
|
||||
|
||||
struct MenuSink {
|
||||
russian_fonts: bool,
|
||||
catalog: MenuCatalog,
|
||||
names: HashMap<(u8, u32), String>,
|
||||
script_sets: HashMap<(u8, u32), Vec<String>>,
|
||||
script_sets: HashMap<(u8, u32), Vec<RecordedEvent>>,
|
||||
script_set_stack: Vec<Ptr>,
|
||||
branches: Vec<(Option<String>, Vec<MenuEvent>)>,
|
||||
image_links: HashMap<Ptr, ImageLink>,
|
||||
images: Vec<CapturedZoneImage>,
|
||||
|
||||
@@ -617,15 +719,50 @@ impl MenuSink {
|
||||
self.names.insert(ptr_key(slot), name.to_owned());
|
||||
}
|
||||
|
||||
fn record_script_set(&mut self, script: &str) {
|
||||
fn record_script_set(&mut self, event: RecordedEvent) {
|
||||
for body in &self.script_set_stack {
|
||||
self.script_sets
|
||||
.entry(ptr_key(*body))
|
||||
.or_default()
|
||||
.push(script.to_owned());
|
||||
.push(event.clone());
|
||||
}
|
||||
}
|
||||
|
||||
fn push_event(&mut self, menu: &str, kind: MenuScriptKind, event: MenuEvent) {
|
||||
if let Some((_, body)) = self.branches.last_mut() {
|
||||
body.push(event);
|
||||
return;
|
||||
}
|
||||
let Some(def) = self.catalog.menus.get_mut(menu) else {
|
||||
return;
|
||||
};
|
||||
let list = match kind {
|
||||
MenuScriptKind::OnOpen => &mut def.handlers.open,
|
||||
MenuScriptKind::OnClose => &mut def.handlers.close,
|
||||
MenuScriptKind::OnCloseRequest => &mut def.handlers.close_request,
|
||||
MenuScriptKind::OnEsc => &mut def.handlers.esc,
|
||||
MenuScriptKind::ExecKey => return,
|
||||
item_kind => {
|
||||
let Some(row) = def.items.last_mut() else {
|
||||
return;
|
||||
};
|
||||
match item_kind {
|
||||
MenuScriptKind::Action => &mut row.handlers.action,
|
||||
MenuScriptKind::Accept => &mut row.handlers.accept,
|
||||
MenuScriptKind::MouseEnter | MenuScriptKind::MouseEnterText => {
|
||||
&mut row.handlers.mouse_enter
|
||||
}
|
||||
MenuScriptKind::MouseExit | MenuScriptKind::MouseExitText => {
|
||||
&mut row.handlers.mouse_exit
|
||||
}
|
||||
MenuScriptKind::OnFocus => &mut row.handlers.focus,
|
||||
_ => &mut row.handlers.leave_focus,
|
||||
}
|
||||
}
|
||||
};
|
||||
list.push(event);
|
||||
}
|
||||
|
||||
fn bind_image(&mut self, slot: Ptr, link: ImageLink) {
|
||||
if block_is_aliasable(slot.block) {
|
||||
self.image_links.insert(slot, link);
|
||||
@@ -686,6 +823,7 @@ impl MenuSink {
|
||||
let index = self.images.len();
|
||||
self.images.push(CapturedZoneImage {
|
||||
name: name.as_str().to_owned(),
|
||||
use_srgb_reads: geometry.use_srgb_reads,
|
||||
width: geometry.width,
|
||||
height: geometry.height,
|
||||
format: geometry.format,
|
||||
@@ -760,6 +898,7 @@ impl MenuSink {
|
||||
self.catalog.material_2d_plans.insert(
|
||||
key.clone(),
|
||||
HudMaterialPlan {
|
||||
texture_atlas: geometry.texture_atlas,
|
||||
technique_slots,
|
||||
state_bits_entry: geometry.state_bits_entry,
|
||||
state_rows: bits,
|
||||
@@ -773,6 +912,9 @@ impl MenuSink {
|
||||
self.catalog
|
||||
.material_images
|
||||
.insert(key.clone(), captured.name.clone());
|
||||
self.catalog
|
||||
.material_srgb_reads
|
||||
.insert(key.clone(), captured.use_srgb_reads);
|
||||
}
|
||||
self.material_ts2d.insert(key, image);
|
||||
}
|
||||
@@ -812,13 +954,19 @@ impl MenuSink {
|
||||
"hud_suitcase_bomb",
|
||||
"compassping_friendly_mp",
|
||||
"compassping_enemyfiring",
|
||||
"hud_fofbox_self",
|
||||
"hud_fofbox_hostile",
|
||||
]
|
||||
.map(str::to_owned),
|
||||
);
|
||||
materials.extend(
|
||||
self.material_ts2d
|
||||
.keys()
|
||||
.filter(|name| name.starts_with("rank_") || name.starts_with("killicon"))
|
||||
.filter(|name| {
|
||||
name.starts_with("rank_")
|
||||
|| name.starts_with("killicon")
|
||||
|| name.starts_with("hud_minimap_")
|
||||
})
|
||||
.cloned(),
|
||||
);
|
||||
if let Some(table) = self.catalog.string_table("mp/rankIconTable.csv") {
|
||||
@@ -832,6 +980,22 @@ impl MenuSink {
|
||||
}
|
||||
}
|
||||
}
|
||||
// These icons are selected through expressions, so static backgrounds do
|
||||
// not enumerate them. Some (including ACOG) only live in the fastfile.
|
||||
for (name, column) in [
|
||||
("mp/statsTable.csv", 6),
|
||||
("mp/attachmentTable.csv", 6),
|
||||
("mp/perkTable.csv", 3),
|
||||
] {
|
||||
if let Some(table) = self.catalog.string_table(name) {
|
||||
for row in 0..table.rows {
|
||||
let material = table.cell(row as i32, column);
|
||||
if !material.is_empty() {
|
||||
materials.push(material.to_owned());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
materials.sort_unstable();
|
||||
materials.dedup();
|
||||
for material in materials {
|
||||
@@ -1051,6 +1215,7 @@ impl AssetLinkSink for MenuSink {
|
||||
def.window_background = rec.window_background.to_owned();
|
||||
def.set_expr_dvars(rec.expr_dvars.to_owned());
|
||||
def.fullscreen = rec.fullscreen;
|
||||
def.focus_color = Some(rec.focus_color);
|
||||
def.rect = MenuRect::from(rec.rect);
|
||||
Ok(())
|
||||
}
|
||||
@@ -1131,6 +1296,7 @@ impl AssetLinkSink for MenuSink {
|
||||
item.mouse_enter_ptr = rec.mouse_enter_ptr;
|
||||
item.on_focus_ptr = rec.on_focus_ptr;
|
||||
item.static_flags = rec.static_flags;
|
||||
item.dvar_flags = rec.dvar_flags;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -1196,6 +1362,29 @@ impl AssetLinkSink for MenuSink {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn capture_item_choice(
|
||||
&mut self,
|
||||
menu: &str,
|
||||
_item: &str,
|
||||
label: &str,
|
||||
value: fastfile_iw4::MenuChoiceValue<'_>,
|
||||
) -> fastfile_iw4::Result<()> {
|
||||
let Some(def) = self.catalog.menus.get_mut(menu) else {
|
||||
return Ok(());
|
||||
};
|
||||
if label.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
if let Some(item) = def.items.last_mut() {
|
||||
let value = match value {
|
||||
fastfile_iw4::MenuChoiceValue::Str(text) => text.to_owned(),
|
||||
fastfile_iw4::MenuChoiceValue::Float(number) => number.to_string(),
|
||||
};
|
||||
item.choices.push((label.to_owned(), value));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn capture_item_material_exp(
|
||||
&mut self,
|
||||
menu: &str,
|
||||
@@ -1218,7 +1407,8 @@ impl AssetLinkSink for MenuSink {
|
||||
kind: MenuScriptKind,
|
||||
script: &str,
|
||||
) -> fastfile_iw4::Result<()> {
|
||||
self.record_script_set(script);
|
||||
self.record_script_set(RecordedEvent::Script(script.to_owned()));
|
||||
self.push_event(menu, kind, MenuEvent::Script(script.to_owned()));
|
||||
let Some(def) = self.catalog.menus.get_mut(menu) else {
|
||||
return Ok(());
|
||||
};
|
||||
@@ -1255,6 +1445,20 @@ impl AssetLinkSink for MenuSink {
|
||||
name: &str,
|
||||
expr: &str,
|
||||
) -> fastfile_iw4::Result<()> {
|
||||
self.record_script_set(RecordedEvent::SetLocalVar(
|
||||
var_kind,
|
||||
name.to_owned(),
|
||||
expr.to_owned(),
|
||||
));
|
||||
self.push_event(
|
||||
menu,
|
||||
kind,
|
||||
MenuEvent::SetLocalVar {
|
||||
kind: var_kind,
|
||||
name: name.to_owned(),
|
||||
expr: expr.to_owned(),
|
||||
},
|
||||
);
|
||||
if !item.is_empty() || kind != MenuScriptKind::OnOpen {
|
||||
return Ok(());
|
||||
}
|
||||
@@ -1292,15 +1496,60 @@ impl AssetLinkSink for MenuSink {
|
||||
item: &str,
|
||||
kind: MenuScriptKind,
|
||||
) -> fastfile_iw4::Result<()> {
|
||||
let Some(scripts) = self.script_sets.get(&ptr_key(body)).cloned() else {
|
||||
let Some(events) = self.script_sets.get(&ptr_key(body)).cloned() else {
|
||||
return Ok(());
|
||||
};
|
||||
for script in scripts {
|
||||
self.capture_menu_script(menu, item, kind, &script)?;
|
||||
for event in events {
|
||||
match event {
|
||||
RecordedEvent::Script(script) => {
|
||||
self.capture_menu_script(menu, item, kind, &script)?
|
||||
}
|
||||
RecordedEvent::Begin(condition) => {
|
||||
self.begin_menu_event_branch(menu, item, kind, condition.as_deref())?
|
||||
}
|
||||
RecordedEvent::End => self.end_menu_event_branch(menu, item, kind)?,
|
||||
RecordedEvent::SetLocalVar(var_kind, name, expr) => {
|
||||
self.capture_menu_set_local_var(menu, item, kind, var_kind, &name, &expr)?
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn begin_menu_event_branch(
|
||||
&mut self,
|
||||
_menu: &str,
|
||||
_item: &str,
|
||||
_kind: MenuScriptKind,
|
||||
condition: Option<&str>,
|
||||
) -> fastfile_iw4::Result<()> {
|
||||
self.record_script_set(RecordedEvent::Begin(condition.map(str::to_owned)));
|
||||
self.branches
|
||||
.push((condition.map(str::to_owned), Vec::new()));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn end_menu_event_branch(
|
||||
&mut self,
|
||||
menu: &str,
|
||||
_item: &str,
|
||||
kind: MenuScriptKind,
|
||||
) -> fastfile_iw4::Result<()> {
|
||||
self.record_script_set(RecordedEvent::End);
|
||||
let Some((condition, body)) = self.branches.pop() else {
|
||||
return Ok(());
|
||||
};
|
||||
let event = match condition {
|
||||
Some(condition) => MenuEvent::If {
|
||||
condition,
|
||||
then: body,
|
||||
},
|
||||
None => MenuEvent::Else(body),
|
||||
};
|
||||
self.push_event(menu, kind, event);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn capture_font(&mut self, rec: &FontCapture<'_>) -> fastfile_iw4::Result<()> {
|
||||
let mut glyphs = Vec::new();
|
||||
for chunk in rec.glyphs.chunks_exact(sz::GLYPH) {
|
||||
@@ -1308,6 +1557,60 @@ impl AssetLinkSink for MenuSink {
|
||||
row.copy_from_slice(chunk);
|
||||
glyphs.push(GlyphCapture::from_row(&row));
|
||||
}
|
||||
// Russian zone atlases index Cyrillic by Windows-1251 bytes. Keep
|
||||
// those entries for legacy localized strings and add Unicode aliases
|
||||
// for UTF-8 user text. Other language atlases must not reinterpret them.
|
||||
if self.russian_fonts {
|
||||
let aliases: Vec<_> = glyphs
|
||||
.iter()
|
||||
.filter_map(|glyph| {
|
||||
let letter = match glyph.letter {
|
||||
0xc0..=0xff => 0x0410 + glyph.letter - 0xc0,
|
||||
0x80 => 0x0402,
|
||||
0x81 => 0x0403,
|
||||
0x83 => 0x0453,
|
||||
0x8a => 0x0409,
|
||||
0x8c => 0x040a,
|
||||
0x8d => 0x040c,
|
||||
0x8e => 0x040b,
|
||||
0x8f => 0x040f,
|
||||
0x90 => 0x0452,
|
||||
0x9a => 0x0459,
|
||||
0x9c => 0x045a,
|
||||
0x9d => 0x045c,
|
||||
0x9e => 0x045b,
|
||||
0x9f => 0x045f,
|
||||
0xa1 => 0x040e,
|
||||
0xa2 => 0x045e,
|
||||
0xa3 => 0x0408,
|
||||
0xa5 => 0x0490,
|
||||
0xa8 => 0x0401,
|
||||
0xaa => 0x0404,
|
||||
0xaf => 0x0407,
|
||||
0xb2 => 0x0406,
|
||||
0xb3 => 0x0456,
|
||||
0xb4 => 0x0491,
|
||||
0xb8 => 0x0451,
|
||||
0xba => 0x0454,
|
||||
0xbc => 0x0458,
|
||||
0xbd => 0x0405,
|
||||
0xbe => 0x0455,
|
||||
0xbf => 0x0457,
|
||||
_ => return None,
|
||||
};
|
||||
if glyphs.iter().any(|existing| existing.letter == letter) {
|
||||
return None;
|
||||
}
|
||||
let mut alias = *glyph;
|
||||
alias.letter = letter;
|
||||
Some(alias)
|
||||
})
|
||||
.collect();
|
||||
glyphs.extend(aliases);
|
||||
if let Some(extended) = glyphs.get_mut(0x60..) {
|
||||
extended.sort_by_key(|glyph| glyph.letter);
|
||||
}
|
||||
}
|
||||
self.catalog.fonts.insert(
|
||||
rec.name.to_ascii_lowercase(),
|
||||
FontDef {
|
||||
@@ -1332,7 +1635,10 @@ impl AssetLinkSink for MenuSink {
|
||||
|| n.rsplit('/')
|
||||
.next()
|
||||
.is_some_and(|leaf| leaf.eq_ignore_ascii_case("basemaps.arena"));
|
||||
if !is_arena {
|
||||
let lower = n.to_ascii_lowercase();
|
||||
let is_config = lower.ends_with(".cfg");
|
||||
let is_shock = lower.starts_with("shock/") && lower.ends_with(".shock");
|
||||
if !is_arena && !is_config && !is_shock {
|
||||
return Ok(());
|
||||
}
|
||||
let bytes = if zlib_compressed {
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use serde::Deserialize;
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::{MenuCatalog, MenuDef, MenuEvent, MenuItem};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(untagged)]
|
||||
enum Definition {
|
||||
Variant(Variant),
|
||||
Complete(MenuDef),
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct Variant {
|
||||
name: String,
|
||||
base: String,
|
||||
#[serde(default)]
|
||||
fields: BTreeMap<String, Value>,
|
||||
#[serde(default)]
|
||||
item_overrides: Vec<ItemOverride>,
|
||||
#[serde(default)]
|
||||
append_items: Vec<MenuItem>,
|
||||
#[serde(default)]
|
||||
item_copies: Vec<ItemCopy>,
|
||||
#[serde(default)]
|
||||
open_prefix: Vec<MenuEvent>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct ItemOverride {
|
||||
#[serde(default)]
|
||||
name: Option<String>,
|
||||
#[serde(default)]
|
||||
index: Option<usize>,
|
||||
fields: BTreeMap<String, Value>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct ItemCopy {
|
||||
index: usize,
|
||||
fields: BTreeMap<String, Value>,
|
||||
}
|
||||
|
||||
fn merge(target: &mut Value, fields: BTreeMap<String, Value>) -> Result<(), String> {
|
||||
let object = target
|
||||
.as_object_mut()
|
||||
.ok_or("Menu override requires an object")?;
|
||||
for (key, value) in fields {
|
||||
if let Value::Object(child) = value {
|
||||
if let Some(existing) = object.get_mut(&key).filter(|value| value.is_object()) {
|
||||
merge(existing, child.into_iter().collect())?;
|
||||
} else {
|
||||
object.insert(key, Value::Object(child));
|
||||
}
|
||||
} else {
|
||||
object.insert(key, value);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn load(source: &str, catalog: &MenuCatalog) -> Result<Vec<MenuDef>, String> {
|
||||
let definitions: Vec<Definition> =
|
||||
serde_json::from_str(source).map_err(|error| format!("Menu definitions: {error}"))?;
|
||||
let mut resolved = BTreeMap::<String, MenuDef>::new();
|
||||
for definition in definitions {
|
||||
let def = match definition {
|
||||
Definition::Complete(def) => def,
|
||||
Definition::Variant(variant) => {
|
||||
let base = resolved
|
||||
.get(&variant.base)
|
||||
.or_else(|| catalog.get(&variant.base))
|
||||
.ok_or_else(|| {
|
||||
format!(
|
||||
"Menu `{}` requires missing base `{}`",
|
||||
variant.name, variant.base
|
||||
)
|
||||
})?;
|
||||
let mut value = serde_json::to_value(base).map_err(|error| error.to_string())?;
|
||||
merge(&mut value, variant.fields)?;
|
||||
let mut def: MenuDef = serde_json::from_value(value)
|
||||
.map_err(|error| format!("Menu `{}`: {error}", variant.name))?;
|
||||
def.name = variant.name;
|
||||
for item in variant.item_overrides {
|
||||
let index = match (item.name.as_ref(), item.index) {
|
||||
(Some(name), None) => {
|
||||
let matches: Vec<_> = def
|
||||
.items
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, item)| &item.name == name)
|
||||
.map(|(index, _)| index)
|
||||
.collect();
|
||||
match matches.as_slice() {
|
||||
[index] => *index,
|
||||
_ => {
|
||||
return Err(format!(
|
||||
"Menu `{}` has no unique item `{name}`",
|
||||
def.name
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
(None, Some(index)) => index,
|
||||
_ => {
|
||||
return Err(format!(
|
||||
"Menu `{}` item override requires one selector",
|
||||
def.name
|
||||
));
|
||||
}
|
||||
};
|
||||
let target = def
|
||||
.items
|
||||
.get_mut(index)
|
||||
.ok_or_else(|| format!("Menu `{}` has no item {index}", def.name))?;
|
||||
let mut value =
|
||||
serde_json::to_value(&*target).map_err(|error| error.to_string())?;
|
||||
merge(&mut value, item.fields)?;
|
||||
*target = serde_json::from_value(value)
|
||||
.map_err(|error| format!("Menu `{}` item {index}: {error}", def.name))?;
|
||||
}
|
||||
for copy in variant.item_copies {
|
||||
let source = base.items.get(copy.index).ok_or_else(|| {
|
||||
format!("Menu `{}` has no source item {}", variant.base, copy.index)
|
||||
})?;
|
||||
let mut value =
|
||||
serde_json::to_value(source).map_err(|error| error.to_string())?;
|
||||
merge(&mut value, copy.fields)?;
|
||||
def.items
|
||||
.push(serde_json::from_value(value).map_err(|error| {
|
||||
format!("Menu `{}` copied item: {error}", def.name)
|
||||
})?);
|
||||
}
|
||||
def.items.extend(variant.append_items);
|
||||
def.handlers.open.splice(0..0, variant.open_prefix);
|
||||
def
|
||||
}
|
||||
};
|
||||
if def.name.is_empty() || resolved.contains_key(&def.name) {
|
||||
return Err(format!("Empty or duplicate menu name `{}`", def.name));
|
||||
}
|
||||
resolved.insert(def.name.clone(), def);
|
||||
}
|
||||
Ok(resolved.into_values().collect())
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::asset_graph::{AssetEdgeCensus, AssetEdgeFromPtrs, AssetEdgeReason, ZoneOwner};
|
||||
use asset_core::{AssetEdge, AssetEdgeCensus, AssetEdgeReason, MaterialSpace, ZoneOwner};
|
||||
use crate::asset_graph::AssetEdgeFromPtrs;
|
||||
use fastfile_iw4::{Ptr, Result, TracerDefGeometry, ZoneStream};
|
||||
|
||||
pub type TracerMaterial = crate::asset_graph::AssetEdge<crate::asset_graph::MaterialSpace>;
|
||||
pub type TracerMaterial = AssetEdge<MaterialSpace>;
|
||||
|
||||
pub type TracerMaterialReason = AssetEdgeReason;
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ fn mint_weapon_revision() -> u64 {
|
||||
NEXT.fetch_add(1, Ordering::Relaxed)
|
||||
}
|
||||
|
||||
use crate::asset_graph::{
|
||||
use asset_core::{
|
||||
AssetEdge, AssetEdgeCensus, AssetEdgeReason, FpvMeshSpace, FxSpace, MaterialSpace,
|
||||
ProjectileModelSpace, TracerSpace, WorldWeaponSpace, XAnimSpace, ZoneOwner,
|
||||
};
|
||||
@@ -135,13 +135,15 @@ pub struct WeaponBodyFacts {
|
||||
|
||||
pub knife_model: u32,
|
||||
|
||||
pub use_as_melee: bool,
|
||||
|
||||
pub quick_raise_time_ms: i32,
|
||||
|
||||
pub quick_drop_time_ms: i32,
|
||||
|
||||
pub select_requires_ammo_at_0x667: Option<bool>,
|
||||
pub select_requires_ammo: Option<bool>,
|
||||
|
||||
pub offhand_hold_is_cancelable_at_0x681: Option<bool>,
|
||||
pub offhand_hold_is_cancelable: Option<bool>,
|
||||
|
||||
pub move_speed_scale: f32,
|
||||
|
||||
@@ -149,15 +151,15 @@ pub struct WeaponBodyFacts {
|
||||
|
||||
pub sprint_duration_scale: f32,
|
||||
|
||||
pub stance_ofs_at_0x168: [f32; 3],
|
||||
pub ducked_ofs: [f32; 3],
|
||||
|
||||
pub stance_ofs_at_0x18c: [f32; 3],
|
||||
pub prone_ofs: [f32; 3],
|
||||
|
||||
pub night_vision_wear_time: i32,
|
||||
|
||||
pub ads_bob_factor_at_0x330: f32,
|
||||
pub ads_bob_factor: f32,
|
||||
|
||||
pub ads_view_bob_mult_at_0x334: f32,
|
||||
pub ads_view_bob_mult: f32,
|
||||
|
||||
pub movement: WeaponMovementOfsInputs,
|
||||
|
||||
@@ -1118,7 +1120,7 @@ impl WeaponCombatFx {
|
||||
self.present_bound(self.explosion, &self.explosion_hint)
|
||||
}
|
||||
|
||||
pub fn fx_edges(&self) -> [AssetEdge<FxSpace>; 7] {
|
||||
pub fn edges(&self) -> [AssetEdge<FxSpace>; 7] {
|
||||
[
|
||||
self.view_flash,
|
||||
self.world_flash,
|
||||
@@ -1214,6 +1216,8 @@ pub struct WeaponCatalog {
|
||||
strings: ScriptStrings,
|
||||
iw5_attachments: HashMap<String, Iw5ScopeRow>,
|
||||
capture_ns: crate::AssetNamespace,
|
||||
vehicle_turrets: HashMap<String, String>,
|
||||
vehicle_compass: HashMap<String, ([String; 2], [i32; 2])>,
|
||||
}
|
||||
|
||||
impl WeaponCatalog {
|
||||
@@ -1334,6 +1338,21 @@ impl WeaponCatalog {
|
||||
self.capture_ns = ns;
|
||||
}
|
||||
|
||||
pub fn capture_vehicle(&mut self, stream: &ZoneStream<'_>) {
|
||||
let Some((Some(name), turret)) = stream.vehicle() else {
|
||||
return;
|
||||
};
|
||||
let Some(name) = read_name(stream, name) else {
|
||||
return;
|
||||
};
|
||||
if let Some(turret) = turret.and_then(|ptr| read_name(stream, ptr)) {
|
||||
self.vehicle_turrets.insert(name.clone(), turret);
|
||||
}
|
||||
let (icons, size) = stream.vehicle_compass();
|
||||
self.vehicle_compass
|
||||
.insert(name, (icons.map(str::to_owned), size));
|
||||
}
|
||||
|
||||
pub fn capture(&mut self, stream: &ZoneStream<'_>) {
|
||||
let Some(geometry) = stream.weapon() else {
|
||||
return;
|
||||
@@ -1392,9 +1411,7 @@ impl WeaponCatalog {
|
||||
.and_then(|p| read_name(stream, p)),
|
||||
name: name.to_owned(),
|
||||
weap_def: geometry.weap_def.map(ptr_key),
|
||||
display_name_key: geometry
|
||||
.display_name_at_0x8
|
||||
.and_then(|ptr| read_name(stream, ptr)),
|
||||
display_name_key: geometry.display_name.and_then(|ptr| read_name(stream, ptr)),
|
||||
reticle: WeaponReticleAssets {
|
||||
center_material: None,
|
||||
side_material: None,
|
||||
@@ -1402,7 +1419,7 @@ impl WeaponCatalog {
|
||||
side_edge: AssetEdge::Absent,
|
||||
center_image: None,
|
||||
side_image: None,
|
||||
center_size: geometry.reticle_center_size_at_0x128,
|
||||
center_size: geometry.reticle_center_size,
|
||||
side_size: geometry.i_reticle_side_size,
|
||||
center_authored: geometry.reticle_center_material_slot.is_some(),
|
||||
side_authored: geometry.reticle_side_material_slot.is_some(),
|
||||
@@ -1665,18 +1682,19 @@ impl WeaponCatalog {
|
||||
melee_charge_time_ms: geometry.melee_charge_time_ms,
|
||||
melee_charge_delay_ms: geometry.melee_charge_delay_ms,
|
||||
knife_model: geometry.knife_model,
|
||||
use_as_melee: false,
|
||||
quick_raise_time_ms: geometry.quick_raise_time_ms,
|
||||
quick_drop_time_ms: geometry.quick_drop_time_ms,
|
||||
select_requires_ammo_at_0x667: geometry.select_requires_ammo_at_0x667,
|
||||
offhand_hold_is_cancelable_at_0x681: geometry.offhand_hold_is_cancelable_at_0x681,
|
||||
select_requires_ammo: geometry.select_requires_ammo,
|
||||
offhand_hold_is_cancelable: geometry.offhand_hold_is_cancelable,
|
||||
move_speed_scale: geometry.move_speed_scale,
|
||||
ads_move_speed_scale: geometry.ads_move_speed_scale,
|
||||
sprint_duration_scale: geometry.sprint_duration_scale,
|
||||
stance_ofs_at_0x168: geometry.stance_ofs_at_0x168,
|
||||
stance_ofs_at_0x18c: geometry.stance_ofs_at_0x18c,
|
||||
ducked_ofs: geometry.ducked_ofs,
|
||||
prone_ofs: geometry.prone_ofs,
|
||||
night_vision_wear_time: geometry.night_vision_wear_time,
|
||||
ads_bob_factor_at_0x330: geometry.ads_bob_factor_at_0x330,
|
||||
ads_view_bob_mult_at_0x334: geometry.ads_view_bob_mult_at_0x334,
|
||||
ads_bob_factor: geometry.ads_bob_factor,
|
||||
ads_view_bob_mult: geometry.ads_view_bob_mult,
|
||||
movement: movement_from_capture(geometry.movement),
|
||||
idle: idle_from_capture(geometry.idle),
|
||||
clip_size: geometry.clip_size,
|
||||
@@ -2010,7 +2028,7 @@ impl WeaponCatalog {
|
||||
}
|
||||
|
||||
pub fn resolve_projectile_fx_edges(&mut self, fx: &crate::FxCatalog) {
|
||||
let ns = crate::fx_body_namespace(self.capture_ns);
|
||||
let ns = crate::body_namespace(self.capture_ns);
|
||||
for entry in &mut self.entries {
|
||||
stamp_fx_edge(
|
||||
entry.proj_trail_slot,
|
||||
@@ -2364,7 +2382,7 @@ impl WeaponCatalog {
|
||||
pub fn combat_fx_census(&self) -> AssetEdgeCensus {
|
||||
let mut census = AssetEdgeCensus::default();
|
||||
for entry in &self.entries {
|
||||
for edge in entry.combat_fx.fx_edges() {
|
||||
for edge in entry.combat_fx.edges() {
|
||||
census.push(edge);
|
||||
}
|
||||
}
|
||||
@@ -2393,7 +2411,10 @@ impl WeaponCatalog {
|
||||
}
|
||||
|
||||
pub fn into_build(self) -> WeaponBuild {
|
||||
WeaponBuild::from_catalog(self.entries, self.iw5_attachments)
|
||||
let mut build = WeaponBuild::from_catalog(self.entries, self.iw5_attachments);
|
||||
build.registry.vehicle_turrets = self.vehicle_turrets;
|
||||
build.registry.vehicle_compass = self.vehicle_compass;
|
||||
build
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2441,7 +2462,7 @@ fn stamp_combat_fx(
|
||||
fx: &crate::FxCatalog,
|
||||
tracers: &crate::TracerCatalog,
|
||||
) {
|
||||
let ns = crate::fx_body_namespace(ns);
|
||||
let ns = crate::body_namespace(ns);
|
||||
combat.namespace = ns;
|
||||
stamp_fx_edge(
|
||||
slots.view_flash,
|
||||
@@ -2545,38 +2566,6 @@ fn sound_alias_in_bank<'a>(
|
||||
Some((catalog.namespace_of_alias(order), catalog.name_at(order)?))
|
||||
}
|
||||
|
||||
fn xanim_hint_edge(
|
||||
hint: Option<&str>,
|
||||
ns: crate::AssetNamespace,
|
||||
xanims: &crate::XAnimCatalog,
|
||||
) -> AssetEdge<XAnimSpace> {
|
||||
let hint = hint.filter(|name| !name.is_empty());
|
||||
match hint {
|
||||
None => AssetEdge::Absent,
|
||||
Some(name) => match xanims.index_by_name(ns, name) {
|
||||
Some(index) => AssetEdge::bind_order(index, xanims.zone_of(index)),
|
||||
None => AssetEdge::Unresolved(AssetEdgeReason::CatalogMiss),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn fx_hint_edge(
|
||||
authored_slot: bool,
|
||||
hint: Option<&str>,
|
||||
ns: crate::AssetNamespace,
|
||||
fx: &crate::FxCatalog,
|
||||
) -> AssetEdge<FxSpace> {
|
||||
let hint = hint.filter(|name| !name.is_empty());
|
||||
if !authored_slot && hint.is_none() {
|
||||
return AssetEdge::Absent;
|
||||
}
|
||||
let ns = crate::fx_body_namespace(ns);
|
||||
match hint.and_then(|name| fx.index_in(ns, name)) {
|
||||
Some(index) => AssetEdge::bind_order(index, fx.zone_of(index)),
|
||||
None => AssetEdge::Unresolved(AssetEdgeReason::CatalogMiss),
|
||||
}
|
||||
}
|
||||
|
||||
fn ptr_key(p: Ptr) -> (u8, u32) {
|
||||
(p.block, p.offset)
|
||||
}
|
||||
@@ -2960,6 +2949,14 @@ fn leftover_t5_sounds(
|
||||
reload_end_player: leftover_t5_cstr(stream, body, sz::WEAPON_DEF_SND_RELOAD_END_PLAYER_OFF),
|
||||
raise_player: leftover_t5_cstr(stream, body, sz::WEAPON_DEF_SND_RAISE_PLAYER_OFF),
|
||||
putaway_player: leftover_t5_cstr(stream, body, sz::WEAPON_DEF_SND_PUTAWAY_PLAYER_OFF),
|
||||
melee_swipe: leftover_t5_cstr(stream, body, sz::WEAPON_DEF_SND_MELEE_SWIPE_OFF),
|
||||
melee_swipe_player: leftover_t5_cstr(
|
||||
stream,
|
||||
body,
|
||||
sz::WEAPON_DEF_SND_MELEE_SWIPE_PLAYER_OFF,
|
||||
),
|
||||
melee_hit: leftover_t5_cstr(stream, body, sz::WEAPON_DEF_SND_MELEE_HIT_OFF),
|
||||
melee_miss: leftover_t5_cstr(stream, body, sz::WEAPON_DEF_SND_MELEE_MISS_OFF),
|
||||
notetrack_sound_map: leftover_t5_script_string_map(
|
||||
stream,
|
||||
strings,
|
||||
@@ -3028,7 +3025,7 @@ fn capture_t5_body_facts(
|
||||
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()),
|
||||
select_requires_ammo: Some(leftover_t5_select_requires_ammo()),
|
||||
..WeaponBodyFacts::default()
|
||||
};
|
||||
if let Some(body) = geometry.weap_def {
|
||||
@@ -3090,7 +3087,7 @@ fn capture_t5_body_facts(
|
||||
facts.hold_fire_time_ms = i32_at_t5(stream, body, sz::WEAPON_DEF_HOLD_FIRE_TIME_OFF);
|
||||
facts.fuse_time_ms = i32_at_t5(stream, body, sz::WEAPON_DEF_FUSE_TIME_OFF);
|
||||
facts.cook_off_hold = u8_at_t5(stream, body, sz::WEAPON_DEF_COOK_OFF_HOLD_OFF) != 0;
|
||||
facts.offhand_hold_is_cancelable_at_0x681 =
|
||||
facts.offhand_hold_is_cancelable =
|
||||
Some(u8_at_t5(stream, body, sz::WEAPON_DEF_OFFHAND_HOLD_IS_CANCELABLE_OFF) != 0);
|
||||
facts.parallel_bounce = read_bounce_array_t5(stream, body, sz::WEAPON_DEF_PARALLEL_BOUNCE_OFF);
|
||||
facts.perpendicular_bounce =
|
||||
@@ -3098,6 +3095,12 @@ fn capture_t5_body_facts(
|
||||
facts.fire_delay_ms = i32_at_t5(stream, body, sz::WEAPON_DEF_FIRE_DELAY_OFF);
|
||||
facts.quick_drop_time_ms = i32_at_t5(stream, body, sz::WEAPON_QUICK_DROP_TIME_OFF);
|
||||
facts.quick_raise_time_ms = i32_at_t5(stream, body, sz::WEAPON_QUICK_RAISE_TIME_OFF);
|
||||
facts.melee_damage = i32_at_t5(stream, body, sz::WEAPON_DEF_MELEE_DAMAGE_OFF);
|
||||
facts.melee_time_ms = i32_at_t5(stream, body, sz::WEAPON_DEF_MELEE_TIME_OFF);
|
||||
facts.melee_delay_ms = i32_at_t5(stream, body, sz::WEAPON_DEF_MELEE_DELAY_OFF);
|
||||
facts.melee_charge_time_ms = i32_at_t5(stream, body, sz::WEAPON_DEF_MELEE_CHARGE_TIME_OFF);
|
||||
facts.melee_charge_delay_ms = i32_at_t5(stream, body, sz::WEAPON_DEF_MELEE_CHARGE_DELAY_OFF);
|
||||
facts.use_as_melee = u8_at_t5(stream, body, sz::WEAPON_DEF_USE_AS_MELEE_OFF) != 0;
|
||||
facts.rechamber_bolt_delay_ms = i32_at_t5(stream, body, sz::WEAPON_DEF_RECHAMBER_BOLT_TIME_OFF);
|
||||
|
||||
facts.reload_show_rocket_time_ms = i32_at_t5(stream, body, 0x3d4);
|
||||
@@ -3139,12 +3142,12 @@ fn capture_t5_body_facts(
|
||||
facts.no_partial_reload = u8_at_t5(stream, body, sz::WEAPON_DEF_NO_PARTIAL_RELOAD_OFF) != 0;
|
||||
facts.segmented_reload = u8_at_t5(stream, body, sz::WEAPON_DEF_SEGMENTED_RELOAD_OFF) != 0;
|
||||
facts.idle = WeaponIdleInputs {
|
||||
ads_idle_amount_at_0x36c: f32_at_t5(stream, body, sz::WEAPON_DEF_ADS_IDLE_AMOUNT_OFF),
|
||||
hip_idle_amount_at_0x370: f32_at_t5(stream, body, sz::WEAPON_DEF_HIP_IDLE_AMOUNT_OFF),
|
||||
ads_idle_speed_at_0x374: f32_at_t5(stream, body, sz::WEAPON_DEF_ADS_IDLE_SPEED_OFF),
|
||||
hip_idle_speed_at_0x378: f32_at_t5(stream, body, sz::WEAPON_DEF_HIP_IDLE_SPEED_OFF),
|
||||
idle_crouch_factor_at_0x37c: f32_at_t5(stream, body, sz::WEAPON_DEF_IDLE_CROUCH_FACTOR_OFF),
|
||||
idle_prone_factor_at_0x380: f32_at_t5(stream, body, sz::WEAPON_DEF_IDLE_PRONE_FACTOR_OFF),
|
||||
ads_idle_amount: f32_at_t5(stream, body, sz::WEAPON_DEF_ADS_IDLE_AMOUNT_OFF),
|
||||
hip_idle_amount: f32_at_t5(stream, body, sz::WEAPON_DEF_HIP_IDLE_AMOUNT_OFF),
|
||||
ads_idle_speed: f32_at_t5(stream, body, sz::WEAPON_DEF_ADS_IDLE_SPEED_OFF),
|
||||
hip_idle_speed: f32_at_t5(stream, body, sz::WEAPON_DEF_HIP_IDLE_SPEED_OFF),
|
||||
idle_crouch_factor: f32_at_t5(stream, body, sz::WEAPON_DEF_IDLE_CROUCH_FACTOR_OFF),
|
||||
idle_prone_factor: f32_at_t5(stream, body, sz::WEAPON_DEF_IDLE_PRONE_FACTOR_OFF),
|
||||
};
|
||||
facts.inherits_perks = leftover_t5_inherits_host_perks();
|
||||
facts.kick = leftover_t5_kick(stream, geometry);
|
||||
@@ -3506,7 +3509,7 @@ fn capture_iw5_body_facts(
|
||||
u8_at_iw5(stream, body, sz::WEAPON_DEF_NO_PARTIAL_RELOAD_OFF, 2456) != 0;
|
||||
facts.segmented_reload =
|
||||
u8_at_iw5(stream, body, sz::WEAPON_DEF_SEGMENTED_RELOAD_OFF, 2457) != 0;
|
||||
facts.select_requires_ammo_at_0x667 = Some(
|
||||
facts.select_requires_ammo = Some(
|
||||
u8_at_iw5(
|
||||
stream,
|
||||
body,
|
||||
@@ -3514,7 +3517,7 @@ fn capture_iw5_body_facts(
|
||||
2450,
|
||||
) != 0,
|
||||
);
|
||||
facts.offhand_hold_is_cancelable_at_0x681 = Some(
|
||||
facts.offhand_hold_is_cancelable = Some(
|
||||
u8_at_iw5(
|
||||
stream,
|
||||
body,
|
||||
@@ -3523,32 +3526,12 @@ fn capture_iw5_body_facts(
|
||||
) != 0,
|
||||
);
|
||||
facts.idle = WeaponIdleInputs {
|
||||
ads_idle_amount_at_0x36c: f32_at_iw5(
|
||||
stream,
|
||||
body,
|
||||
sz::WEAPON_DEF_ADS_IDLE_AMOUNT_OFF,
|
||||
1472,
|
||||
),
|
||||
hip_idle_amount_at_0x370: f32_at_iw5(
|
||||
stream,
|
||||
body,
|
||||
sz::WEAPON_DEF_HIP_IDLE_AMOUNT_OFF,
|
||||
1476,
|
||||
),
|
||||
ads_idle_speed_at_0x374: f32_at_iw5(stream, body, sz::WEAPON_DEF_ADS_IDLE_SPEED_OFF, 1480),
|
||||
hip_idle_speed_at_0x378: f32_at_iw5(stream, body, sz::WEAPON_DEF_HIP_IDLE_SPEED_OFF, 1484),
|
||||
idle_crouch_factor_at_0x37c: f32_at_iw5(
|
||||
stream,
|
||||
body,
|
||||
sz::WEAPON_DEF_IDLE_CROUCH_FACTOR_OFF,
|
||||
1488,
|
||||
),
|
||||
idle_prone_factor_at_0x380: f32_at_iw5(
|
||||
stream,
|
||||
body,
|
||||
sz::WEAPON_DEF_IDLE_PRONE_FACTOR_OFF,
|
||||
1492,
|
||||
),
|
||||
ads_idle_amount: f32_at_iw5(stream, body, sz::WEAPON_DEF_ADS_IDLE_AMOUNT_OFF, 1472),
|
||||
hip_idle_amount: f32_at_iw5(stream, body, sz::WEAPON_DEF_HIP_IDLE_AMOUNT_OFF, 1476),
|
||||
ads_idle_speed: f32_at_iw5(stream, body, sz::WEAPON_DEF_ADS_IDLE_SPEED_OFF, 1480),
|
||||
hip_idle_speed: f32_at_iw5(stream, body, sz::WEAPON_DEF_HIP_IDLE_SPEED_OFF, 1484),
|
||||
idle_crouch_factor: f32_at_iw5(stream, body, sz::WEAPON_DEF_IDLE_CROUCH_FACTOR_OFF, 1488),
|
||||
idle_prone_factor: f32_at_iw5(stream, body, sz::WEAPON_DEF_IDLE_PRONE_FACTOR_OFF, 1492),
|
||||
};
|
||||
facts.offhand_class = i32_at_iw5(stream, body, sz::WEAPON_DEF_OFFHAND_CLASS_OFF, 104);
|
||||
facts.hold_fire_time_ms = i32_at_iw5(stream, body, sz::WEAPON_DEF_HOLD_FIRE_TIME_OFF, 948);
|
||||
@@ -4033,17 +4016,17 @@ fn read_iw5_combat_fx(
|
||||
let Some(body) = geometry.weap_def else {
|
||||
return WeaponCombatFx::default();
|
||||
};
|
||||
let fx_name = |x86, x64| match stream.ptr_at(body, stream.layout(x86, x64)) {
|
||||
let name = |x86, x64| match stream.ptr_at(body, stream.layout(x86, x64)) {
|
||||
Ok(fastfile_iw5::ZonePtr::Offset(q)) => fx_name_at_slot(stream.resolve_alias(q)),
|
||||
_ => None,
|
||||
};
|
||||
let view_last_shot_eject_hint = fx_name(sz::WEAPON_DEF_VIEW_LAST_SHOT_EJECT_OFF, 544);
|
||||
let world_last_shot_eject_hint = fx_name(sz::WEAPON_DEF_WORLD_LAST_SHOT_EJECT_OFF, 552);
|
||||
let view_last_shot_eject_hint = name(sz::WEAPON_DEF_VIEW_LAST_SHOT_EJECT_OFF, 544);
|
||||
let world_last_shot_eject_hint = name(sz::WEAPON_DEF_WORLD_LAST_SHOT_EJECT_OFF, 552);
|
||||
WeaponCombatFx {
|
||||
view_flash_hint: fx_name(sz::WEAPON_DEF_VIEW_FLASH_OFF, 112),
|
||||
world_flash_hint: fx_name(sz::WEAPON_DEF_WORLD_FLASH_OFF, 120),
|
||||
view_shell_eject_hint: fx_name(sz::WEAPON_DEF_VIEW_SHELL_EJECT_OFF, 528),
|
||||
world_shell_eject_hint: fx_name(sz::WEAPON_DEF_WORLD_SHELL_EJECT_OFF, 536),
|
||||
view_flash_hint: name(sz::WEAPON_DEF_VIEW_FLASH_OFF, 112),
|
||||
world_flash_hint: name(sz::WEAPON_DEF_WORLD_FLASH_OFF, 120),
|
||||
view_shell_eject_hint: name(sz::WEAPON_DEF_VIEW_SHELL_EJECT_OFF, 528),
|
||||
world_shell_eject_hint: name(sz::WEAPON_DEF_WORLD_SHELL_EJECT_OFF, 536),
|
||||
last_shot_eject_pair_authored: view_last_shot_eject_hint.is_some()
|
||||
&& world_last_shot_eject_hint.is_some(),
|
||||
view_last_shot_eject_hint,
|
||||
@@ -4058,7 +4041,7 @@ fn read_iw5_fx_overrides(
|
||||
fx_name_at_slot: &dyn Fn(fastfile_iw5::Ptr) -> Option<String>,
|
||||
) -> Vec<Iw5FxOverride> {
|
||||
geometry
|
||||
.fx_overrides(stream)
|
||||
.overrides(stream)
|
||||
.map(|row| Iw5FxOverride {
|
||||
attachment1: row.attachment1,
|
||||
attachment2: row.attachment2,
|
||||
@@ -4516,60 +4499,60 @@ fn stance_ofs_captured(duck: &[f32; 3], prone: &[f32; 3]) -> bool {
|
||||
}
|
||||
|
||||
fn movement_ofs_captured(m: &WeaponMovementOfsInputs) -> bool {
|
||||
m.stand_move_at_0x138.iter().any(|v| *v != 0.0)
|
||||
|| m.stand_rot_at_0x144.iter().any(|v| *v != 0.0)
|
||||
|| m.strafe_move_at_0x150.iter().any(|v| *v != 0.0)
|
||||
|| m.strafe_rot_at_0x15c.iter().any(|v| *v != 0.0)
|
||||
|| m.ducked_move_at_0x174.iter().any(|v| *v != 0.0)
|
||||
|| m.ducked_rot_at_0x180.iter().any(|v| *v != 0.0)
|
||||
|| m.prone_move_at_0x198.iter().any(|v| *v != 0.0)
|
||||
|| m.prone_rot_at_0x1a4.iter().any(|v| *v != 0.0)
|
||||
|| m.pos_move_rate_at_0x1b0 != 0.0
|
||||
|| m.pos_prone_move_rate_at_0x1b4 != 0.0
|
||||
|| m.stand_move_min_speed_at_0x1b8 != 0.0
|
||||
|| m.ducked_move_min_speed_at_0x1bc != 0.0
|
||||
|| m.prone_move_min_speed_at_0x1c0 != 0.0
|
||||
|| m.pos_rot_rate_at_0x1c4 != 0.0
|
||||
|| m.pos_prone_rot_rate_at_0x1c8 != 0.0
|
||||
m.stand_move.iter().any(|v| *v != 0.0)
|
||||
|| m.stand_rot.iter().any(|v| *v != 0.0)
|
||||
|| m.strafe_move.iter().any(|v| *v != 0.0)
|
||||
|| m.strafe_rot.iter().any(|v| *v != 0.0)
|
||||
|| m.ducked_move.iter().any(|v| *v != 0.0)
|
||||
|| m.ducked_rot.iter().any(|v| *v != 0.0)
|
||||
|| m.prone_move.iter().any(|v| *v != 0.0)
|
||||
|| m.prone_rot.iter().any(|v| *v != 0.0)
|
||||
|| m.pos_move_rate != 0.0
|
||||
|| m.pos_prone_move_rate != 0.0
|
||||
|| m.stand_move_min_speed != 0.0
|
||||
|| m.ducked_move_min_speed != 0.0
|
||||
|| m.prone_move_min_speed != 0.0
|
||||
|| m.pos_rot_rate != 0.0
|
||||
|| m.pos_prone_rot_rate != 0.0
|
||||
}
|
||||
|
||||
fn movement_from_capture(c: WeaponMovementOfsCapture) -> WeaponMovementOfsInputs {
|
||||
WeaponMovementOfsInputs {
|
||||
stand_move_at_0x138: c.stand_move_at_0x138,
|
||||
stand_rot_at_0x144: c.stand_rot_at_0x144,
|
||||
strafe_move_at_0x150: c.strafe_move_at_0x150,
|
||||
strafe_rot_at_0x15c: c.strafe_rot_at_0x15c,
|
||||
ducked_move_at_0x174: c.ducked_move_at_0x174,
|
||||
ducked_rot_at_0x180: c.ducked_rot_at_0x180,
|
||||
prone_move_at_0x198: c.prone_move_at_0x198,
|
||||
prone_rot_at_0x1a4: c.prone_rot_at_0x1a4,
|
||||
pos_move_rate_at_0x1b0: c.pos_move_rate_at_0x1b0,
|
||||
pos_prone_move_rate_at_0x1b4: c.pos_prone_move_rate_at_0x1b4,
|
||||
stand_move_min_speed_at_0x1b8: c.stand_move_min_speed_at_0x1b8,
|
||||
ducked_move_min_speed_at_0x1bc: c.ducked_move_min_speed_at_0x1bc,
|
||||
prone_move_min_speed_at_0x1c0: c.prone_move_min_speed_at_0x1c0,
|
||||
pos_rot_rate_at_0x1c4: c.pos_rot_rate_at_0x1c4,
|
||||
pos_prone_rot_rate_at_0x1c8: c.pos_prone_rot_rate_at_0x1c8,
|
||||
stand_move: c.stand_move,
|
||||
stand_rot: c.stand_rot,
|
||||
strafe_move: c.strafe_move,
|
||||
strafe_rot: c.strafe_rot,
|
||||
ducked_move: c.ducked_move,
|
||||
ducked_rot: c.ducked_rot,
|
||||
prone_move: c.prone_move,
|
||||
prone_rot: c.prone_rot,
|
||||
pos_move_rate: c.pos_move_rate,
|
||||
pos_prone_move_rate: c.pos_prone_move_rate,
|
||||
stand_move_min_speed: c.stand_move_min_speed,
|
||||
ducked_move_min_speed: c.ducked_move_min_speed,
|
||||
prone_move_min_speed: c.prone_move_min_speed,
|
||||
pos_rot_rate: c.pos_rot_rate,
|
||||
pos_prone_rot_rate: c.pos_prone_rot_rate,
|
||||
}
|
||||
}
|
||||
|
||||
fn idle_captured(i: &WeaponIdleInputs) -> bool {
|
||||
i.ads_idle_amount_at_0x36c != 0.0
|
||||
|| i.hip_idle_amount_at_0x370 != 0.0
|
||||
|| i.ads_idle_speed_at_0x374 != 0.0
|
||||
|| i.hip_idle_speed_at_0x378 != 0.0
|
||||
|| i.idle_crouch_factor_at_0x37c != 0.0
|
||||
|| i.idle_prone_factor_at_0x380 != 0.0
|
||||
i.ads_idle_amount != 0.0
|
||||
|| i.hip_idle_amount != 0.0
|
||||
|| i.ads_idle_speed != 0.0
|
||||
|| i.hip_idle_speed != 0.0
|
||||
|| i.idle_crouch_factor != 0.0
|
||||
|| i.idle_prone_factor != 0.0
|
||||
}
|
||||
|
||||
fn idle_from_capture(c: WeaponIdleCapture) -> WeaponIdleInputs {
|
||||
WeaponIdleInputs {
|
||||
ads_idle_amount_at_0x36c: c.ads_idle_amount_at_0x36c,
|
||||
hip_idle_amount_at_0x370: c.hip_idle_amount_at_0x370,
|
||||
ads_idle_speed_at_0x374: c.ads_idle_speed_at_0x374,
|
||||
hip_idle_speed_at_0x378: c.hip_idle_speed_at_0x378,
|
||||
idle_crouch_factor_at_0x37c: c.idle_crouch_factor_at_0x37c,
|
||||
idle_prone_factor_at_0x380: c.idle_prone_factor_at_0x380,
|
||||
ads_idle_amount: c.ads_idle_amount,
|
||||
hip_idle_amount: c.hip_idle_amount,
|
||||
ads_idle_speed: c.ads_idle_speed,
|
||||
hip_idle_speed: c.hip_idle_speed,
|
||||
idle_crouch_factor: c.idle_crouch_factor,
|
||||
idle_prone_factor: c.idle_prone_factor,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4621,11 +4604,11 @@ fn merge_body_facts(dst: &mut WeaponBodyFacts, src: WeaponBodyFacts) {
|
||||
if !sway_body_captured(&dst.sway) && sway_body_captured(&src.sway) {
|
||||
dst.sway = src.sway;
|
||||
}
|
||||
if !stance_ofs_captured(&dst.stance_ofs_at_0x168, &dst.stance_ofs_at_0x18c)
|
||||
&& stance_ofs_captured(&src.stance_ofs_at_0x168, &src.stance_ofs_at_0x18c)
|
||||
if !stance_ofs_captured(&dst.ducked_ofs, &dst.prone_ofs)
|
||||
&& stance_ofs_captured(&src.ducked_ofs, &src.prone_ofs)
|
||||
{
|
||||
dst.stance_ofs_at_0x168 = src.stance_ofs_at_0x168;
|
||||
dst.stance_ofs_at_0x18c = src.stance_ofs_at_0x18c;
|
||||
dst.ducked_ofs = src.ducked_ofs;
|
||||
dst.prone_ofs = src.prone_ofs;
|
||||
}
|
||||
if dst.night_vision_wear_time == 0 {
|
||||
dst.night_vision_wear_time = src.night_vision_wear_time;
|
||||
@@ -4637,12 +4620,12 @@ fn merge_body_facts(dst: &mut WeaponBodyFacts, src: WeaponBodyFacts) {
|
||||
if !idle_captured(&dst.idle) && idle_captured(&src.idle) {
|
||||
dst.idle = src.idle;
|
||||
}
|
||||
if dst.select_requires_ammo_at_0x667.is_none() {
|
||||
dst.select_requires_ammo_at_0x667 = src.select_requires_ammo_at_0x667;
|
||||
if dst.select_requires_ammo.is_none() {
|
||||
dst.select_requires_ammo = src.select_requires_ammo;
|
||||
dst.quick_drop_time_ms = src.quick_drop_time_ms;
|
||||
}
|
||||
if dst.offhand_hold_is_cancelable_at_0x681.is_none() {
|
||||
dst.offhand_hold_is_cancelable_at_0x681 = src.offhand_hold_is_cancelable_at_0x681;
|
||||
if dst.offhand_hold_is_cancelable.is_none() {
|
||||
dst.offhand_hold_is_cancelable = src.offhand_hold_is_cancelable;
|
||||
}
|
||||
if dst.drop_time_ms == 0 {
|
||||
dst.drop_time_ms = src.drop_time_ms;
|
||||
@@ -5054,6 +5037,9 @@ pub struct WeaponRegistry {
|
||||
alternate_fpv: HashMap<(u32, u32), [Option<Result<crate::FpvSideAssemblies, String>>; 2]>,
|
||||
fpv_clip_tracks: Arc<crate::FpvClipTracks>,
|
||||
|
||||
vehicle_turrets: HashMap<String, String>,
|
||||
vehicle_compass: HashMap<String, ([String; 2], [i32; 2])>,
|
||||
|
||||
revision: u64,
|
||||
}
|
||||
|
||||
@@ -5202,7 +5188,13 @@ impl WeaponBuild {
|
||||
census
|
||||
}
|
||||
|
||||
pub fn absorb(&mut self, other: Self) {
|
||||
pub fn absorb(&mut self, mut other: Self) {
|
||||
self.registry
|
||||
.vehicle_compass
|
||||
.extend(std::mem::take(&mut other.registry.vehicle_compass));
|
||||
self.registry
|
||||
.vehicle_turrets
|
||||
.extend(std::mem::take(&mut other.registry.vehicle_turrets));
|
||||
if other.registry.is_empty() {
|
||||
self.registry
|
||||
.iw5_attachments
|
||||
@@ -5211,6 +5203,8 @@ impl WeaponBuild {
|
||||
return;
|
||||
}
|
||||
if self.registry.rows.is_empty() {
|
||||
other.registry.vehicle_turrets = std::mem::take(&mut self.registry.vehicle_turrets);
|
||||
other.registry.vehicle_compass = std::mem::take(&mut self.registry.vehicle_compass);
|
||||
*self = other;
|
||||
return;
|
||||
}
|
||||
@@ -5312,23 +5306,20 @@ impl WeaponBuild {
|
||||
pub fn resolve_projectile_fx_edges(&mut self, fx: &crate::FxCatalog) {
|
||||
for row in &mut self.registry.rows {
|
||||
row.projectile_fx = WeaponProjectileFx {
|
||||
trail: fx_hint_edge(
|
||||
trail: fx.hint_edge(
|
||||
row.proj_trail_from_slot,
|
||||
row.proj_trail.as_deref(),
|
||||
row.namespace,
|
||||
fx,
|
||||
),
|
||||
beacon: fx_hint_edge(
|
||||
beacon: fx.hint_edge(
|
||||
row.proj_beacon_from_slot,
|
||||
row.proj_beacon.as_deref(),
|
||||
row.namespace,
|
||||
fx,
|
||||
),
|
||||
ignition: fx_hint_edge(
|
||||
ignition: fx.hint_edge(
|
||||
row.proj_ignition_from_slot,
|
||||
row.proj_ignition.as_deref(),
|
||||
row.namespace,
|
||||
fx,
|
||||
),
|
||||
};
|
||||
}
|
||||
@@ -5338,14 +5329,14 @@ impl WeaponBuild {
|
||||
for row in &mut self.registry.rows {
|
||||
let mut edges = [AssetEdge::Absent; WEAPON_ANIM_SLOTS];
|
||||
for (edge, hint) in edges.iter_mut().zip(row.sz_xanims.iter()) {
|
||||
*edge = xanim_hint_edge(hint.as_deref(), row.namespace, xanims);
|
||||
*edge = xanims.hint_edge(hint.as_deref(), row.namespace);
|
||||
}
|
||||
row.sz_xanim_edges = edges;
|
||||
row.sz_xanim_right_edges = std::array::from_fn(|slot| {
|
||||
xanim_hint_edge(row.sz_xanims_right[slot].as_deref(), row.namespace, xanims)
|
||||
xanims.hint_edge(row.sz_xanims_right[slot].as_deref(), row.namespace)
|
||||
});
|
||||
row.sz_xanim_left_edges = std::array::from_fn(|slot| {
|
||||
xanim_hint_edge(row.sz_xanims_left[slot].as_deref(), row.namespace, xanims)
|
||||
xanims.hint_edge(row.sz_xanims_left[slot].as_deref(), row.namespace)
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -5957,6 +5948,8 @@ impl WeaponBuild {
|
||||
families: crate::WeaponFamilies::default(),
|
||||
alternate_fpv: HashMap::new(),
|
||||
fpv_clip_tracks: Arc::default(),
|
||||
vehicle_turrets: HashMap::new(),
|
||||
vehicle_compass: HashMap::new(),
|
||||
revision: mint_weapon_revision(),
|
||||
};
|
||||
registry.rebuild_name_maps();
|
||||
@@ -5969,6 +5962,19 @@ impl WeaponBuild {
|
||||
}
|
||||
|
||||
impl WeaponRegistry {
|
||||
pub fn vehicle_compass(&self) -> impl Iterator<Item = (&str, &[String; 2], [i32; 2])> {
|
||||
self.vehicle_compass
|
||||
.iter()
|
||||
.map(|(name, (icons, size))| (name.as_str(), icons, *size))
|
||||
}
|
||||
|
||||
pub fn vehicle_turrets(&self) -> Vec<(String, String)> {
|
||||
self.vehicle_turrets
|
||||
.iter()
|
||||
.map(|(vehicle, weapon)| (vehicle.clone(), weapon.clone()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn world_catalog_identity(&self) -> u64 {
|
||||
self.world_catalog_identity
|
||||
}
|
||||
@@ -6358,6 +6364,15 @@ impl WeaponRegistry {
|
||||
}
|
||||
}
|
||||
|
||||
if scope.is_some() || alternate {
|
||||
row.overlay_material = None;
|
||||
row.overlay_image = None;
|
||||
row.overlay_material_from_slot = false;
|
||||
row.facts.overlay_reticle = 0;
|
||||
row.facts.overlay_interface = 0;
|
||||
row.facts.ads_overlay_width = 0.0;
|
||||
row.facts.ads_overlay_height = 0.0;
|
||||
}
|
||||
if let Some(scope) = assets.iter().find(|asset| asset.overlay.is_some()) {
|
||||
if let Some(overlay) = scope
|
||||
.overlay
|
||||
@@ -6957,6 +6972,22 @@ impl WeaponRegistry {
|
||||
self.rows.get(index as usize).map(|row| row.facts)
|
||||
}
|
||||
|
||||
pub fn melee_weapon_of(&self, index: u32) -> u32 {
|
||||
let Some(row) = self.rows.get(index as usize) else {
|
||||
return index;
|
||||
};
|
||||
if row.namespace != crate::AssetNamespace::T5 || row.facts.use_as_melee {
|
||||
return index;
|
||||
}
|
||||
self.rows
|
||||
.iter()
|
||||
.position(|r| {
|
||||
r.namespace == crate::AssetNamespace::T5
|
||||
&& r.name.strip_suffix("_mp").unwrap_or(&r.name) == "knife"
|
||||
})
|
||||
.map_or(index, |i| i as u32)
|
||||
}
|
||||
|
||||
pub fn alternate_name_of(&self, index: u32) -> Option<&str> {
|
||||
self.rows
|
||||
.get(index as usize)?
|
||||
@@ -7075,6 +7106,21 @@ impl WeaponRegistry {
|
||||
gsc_weapon_script_name(self.name_of(index))
|
||||
}
|
||||
|
||||
pub fn world_models_table(&self) -> Vec<(String, Vec<String>)> {
|
||||
(0..self.rows.len() as u32)
|
||||
.map(|i| {
|
||||
let model = self.world_model_of(i).unwrap_or_default().to_owned();
|
||||
(model, self.hide_tags_of(i).to_vec())
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn projectile_models_table(&self) -> Vec<String> {
|
||||
(0..self.rows.len() as u32)
|
||||
.map(|i| self.projectile_model_of(i).unwrap_or_default().to_owned())
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn script_names_table(&self) -> Vec<String> {
|
||||
(0..self.rows.len())
|
||||
.map(|i| self.script_name_of(i as u32))
|
||||
@@ -7260,7 +7306,7 @@ impl WeaponRegistry {
|
||||
pub fn combat_fx_census(&self) -> AssetEdgeCensus {
|
||||
let mut census = AssetEdgeCensus::default();
|
||||
for row in self.rows.iter().skip(1) {
|
||||
for edge in row.combat_fx.fx_edges() {
|
||||
for edge in row.combat_fx.edges() {
|
||||
census.push(edge);
|
||||
}
|
||||
}
|
||||
@@ -7273,7 +7319,7 @@ impl WeaponRegistry {
|
||||
.skip(1)
|
||||
.filter(|row| {
|
||||
row.combat_fx
|
||||
.fx_edges()
|
||||
.edges()
|
||||
.iter()
|
||||
.any(|edge| edge.is_unresolved())
|
||||
})
|
||||
@@ -7616,8 +7662,8 @@ fn apply_iw5_parameter_blocks(facts: &mut WeaponBodyFacts, assets: &[&Iw5ScopeRo
|
||||
facts.ads_zoom_fov = ads.ads_zoom_fov;
|
||||
facts.ads_zoom_in_frac = ads.ads_zoom_in_frac;
|
||||
facts.ads_zoom_out_frac = ads.ads_zoom_out_frac;
|
||||
facts.ads_bob_factor_at_0x330 = ads.ads_bob_factor;
|
||||
facts.ads_view_bob_mult_at_0x334 = ads.ads_view_bob_mult;
|
||||
facts.ads_bob_factor = ads.ads_bob_factor;
|
||||
facts.ads_view_bob_mult = ads.ads_view_bob_mult;
|
||||
// Attachment transition times are seconds; the rates are per millisecond.
|
||||
if ads.ads_trans_in_time > 0.0 {
|
||||
facts.ads_in_rate = 1.0 / (ads.ads_trans_in_time * 1000.0);
|
||||
@@ -7694,14 +7740,14 @@ fn apply_iw5_parameter_blocks(facts: &mut WeaponBodyFacts, assets: &[&Iw5ScopeRo
|
||||
}
|
||||
|
||||
if let Some(idle) = iw5_first_block(assets, |a| a.idle_settings) {
|
||||
facts.idle.hip_idle_amount_at_0x370 = idle.hip_idle_amount;
|
||||
facts.idle.hip_idle_speed_at_0x378 = idle.hip_idle_speed;
|
||||
facts.idle.idle_crouch_factor_at_0x37c = idle.idle_crouch_factor;
|
||||
facts.idle.idle_prone_factor_at_0x380 = idle.idle_prone_factor;
|
||||
facts.idle.hip_idle_amount = idle.hip_idle_amount;
|
||||
facts.idle.hip_idle_speed = idle.hip_idle_speed;
|
||||
facts.idle.idle_crouch_factor = idle.idle_crouch_factor;
|
||||
facts.idle.idle_prone_factor = idle.idle_prone_factor;
|
||||
}
|
||||
let idle_scale = iw5_scale_product(assets, |s| s.idle_settings);
|
||||
facts.idle.hip_idle_amount_at_0x370 *= idle_scale;
|
||||
facts.idle.ads_idle_amount_at_0x36c *= idle_scale;
|
||||
facts.idle.hip_idle_amount *= idle_scale;
|
||||
facts.idle.ads_idle_amount *= idle_scale;
|
||||
|
||||
if let Some(spread) = iw5_first_block(assets, |a| a.hip_spread) {
|
||||
let v = spread.values;
|
||||
|
||||
@@ -56,15 +56,15 @@ pub use snd_alias::{
|
||||
SND_CURVE_DEFAULT_ASSET_NAME, SND_CURVE_EVAL_OUT_OF_RANGE, SND_CURVE_FILENAME,
|
||||
SND_CURVE_KNOT_COUNT, SND_CURVE_KNOT_STRIDE, SND_CURVE_KNOTS, SND_CURVE_MAX_KNOTS,
|
||||
SND_ENTCHANNEL_DEFAULT_MAX_VOICES, SND_ENTCHANNEL_FILE, SND_ENTCHANNEL_MAX, SndAliasFlags,
|
||||
SndAliasSampleKind, snd_attenuate, snd_curve_eval,
|
||||
SndAliasSampleKind, attenuate, curve_eval,
|
||||
};
|
||||
pub use snd_pick::{
|
||||
SND_LCG_ADD, SND_LCG_MUL, SND_LCG_UNIT_SCALE, lerp_range, pick_weighted_variant_index,
|
||||
snd_advance_lcg, snd_unit_random,
|
||||
SND_LCG_ADD, SND_LCG_MUL, SND_LCG_UNIT_SCALE, advance_lcg, lerp_range,
|
||||
pick_weighted_variant_index, unit_random,
|
||||
};
|
||||
pub use snd_voice::{
|
||||
SND_VOICE_FINISHED_FRACTION, SndVoiceOccupant, SndVoiceRequest, snd_entity_channel_matches,
|
||||
snd_has_free_voice, snd_pick_voice_slot, snd_voice_metric_2d,
|
||||
SND_VOICE_FINISHED_FRACTION, SndVoiceOccupant, SndVoiceRequest, entity_channel_matches,
|
||||
has_free_voice, pick_voice_slot, voice_metric_2d,
|
||||
};
|
||||
pub use state_bits::{
|
||||
AlphaTest, D3DCULL_CCW, D3DCULL_CW, D3DCULL_NONE, D3DRS_ALPHAFUNC, D3DRS_ALPHAREF,
|
||||
|
||||
@@ -145,7 +145,7 @@ impl SndAliasFlags {
|
||||
|
||||
pub const SND_CURVE_EVAL_OUT_OF_RANGE: f32 = -1.0;
|
||||
|
||||
pub fn snd_curve_eval(knots: &[[f32; 2]], frac: f32) -> f32 {
|
||||
pub fn curve_eval(knots: &[[f32; 2]], frac: f32) -> f32 {
|
||||
let n = knots.len();
|
||||
if n <= 1 {
|
||||
return SND_CURVE_EVAL_OUT_OF_RANGE;
|
||||
@@ -164,7 +164,7 @@ pub fn snd_curve_eval(knots: &[[f32; 2]], frac: f32) -> f32 {
|
||||
(y1 - y0) * ((frac - x0) / (x1 - x0)) + y0
|
||||
}
|
||||
|
||||
pub fn snd_attenuate(knots: &[[f32; 2]], dist: f32, dist_min: f32, dist_max: f32) -> f32 {
|
||||
pub fn attenuate(knots: &[[f32; 2]], dist: f32, dist_min: f32, dist_max: f32) -> f32 {
|
||||
let delta = dist - dist_min;
|
||||
if delta <= 0.0 {
|
||||
return 1.0;
|
||||
@@ -173,5 +173,5 @@ pub fn snd_attenuate(knots: &[[f32; 2]], dist: f32, dist_min: f32, dist_max: f32
|
||||
if frac >= 1.0 {
|
||||
return 0.0;
|
||||
}
|
||||
snd_curve_eval(knots, frac)
|
||||
curve_eval(knots, frac)
|
||||
}
|
||||
|
||||
@@ -4,12 +4,12 @@ pub const SND_LCG_ADD: u32 = 0x269ec3;
|
||||
|
||||
pub const SND_LCG_UNIT_SCALE: f32 = 32768.0;
|
||||
|
||||
pub fn snd_advance_lcg(state: &mut u32) {
|
||||
pub fn advance_lcg(state: &mut u32) {
|
||||
*state = state.wrapping_mul(SND_LCG_MUL).wrapping_add(SND_LCG_ADD);
|
||||
}
|
||||
|
||||
pub fn snd_unit_random(state: &mut u32) -> f32 {
|
||||
snd_advance_lcg(state);
|
||||
pub fn unit_random(state: &mut u32) -> f32 {
|
||||
advance_lcg(state);
|
||||
((*state >> 16) & 0x7fff) as f32 / SND_LCG_UNIT_SCALE
|
||||
}
|
||||
|
||||
@@ -35,9 +35,9 @@ pub fn pick_weighted_variant_index(weights: &[f32], rng: &mut u32, avoid: Option
|
||||
.map(|(i, &w)| if avoid == Some(i) && n > 1 { 0.0 } else { w })
|
||||
.sum();
|
||||
if total <= 1e-8 {
|
||||
return ((snd_unit_random(rng) * n as f32) as usize).min(n - 1);
|
||||
return ((unit_random(rng) * n as f32) as usize).min(n - 1);
|
||||
}
|
||||
let mut cursor = snd_unit_random(rng) * total;
|
||||
let mut cursor = unit_random(rng) * total;
|
||||
for (index, &weight) in weights.iter().enumerate() {
|
||||
let w = if avoid == Some(index) && n > 1 {
|
||||
0.0
|
||||
|
||||
@@ -20,18 +20,18 @@ pub struct SndVoiceRequest {
|
||||
pub has_subtitle: bool,
|
||||
}
|
||||
|
||||
pub fn snd_voice_metric_2d(volume: f32) -> f32 {
|
||||
pub fn voice_metric_2d(volume: f32) -> f32 {
|
||||
-volume
|
||||
}
|
||||
|
||||
pub fn snd_has_free_voice(voice_count: i32, loading_streams: i32, max_voices: i32) -> bool {
|
||||
pub fn has_free_voice(voice_count: i32, loading_streams: i32, max_voices: i32) -> bool {
|
||||
if max_voices <= 0 {
|
||||
return false;
|
||||
}
|
||||
voice_count.saturating_add(loading_streams) < max_voices
|
||||
}
|
||||
|
||||
pub fn snd_entity_channel_matches(
|
||||
pub fn entity_channel_matches(
|
||||
occupant_snd_ent: u32,
|
||||
occupant_channel: u32,
|
||||
snd_ent: u32,
|
||||
@@ -40,10 +40,7 @@ pub fn snd_entity_channel_matches(
|
||||
occupant_snd_ent == snd_ent && occupant_channel == channel
|
||||
}
|
||||
|
||||
pub fn snd_pick_voice_slot(
|
||||
request: &SndVoiceRequest,
|
||||
occupants: &[SndVoiceOccupant],
|
||||
) -> Option<usize> {
|
||||
pub fn pick_voice_slot(request: &SndVoiceRequest, occupants: &[SndVoiceOccupant]) -> Option<usize> {
|
||||
let mut bar_prio = request.priority;
|
||||
let mut best_metric = request.metric;
|
||||
let mut replaceable = None;
|
||||
|
||||
@@ -396,6 +396,7 @@ pub struct AuthoredMaterial {
|
||||
pub texture_atlas: Option<[u8; 2]>,
|
||||
|
||||
pub surface_type_bits: Option<u32>,
|
||||
pub t5_layered_surface_types: Option<u32>,
|
||||
|
||||
pub state_flags: u8,
|
||||
|
||||
@@ -1479,6 +1480,7 @@ impl MaterialCatalog {
|
||||
info_game_flags: geometry.info_game_flags,
|
||||
texture_atlas: Some(geometry.texture_atlas),
|
||||
surface_type_bits: geometry.surface_type_bits,
|
||||
t5_layered_surface_types: None,
|
||||
state_flags: geometry.state_flags,
|
||||
camera_region: geometry.camera_region,
|
||||
state_bits: read_state_bits(
|
||||
@@ -2023,6 +2025,16 @@ fn remap_t5_owned_shader_argument(argument: OwnedShaderArgument) -> Option<Owned
|
||||
row_count,
|
||||
}
|
||||
}),
|
||||
OwnedShaderArgument::CodePixelSampler { destination, index }
|
||||
if (0x1d..=0x20).contains(&index) =>
|
||||
{
|
||||
Some(OwnedShaderArgument::MaterialPixelSampler {
|
||||
destination,
|
||||
name_hash: crate::t5_code_remap::terrain_scorch_binding_hash(
|
||||
(index - 0x1d) as usize,
|
||||
),
|
||||
})
|
||||
}
|
||||
OwnedShaderArgument::CodePixelSampler { destination, index } => {
|
||||
crate::t5_code_remap::remap_code_texture_index(index)
|
||||
.map(|index| OwnedShaderArgument::CodePixelSampler { destination, index })
|
||||
@@ -2445,6 +2457,7 @@ impl MaterialCatalog {
|
||||
info_game_flags: geometry.info_game_flags,
|
||||
texture_atlas: None,
|
||||
surface_type_bits: Some(geometry.surface_type_bits),
|
||||
t5_layered_surface_types: Some(geometry.layered_surface_types),
|
||||
state_flags: geometry.state_flags,
|
||||
camera_region: geometry.camera_region,
|
||||
state_bits: read_state_bits(
|
||||
@@ -2774,6 +2787,7 @@ impl MaterialCatalog {
|
||||
info_game_flags,
|
||||
texture_atlas: None,
|
||||
surface_type_bits: None,
|
||||
t5_layered_surface_types: None,
|
||||
state_flags,
|
||||
camera_region,
|
||||
state_bits: read_state_bits(
|
||||
|
||||
@@ -1248,13 +1248,7 @@ pub fn decode_ui_image(
|
||||
let Some(main) = trees.main_for(key.namespace) else {
|
||||
return Ok(None);
|
||||
};
|
||||
if let Some(image) = decode_ui_image_from_main(main, &key.name)? {
|
||||
return Ok(Some(image));
|
||||
}
|
||||
if let Some(zone) = key.name.strip_prefix("preview_mp_") {
|
||||
return decode_ui_image_from_main(main, &format!("loadscreen_mp_{zone}"));
|
||||
}
|
||||
return Ok(None);
|
||||
return decode_ui_image_from_main(main, &key.name);
|
||||
}
|
||||
let name = crate::AssetRef::bare_name(image_name);
|
||||
for main in ui_decode_mains(games_root) {
|
||||
@@ -1272,18 +1266,28 @@ pub fn decode_ui_image_from_main(
|
||||
) -> Result<Option<(u32, u32, Vec<u8>)>, String> {
|
||||
let name = crate::AssetRef::bare_name(image_name);
|
||||
let index = IwdIndex::open(main)?;
|
||||
match index.decode(name).map(expand_top_level) {
|
||||
Some(Ok(image)) => Ok(Some(image)),
|
||||
Some(Err(error)) => {
|
||||
diag::warn!(
|
||||
Zone,
|
||||
"ui-image: decode {name} from {}: {error}",
|
||||
main.display()
|
||||
);
|
||||
Ok(None)
|
||||
let fallback = name
|
||||
.strip_prefix("preview_")
|
||||
.map(|map| format!("loadscreen_{map}"))
|
||||
.or_else(|| {
|
||||
name.strip_prefix("menu_mp_map_select_")
|
||||
.and_then(|map| map.strip_suffix("_big"))
|
||||
.map(|map| format!("loadscreen_mp_{map}"))
|
||||
});
|
||||
for candidate in std::iter::once(name).chain(fallback.as_deref()) {
|
||||
match index.decode(candidate).map(expand_top_level) {
|
||||
Some(Ok(image)) => return Ok(Some(image)),
|
||||
Some(Err(error)) => {
|
||||
diag::warn!(
|
||||
Zone,
|
||||
"ui-image: decode {candidate} from {}: {error}",
|
||||
main.display()
|
||||
);
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
None => Ok(None),
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn ui_decode_mains(games_root: &Path) -> Vec<PathBuf> {
|
||||
@@ -1959,7 +1963,7 @@ fn decode_blocks(
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
pub fn retail_lit_color(albedo_rgb: [f32; 3], lighting: [f32; 3]) -> [f32; 3] {
|
||||
pub fn lit_color(albedo_rgb: [f32; 3], lighting: [f32; 3]) -> [f32; 3] {
|
||||
let albedo = lighting_iw4::lit_albedo(albedo_rgb, [1.0, 1.0, 1.0]);
|
||||
lighting_iw4::lit_fragment_color(albedo, lighting, [0.0, 0.0, 0.0])
|
||||
}
|
||||
@@ -2048,7 +2052,7 @@ pub fn decode_dxt5nm_xy(rgba: [f32; 4]) -> [f32; 2] {
|
||||
]
|
||||
}
|
||||
|
||||
pub fn retail_lightmap_bake(
|
||||
pub fn lightmap_bake(
|
||||
page0_rgb: [f32; 3],
|
||||
page1_rgb: [f32; 3],
|
||||
lm_dir: [f32; 2],
|
||||
|
||||
@@ -736,3 +736,9 @@ pub fn remap_code_texture_index(t5: u32) -> Option<u32> {
|
||||
.flatten()
|
||||
.map(u32::from)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn terrain_scorch_binding_hash(layer: usize) -> u32 {
|
||||
let name = format!("iw4l_t5_terrain_scorch_{layer}");
|
||||
crate::fnv1a64(name.as_bytes()) as u32
|
||||
}
|
||||
|
||||
@@ -36,10 +36,7 @@ pub fn xmodel_lod_for_dist(num_lods: u8, lod_dist: [f32; 4], dist: f32) -> Optio
|
||||
None
|
||||
}
|
||||
|
||||
pub fn dobj_has_lod_for_dist(
|
||||
submodels: impl IntoIterator<Item = (u8, [f32; 4])>,
|
||||
dist: f32,
|
||||
) -> bool {
|
||||
pub fn has_lod_for_dist(submodels: impl IntoIterator<Item = (u8, [f32; 4])>, dist: f32) -> bool {
|
||||
let mut saw = false;
|
||||
for (num_lods, lod_dist) in submodels {
|
||||
saw = true;
|
||||
@@ -210,6 +207,20 @@ impl SharedXModelSurfaces {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn retain_iw5(
|
||||
&mut self,
|
||||
stream: &fastfile_iw5::ZoneStream<'_>,
|
||||
geometry: fastfile_iw5::XModelGeometry,
|
||||
skel: std::sync::Arc<ModelSkel>,
|
||||
) {
|
||||
if geometry.surfaces.is_some()
|
||||
&& let Some(name) = geometry.surfaces_name.and_then(|p| stream.cstr(p).ok())
|
||||
&& !name.starts_with(',')
|
||||
{
|
||||
self.0.insert(name.to_owned(), (skel, 0));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn capture_xmodel_skel_with_shared(
|
||||
@@ -1371,7 +1382,7 @@ pub fn capture_fpv_skel_iw5(
|
||||
if model_kind(&name) != Some(ModelKind::Fpv) {
|
||||
return None;
|
||||
}
|
||||
capture_model_skel_iw5(stream, strings, geometry, name, materials)
|
||||
capture_model_skel_iw5(stream, strings, geometry, name, materials, None)
|
||||
}
|
||||
|
||||
pub fn capture_world_weapon_skel_iw5(
|
||||
@@ -1384,7 +1395,7 @@ pub fn capture_world_weapon_skel_iw5(
|
||||
if model_kind(&name) != Some(ModelKind::WorldWeapon) {
|
||||
return None;
|
||||
}
|
||||
capture_model_skel_iw5(stream, strings, geometry, name, materials)
|
||||
capture_model_skel_iw5(stream, strings, geometry, name, materials, None)
|
||||
}
|
||||
|
||||
pub fn capture_xmodel_skel_iw5(
|
||||
@@ -1394,7 +1405,25 @@ pub fn capture_xmodel_skel_iw5(
|
||||
materials: &MaterialCatalog,
|
||||
) -> Option<ModelSkel> {
|
||||
let name = geometry.name.and_then(|p| stream.cstr(p).ok())?.to_owned();
|
||||
capture_model_skel_iw5(stream, strings, geometry, name, Some(materials))
|
||||
capture_model_skel_iw5(stream, strings, geometry, name, Some(materials), None)
|
||||
}
|
||||
|
||||
pub fn capture_xmodel_skel_iw5_with_shared(
|
||||
stream: &fastfile_iw5::ZoneStream<'_>,
|
||||
strings: &fastfile_iw5::ScriptStrings,
|
||||
geometry: fastfile_iw5::XModelGeometry,
|
||||
materials: &MaterialCatalog,
|
||||
shared: &SharedXModelSurfaces,
|
||||
) -> Option<ModelSkel> {
|
||||
let name = geometry.name.and_then(|p| stream.cstr(p).ok())?.to_owned();
|
||||
capture_model_skel_iw5(
|
||||
stream,
|
||||
strings,
|
||||
geometry,
|
||||
name,
|
||||
Some(materials),
|
||||
Some(shared),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn capture_body_skel_iw5(
|
||||
@@ -1407,7 +1436,7 @@ pub fn capture_body_skel_iw5(
|
||||
if model_kind(&name) != Some(ModelKind::Soldier) {
|
||||
return None;
|
||||
}
|
||||
capture_model_skel_iw5(stream, strings, geometry, name, materials)
|
||||
capture_model_skel_iw5(stream, strings, geometry, name, materials, None)
|
||||
}
|
||||
|
||||
fn capture_model_skel_iw5(
|
||||
@@ -1416,10 +1445,10 @@ fn capture_model_skel_iw5(
|
||||
geometry: fastfile_iw5::XModelGeometry,
|
||||
name: String,
|
||||
materials: Option<&MaterialCatalog>,
|
||||
shared: Option<&SharedXModelSurfaces>,
|
||||
) -> Option<ModelSkel> {
|
||||
use fastfile_iw5::size as iw5sz;
|
||||
|
||||
let surfaces = geometry.surfaces?;
|
||||
let bone_names = geometry.bone_names?;
|
||||
let base_mat = geometry.base_mat?;
|
||||
|
||||
@@ -1455,6 +1484,43 @@ fn capture_model_skel_iw5(
|
||||
|
||||
let num_child = geometry.num_bones.saturating_sub(geometry.num_root_bones);
|
||||
let pose = capture_pose_src_iw5(stream, &geometry, &bone_name_strs, &bones, num_child);
|
||||
let material_at = |surface_index: usize| {
|
||||
geometry.material_handles.and_then(|handles| {
|
||||
materials?.material_index(Ptr {
|
||||
block: handles.block,
|
||||
offset: handles.offset + (surface_index * stream.pointer_bytes()) as u32,
|
||||
})
|
||||
})
|
||||
};
|
||||
|
||||
let Some(surfaces) = geometry.surfaces else {
|
||||
let surfaces_name = stream
|
||||
.cstr(geometry.surfaces_name?)
|
||||
.ok()?
|
||||
.trim_start_matches(',');
|
||||
let (source, _) = shared?.0.get(surfaces_name)?;
|
||||
if source.surface_vertex_ranges.len() != geometry.surface_count
|
||||
|| source.vert_skin.iter().any(|skin| {
|
||||
skin.bones
|
||||
.iter()
|
||||
.zip(skin.weights)
|
||||
.any(|(bone, weight)| weight != 0.0 && usize::from(*bone) >= geometry.num_bones)
|
||||
})
|
||||
{
|
||||
return None;
|
||||
}
|
||||
return Some(ModelSkel {
|
||||
name,
|
||||
bones,
|
||||
bone_names: bone_name_strs,
|
||||
tag_view,
|
||||
tag_weapon,
|
||||
pose,
|
||||
surface_materials: (0..geometry.surface_count).map(material_at).collect(),
|
||||
radius: geometry.radius,
|
||||
..(**source).clone()
|
||||
});
|
||||
};
|
||||
|
||||
let mut positions = Vec::new();
|
||||
let mut normals = Vec::new();
|
||||
@@ -1547,12 +1613,7 @@ fn capture_model_skel_iw5(
|
||||
}
|
||||
surface_vertex_ranges.push((base, vertex_count));
|
||||
surface_index_ranges.push((index_start, indices.len() - index_start));
|
||||
surface_materials.push(geometry.material_handles.and_then(|handles| {
|
||||
materials?.material_index(Ptr {
|
||||
block: handles.block,
|
||||
offset: handles.offset + (surface_index * stream.pointer_bytes()) as u32,
|
||||
})
|
||||
}));
|
||||
surface_materials.push(material_at(surface_index));
|
||||
}
|
||||
|
||||
Some(ModelSkel {
|
||||
|
||||
@@ -416,7 +416,7 @@ fn find_zone_stem(
|
||||
match find_stem_file(root, game, &prefixed) {
|
||||
Ok(mut found) => {
|
||||
let note = format!(
|
||||
"zone alias: `{stem}` → `{prefixed}` (MP stem preferred over bare `{stem}.ff`)"
|
||||
"zone alias: `{stem}` -> `{prefixed}` (MP stem preferred over bare `{stem}.ff`)"
|
||||
);
|
||||
found.alias_note = Some(note.clone());
|
||||
match std::fs::metadata(&found.path) {
|
||||
@@ -593,8 +593,23 @@ pub fn games_content_report(root: &GamesRoot) -> Vec<String> {
|
||||
report
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
||||
pub struct MapPack {
|
||||
pub label: String,
|
||||
pub maps: Vec<String>,
|
||||
}
|
||||
|
||||
pub fn list_mp_maps(root: &GamesRoot) -> Vec<String> {
|
||||
let mut maps = Vec::new();
|
||||
let mut maps: Vec<String> = list_mp_map_packs(root)
|
||||
.into_iter()
|
||||
.flat_map(|pack| pack.maps)
|
||||
.collect();
|
||||
maps.sort();
|
||||
maps
|
||||
}
|
||||
|
||||
pub fn list_mp_map_packs(root: &GamesRoot) -> Vec<MapPack> {
|
||||
let mut zones = Vec::new();
|
||||
for entry in game_files(&root.0) {
|
||||
let path = match entry {
|
||||
Ok(path) => path,
|
||||
@@ -610,16 +625,57 @@ pub fn list_mp_maps(root: &GamesRoot) -> Vec<String> {
|
||||
let is_ff = path
|
||||
.extension()
|
||||
.is_some_and(|ext| ext.eq_ignore_ascii_case("ff"));
|
||||
if !(is_ff && stem.starts_with("mp_") && !stem.ends_with("_load")) {
|
||||
if !(is_ff && stem.starts_with("mp_")) {
|
||||
continue;
|
||||
}
|
||||
if let Some(game) = zone_game_for_path(&path) {
|
||||
maps.push(format!("{}:{stem}", game.prefix()));
|
||||
zones.push((game.prefix(), stem, map_pack_folder(&path)));
|
||||
}
|
||||
}
|
||||
maps.sort();
|
||||
maps.dedup();
|
||||
maps
|
||||
let loads: HashSet<(&str, &str)> = zones
|
||||
.iter()
|
||||
.filter_map(|(game, stem, _)| stem.strip_suffix("_load").map(|map| (*game, map)))
|
||||
.collect();
|
||||
let mut packs: BTreeMap<(&str, &str), Vec<String>> = BTreeMap::new();
|
||||
let mut seen = HashSet::new();
|
||||
for (game, stem, folder) in &zones {
|
||||
if stem.ends_with("_load")
|
||||
|| !(loads.contains(&(*game, stem.as_str())) || !loads.iter().any(|(g, _)| g == game))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
let map = format!("{game}:{stem}");
|
||||
if seen.insert(map.clone()) {
|
||||
packs.entry((game, folder)).or_default().push(map);
|
||||
}
|
||||
}
|
||||
packs
|
||||
.into_iter()
|
||||
.map(|((game, folder), mut maps)| {
|
||||
maps.sort();
|
||||
MapPack {
|
||||
label: format!("{} {folder}", game.to_ascii_uppercase()),
|
||||
maps,
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn map_pack_folder(zone_ff: &Path) -> String {
|
||||
game_root_for_zone(zone_ff)
|
||||
.ok()
|
||||
.and_then(|tree| {
|
||||
zone_ff
|
||||
.strip_prefix(tree.join("zone"))
|
||||
.ok()?
|
||||
.components()
|
||||
.next()?
|
||||
.as_os_str()
|
||||
.to_str()
|
||||
.map(str::to_ascii_uppercase)
|
||||
})
|
||||
.filter(|folder| folder.starts_with("DLC"))
|
||||
.unwrap_or_else(|| "BASE".to_owned())
|
||||
}
|
||||
|
||||
pub fn ensure_artifacts_dir() -> Result<PathBuf, String> {
|
||||
|
||||
@@ -9,12 +9,12 @@ pub mod zone;
|
||||
pub use artifact_cache::{CacheFlight, cache_flight, cache_get, cache_put, fnv1a64, fnv1a64_more};
|
||||
pub use asset_core::ZoneGame;
|
||||
pub use discover::{
|
||||
GamesRoot, ZoneFile, ensure_artifacts_dir, find_common_mp_for_envelope,
|
||||
GamesRoot, MapPack, ZoneFile, ensure_artifacts_dir, find_common_mp_for_envelope,
|
||||
find_common_mp_for_zone, find_localized_common_mp_for_zone, find_runtime_common_mp,
|
||||
find_runtime_zone, find_zone_file, find_zone_file_version, find_zone_for_tree,
|
||||
game_root_for_zone, games_content_report, games_root_from_env, games_root_report,
|
||||
group_mp_maps, list_mp_maps, load_dotenv, map_load_title, peek_zone_version, split_zone_key,
|
||||
zone_game_for_path, zone_version,
|
||||
group_mp_maps, list_mp_map_packs, list_mp_maps, load_dotenv, map_load_title, peek_zone_version,
|
||||
split_zone_key, zone_game_for_path, zone_version,
|
||||
};
|
||||
pub use iwd::{
|
||||
IwdFile, IwdIndex, IwdSoundIndex, cached_iwd_dirs, game_main_for_zone, game_mains_under,
|
||||
|
||||
@@ -50,6 +50,8 @@ pub struct ClipCollision {
|
||||
|
||||
pub cmodels: Vec<ClipCmodel>,
|
||||
|
||||
pub trigger_models: Vec<Vec<crate::MapTriggerHull>>,
|
||||
|
||||
pub static_models: Vec<ClipPlacedStaticModel>,
|
||||
}
|
||||
|
||||
@@ -1724,6 +1726,15 @@ const T5_CBRUSH_SIDE: usize = 12;
|
||||
|
||||
const T5_CBRUSH_SIDE_SFLAGS: usize = 8;
|
||||
|
||||
// T5 mantle bits are shifted relative to IW4. Its mount bit is not mantle-over.
|
||||
fn t5_surface_flags(flags: u32) -> u32 {
|
||||
let mantle_on = flags & 0x0400_0000 != 0;
|
||||
let mantle_over = flags & 0x0800_0000 != 0;
|
||||
(flags & !0x1e00_0000)
|
||||
| if mantle_on { 0x0200_0000 } else { 0 }
|
||||
| if mantle_over { 0x0400_0000 } else { 0 }
|
||||
}
|
||||
|
||||
pub fn build_t5_clip_collision(
|
||||
s: &fastfile_t5::ZoneStream<'_>,
|
||||
g: fastfile_t5::ClipMapGeometry,
|
||||
@@ -1771,7 +1782,8 @@ pub fn build_t5_clip_collision(
|
||||
.map_err(|_| ClipCollisionError::Truncated)?;
|
||||
}
|
||||
let mut plane_surface_flags = Vec::with_capacity(6);
|
||||
plane_surface_flags.extend_from_slice(&t5_axial_plane_flags(axial_sflags));
|
||||
plane_surface_flags
|
||||
.extend_from_slice(&t5_axial_plane_flags(axial_sflags.map(t5_surface_flags)));
|
||||
|
||||
let numsides = s
|
||||
.u32_at(brush, T5_BRUSH_NUMSIDES)
|
||||
@@ -1781,10 +1793,10 @@ pub fn build_t5_clip_collision(
|
||||
let side = sides.at(j * T5_CBRUSH_SIDE);
|
||||
if let Some(plane) = read_t5_side_plane(s, side)? {
|
||||
planes.push(plane);
|
||||
plane_surface_flags.push(
|
||||
plane_surface_flags.push(t5_surface_flags(
|
||||
s.u32_at(side, T5_CBRUSH_SIDE_SFLAGS)
|
||||
.map_err(|_| ClipCollisionError::Truncated)?,
|
||||
);
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1864,7 +1876,9 @@ fn extract_t5_mesh_tables(
|
||||
let end = bytes.iter().position(|&b| b == 0).unwrap_or(bytes.len());
|
||||
out.materials.push(ClipMapMaterial {
|
||||
name: String::from_utf8_lossy(&bytes[..end]).into_owned(),
|
||||
surface_flags: s.u32_at(p, 64).map_err(|_| ClipCollisionError::Truncated)?,
|
||||
surface_flags: t5_surface_flags(
|
||||
s.u32_at(p, 64).map_err(|_| ClipCollisionError::Truncated)?,
|
||||
),
|
||||
content_flags: s.u32_at(p, 68).map_err(|_| ClipCollisionError::Truncated)?,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ impl MapCompassSource {
|
||||
let Some(text) = crate::decode_rawfile_text(data, zlib_compressed) else {
|
||||
return;
|
||||
};
|
||||
let text = strip_gsc_comments(&text);
|
||||
let Some(material) = setup_minimap_material(&text) else {
|
||||
return;
|
||||
};
|
||||
@@ -32,6 +33,20 @@ impl MapCompassSource {
|
||||
self.decl.script = Some(name.to_owned());
|
||||
}
|
||||
|
||||
pub fn capture_iw5(&mut self, name: &str, compressed_stack: &[u8]) {
|
||||
if self.decl.material.is_some() {
|
||||
return;
|
||||
}
|
||||
let Ok(stack) = asset_transport::inflate_zlib(compressed_stack) else {
|
||||
return;
|
||||
};
|
||||
let Some(material) = iw5_minimap_literal(&stack) else {
|
||||
return;
|
||||
};
|
||||
self.decl.material = Some(material.to_owned());
|
||||
self.decl.script = Some(name.to_owned());
|
||||
}
|
||||
|
||||
pub fn resolve(mut self, materials: &MaterialCatalog) -> MapCompassDeclaration {
|
||||
if let Some(name) = self.decl.material.as_deref() {
|
||||
self.decl.image = materials
|
||||
@@ -45,10 +60,48 @@ impl MapCompassSource {
|
||||
}
|
||||
}
|
||||
|
||||
// T5 maps set up the `_wager` minimap first;
|
||||
// that image covers only the wager play area.
|
||||
pub fn setup_minimap_material(script: &str) -> Option<&str> {
|
||||
let lower = script.to_ascii_lowercase();
|
||||
let call = lower.find("setupminimap")?;
|
||||
quoted_after(script, call + "setupminimap".len())
|
||||
let mut calls = lower
|
||||
.match_indices("setupminimap")
|
||||
.filter_map(|(at, key)| quoted_after(script, at + key.len()));
|
||||
let first = calls.next()?;
|
||||
if !first.ends_with("_wager") {
|
||||
return Some(first);
|
||||
}
|
||||
calls.find(|m| !m.ends_with("_wager")).or(Some(first))
|
||||
}
|
||||
|
||||
fn strip_gsc_comments(script: &str) -> String {
|
||||
let mut out = String::with_capacity(script.len());
|
||||
let mut rest = script;
|
||||
while let Some(c) = rest.chars().next() {
|
||||
if c == '"' {
|
||||
let len = rest[1..].find('"').map_or(rest.len(), |n| n + 2);
|
||||
out.push_str(&rest[..len]);
|
||||
rest = &rest[len..];
|
||||
} else if rest.starts_with("//") {
|
||||
rest = &rest[rest.find('\n').unwrap_or(rest.len())..];
|
||||
} else if rest.starts_with("/*") {
|
||||
rest = rest[2..].find("*/").map_or("", |n| &rest[n + 4..]);
|
||||
} else {
|
||||
out.push(c);
|
||||
rest = &rest[c.len_utf8()..];
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
// Compiled IW5 map scripts keep the minimap name only as a stack
|
||||
// literal; the call itself is a token id, so the literal's prefix is the
|
||||
// anchor. `resolve` rejects a name that is not a material in the zone.
|
||||
fn iw5_minimap_literal(stack: &[u8]) -> Option<&str> {
|
||||
const PREFIX: &[u8] = b"compass_map_";
|
||||
let start = stack.windows(PREFIX.len()).position(|w| w == PREFIX)?;
|
||||
let len = stack[start..].iter().position(|&b| b == 0)?;
|
||||
std::str::from_utf8(&stack[start..start + len]).ok()
|
||||
}
|
||||
|
||||
pub fn compass_max_range(script: &str) -> Option<f32> {
|
||||
|
||||
@@ -204,7 +204,7 @@ fn decode_createart_bytes(data: &[u8], zlib_compressed: bool) -> Option<Vec<u8>>
|
||||
if std::str::from_utf8(trimmed).is_ok() {
|
||||
return Some(trimmed.to_vec());
|
||||
}
|
||||
decode_t5_packed_gsc(data)
|
||||
decode_packed_rawfile(data)
|
||||
}
|
||||
|
||||
pub fn decode_rawfile_text(data: &[u8], zlib_compressed: bool) -> Option<String> {
|
||||
@@ -214,7 +214,7 @@ pub fn decode_rawfile_text(data: &[u8], zlib_compressed: bool) -> Option<String>
|
||||
.map(|s| s.trim_end_matches('\0').to_owned())
|
||||
}
|
||||
|
||||
fn decode_t5_packed_gsc(data: &[u8]) -> Option<Vec<u8>> {
|
||||
pub fn decode_packed_rawfile(data: &[u8]) -> Option<Vec<u8>> {
|
||||
let data = data.strip_suffix(&[0]).unwrap_or(data);
|
||||
if data.len() < 8 {
|
||||
return None;
|
||||
|
||||
@@ -71,7 +71,7 @@ pub const RETAIL_DYN_ENT_PROPS: [DynEntProps; 3] = [
|
||||
},
|
||||
];
|
||||
|
||||
pub fn retail_dyn_ent_props(ty: DynEntType) -> Option<DynEntProps> {
|
||||
pub fn dyn_ent_props(ty: DynEntType) -> Option<DynEntProps> {
|
||||
RETAIL_DYN_ENT_PROPS.get(ty.as_u8() as usize).copied()
|
||||
}
|
||||
|
||||
@@ -250,7 +250,7 @@ pub fn build_dyn_ent_catalog(
|
||||
stream: &ZoneStream<'_>,
|
||||
geometry: ClipMapGeometry,
|
||||
mut xmodel_name: impl FnMut(Ptr) -> Option<String>,
|
||||
mut fx_name: impl FnMut(Ptr) -> Option<String>,
|
||||
mut name: impl FnMut(Ptr) -> Option<String>,
|
||||
mut preset_at: impl FnMut(Ptr) -> Option<OwnedPhysPreset>,
|
||||
) -> DynEntCatalog {
|
||||
let mut catalog = DynEntCatalog::default();
|
||||
@@ -260,7 +260,7 @@ pub fn build_dyn_ent_catalog(
|
||||
geometry.dyn_ent_defs[DYNENT_DRAW_MODEL],
|
||||
DynEntDrawType::Model,
|
||||
&mut xmodel_name,
|
||||
&mut fx_name,
|
||||
&mut name,
|
||||
&mut preset_at,
|
||||
);
|
||||
let (brushes, t1, x1, p1) = copy_list(
|
||||
@@ -269,7 +269,7 @@ pub fn build_dyn_ent_catalog(
|
||||
geometry.dyn_ent_defs[DYNENT_DRAW_BRUSH],
|
||||
DynEntDrawType::Brush,
|
||||
&mut xmodel_name,
|
||||
&mut fx_name,
|
||||
&mut name,
|
||||
&mut preset_at,
|
||||
);
|
||||
catalog.models = models;
|
||||
@@ -286,7 +286,7 @@ fn copy_list(
|
||||
defs: Option<Ptr>,
|
||||
draw_type: DynEntDrawType,
|
||||
xmodel_name: &mut impl FnMut(Ptr) -> Option<String>,
|
||||
fx_name: &mut impl FnMut(Ptr) -> Option<String>,
|
||||
name: &mut impl FnMut(Ptr) -> Option<String>,
|
||||
preset_at: &mut impl FnMut(Ptr) -> Option<OwnedPhysPreset>,
|
||||
) -> (Vec<DynEntDef>, usize, usize, usize) {
|
||||
let mut out = Vec::new();
|
||||
@@ -324,7 +324,7 @@ fn copy_list(
|
||||
xmodel,
|
||||
brush_model: scalars.brush_model,
|
||||
physics_brush_model: scalars.physics_brush_model,
|
||||
destroy_fx: fx_name(def.at(stream.layout(40, 48))),
|
||||
destroy_fx: name(def.at(stream.layout(40, 48))),
|
||||
phys_preset,
|
||||
health: scalars.health,
|
||||
phys_mass: scalars.phys_mass,
|
||||
|
||||
@@ -5,8 +5,8 @@ use asset_material::MaterialDefinitions;
|
||||
use fastfile_iw4::{Ptr, ZonePtr, ZoneStream};
|
||||
use fx_iw4::{
|
||||
FX_GLASS_DEF, FX_GLASS_GEOMETRY_DATA, FX_GLASS_INIT_PIECE_STATE, FX_GLASS_PIECE_PLACE,
|
||||
FX_GLASS_PIECE_STATE, fx_glass_place_origin, fx_glass_place_quat, fx_glass_reset_copy_geo,
|
||||
fx_glass_reset_copy_piece, fx_glass_state_geo_start, fx_unit_quat_to_axis,
|
||||
FX_GLASS_PIECE_STATE, glass_place_origin, glass_place_quat, glass_reset_copy_geo,
|
||||
glass_reset_copy_piece, glass_state_geo_start, unit_quat_to_axis,
|
||||
};
|
||||
use weapon_iw4::CONTENTS_GLASS;
|
||||
|
||||
@@ -47,7 +47,7 @@ pub struct GlassZoneCensus {
|
||||
impl GlassZoneCensus {
|
||||
pub fn report_line(&self) -> String {
|
||||
format!(
|
||||
"glass zone: fx init_pieces={} defs={} init_geo={} | G_GlassData pieces={} names={} | clip encoded={} unique={} surf9={} contents_glass={} (not tess, not hitType=4)",
|
||||
"glass zone: fx init_pieces={} defs={} init_geo={} | glass data pieces={} names={} | clip encoded={} unique={} surf9={} contents_glass={} (not tess, not hitType=4)",
|
||||
self.fx_init_piece_n,
|
||||
self.fx_def_n,
|
||||
self.fx_init_geo_n,
|
||||
@@ -110,7 +110,7 @@ fn fill_fx(out: &mut GlassZoneCensus, s: &ZoneStream<'_>) {
|
||||
}
|
||||
|
||||
fn fill_g_glass(out: &mut GlassZoneCensus, s: &ZoneStream<'_>) {
|
||||
let Some(g) = s.g_glass_data() else {
|
||||
let Some(g) = s.glass_data() else {
|
||||
return;
|
||||
};
|
||||
out.g_recorded = true;
|
||||
@@ -167,8 +167,8 @@ impl FxGlassReset {
|
||||
|
||||
pub fn pane_basis(&self, piece: usize) -> Option<([f32; 3], [f32; 3], [f32; 3])> {
|
||||
let place = self.piece_places.get(piece)?;
|
||||
let origin = fx_glass_place_origin(place);
|
||||
let axis = fx_unit_quat_to_axis(fx_glass_place_quat(place));
|
||||
let origin = glass_place_origin(place);
|
||||
let axis = unit_quat_to_axis(glass_place_quat(place));
|
||||
Some((origin, axis[0], axis[1]))
|
||||
}
|
||||
|
||||
@@ -206,7 +206,7 @@ impl FxGlassReset {
|
||||
}
|
||||
|
||||
pub fn geo_start(&self, piece: usize) -> Option<u16> {
|
||||
self.piece_states.get(piece).map(fx_glass_state_geo_start)
|
||||
self.piece_states.get(piece).map(glass_state_geo_start)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -230,7 +230,7 @@ pub fn build_fx_glass_reset(stream: &ZoneStream<'_>) -> Option<FxGlassReset> {
|
||||
let mut geo_cursor = 0u16;
|
||||
for (i, init) in init_piece_states.iter().enumerate() {
|
||||
let piece = u16::try_from(i).ok()?;
|
||||
let out = fx_glass_reset_copy_piece(init, piece, geo_cursor, &defs);
|
||||
let out = glass_reset_copy_piece(init, piece, geo_cursor, &defs);
|
||||
geo_cursor = out.next_geo_start;
|
||||
piece_places.push(out.place);
|
||||
piece_states.push(out.state);
|
||||
@@ -239,7 +239,7 @@ pub fn build_fx_glass_reset(stream: &ZoneStream<'_>) -> Option<FxGlassReset> {
|
||||
|
||||
let mut geo_flat = vec![0u8; init_geo_data.len() * FX_GLASS_GEOMETRY_DATA];
|
||||
let src: Vec<u8> = init_geo_data.iter().flatten().copied().collect();
|
||||
fx_glass_reset_copy_geo(&mut geo_flat, &src);
|
||||
glass_reset_copy_geo(&mut geo_flat, &src);
|
||||
let mut geo_data = Vec::with_capacity(init_geo_data.len());
|
||||
for chunk in geo_flat.chunks_exact(FX_GLASS_GEOMETRY_DATA) {
|
||||
let mut word = [0u8; FX_GLASS_GEOMETRY_DATA];
|
||||
|
||||
@@ -6,6 +6,7 @@ mod createart;
|
||||
mod draw_policy;
|
||||
mod dyn_ents;
|
||||
mod glass_catalog;
|
||||
mod map_declarations;
|
||||
mod map_entities;
|
||||
pub mod model_mesh;
|
||||
mod vision;
|
||||
@@ -27,6 +28,7 @@ pub use createart::*;
|
||||
pub use draw_policy::WorldDrawPolicy;
|
||||
pub use dyn_ents::*;
|
||||
pub use glass_catalog::*;
|
||||
pub use map_declarations::{MapDeclarations, stack_literals as iw5_stack_literals};
|
||||
pub use map_entities::*;
|
||||
pub use model_mesh::*;
|
||||
pub use vision::*;
|
||||
|
||||
@@ -0,0 +1,222 @@
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::fmt::Write as _;
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct MapDeclarations {
|
||||
compass: Option<String>,
|
||||
teams: Option<(String, String)>,
|
||||
preanims: BTreeSet<String>,
|
||||
effects: BTreeMap<String, String>,
|
||||
props: Vec<(String, String)>,
|
||||
}
|
||||
|
||||
impl MapDeclarations {
|
||||
pub fn capture(&mut self, name: &str, compressed_stack: &[u8]) {
|
||||
let Ok(stack) = asset_transport::inflate_zlib(compressed_stack) else {
|
||||
return;
|
||||
};
|
||||
let literals = stack_literals(&stack);
|
||||
if literals.iter().any(|l| l.starts_with("compass_map_")) {
|
||||
self.capture_map_main(&literals);
|
||||
} else if let Some(prop) = prop_script(&literals) {
|
||||
self.props.push(prop);
|
||||
} else if !name.contains('/') {
|
||||
self.capture_destructible_anims(&literals);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_t5_map_main(source: &str) -> Self {
|
||||
let compass = source
|
||||
.split("setupMiniMap")
|
||||
.skip(1)
|
||||
.filter_map(|call| call.split('"').nth(1))
|
||||
.find(|name| name.starts_with("compass_map_") && !name.ends_with("_wager"))
|
||||
.map(str::to_owned);
|
||||
Self {
|
||||
compass,
|
||||
..Self::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_teams(&mut self, attackers: &str, defenders: &str) {
|
||||
self.teams = Some((attackers.to_owned(), defenders.to_owned()));
|
||||
}
|
||||
|
||||
fn capture_map_main(&mut self, literals: &[String]) {
|
||||
if self.compass.is_none() {
|
||||
self.compass = literals
|
||||
.iter()
|
||||
.find(|l| l.starts_with("compass_map_"))
|
||||
.cloned();
|
||||
}
|
||||
let team_before = |key: &str| {
|
||||
let at = literals.iter().position(|l| l == key)?;
|
||||
let team = literals.get(at.checked_sub(1)?)?;
|
||||
matches!(team.as_str(), "allies" | "axis").then(|| team.clone())
|
||||
};
|
||||
let other = |team: &str| if team == "axis" { "allies" } else { "axis" }.to_owned();
|
||||
let teams = match (team_before("attackers"), team_before("defenders")) {
|
||||
(Some(a), Some(d)) => (a, d),
|
||||
(Some(a), None) => {
|
||||
let d = other(&a);
|
||||
(a, d)
|
||||
}
|
||||
(None, Some(d)) => (other(&d), d),
|
||||
(None, None) => ("allies".to_owned(), "axis".to_owned()),
|
||||
};
|
||||
self.teams.get_or_insert(teams);
|
||||
}
|
||||
|
||||
fn capture_destructible_anims(&mut self, literals: &[String]) {
|
||||
if literals.len() < 2 || literals.len() % 2 != 0 {
|
||||
return;
|
||||
}
|
||||
let pairs: Vec<(&String, &String)> = literals
|
||||
.chunks(2)
|
||||
.map(|pair| (&pair[0], &pair[1]))
|
||||
.collect();
|
||||
let anim = |(a, b): &(&String, &String)| a == b && !a.contains('/');
|
||||
let effect = |(a, b): &(&String, &String)| a.contains('/') && !b.contains('/');
|
||||
if !pairs.iter().any(anim) || !pairs.iter().all(|p| anim(p) || effect(p)) {
|
||||
return;
|
||||
}
|
||||
for (a, b) in pairs {
|
||||
if a == b {
|
||||
self.preanims.insert(a.clone());
|
||||
} else {
|
||||
self.effects.insert(b.clone(), a.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.compass.is_none() && self.preanims.is_empty() && self.props.is_empty()
|
||||
}
|
||||
|
||||
pub fn map_script(&self, entities: &str) -> String {
|
||||
let mut out = String::from("#using_animtree( \"destructibles\" );\n\nmain()\n{\n");
|
||||
if !self.preanims.is_empty() {
|
||||
out.push_str("\tif ( !isdefined( level._destructible_preanims ) )\n");
|
||||
out.push_str("\t\tlevel._destructible_preanims = [];\n");
|
||||
}
|
||||
for name in &self.preanims {
|
||||
let _ = writeln!(
|
||||
out,
|
||||
"\tlevel._destructible_preanims[ \"{name}\" ] = %{name};"
|
||||
);
|
||||
}
|
||||
if !self.effects.is_empty() {
|
||||
out.push_str("\tif ( !isdefined( level._effect ) )\n\t\tlevel._effect = [];\n");
|
||||
}
|
||||
for (key, path) in &self.effects {
|
||||
let _ = writeln!(out, "\tlevel._effect[ \"{key}\" ] = loadfx( \"{path}\" );");
|
||||
}
|
||||
let models = self.prop_models(entities);
|
||||
if !models.is_empty() {
|
||||
out.push_str("\tif ( !isdefined( level.anim_prop_models ) )\n");
|
||||
out.push_str("\t\tlevel.anim_prop_models = [];\n");
|
||||
}
|
||||
for (model, (anim, key)) in &models {
|
||||
let _ = writeln!(
|
||||
out,
|
||||
"\tlevel.anim_prop_models[ \"{model}\" ][ \"{key}\" ] = \"{anim}\";"
|
||||
);
|
||||
}
|
||||
out.push_str("\n\tmaps\\mp\\_load::main();\n");
|
||||
if let Some(compass) = &self.compass {
|
||||
let _ = writeln!(out, "\tmaps\\mp\\_compass::setupMiniMap( \"{compass}\" );");
|
||||
}
|
||||
let (attackers, defenders) = self
|
||||
.teams
|
||||
.clone()
|
||||
.unwrap_or_else(|| ("allies".to_owned(), "axis".to_owned()));
|
||||
let _ = writeln!(out, "\n\tgame[ \"attackers\" ] = \"{attackers}\";");
|
||||
let _ = writeln!(out, "\tgame[ \"defenders\" ] = \"{defenders}\";");
|
||||
out.push_str("}\n");
|
||||
out
|
||||
}
|
||||
|
||||
fn prop_models(&self, entities: &str) -> BTreeMap<String, (String, String)> {
|
||||
let mut modules: BTreeMap<String, BTreeSet<String>> = BTreeMap::new();
|
||||
for block in entity_blocks(entities) {
|
||||
if block.get("targetname").map(String::as_str) != Some("animated_model")
|
||||
|| block.contains_key("animation")
|
||||
{
|
||||
continue;
|
||||
}
|
||||
let (Some(model), Some(module)) = (block.get("model"), block.get(PRECACHE_KEY)) else {
|
||||
continue;
|
||||
};
|
||||
let Some(module) = module.strip_prefix("maps animated_models ") else {
|
||||
continue;
|
||||
};
|
||||
modules
|
||||
.entry(module.to_owned())
|
||||
.or_default()
|
||||
.insert(model.clone());
|
||||
}
|
||||
// A module is bound only to the prop script that names it; equal counts
|
||||
// of leftovers prove nothing about which belongs to which.
|
||||
let chosen: BTreeMap<&str, (String, String)> = modules
|
||||
.keys()
|
||||
.map(|module| {
|
||||
let prop = self
|
||||
.props
|
||||
.iter()
|
||||
.find(|(anim, _)| anim == module)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| (module.clone(), "default".to_owned()));
|
||||
(module.as_str(), prop)
|
||||
})
|
||||
.collect();
|
||||
let mut models = BTreeMap::new();
|
||||
for (module, names) in &modules {
|
||||
for model in names {
|
||||
models.insert(model.clone(), chosen[module.as_str()].clone());
|
||||
}
|
||||
}
|
||||
models
|
||||
}
|
||||
}
|
||||
|
||||
const PRECACHE_KEY: &str = "2818";
|
||||
|
||||
fn prop_script(literals: &[String]) -> Option<(String, String)> {
|
||||
let body = match literals {
|
||||
[mp, tree, body @ ..] if mp == "mp_" && tree == "animated_props" => body,
|
||||
body => body,
|
||||
};
|
||||
match body {
|
||||
[a, k, a2, k2] if a == a2 && k == k2 && a != k && !a.contains('/') => {
|
||||
Some((a.clone(), k.clone()))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn stack_literals(stack: &[u8]) -> Vec<String> {
|
||||
stack
|
||||
.split(|&b| b == 0)
|
||||
.filter(|run| run.len() > 1 && run.iter().all(|&b| (0x20..0x7f).contains(&b)))
|
||||
.map(|run| String::from_utf8_lossy(run).into_owned())
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn entity_blocks(text: &str) -> Vec<BTreeMap<String, String>> {
|
||||
let mut blocks = Vec::new();
|
||||
let mut current: Option<BTreeMap<String, String>> = None;
|
||||
for line in text.lines() {
|
||||
let line = line.trim();
|
||||
if line == "{" {
|
||||
current = Some(BTreeMap::new());
|
||||
} else if line == "}" {
|
||||
blocks.extend(current.take());
|
||||
} else if let Some(block) = current.as_mut() {
|
||||
let mut quoted = line.split('"').skip(1).step_by(2);
|
||||
if let (Some(key), Some(value)) = (quoted.next(), quoted.next()) {
|
||||
block.insert(key.to_owned(), value.to_owned());
|
||||
}
|
||||
}
|
||||
}
|
||||
blocks
|
||||
}
|
||||
@@ -138,7 +138,6 @@ pub fn exploding_prop_machine(
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct MapUseTrigger {
|
||||
pub hulls: Option<Vec<MapTriggerHull>>,
|
||||
pub target: String,
|
||||
pub source_ordinal: u32,
|
||||
pub classname: String,
|
||||
@@ -183,7 +182,6 @@ pub fn parse_map_use_triggers(text: &str) -> Vec<MapUseTrigger> {
|
||||
.find(|(name, _)| *name == target)
|
||||
.map(|(_, angles)| *angles);
|
||||
Some(MapUseTrigger {
|
||||
hulls: None,
|
||||
target: target.to_owned(),
|
||||
source_ordinal: u32::try_from(ordinal).ok()?,
|
||||
classname: classname.to_owned(),
|
||||
@@ -299,25 +297,6 @@ pub fn map_script_structs_iw5(s: &fastfile_iw5::ZoneStream<'_>) -> Vec<MapScript
|
||||
parse_map_script_structs(text)
|
||||
}
|
||||
|
||||
pub fn map_use_triggers(s: &ZoneStream<'_>) -> Vec<MapUseTrigger> {
|
||||
let Some(text) = entity_string(s) else {
|
||||
return Vec::new();
|
||||
};
|
||||
let mut triggers = parse_map_use_triggers(text);
|
||||
if let Some(geo) = s.map_ents() {
|
||||
for trigger in &mut triggers {
|
||||
if let Some(index) = trigger
|
||||
.model
|
||||
.strip_prefix('?')
|
||||
.and_then(|v| v.parse::<usize>().ok())
|
||||
{
|
||||
trigger.hulls = capture_trigger_hulls(s, geo, index);
|
||||
}
|
||||
}
|
||||
}
|
||||
triggers
|
||||
}
|
||||
|
||||
pub fn map_use_triggers_t5(s: &fastfile_t5::ZoneStream<'_>) -> Vec<MapUseTrigger> {
|
||||
let Some(text) = entity_string_t5(s) else {
|
||||
return Vec::new();
|
||||
@@ -329,15 +308,7 @@ pub fn map_use_triggers_iw5(s: &fastfile_iw5::ZoneStream<'_>) -> Vec<MapUseTrigg
|
||||
let Some(text) = entity_string_iw5(s) else {
|
||||
return Vec::new();
|
||||
};
|
||||
let mut triggers = parse_map_use_triggers(text);
|
||||
if let Some(geo) = s.map_ents() {
|
||||
for trigger in &mut triggers {
|
||||
if let Some(index) = trigger.model.strip_prefix('?').and_then(|v| v.parse().ok()) {
|
||||
trigger.hulls = capture_iw5_trigger_hulls(s, geo, index);
|
||||
}
|
||||
}
|
||||
}
|
||||
triggers
|
||||
parse_map_use_triggers(text)
|
||||
}
|
||||
|
||||
fn has_ascii_prefix(value: &str, prefix: &str) -> bool {
|
||||
@@ -399,6 +370,14 @@ pub fn worldspawn_north_yaw(s: &ZoneStream<'_>) -> Option<f32> {
|
||||
parse_worldspawn_north_yaw(text)
|
||||
}
|
||||
|
||||
pub fn airstrike_height(s: &ZoneStream<'_>) -> Option<f32> {
|
||||
parse_airstrike_height(entity_string(s)?)
|
||||
}
|
||||
|
||||
pub fn airstrike_height_iw5(s: &fastfile_iw5::ZoneStream<'_>) -> Option<f32> {
|
||||
parse_airstrike_height(entity_string_iw5(s)?)
|
||||
}
|
||||
|
||||
pub fn minimap_corners_t5(s: &fastfile_t5::ZoneStream<'_>) -> Option<MinimapCorners> {
|
||||
let text = entity_string_t5(s)?;
|
||||
parse_minimap_corners(text)
|
||||
@@ -447,6 +426,126 @@ pub fn dm_spawn_points_t5(s: &fastfile_t5::ZoneStream<'_>) -> Vec<SpawnPoint> {
|
||||
spawns
|
||||
}
|
||||
|
||||
pub fn t5_entities_for_iw4_rules(text: &str) -> String {
|
||||
type Block = Vec<(String, String)>;
|
||||
fn get<'a>(block: &'a Block, key: &str) -> Option<&'a str> {
|
||||
block
|
||||
.iter()
|
||||
.find(|(k, _)| k == key)
|
||||
.map(|(_, v)| v.as_str())
|
||||
}
|
||||
fn set(block: &mut Block, key: &str, value: String) {
|
||||
match block.iter_mut().find(|(k, _)| k == key) {
|
||||
Some((_, v)) => *v = value,
|
||||
None => block.push((key.to_owned(), value)),
|
||||
}
|
||||
}
|
||||
|
||||
let mut blocks: Vec<Block> = Vec::new();
|
||||
let mut open: Option<Block> = None;
|
||||
for line in text.lines() {
|
||||
match (line.trim(), open.as_mut()) {
|
||||
("{", None) => open = Some(Vec::new()),
|
||||
("}", Some(_)) => blocks.extend(open.take()),
|
||||
(trimmed, Some(block)) => {
|
||||
let mut quoted = trimmed.split('"').skip(1).step_by(2);
|
||||
if let (Some(key), Some(value)) = (quoted.next(), quoted.next()) {
|
||||
block.push((key.to_owned(), value.to_owned()));
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
let mut flag_visuals: Vec<(String, String)> = Vec::new();
|
||||
let mut spawned: Vec<Block> = Vec::new();
|
||||
for block in &mut blocks {
|
||||
let team = get(block, "script_team").map(str::to_owned);
|
||||
if let Some(suffix) = get(block, "classname").and_then(|c| c.strip_prefix("mp_dem_spawn_"))
|
||||
{
|
||||
set(block, "classname", format!("mp_dd_spawn_{suffix}"));
|
||||
}
|
||||
match (get(block, "targetname"), team.as_deref()) {
|
||||
(Some("bombzone_dem"), _) => set(block, "targetname", "dd_bombzone".to_owned()),
|
||||
(Some("ctf_flag_zone_trig"), Some(team)) => {
|
||||
set(block, "targetname", format!("ctf_zone_{team}"));
|
||||
if get(block, "height").is_none() {
|
||||
set(block, "height", "128".to_owned());
|
||||
}
|
||||
}
|
||||
(Some("ctf_flag_pickup_trig"), Some(team)) => match get(block, "target") {
|
||||
Some(target) => flag_visuals.push((target.to_owned(), format!("ctf_flag_{team}"))),
|
||||
None => spawned.push(
|
||||
[("classname", "script_model".to_owned())]
|
||||
.into_iter()
|
||||
.chain(
|
||||
["origin", "angles"]
|
||||
.into_iter()
|
||||
.filter_map(|key| Some((key, get(block, key)?.to_owned()))),
|
||||
)
|
||||
.chain([("targetname", format!("ctf_flag_{team}"))])
|
||||
.map(|(k, v)| (k.to_owned(), v))
|
||||
.collect(),
|
||||
),
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
if let Some(names) = get(block, "script_gameobjectname") {
|
||||
let mut tokens: Vec<&str> = Vec::new();
|
||||
for token in names.split_whitespace() {
|
||||
let token = match token {
|
||||
"bombzone_dem" => "dd_bombzone",
|
||||
token => token,
|
||||
};
|
||||
tokens.push(token);
|
||||
tokens.extend(match token {
|
||||
"dem" => Some("dd"),
|
||||
"tdm" => Some("war"),
|
||||
"koth" => Some("hq"),
|
||||
_ => None,
|
||||
});
|
||||
}
|
||||
let mut seen = std::collections::BTreeSet::new();
|
||||
tokens.retain(|token| seen.insert(*token));
|
||||
let names = tokens.join(" ");
|
||||
set(block, "script_gameobjectname", names);
|
||||
}
|
||||
}
|
||||
for block in &mut blocks {
|
||||
if let Some((_, name)) = get(block, "targetname")
|
||||
.and_then(|own| flag_visuals.iter().find(|(target, _)| target == own))
|
||||
{
|
||||
let name = name.clone();
|
||||
set(block, "targetname", name);
|
||||
}
|
||||
}
|
||||
blocks.extend(spawned);
|
||||
if !blocks
|
||||
.iter()
|
||||
.any(|block| get(block, "targetname") == Some("care_package"))
|
||||
{
|
||||
blocks.push(vec![
|
||||
("classname".into(), "script_model".into()),
|
||||
("targetname".into(), "care_package".into()),
|
||||
("target".into(), "care_package_collision".into()),
|
||||
]);
|
||||
blocks.push(vec![
|
||||
("classname".into(), "script_brushmodel".into()),
|
||||
("targetname".into(), "care_package_collision".into()),
|
||||
]);
|
||||
}
|
||||
|
||||
let mut out = String::with_capacity(text.len() + text.len() / 16);
|
||||
for block in blocks {
|
||||
out.push_str("{\n");
|
||||
for (key, value) in block {
|
||||
out.push_str(&format!("\"{key}\" \"{value}\"\n"));
|
||||
}
|
||||
out.push_str("}\n");
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
pub fn script_model_placements_t5(s: &fastfile_t5::ZoneStream<'_>) -> Vec<ScriptModelPlacement> {
|
||||
let Some(text) = entity_string_t5(s) else {
|
||||
return Vec::new();
|
||||
@@ -509,6 +608,14 @@ pub fn map_ents_entity_string<'a>(s: &'a ZoneStream<'_>) -> Option<&'a str> {
|
||||
entity_string(s)
|
||||
}
|
||||
|
||||
pub fn map_ents_entity_string_t5<'a>(s: &'a fastfile_t5::ZoneStream<'_>) -> Option<&'a str> {
|
||||
entity_string_t5(s)
|
||||
}
|
||||
|
||||
pub fn map_ents_entity_string_iw5<'a>(s: &'a fastfile_iw5::ZoneStream<'_>) -> Option<&'a str> {
|
||||
entity_string_iw5(s)
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct MapEntsKeyCensus {
|
||||
pub quoted_pairs: u32,
|
||||
@@ -647,6 +754,13 @@ fn parse_minimap_corners(text: &str) -> Option<MinimapCorners> {
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_airstrike_height(text: &str) -> Option<f32> {
|
||||
parse_entities(text)
|
||||
.find(|e| e.targetname == Some("airstrikeheight"))
|
||||
.and_then(|e| e.origin)
|
||||
.map(|origin| origin[2])
|
||||
}
|
||||
|
||||
fn parse_worldspawn_north_yaw(text: &str) -> Option<f32> {
|
||||
parse_entities(text)
|
||||
.find(|e| e.classname == Some("worldspawn"))
|
||||
@@ -916,15 +1030,55 @@ enum EntityKey {
|
||||
Other,
|
||||
}
|
||||
|
||||
const IW5_KEY_CLASSNAME: &str = "1668";
|
||||
const IW5_KEY_ORIGIN: &str = "1669";
|
||||
const IW5_KEY_MODEL: &str = "1670";
|
||||
const IW5_KEY_TARGET: &str = "1672";
|
||||
const IW5_KEY_TARGETNAME: &str = "1673";
|
||||
const IW5_KEY_ANGLES: &str = "1677";
|
||||
const IW5_KEY_GAMEOBJECT: &str = "11848";
|
||||
const IW5_KEY_DESTRUCTIBLE_TYPE: &str = "2369";
|
||||
const IW5_KEY_LT_ORIGIN: &str = "2814";
|
||||
const IW5_KEYS: &[(&str, &str)] = &[
|
||||
("1668", "classname"),
|
||||
("1669", "origin"),
|
||||
("1670", "model"),
|
||||
("1671", "spawnflags"),
|
||||
("1672", "target"),
|
||||
("1673", "targetname"),
|
||||
("1676", "dmg"),
|
||||
("1677", "angles"),
|
||||
("1679", "script_linkname"),
|
||||
("1774", "script_noteworthy"),
|
||||
("1782", "radius"),
|
||||
("1783", "height"),
|
||||
("2009", "script_exploder"),
|
||||
("2328", "script_linkto"),
|
||||
("2369", "destructible_type"),
|
||||
("2814", "ltOrigin"),
|
||||
("7864", "script_prefab_exploder"),
|
||||
("11039", "animation"),
|
||||
("11848", "script_gameobjectname"),
|
||||
("11996", "script_label"),
|
||||
("12906", "script_type"),
|
||||
];
|
||||
|
||||
fn iw5_key_name(key: &str) -> Option<&'static str> {
|
||||
IW5_KEYS
|
||||
.iter()
|
||||
.find(|(id, _)| *id == key)
|
||||
.map(|(_, name)| *name)
|
||||
}
|
||||
|
||||
pub fn iw5_entity_string_named(text: &str) -> String {
|
||||
let mut out = String::with_capacity(text.len() + text.len() / 4);
|
||||
for line in text.lines() {
|
||||
let trimmed = line.trim();
|
||||
match trimmed.split_once(' ') {
|
||||
Some((key, value)) if key.bytes().all(|b| b.is_ascii_digit()) && !key.is_empty() => {
|
||||
let name = iw5_key_name(key).unwrap_or(key);
|
||||
out.push('"');
|
||||
out.push_str(name);
|
||||
out.push_str("\" ");
|
||||
out.push_str(value.trim());
|
||||
}
|
||||
_ => out.push_str(trimmed),
|
||||
}
|
||||
out.push('\n');
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn entity_pair(line: &str) -> Option<(EntityKey, &str)> {
|
||||
if let Some((key, value)) = quoted_pair(line) {
|
||||
@@ -967,23 +1121,7 @@ fn numbered_pair(line: &str) -> Option<(EntityKey, &str)> {
|
||||
let line = line.trim();
|
||||
let (key, rest) = line.split_once(' ')?;
|
||||
|
||||
let key = match key {
|
||||
IW5_KEY_CLASSNAME => EntityKey::Classname,
|
||||
IW5_KEY_ORIGIN => EntityKey::Origin,
|
||||
IW5_KEY_ANGLES => EntityKey::Angles,
|
||||
IW5_KEY_MODEL => EntityKey::Model,
|
||||
IW5_KEY_TARGET => EntityKey::Target,
|
||||
IW5_KEY_TARGETNAME => EntityKey::Targetname,
|
||||
IW5_KEY_GAMEOBJECT => EntityKey::Gameobject,
|
||||
IW5_KEY_DESTRUCTIBLE_TYPE => EntityKey::DestructibleType,
|
||||
IW5_KEY_LT_ORIGIN => EntityKey::LtOrigin,
|
||||
"11996" => EntityKey::ScriptLabel,
|
||||
"1782" => EntityKey::Radius,
|
||||
"1783" => EntityKey::Height,
|
||||
"2009" => EntityKey::ScriptExploder,
|
||||
"7864" => EntityKey::ScriptPrefabExploder,
|
||||
named => named_key(named),
|
||||
};
|
||||
let key = named_key(iw5_key_name(key).unwrap_or(key));
|
||||
let mut quotes = rest.match_indices('"').map(|(index, _)| index);
|
||||
let a = quotes.next()?;
|
||||
let b = quotes.next()?;
|
||||
@@ -1019,6 +1157,24 @@ pub struct MapTriggerHull {
|
||||
pub half: [f32; 3],
|
||||
pub slabs: Vec<([f32; 3], f32, f32)>,
|
||||
}
|
||||
pub fn trigger_models(s: &ZoneStream<'_>) -> Vec<Vec<MapTriggerHull>> {
|
||||
let Some(geo) = s.map_ents() else {
|
||||
return Vec::new();
|
||||
};
|
||||
(0..geo.trigger_model_count)
|
||||
.map(|n| capture_trigger_hulls(s, geo, n).unwrap_or_default())
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn trigger_models_iw5(s: &fastfile_iw5::ZoneStream<'_>) -> Vec<Vec<MapTriggerHull>> {
|
||||
let Some(geo) = s.map_ents() else {
|
||||
return Vec::new();
|
||||
};
|
||||
(0..geo.trigger_model_count)
|
||||
.map(|n| capture_iw5_trigger_hulls(s, geo, n).unwrap_or_default())
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn capture_trigger_hulls(
|
||||
s: &ZoneStream<'_>,
|
||||
g: fastfile_iw4::MapEntsGeometry,
|
||||
@@ -1118,41 +1274,3 @@ fn capture_iw5_trigger_hulls(
|
||||
}
|
||||
Some(out)
|
||||
}
|
||||
|
||||
pub fn capture_brush_trigger_hulls(triggers: &mut [MapUseTrigger], clip: &crate::ClipCollision) {
|
||||
for trigger in triggers {
|
||||
trigger.hulls = (|| {
|
||||
let index: usize = trigger.model.strip_prefix('*')?.parse().ok()?;
|
||||
let model = clip.cmodels.get(index)?;
|
||||
let first = model.first_brush as usize;
|
||||
let ids = clip
|
||||
.leafbrushes
|
||||
.get(first..first.checked_add(model.num_brushes as usize)?)?;
|
||||
ids.iter()
|
||||
.map(|&id| {
|
||||
let brush = clip.brushes.get(id as usize)?;
|
||||
let planes = &brush.planes;
|
||||
if planes.len() < 6 {
|
||||
return None;
|
||||
}
|
||||
let mins: [f32; 3] = std::array::from_fn(|i| -planes[i * 2 + 1][3]);
|
||||
let maxs: [f32; 3] = std::array::from_fn(|i| planes[i * 2][3]);
|
||||
let mid = std::array::from_fn(|i| (mins[i] + maxs[i]) * 0.5);
|
||||
let half = std::array::from_fn(|i| (maxs[i] - mins[i]) * 0.5);
|
||||
let slabs = planes[6..]
|
||||
.iter()
|
||||
.map(|p| {
|
||||
let dir = [p[0], p[1], p[2]];
|
||||
// The opposite slab face is outside the brush's axial bounds.
|
||||
let lower = (0..3)
|
||||
.map(|i| dir[i] * if dir[i] >= 0.0 { mins[i] } else { maxs[i] })
|
||||
.sum::<f32>();
|
||||
(dir, (lower + p[3]) * 0.5, (p[3] - lower) * 0.5)
|
||||
})
|
||||
.collect();
|
||||
Some(MapTriggerHull { mid, half, slabs })
|
||||
})
|
||||
.collect()
|
||||
})();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -221,11 +221,11 @@ pub struct ModelSurfaceDraw {
|
||||
|
||||
pub xsurface_vert_offset: u16,
|
||||
|
||||
pub collision: RetailXSurfaceCollisionPayload,
|
||||
pub collision: XSurfaceCollisionPayload,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum RetailXSurfaceCollisionPayload {
|
||||
pub enum XSurfaceCollisionPayload {
|
||||
Iw4(Vec<OwnedXRigidVertListCollision>),
|
||||
Unavailable { source_layout: &'static str },
|
||||
}
|
||||
@@ -246,12 +246,12 @@ pub struct OwnedXSurfaceCollisionTree {
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum RetailPackedVertexPayload {
|
||||
pub enum PackedVertexPayload {
|
||||
Iw4(Vec<[u8; asset_iw4::size::GFX_PACKED_VERTEX]>),
|
||||
Unavailable { source_layout: &'static str },
|
||||
}
|
||||
|
||||
impl Default for RetailPackedVertexPayload {
|
||||
impl Default for PackedVertexPayload {
|
||||
fn default() -> Self {
|
||||
Self::Unavailable {
|
||||
source_layout: "packed vertex payload not installed",
|
||||
@@ -353,7 +353,7 @@ pub struct MapXModelSceneCatalog {
|
||||
|
||||
phys_preset_name_hint_n: usize,
|
||||
|
||||
dynent_phys_preset_n: usize,
|
||||
phys_preset_n: usize,
|
||||
|
||||
surface_materials: BTreeMap<MapXModelAssetKey, Vec<Option<crate::MaterialIndex>>>,
|
||||
resolved: bool,
|
||||
@@ -441,6 +441,25 @@ impl MapXModelSceneCatalog {
|
||||
self.assets.entry(key).or_insert(asset);
|
||||
}
|
||||
|
||||
pub fn remap_walk_materials(&mut self, donor_to_host: &[usize]) {
|
||||
for asset in self.assets.values_mut() {
|
||||
let (MapXModelSceneAsset::Iw4(skel)
|
||||
| MapXModelSceneAsset::T5(skel)
|
||||
| MapXModelSceneAsset::Iw5(skel)) = asset
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let skel = Arc::make_mut(skel);
|
||||
for slot in &mut skel.surface_materials {
|
||||
*slot = slot.and_then(|local| {
|
||||
donor_to_host
|
||||
.get(local.get())
|
||||
.map(|&host| asset_core::WalkLocalMaterialIndex::from_walk(host))
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn absorb_captured(&mut self, mut earlier: Self) {
|
||||
for (index, key) in earlier.order.into_iter().enumerate() {
|
||||
let asset = earlier
|
||||
@@ -468,10 +487,51 @@ impl MapXModelSceneCatalog {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn retain_names(&mut self, names: &std::collections::BTreeSet<String>) {
|
||||
self.assets.retain(|key, _| names.contains(&key.0));
|
||||
self.surface_materials
|
||||
.retain(|key, _| names.contains(&key.0));
|
||||
let mut at = 0;
|
||||
self.order.retain(|key| {
|
||||
let keep = names.contains(&key.0);
|
||||
if keep {
|
||||
at += 1;
|
||||
} else {
|
||||
self.zones.remove(at);
|
||||
}
|
||||
keep
|
||||
});
|
||||
}
|
||||
|
||||
pub fn set_capture_zone(&mut self, zone: ZoneOwner) {
|
||||
self.capture_zone = zone;
|
||||
}
|
||||
|
||||
pub fn alias(&mut self, from: &str, to: &str) -> bool {
|
||||
let Some(asset) = self
|
||||
.assets
|
||||
.get(to)
|
||||
.filter(|asset| !matches!(asset, MapXModelSceneAsset::Unavailable { .. }))
|
||||
.cloned()
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
let zone = self
|
||||
.index_by_name(to)
|
||||
.map(|index| self.zone_of(index))
|
||||
.unwrap_or_default();
|
||||
let key = MapXModelAssetKey(from.to_owned());
|
||||
match self.order.iter().position(|k| k == &key) {
|
||||
Some(slot) => self.zones[slot] = zone,
|
||||
None => {
|
||||
self.order.push(key.clone());
|
||||
self.zones.push(zone);
|
||||
}
|
||||
}
|
||||
self.assets.insert(key, asset);
|
||||
true
|
||||
}
|
||||
|
||||
pub fn zone_of(&self, index: usize) -> ZoneOwner {
|
||||
self.zones.get(index).copied().unwrap_or_default()
|
||||
}
|
||||
@@ -505,12 +565,12 @@ impl MapXModelSceneCatalog {
|
||||
self.phys_preset_name_hint_n
|
||||
}
|
||||
|
||||
pub fn dynent_phys_preset_n(&self) -> usize {
|
||||
self.dynent_phys_preset_n
|
||||
pub fn phys_preset_n(&self) -> usize {
|
||||
self.phys_preset_n
|
||||
}
|
||||
|
||||
pub fn set_dynent_phys_preset_n(&mut self, n: usize) {
|
||||
self.dynent_phys_preset_n = n;
|
||||
self.phys_preset_n = n;
|
||||
}
|
||||
|
||||
pub fn note_xmodel_phys_preset(&mut self, slot: bool, named: bool) {
|
||||
@@ -600,8 +660,6 @@ pub struct PreparedMapModels {
|
||||
|
||||
pub script_brush_models: Vec<crate::ScriptBrushModelPlacement>,
|
||||
|
||||
pub map_use_triggers: Vec<crate::MapUseTrigger>,
|
||||
|
||||
pub flag_descriptors: Vec<crate::FlagDescriptor>,
|
||||
|
||||
pub script_structs: Vec<crate::MapScriptStruct>,
|
||||
@@ -1322,7 +1380,7 @@ pub fn build_iw5_xmodel_mesh(
|
||||
)),
|
||||
xsurface_base_index: 0,
|
||||
xsurface_vert_offset: 0,
|
||||
collision: RetailXSurfaceCollisionPayload::Unavailable {
|
||||
collision: XSurfaceCollisionPayload::Unavailable {
|
||||
source_layout: "IW5 XSurface collision layout not retained",
|
||||
},
|
||||
});
|
||||
@@ -1355,10 +1413,10 @@ fn capture_iw4_xsurface_collision(
|
||||
surface: Ptr,
|
||||
surface_index: usize,
|
||||
surface_tri_count: usize,
|
||||
) -> Result<RetailXSurfaceCollisionPayload, ModelMeshError> {
|
||||
) -> Result<XSurfaceCollisionPayload, ModelMeshError> {
|
||||
let count = stream.u32_at(surface, stream.layout(32, 48))? as usize;
|
||||
if count == 0 {
|
||||
return Ok(RetailXSurfaceCollisionPayload::Iw4(Vec::new()));
|
||||
return Ok(XSurfaceCollisionPayload::Iw4(Vec::new()));
|
||||
}
|
||||
let lists = resolved_ptr(stream, surface, stream.layout(36, 56))?;
|
||||
stream.slice_at(
|
||||
@@ -1404,7 +1462,7 @@ fn capture_iw4_xsurface_collision(
|
||||
tree,
|
||||
});
|
||||
}
|
||||
Ok(RetailXSurfaceCollisionPayload::Iw4(trees))
|
||||
Ok(XSurfaceCollisionPayload::Iw4(trees))
|
||||
}
|
||||
|
||||
fn capture_iw4_collision_tree(
|
||||
@@ -1483,10 +1541,10 @@ fn capture_t5_xsurface_collision(
|
||||
surface: fastfile_t5::Ptr,
|
||||
surface_index: usize,
|
||||
surface_tri_count: usize,
|
||||
) -> Result<RetailXSurfaceCollisionPayload, ModelMeshError> {
|
||||
) -> Result<XSurfaceCollisionPayload, ModelMeshError> {
|
||||
let count = stream.u8_at(surface, 1)? as usize;
|
||||
if count == 0 {
|
||||
return Ok(RetailXSurfaceCollisionPayload::Iw4(Vec::new()));
|
||||
return Ok(XSurfaceCollisionPayload::Iw4(Vec::new()));
|
||||
}
|
||||
let lists = resolved_ptr_t5(stream, surface, 40)?;
|
||||
stream.slice_at(lists, 0, count * fastfile_t5::size::XRIGID_VERT_LIST)?;
|
||||
@@ -1530,7 +1588,7 @@ fn capture_t5_xsurface_collision(
|
||||
tree,
|
||||
});
|
||||
}
|
||||
Ok(RetailXSurfaceCollisionPayload::Iw4(trees))
|
||||
Ok(XSurfaceCollisionPayload::Iw4(trees))
|
||||
}
|
||||
|
||||
fn capture_t5_collision_tree(
|
||||
|
||||
@@ -258,7 +258,7 @@ fn reject_negative_half(
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum RetailWorldVertexPayload {
|
||||
pub enum WorldVertexPayload {
|
||||
Iw4(Vec<[u8; asset_iw4::size::GFX_WORLD_VERTEX]>),
|
||||
|
||||
Iw5(Vec<[u8; fastfile_iw5::size::GFX_WORLD_VERTEX]>),
|
||||
@@ -267,7 +267,7 @@ pub enum RetailWorldVertexPayload {
|
||||
Unavailable { source_layout: &'static str },
|
||||
}
|
||||
|
||||
impl Default for RetailWorldVertexPayload {
|
||||
impl Default for WorldVertexPayload {
|
||||
fn default() -> Self {
|
||||
Self::Unavailable {
|
||||
source_layout: "world vertex payload not installed",
|
||||
@@ -275,7 +275,7 @@ impl Default for RetailWorldVertexPayload {
|
||||
}
|
||||
}
|
||||
|
||||
impl RetailWorldVertexPayload {
|
||||
impl WorldVertexPayload {
|
||||
pub fn type2_stream0(
|
||||
&self,
|
||||
) -> Result<&[[u8; asset_iw4::size::GFX_WORLD_VERTEX]], &'static str> {
|
||||
@@ -308,7 +308,7 @@ pub struct WorldDraw {
|
||||
pub lightmap: Result<Vec<Option<WorldLightmap>>, WorldLightmapGap>,
|
||||
pub stats: WorldMeshStats,
|
||||
|
||||
pub retail_vertices: RetailWorldVertexPayload,
|
||||
pub packed_vertices: WorldVertexPayload,
|
||||
|
||||
pub vertex_layer: Vec<u8>,
|
||||
|
||||
@@ -551,8 +551,8 @@ pub struct WorldLightRegionHull {
|
||||
}
|
||||
|
||||
impl WorldPrimaryLight {
|
||||
pub fn cull_input(&self) -> lighting_iw4::ComPrimaryLightCull {
|
||||
lighting_iw4::ComPrimaryLightCull {
|
||||
pub fn cull_input(&self) -> lighting_iw4::PrimaryLightCull {
|
||||
lighting_iw4::PrimaryLightCull {
|
||||
light_type: self.light_type,
|
||||
origin: self.origin,
|
||||
direction: self.direction,
|
||||
@@ -686,7 +686,7 @@ pub fn build_world_draw(
|
||||
return Err(WorldMeshError::NoGeometry);
|
||||
};
|
||||
|
||||
let mut retail_vertices = Vec::with_capacity(geometry.vertex_count);
|
||||
let mut packed_vertices = Vec::with_capacity(geometry.vertex_count);
|
||||
let mut positions = Vec::with_capacity(geometry.vertex_count);
|
||||
let mut normals = Vec::with_capacity(geometry.vertex_count);
|
||||
let mut tangents = Vec::with_capacity(geometry.vertex_count);
|
||||
@@ -698,11 +698,11 @@ pub fn build_world_draw(
|
||||
|
||||
for i in 0..geometry.vertex_count {
|
||||
let v = vertices.at(i * sz::GFX_WORLD_VERTEX);
|
||||
let mut retail = [0u8; sz::GFX_WORLD_VERTEX];
|
||||
for (offset, byte) in retail.iter_mut().enumerate() {
|
||||
let mut packed = [0u8; sz::GFX_WORLD_VERTEX];
|
||||
for (offset, byte) in packed.iter_mut().enumerate() {
|
||||
*byte = s.u8_at(v, offset)?;
|
||||
}
|
||||
retail_vertices.push(retail);
|
||||
packed_vertices.push(packed);
|
||||
let xyz = [s.f32_at(v, 0)?, s.f32_at(v, 4)?, s.f32_at(v, 8)?];
|
||||
for axis in 0..3 {
|
||||
min[axis] = min[axis].min(xyz[axis]);
|
||||
@@ -874,7 +874,7 @@ pub fn build_world_draw(
|
||||
batches,
|
||||
lightmap,
|
||||
stats,
|
||||
retail_vertices: RetailWorldVertexPayload::Iw4(retail_vertices),
|
||||
packed_vertices: WorldVertexPayload::Iw4(packed_vertices),
|
||||
vertex_layer: Vec::new(),
|
||||
surface_vertex_layer: Vec::new(),
|
||||
surface_first_vertex: surface_draw_fields
|
||||
|
||||
@@ -8,8 +8,8 @@ use fastfile_iw5::{GfxImageGeometry, GfxWorldGeometry, MAX_LIGHTMAP_PAGES, ZoneP
|
||||
|
||||
use crate::world_draw::{
|
||||
CameraRangeKind, CameraSurfRange, CameraSurfRanges, CapturedLightDef, DpvsWorldData,
|
||||
OwnedPortal, RetailWorldVertexPayload, SurfaceDrawFields, WorldDraw, WorldLightRegionHull,
|
||||
WorldLightmap, WorldLightmapGap, WorldPrimaryLight, WorldReflectionProbe,
|
||||
OwnedPortal, SurfaceDrawFields, WorldDraw, WorldLightRegionHull, WorldLightmap,
|
||||
WorldLightmapGap, WorldPrimaryLight, WorldReflectionProbe, WorldVertexPayload,
|
||||
make_material_batches, primary_mask_payload, scatter_vertex_layer_type3,
|
||||
secondary_page_payloads,
|
||||
};
|
||||
@@ -38,7 +38,7 @@ pub fn build_iw5_world_draw(
|
||||
return Err(WorldMeshError::NoGeometry);
|
||||
};
|
||||
|
||||
let mut retail_vertices = Vec::with_capacity(geometry.vertex_count);
|
||||
let mut packed_vertices = Vec::with_capacity(geometry.vertex_count);
|
||||
let mut positions = Vec::with_capacity(geometry.vertex_count);
|
||||
let mut normals = Vec::with_capacity(geometry.vertex_count);
|
||||
let mut tangents = Vec::with_capacity(geometry.vertex_count);
|
||||
@@ -50,11 +50,11 @@ pub fn build_iw5_world_draw(
|
||||
|
||||
for i in 0..geometry.vertex_count {
|
||||
let v = vertices.at(i * sz::GFX_WORLD_VERTEX);
|
||||
let mut retail = [0u8; sz::GFX_WORLD_VERTEX];
|
||||
for (offset, byte) in retail.iter_mut().enumerate() {
|
||||
let mut packed = [0u8; sz::GFX_WORLD_VERTEX];
|
||||
for (offset, byte) in packed.iter_mut().enumerate() {
|
||||
*byte = s.u8_at(v, offset).map_err(WorldMeshError::from)?;
|
||||
}
|
||||
retail_vertices.push(retail);
|
||||
packed_vertices.push(packed);
|
||||
let xyz = [
|
||||
s.f32_at(v, 0).map_err(WorldMeshError::from)?,
|
||||
s.f32_at(v, 4).map_err(WorldMeshError::from)?,
|
||||
@@ -261,7 +261,7 @@ pub fn build_iw5_world_draw(
|
||||
batches,
|
||||
lightmap,
|
||||
stats,
|
||||
retail_vertices: RetailWorldVertexPayload::Iw5(retail_vertices),
|
||||
packed_vertices: WorldVertexPayload::Iw5(packed_vertices),
|
||||
vertex_layer,
|
||||
surface_vertex_layer,
|
||||
surface_first_vertex,
|
||||
|
||||
@@ -13,8 +13,8 @@ use fastfile_t5::{GfxWorldGeometry, ZonePtr, ZoneStream};
|
||||
|
||||
use crate::world_draw::{
|
||||
CameraRangeKind, CameraSurfRange, CameraSurfRanges, DpvsWorldData, OwnedPortal,
|
||||
RetailWorldVertexPayload, SurfaceDrawFields, WorldBatch, WorldDraw, WorldLightmap,
|
||||
WorldLightmapGap, WorldPrimaryLight, WorldReflectionProbe,
|
||||
SurfaceDrawFields, WorldBatch, WorldDraw, WorldLightmap, WorldLightmapGap, WorldPrimaryLight,
|
||||
WorldReflectionProbe, WorldVertexPayload,
|
||||
};
|
||||
use crate::world_mesh::{
|
||||
WorldMeshError, WorldMeshStats, normalize_or_up, unpack_color, unpack_unit_vec,
|
||||
@@ -86,7 +86,7 @@ fn iw4_ptr(p: fastfile_t5::Ptr) -> Iw4Ptr {
|
||||
pub fn build_t5_world_draw(
|
||||
s: &ZoneStream<'_>,
|
||||
geometry: GfxWorldGeometry,
|
||||
materials: MaterialCatalog,
|
||||
mut materials: MaterialCatalog,
|
||||
) -> Result<(WorldDraw, MaterialCatalog), WorldMeshError> {
|
||||
let Some(vertices) = geometry.vertices else {
|
||||
return Err(WorldMeshError::NoGeometry);
|
||||
@@ -98,7 +98,63 @@ pub fn build_t5_world_draw(
|
||||
return Err(WorldMeshError::NoGeometry);
|
||||
};
|
||||
|
||||
let mut retail_vertices = Vec::with_capacity(geometry.vertex_count);
|
||||
if let Some(table) = geometry.terrain_scorch_images {
|
||||
let images: [Option<usize>; 31] =
|
||||
std::array::from_fn(|index| materials.image_index(iw4_ptr(table.at(index * 4))));
|
||||
let white = materials
|
||||
.image_index_by_key(asset_material::AssetNamespace::T5, "$white")
|
||||
.unwrap_or_else(|| {
|
||||
let index = materials.images.len();
|
||||
materials.images.push(asset_material::AuthoredImage {
|
||||
namespace: asset_material::AssetNamespace::T5,
|
||||
name: asset_material::AssetRef::decode("$white"),
|
||||
map_type: 3,
|
||||
semantic: 2,
|
||||
category: 1,
|
||||
use_srgb_reads: false,
|
||||
width: 1,
|
||||
height: 1,
|
||||
depth: 1,
|
||||
level_count: 1,
|
||||
format: 21,
|
||||
payload: std::sync::Arc::new(vec![255; 4]),
|
||||
decoded: None,
|
||||
common_owned: false,
|
||||
decoded_variant: None,
|
||||
decoded_by: None,
|
||||
pending_decode: None,
|
||||
});
|
||||
index
|
||||
});
|
||||
for material in &mut materials.materials {
|
||||
let Some(types) = material.t5_layered_surface_types else {
|
||||
continue;
|
||||
};
|
||||
for layer in 0..((types >> 29) as usize).min(4) {
|
||||
let surface = ((types >> (layer * 6)) & 0x3f) as usize;
|
||||
let image = images
|
||||
.get(surface)
|
||||
.copied()
|
||||
.flatten()
|
||||
.or(images[0])
|
||||
.or(Some(white));
|
||||
material
|
||||
.textures
|
||||
.push(asset_material::MaterialTextureBinding {
|
||||
name_hash: asset_material::t5_code_remap::terrain_scorch_binding_hash(
|
||||
layer,
|
||||
),
|
||||
name_start: b'i',
|
||||
name_end: b'0' + layer as u8,
|
||||
sampler_state: 0x62,
|
||||
semantic: 2,
|
||||
image,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut packed_vertices = Vec::with_capacity(geometry.vertex_count);
|
||||
let mut positions = Vec::with_capacity(geometry.vertex_count);
|
||||
let mut normals = Vec::with_capacity(geometry.vertex_count);
|
||||
let mut tangents = Vec::with_capacity(geometry.vertex_count);
|
||||
@@ -110,11 +166,11 @@ pub fn build_t5_world_draw(
|
||||
|
||||
for i in 0..geometry.vertex_count {
|
||||
let v = vertices.at(i * iw4_sz::GFX_WORLD_VERTEX);
|
||||
let mut retail = [0u8; fastfile_t5::size::GFX_WORLD_VERTEX];
|
||||
for (offset, byte) in retail.iter_mut().enumerate() {
|
||||
let mut packed = [0u8; fastfile_t5::size::GFX_WORLD_VERTEX];
|
||||
for (offset, byte) in packed.iter_mut().enumerate() {
|
||||
*byte = s.u8_at(v, offset).map_err(|_| WorldMeshError::NoGeometry)?;
|
||||
}
|
||||
retail_vertices.push(retail);
|
||||
packed_vertices.push(packed);
|
||||
let xyz = [
|
||||
s.f32_at(v, 0).map_err(|_| WorldMeshError::NoGeometry)?,
|
||||
s.f32_at(v, 4).map_err(|_| WorldMeshError::NoGeometry)?,
|
||||
@@ -411,7 +467,7 @@ pub fn build_t5_world_draw(
|
||||
batches,
|
||||
lightmap,
|
||||
stats,
|
||||
retail_vertices: RetailWorldVertexPayload::T5(retail_vertices),
|
||||
packed_vertices: WorldVertexPayload::T5(packed_vertices),
|
||||
vertex_layer,
|
||||
surface_vertex_layer,
|
||||
surface_first_vertex,
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
use crate::{
|
||||
MaterialDefinitions, body_catalog::BodyMeshBuild, fpv_catalog::FpvMeshBuild,
|
||||
fx_catalog::FxCatalog, model_mesh::MapXModelSceneCatalog,
|
||||
projectile_mesh_catalog::ProjectileMeshBuild, sound_catalog::SoundCatalog,
|
||||
tracer_catalog::TracerCatalog, weapon_catalog::WeaponCatalog,
|
||||
world_weapon_catalog::WorldWeaponBuild, xanim_catalog::XAnimCatalog,
|
||||
};
|
||||
use asset_anim::XAnimCatalog;
|
||||
use asset_audio::SoundCatalog;
|
||||
pub use asset_core::{
|
||||
AssetEdge, AssetEdgeCensus, AssetEdgeReason, CatalogIndex, FpvMeshIndex, FpvMeshSpace, FxIndex,
|
||||
FxModelIndex, FxModelSpace, FxSpace, IndexSpace, LoadedSoundIndex, LoadedSoundSpace,
|
||||
MapXModelIndex, MapXModelSpace, MaterialIndex, MaterialSpace, ProjectileModelIndex,
|
||||
ProjectileModelSpace, SoundAliasIndex, SoundAliasSpace, TechniqueSetIndex, TechniqueSetSpace,
|
||||
TracerIndex, TracerSpace, WorldWeaponIndex, WorldWeaponSpace, XAnimIndex, XAnimSpace,
|
||||
ZoneOwner,
|
||||
AssetEdge, AssetEdgeCensus, AssetEdgeReason, FpvMeshIndex, FpvMeshSpace, FxIndex, FxModelIndex,
|
||||
FxModelSpace, FxSpace, LoadedSoundIndex, LoadedSoundSpace, MapXModelIndex, MapXModelSpace,
|
||||
MaterialIndex, MaterialSpace, ProjectileModelIndex, ProjectileModelSpace, SoundAliasIndex,
|
||||
SoundAliasSpace, TechniqueSetIndex, TechniqueSetSpace, TracerIndex, TracerSpace,
|
||||
WorldWeaponIndex, WorldWeaponSpace, XAnimIndex, XAnimSpace,
|
||||
};
|
||||
use asset_core::{CatalogIndex, IndexSpace, ZoneOwner};
|
||||
use asset_game::{FxCatalog, TracerCatalog, WeaponCatalog};
|
||||
use asset_material::MaterialDefinitions;
|
||||
use asset_model::{BodyMeshBuild, FpvMeshBuild, ProjectileMeshBuild, WorldWeaponBuild};
|
||||
use asset_world::MapXModelSceneCatalog;
|
||||
|
||||
pub type DeathClipEdge = AssetEdge<XAnimSpace>;
|
||||
|
||||
@@ -270,7 +269,7 @@ pub(crate) fn stamp_destructible_death_edges(
|
||||
husk_hint: hint.husk,
|
||||
clip: {
|
||||
let index = xanims
|
||||
.index_by_name(crate::AssetNamespace::Iw4, hint.clip)
|
||||
.index_by_name(asset_core::AssetNamespace::Iw4, hint.clip)
|
||||
.map(CatalogIndex::<XAnimSpace>::from_order);
|
||||
edge_from_index(
|
||||
hint.clip,
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
pub use asset_transport::{
|
||||
GamesRoot, ZoneFile, ensure_artifacts_dir, find_common_mp_for_envelope,
|
||||
find_common_mp_for_zone, find_localized_common_mp_for_zone, find_runtime_common_mp,
|
||||
find_runtime_zone, find_zone_file, find_zone_file_version, find_zone_for_tree,
|
||||
games_content_report, games_root_from_env, games_root_report, group_mp_maps, list_mp_maps,
|
||||
load_dotenv, map_load_title, peek_zone_version, split_zone_key, zone_game_for_path,
|
||||
};
|
||||
@@ -12,10 +12,9 @@ use bevy::{
|
||||
};
|
||||
use serde_json::{Map, Value, json};
|
||||
|
||||
use crate::{
|
||||
AuthoredMaterial, MaterialCullFace, MaterialDrawMode, PreparedWorld, StaticModelPlacement,
|
||||
TS_COLOR_MAP,
|
||||
};
|
||||
use crate::PreparedWorld;
|
||||
use asset_material::{AuthoredMaterial, MaterialCullFace, MaterialDrawMode, TS_COLOR_MAP};
|
||||
use asset_world::StaticModelPlacement;
|
||||
|
||||
const INCHES_TO_METERS: f32 = 0.0254;
|
||||
type PlacementTrs = ([f32; 3], [f32; 4], [f32; 3]);
|
||||
@@ -191,7 +190,7 @@ struct GltfBuilder {
|
||||
impl GltfBuilder {
|
||||
fn add_world(
|
||||
&mut self,
|
||||
draw: &crate::WorldDraw,
|
||||
draw: &asset_world::WorldDraw,
|
||||
materials: &crate::MatchMaterials,
|
||||
) -> Result<usize, String> {
|
||||
let mut primitives = Vec::new();
|
||||
@@ -228,7 +227,7 @@ impl GltfBuilder {
|
||||
|
||||
fn add_model(
|
||||
&mut self,
|
||||
model: &crate::ModelMesh,
|
||||
model: &asset_world::ModelMesh,
|
||||
materials: &crate::MatchMaterials,
|
||||
) -> Option<usize> {
|
||||
let mut primitives = Vec::new();
|
||||
@@ -352,7 +351,7 @@ impl GltfBuilder {
|
||||
fn material(
|
||||
&mut self,
|
||||
canonical: usize,
|
||||
catalog: &crate::MaterialDefinitions,
|
||||
catalog: &asset_material::MaterialDefinitions,
|
||||
) -> Result<usize, &'static str> {
|
||||
if let Some(cached) = self.material_cache.get(&canonical) {
|
||||
return *cached;
|
||||
@@ -365,7 +364,7 @@ impl GltfBuilder {
|
||||
fn build_material(
|
||||
&mut self,
|
||||
canonical: usize,
|
||||
catalog: &crate::MaterialDefinitions,
|
||||
catalog: &asset_material::MaterialDefinitions,
|
||||
) -> Result<usize, &'static str> {
|
||||
let material = catalog.materials.get(canonical).ok_or("MaterialUnbound")?;
|
||||
let (alpha_mode, alpha_cutoff, coverage_exact) = match catalog.agreed_draw_mode(material) {
|
||||
@@ -442,7 +441,7 @@ impl GltfBuilder {
|
||||
&mut self,
|
||||
image_index: usize,
|
||||
sampler_state: u8,
|
||||
catalog: &crate::MaterialDefinitions,
|
||||
catalog: &asset_material::MaterialDefinitions,
|
||||
) -> Result<usize, &'static str> {
|
||||
if let Some(&texture) = self.texture_cache.get(&(image_index, sampler_state)) {
|
||||
return Ok(texture);
|
||||
@@ -605,7 +604,7 @@ fn canonical_material(local: Option<usize>, remap: &[Option<usize>]) -> Option<u
|
||||
|
||||
fn single_color_binding(
|
||||
material: &AuthoredMaterial,
|
||||
) -> Result<&crate::MaterialTextureBinding, &'static str> {
|
||||
) -> Result<&asset_material::MaterialTextureBinding, &'static str> {
|
||||
let mut bindings = material
|
||||
.textures
|
||||
.iter()
|
||||
|
||||
@@ -3,14 +3,15 @@ use std::collections::HashMap;
|
||||
use bevy::prelude::{Mat3, Quat, Transform, Vec3};
|
||||
use fastfile_iw4::{Ptr, ZoneStream};
|
||||
|
||||
use crate::{
|
||||
MapXModelAssetKey, MapXModelSceneAsset, MapXModelSceneCatalog, MaterialCatalog, ModelMesh,
|
||||
PreparedMapModels, ScriptModelMetadata, ScriptModelPlacement, ScriptModelSceneInstance,
|
||||
StaticModelDraw, StaticModelDrawError, StaticModelInstance, StaticModelPlacement,
|
||||
use asset_material::MaterialCatalog;
|
||||
use asset_world::{
|
||||
MapXModelAssetKey, MapXModelSceneAsset, MapXModelSceneCatalog, ModelMesh, PreparedMapModels,
|
||||
ScriptModelMetadata, ScriptModelPlacement, ScriptModelSceneInstance, StaticModelDraw,
|
||||
StaticModelDrawError, StaticModelInstance, StaticModelPlacement,
|
||||
build_iw5_static_model_instances, build_iw5_xmodel_mesh, build_static_model_instances,
|
||||
build_t5_static_model_instances, build_t5_xmodel_mesh, build_xmodel_mesh, flag_descriptors,
|
||||
flag_descriptors_iw5, flag_descriptors_t5, map_script_structs, map_script_structs_iw5,
|
||||
map_script_structs_t5, map_use_triggers, map_use_triggers_iw5, map_use_triggers_t5,
|
||||
map_script_structs_t5, map_use_triggers_iw5, map_use_triggers_t5,
|
||||
script_brush_model_placements, script_brush_model_placements_iw5,
|
||||
script_brush_model_placements_t5, script_model_placements, script_model_placements_iw5,
|
||||
script_model_placements_t5,
|
||||
@@ -46,7 +47,7 @@ impl MapXModelCatalog {
|
||||
Some(self.meshes.remove(index))
|
||||
}
|
||||
|
||||
pub(crate) fn set_capture_zone(&mut self, zone: crate::ZoneOwner) {
|
||||
pub(crate) fn set_capture_zone(&mut self, zone: asset_core::ZoneOwner) {
|
||||
self.scene_assets.set_capture_zone(zone);
|
||||
}
|
||||
|
||||
@@ -57,7 +58,7 @@ impl MapXModelCatalog {
|
||||
slot: Ptr,
|
||||
insert_slot: Option<Ptr>,
|
||||
strings: &fastfile_iw4::ScriptStrings,
|
||||
phys_presets: &crate::PhysPresetCatalog,
|
||||
phys_presets: &asset_world::PhysPresetCatalog,
|
||||
) {
|
||||
let Some(geometry) = stream.xmodel() else {
|
||||
self.failed += 1;
|
||||
@@ -115,7 +116,7 @@ impl MapXModelCatalog {
|
||||
return;
|
||||
};
|
||||
let scene_key = MapXModelAssetKey(mesh.name.clone());
|
||||
let scene_asset = crate::capture_xmodel_skel_t5(stream, strings, geometry, materials)
|
||||
let scene_asset = asset_model::capture_xmodel_skel_t5(stream, strings, geometry, materials)
|
||||
.map(|skel| MapXModelSceneAsset::T5(std::sync::Arc::new(skel)))
|
||||
.unwrap_or(MapXModelSceneAsset::Unavailable {
|
||||
reason: "T5 XModel scene skeleton capture failed",
|
||||
@@ -144,7 +145,7 @@ impl MapXModelCatalog {
|
||||
stream: &fastfile_t5::ZoneStream<'_>,
|
||||
header: fastfile_t5::Ptr,
|
||||
strings: &fastfile_t5::ScriptStrings,
|
||||
fx: &crate::FxCatalog,
|
||||
fx: &asset_game::FxCatalog,
|
||||
) -> fastfile_t5::Result<()> {
|
||||
use fastfile_t5::ZonePtr;
|
||||
let ZonePtr::Offset(name) = stream.ptr_at(header, 0)? else {
|
||||
@@ -279,17 +280,27 @@ impl MapXModelCatalog {
|
||||
self.failed += 1;
|
||||
return;
|
||||
};
|
||||
let Some(name) = geometry.name.and_then(|p| stream.cstr(p).ok()) else {
|
||||
self.failed += 1;
|
||||
return;
|
||||
};
|
||||
let scene_key = MapXModelAssetKey(name.to_owned());
|
||||
let scene_asset = asset_model::capture_xmodel_skel_iw5_with_shared(
|
||||
stream,
|
||||
strings,
|
||||
geometry,
|
||||
materials,
|
||||
&self.shared_surfaces,
|
||||
)
|
||||
.map(|skel| MapXModelSceneAsset::Iw5(std::sync::Arc::new(skel)))
|
||||
.unwrap_or(MapXModelSceneAsset::Unavailable {
|
||||
reason: "IW5 XModel scene skeleton capture failed",
|
||||
});
|
||||
self.scene_assets.insert(scene_key, scene_asset);
|
||||
let Ok(mesh) = build_iw5_xmodel_mesh(stream, geometry, Some(materials)) else {
|
||||
self.failed += 1;
|
||||
return;
|
||||
};
|
||||
let scene_key = MapXModelAssetKey(mesh.name.clone());
|
||||
let scene_asset = crate::capture_xmodel_skel_iw5(stream, strings, geometry, materials)
|
||||
.map(|skel| MapXModelSceneAsset::Iw5(std::sync::Arc::new(skel)))
|
||||
.unwrap_or(MapXModelSceneAsset::Unavailable {
|
||||
reason: "IW5 XModel scene skeleton capture failed",
|
||||
});
|
||||
self.scene_assets.insert(scene_key, scene_asset);
|
||||
let index = self.meshes.len();
|
||||
self.meshes.push(mesh);
|
||||
let slot = Ptr {
|
||||
@@ -332,9 +343,9 @@ impl MapXModelCatalog {
|
||||
&self,
|
||||
stream: &fastfile_t5::ZoneStream<'_>,
|
||||
geometry: fastfile_t5::ClipMapGeometry,
|
||||
clip: &mut crate::ClipCollision,
|
||||
) -> Result<(), crate::ClipCollisionError> {
|
||||
use crate::ClipCollisionError::{MissingTables, Truncated};
|
||||
clip: &mut asset_world::ClipCollision,
|
||||
) -> Result<(), asset_world::ClipCollisionError> {
|
||||
use asset_world::ClipCollisionError::{MissingTables, Truncated};
|
||||
let Some(rows) = geometry.static_models else {
|
||||
return if geometry.static_model_count == 0 {
|
||||
Ok(())
|
||||
@@ -396,7 +407,7 @@ impl MapXModelCatalog {
|
||||
})
|
||||
.collect(),
|
||||
};
|
||||
clip.static_models.push(crate::ClipPlacedStaticModel {
|
||||
clip.static_models.push(asset_world::ClipPlacedStaticModel {
|
||||
index: index as u32,
|
||||
name: name.to_owned(),
|
||||
model: clipmap_iw4::ClipStaticModel {
|
||||
@@ -444,7 +455,6 @@ pub(crate) fn build_static_model_draw(
|
||||
};
|
||||
let scripts = script_model_placements(stream);
|
||||
let brushes = script_brush_model_placements(stream);
|
||||
let use_triggers = map_use_triggers(stream);
|
||||
let descriptors = flag_descriptors(stream);
|
||||
let structs = map_script_structs(stream);
|
||||
link_model_placements(
|
||||
@@ -452,7 +462,6 @@ pub(crate) fn build_static_model_draw(
|
||||
static_error,
|
||||
&scripts,
|
||||
brushes,
|
||||
use_triggers,
|
||||
descriptors,
|
||||
structs,
|
||||
catalog,
|
||||
@@ -476,11 +485,10 @@ pub(crate) fn build_t5_static_model_draw(
|
||||
};
|
||||
let mut scripts = script_model_placements_t5(stream);
|
||||
let mut brushes = script_brush_model_placements_t5(stream);
|
||||
let mut use_triggers = map_use_triggers_t5(stream);
|
||||
normalize_bomb_sites(
|
||||
&mut scripts,
|
||||
&mut brushes,
|
||||
&mut use_triggers,
|
||||
&map_use_triggers_t5(stream),
|
||||
"bombzone_dem",
|
||||
);
|
||||
let descriptors = flag_descriptors_t5(stream);
|
||||
@@ -490,7 +498,6 @@ pub(crate) fn build_t5_static_model_draw(
|
||||
static_error,
|
||||
&scripts,
|
||||
brushes,
|
||||
use_triggers,
|
||||
descriptors,
|
||||
structs,
|
||||
catalog,
|
||||
@@ -514,8 +521,12 @@ pub(crate) fn build_iw5_static_model_draw(
|
||||
};
|
||||
let mut scripts = script_model_placements_iw5(stream);
|
||||
let mut brushes = script_brush_model_placements_iw5(stream);
|
||||
let mut use_triggers = map_use_triggers_iw5(stream);
|
||||
normalize_bomb_sites(&mut scripts, &mut brushes, &mut use_triggers, "dd_bombzone");
|
||||
normalize_bomb_sites(
|
||||
&mut scripts,
|
||||
&mut brushes,
|
||||
&map_use_triggers_iw5(stream),
|
||||
"dd_bombzone",
|
||||
);
|
||||
let descriptors = flag_descriptors_iw5(stream);
|
||||
let structs = map_script_structs_iw5(stream);
|
||||
link_model_placements(
|
||||
@@ -523,7 +534,6 @@ pub(crate) fn build_iw5_static_model_draw(
|
||||
static_error,
|
||||
&scripts,
|
||||
brushes,
|
||||
use_triggers,
|
||||
descriptors,
|
||||
structs,
|
||||
catalog,
|
||||
@@ -532,9 +542,9 @@ pub(crate) fn build_iw5_static_model_draw(
|
||||
}
|
||||
|
||||
fn normalize_bomb_sites(
|
||||
scripts: &mut [crate::ScriptModelPlacement],
|
||||
brushes: &mut [crate::ScriptBrushModelPlacement],
|
||||
triggers: &mut [crate::MapUseTrigger],
|
||||
scripts: &mut [asset_world::ScriptModelPlacement],
|
||||
brushes: &mut [asset_world::ScriptBrushModelPlacement],
|
||||
triggers: &[asset_world::MapUseTrigger],
|
||||
demolition_tag: &str,
|
||||
) {
|
||||
let has_dedicated_sites = ["a", "b"].into_iter().all(|label| {
|
||||
@@ -564,33 +574,15 @@ fn normalize_bomb_sites(
|
||||
for brush in brushes {
|
||||
normalize(&mut brush.gameobject);
|
||||
}
|
||||
for trigger in triggers {
|
||||
normalize(&mut trigger.gameobject);
|
||||
if trigger.targetname == demolition_tag {
|
||||
trigger.targetname = if trigger
|
||||
.script_label
|
||||
.trim_start_matches('_')
|
||||
.eq_ignore_ascii_case("c")
|
||||
{
|
||||
"dd_overtime_bombzone"
|
||||
} else {
|
||||
"bombzone"
|
||||
}
|
||||
.to_owned();
|
||||
} else if trigger.targetname == "bombzone" && has_dedicated_sites {
|
||||
trigger.targetname = "sd_bombzone".to_owned();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn link_model_placements(
|
||||
placements: Vec<StaticModelInstance>,
|
||||
mut static_error: Option<StaticModelDrawError>,
|
||||
scripts: &[ScriptModelPlacement],
|
||||
script_brush_models: Vec<crate::ScriptBrushModelPlacement>,
|
||||
map_use_triggers: Vec<crate::MapUseTrigger>,
|
||||
flag_descriptors: Vec<crate::FlagDescriptor>,
|
||||
script_structs: Vec<crate::MapScriptStruct>,
|
||||
script_brush_models: Vec<asset_world::ScriptBrushModelPlacement>,
|
||||
flag_descriptors: Vec<asset_world::FlagDescriptor>,
|
||||
script_structs: Vec<asset_world::MapScriptStruct>,
|
||||
mut catalog: MapXModelCatalog,
|
||||
smodel_count: usize,
|
||||
) -> PreparedMapModels {
|
||||
@@ -735,7 +727,6 @@ pub(crate) fn link_model_placements(
|
||||
scene_assets: catalog.scene_assets,
|
||||
script_instances,
|
||||
script_brush_models,
|
||||
map_use_triggers,
|
||||
flag_descriptors,
|
||||
script_structs,
|
||||
script_gaps,
|
||||
|
||||
@@ -7,13 +7,15 @@ use super::{
|
||||
ZoneLane, ZoneWalkSink,
|
||||
};
|
||||
use crate::{
|
||||
ZoneGame, ZoneImage, ZoneMemory, build_clip_collision, build_world_draw,
|
||||
census_entity_string_keys, decode_material_color_maps, decode_reflection_probe_cubemap,
|
||||
dm_spawn_points, intermission_view,
|
||||
lane_capability::{LaneStatus, PreparedCapability},
|
||||
map_ents_entity_string, minimap_corners,
|
||||
progress::{LoadProgress, StageId},
|
||||
session_load::{PreparedWorld, WorldDrawPolicy},
|
||||
session_load::PreparedWorld,
|
||||
};
|
||||
use asset_core::ZoneGame;
|
||||
use asset_material::{decode_material_color_maps, decode_reflection_probe_cubemap};
|
||||
use asset_transport::{LoadProgress, StageId, ZoneImage, ZoneMemory};
|
||||
use asset_world::{
|
||||
WorldDrawPolicy, build_clip_collision, build_world_draw, census_entity_string_keys,
|
||||
dm_spawn_points, intermission_view, map_ents_entity_string, minimap_corners,
|
||||
worldspawn_north_yaw,
|
||||
};
|
||||
|
||||
@@ -61,10 +63,10 @@ impl ZoneLane for Iw4Lane {
|
||||
image: &ZoneImage,
|
||||
progress: &LoadProgress,
|
||||
shared_surfaces: asset_model::SharedXModelSurfaces,
|
||||
material_seed: crate::MaterialCatalog,
|
||||
material_seed: asset_material::MaterialCatalog,
|
||||
common_film_visions: &mut std::collections::BTreeMap<
|
||||
String,
|
||||
Result<crate::FilmVision, crate::FilmVisionParseError>,
|
||||
Result<asset_world::FilmVision, asset_world::FilmVisionParseError>,
|
||||
>,
|
||||
) -> LoadedWorld {
|
||||
let mut report = Vec::new();
|
||||
@@ -86,7 +88,7 @@ impl ZoneLane for Iw4Lane {
|
||||
};
|
||||
|
||||
let stage = progress.begin_scoped(StageId::MapAssets, "memory", None);
|
||||
report.push(crate::zone::xfile_arena_row(
|
||||
report.push(asset_transport::xfile_arena_row(
|
||||
"zone arenas map",
|
||||
&header.block_size,
|
||||
fastfile_iw4::XFILE_BLOCK_TEMP,
|
||||
@@ -114,8 +116,8 @@ impl ZoneLane for Iw4Lane {
|
||||
let seeded_techsets = material_seed.technique_set_facts().to_vec();
|
||||
sink.seed_materials(material_seed);
|
||||
sink.map_xmodels.shared_surfaces = shared_surfaces;
|
||||
sink.set_capture_zone(crate::ZoneOwner::from_zone_path(path));
|
||||
sink.set_capture_ns(crate::AssetNamespace::Iw4);
|
||||
sink.set_capture_zone(asset_core::ZoneOwner::from_zone_path(path));
|
||||
sink.set_capture_ns(asset_core::AssetNamespace::Iw4);
|
||||
sink.sound = Some(asset_audio::ZoneSoundCapture::for_map(
|
||||
path,
|
||||
asset_audio::ZoneGame::Iw4,
|
||||
@@ -154,6 +156,10 @@ impl ZoneLane for Iw4Lane {
|
||||
));
|
||||
|
||||
let compass = std::mem::take(&mut sink.compass).resolve(&materials);
|
||||
let mut scripts = std::mem::take(&mut sink.scripts);
|
||||
if let Some(entities) = map_ents_entity_string(&stream) {
|
||||
scripts.set_entities(entities.to_owned());
|
||||
}
|
||||
let script_sound = std::mem::take(&mut sink.script_sound).finish();
|
||||
let exp_fog = sink.exp_fog.take();
|
||||
let createart_name = sink.createart_name.take();
|
||||
@@ -269,7 +275,7 @@ impl ZoneLane for Iw4Lane {
|
||||
let mut clip = None;
|
||||
let mut clip_report = Vec::new();
|
||||
let mut fx_glass = None;
|
||||
let mut dyn_ents = crate::DynEntCatalog::default();
|
||||
let mut dyn_ents = asset_world::DynEntCatalog::default();
|
||||
let materials_side = &mut materials;
|
||||
|
||||
let image_report = crate::session_load::load_pool().scope(|scope| {
|
||||
@@ -286,12 +292,18 @@ impl ZoneLane for Iw4Lane {
|
||||
clip = match stream.clip_map() {
|
||||
Some(geometry) => match build_clip_collision(&stream, geometry) {
|
||||
Ok(mut clip) => {
|
||||
crate::attach_static_models(
|
||||
asset_world::attach_static_models(
|
||||
&stream,
|
||||
geometry,
|
||||
&sink.xmodel_coll,
|
||||
&mut clip,
|
||||
);
|
||||
clip.trigger_models = asset_world::trigger_models(&stream);
|
||||
clip_report.push(format!(
|
||||
"trigger models: {} ({} with hulls)",
|
||||
clip.trigger_models.len(),
|
||||
clip.trigger_models.iter().filter(|h| !h.is_empty()).count()
|
||||
));
|
||||
clip_report.push(format!(
|
||||
"clipmap: planes={} brushes={} leaves={} nodes={} cmodels={} verts={} tris={} smodels={}",
|
||||
geometry.plane_count,
|
||||
@@ -319,7 +331,7 @@ impl ZoneLane for Iw4Lane {
|
||||
None
|
||||
}
|
||||
};
|
||||
let mut census = crate::build_glass_census(&stream, clip.as_ref());
|
||||
let mut census = asset_world::build_glass_census(&stream, clip.as_ref());
|
||||
if leftover
|
||||
.iter()
|
||||
.any(|(intact, shattered)| !intact.is_empty() || !shattered.is_empty())
|
||||
@@ -337,19 +349,19 @@ impl ZoneLane for Iw4Lane {
|
||||
named
|
||||
));
|
||||
|
||||
fx_glass = crate::build_fx_glass_reset(&stream).map(|mut g| {
|
||||
fx_glass = asset_world::build_fx_glass_reset(&stream).map(|mut g| {
|
||||
g.def_materials = leftover;
|
||||
g
|
||||
});
|
||||
dyn_ents = match stream.clip_map() {
|
||||
Some(geometry) => crate::build_dyn_ent_catalog(
|
||||
Some(geometry) => asset_world::build_dyn_ent_catalog(
|
||||
&stream,
|
||||
geometry,
|
||||
|slot| map_xmodels.name_at_slot(slot).map(str::to_owned),
|
||||
|slot| fx.name_at_slot(slot).map(str::to_owned),
|
||||
|slot| phys_presets.at_slot(slot).cloned(),
|
||||
),
|
||||
None => crate::DynEntCatalog::default(),
|
||||
None => asset_world::DynEntCatalog::default(),
|
||||
};
|
||||
clip_stage.done();
|
||||
});
|
||||
@@ -375,7 +387,7 @@ impl ZoneLane for Iw4Lane {
|
||||
}
|
||||
map_xmodels.set_dynent_phys_preset_n(dyn_ents.phys_preset_named_n());
|
||||
report.push(format!(
|
||||
"xmodel physPreset: slot={} named={} models={} (XModel+0x128 NameHint; DynEnt def+44 named={} is a different graph)",
|
||||
"xmodel physPreset: slot={} named={} models={} (dyn-ent def named={} is a different graph)",
|
||||
map_xmodels.phys_preset_slot_n(),
|
||||
map_xmodels.phys_preset_name_hint_n(),
|
||||
map_xmodels.captured_model_n(),
|
||||
@@ -401,6 +413,7 @@ impl ZoneLane for Iw4Lane {
|
||||
arena_bytes as f64 / (1024.0 * 1024.0),
|
||||
));
|
||||
return LoadedWorld {
|
||||
scripts,
|
||||
sound: map_sound,
|
||||
materials,
|
||||
world: PreparedWorld {
|
||||
@@ -496,9 +509,9 @@ impl ZoneLane for Iw4Lane {
|
||||
"script_brushmodel: {} *N placements (SP_script_brushmodel, not DrawInst)",
|
||||
map_models.script_brush_models.len()
|
||||
));
|
||||
let crate::PreparedMapModels {
|
||||
let asset_world::PreparedMapModels {
|
||||
static_draw:
|
||||
crate::StaticModelDraw {
|
||||
asset_world::StaticModelDraw {
|
||||
meshes: static_model_meshes,
|
||||
placements: static_model_instances,
|
||||
..
|
||||
@@ -506,7 +519,6 @@ impl ZoneLane for Iw4Lane {
|
||||
scene_assets: map_xmodel_scene_assets,
|
||||
script_instances: script_model_instances,
|
||||
script_brush_models,
|
||||
map_use_triggers,
|
||||
flag_descriptors,
|
||||
script_structs,
|
||||
..
|
||||
@@ -515,7 +527,7 @@ impl ZoneLane for Iw4Lane {
|
||||
stage.done();
|
||||
let stage = progress.begin_scoped(StageId::MapAssets, "lighting", None);
|
||||
let smodel_lighting_samples = {
|
||||
use crate::model_lighting::{
|
||||
use asset_model::model_lighting::{
|
||||
OwnedLightGrid, build_smodel_lighting_samples_with_sight,
|
||||
census_lit_fragment_tiles, collect_smodel_lighting_origins,
|
||||
};
|
||||
@@ -595,6 +607,7 @@ impl ZoneLane for Iw4Lane {
|
||||
let intermission_view = intermission_view(&stream);
|
||||
let minimap_corners = minimap_corners(&stream);
|
||||
let north_yaw = worldspawn_north_yaw(&stream);
|
||||
let airstrike_height = asset_world::airstrike_height(&stream);
|
||||
let dm_spawns = dm_spawn_points(&stream);
|
||||
push_mapents_key_census(&mut report, &stream);
|
||||
drop(stream);
|
||||
@@ -751,6 +764,7 @@ impl ZoneLane for Iw4Lane {
|
||||
let world_bounds = draw.stats.bounds;
|
||||
handoff.done();
|
||||
LoadedWorld {
|
||||
scripts,
|
||||
sound: map_sound,
|
||||
materials: map_materials,
|
||||
world: PreparedWorld {
|
||||
@@ -761,7 +775,6 @@ impl ZoneLane for Iw4Lane {
|
||||
map_xmodel_scene_assets,
|
||||
script_model_instances,
|
||||
script_brush_models,
|
||||
map_use_triggers,
|
||||
flag_descriptors,
|
||||
script_structs,
|
||||
dyn_ents,
|
||||
@@ -790,6 +803,7 @@ impl ZoneLane for Iw4Lane {
|
||||
facts: crate::MapFacts {
|
||||
minimap_corners,
|
||||
north_yaw,
|
||||
airstrike_height,
|
||||
compass,
|
||||
script_sound,
|
||||
..Default::default()
|
||||
@@ -807,8 +821,9 @@ impl ZoneLane for Iw4Lane {
|
||||
let arena_bytes = memory.total_bytes();
|
||||
drop(memory);
|
||||
LoadedWorld {
|
||||
scripts,
|
||||
sound: map_sound,
|
||||
materials: crate::MaterialCatalog::default(),
|
||||
materials: asset_material::MaterialCatalog::default(),
|
||||
world: PreparedWorld {
|
||||
fx,
|
||||
fx_models,
|
||||
@@ -850,7 +865,7 @@ impl ZoneLane for Iw4Lane {
|
||||
image: &ZoneImage,
|
||||
progress: &LoadProgress,
|
||||
decode_color_maps: bool,
|
||||
material_seed: crate::MaterialCatalog,
|
||||
material_seed: asset_material::MaterialCatalog,
|
||||
) -> CommonCensus {
|
||||
let zone_name = path.file_stem().map_or_else(
|
||||
|| "common_mp".to_owned(),
|
||||
@@ -875,7 +890,7 @@ impl ZoneLane for Iw4Lane {
|
||||
};
|
||||
}
|
||||
};
|
||||
let mut report_arenas = vec![crate::zone::xfile_arena_row(
|
||||
let mut report_arenas = vec![asset_transport::xfile_arena_row(
|
||||
"zone arenas common_mp",
|
||||
&header.block_size,
|
||||
fastfile_iw4::XFILE_BLOCK_TEMP,
|
||||
@@ -887,8 +902,8 @@ impl ZoneLane for Iw4Lane {
|
||||
None,
|
||||
));
|
||||
sink.seed_materials(material_seed);
|
||||
sink.set_capture_zone(crate::ZoneOwner::intern(&zone_name));
|
||||
sink.set_capture_ns(crate::AssetNamespace::Iw4);
|
||||
sink.set_capture_zone(asset_core::ZoneOwner::intern(&zone_name));
|
||||
sink.set_capture_ns(asset_core::AssetNamespace::Iw4);
|
||||
sink.sound = asset_audio::ZoneSoundCapture::claim_common(
|
||||
path,
|
||||
asset_audio::ZoneGame::Iw4,
|
||||
@@ -954,14 +969,14 @@ impl ZoneLane for Iw4Lane {
|
||||
));
|
||||
weapons.stamp_projectile_model_edges(
|
||||
&sink.projectile_meshes,
|
||||
crate::ZoneOwner::intern(&zone_name),
|
||||
asset_core::ZoneOwner::intern(&zone_name),
|
||||
);
|
||||
weapons.resolve_sz_xanim_edges(&sink.xanims);
|
||||
weapons.resolve_fpv_mesh_edges(&sink.fpv_meshes);
|
||||
weapons.resolve_world_model_edges(&sink.world_weapons);
|
||||
let gun_named = weapons.gun_xmodel_count();
|
||||
report.push(format!(
|
||||
"common_mp weapons: {captured} captures → {} unique catalog ids (sorted; not retail bg_weaponIndex); {gun_named} with gunXModel[0]; {} with szXAnims[IDLE]; {} with any szXAnims slot",
|
||||
"common_mp weapons: {captured} captures → {} unique catalog ids (sorted); {gun_named} with gunXModel[0]; {} with szXAnims[IDLE]; {} with any szXAnims slot",
|
||||
weapons.len(),
|
||||
weapons.idle_anim_count(),
|
||||
weapons.sz_xanims_count()
|
||||
@@ -976,7 +991,7 @@ impl ZoneLane for Iw4Lane {
|
||||
weapons.world_model_count(),
|
||||
));
|
||||
report.push(format!(
|
||||
"common_mp projectileModel @+0x420: slot={} bound={} unresolved_hint={} (pending unclassified XModels {}, retained {})",
|
||||
"common_mp projectileModel: slot={} bound={} unresolved_hint={} (pending unclassified XModels {}, retained {})",
|
||||
weapons.projectile_model_count(),
|
||||
weapons.projectile_model_bound_n(),
|
||||
weapons.projectile_model_name_hint_n(),
|
||||
@@ -1064,7 +1079,7 @@ impl ZoneLane for Iw4Lane {
|
||||
sink.fx.len(),
|
||||
sink.fx.capture_gaps
|
||||
));
|
||||
let light_defs = crate::capture_light_defs(&stream, &sink.materials);
|
||||
let light_defs = asset_world::capture_light_defs(&stream, &sink.materials);
|
||||
report.push(format!(
|
||||
"GfxLightDef common_mp: table={} bodies={} recorded={}",
|
||||
sink.light_def_table,
|
||||
@@ -1095,7 +1110,7 @@ impl ZoneLane for Iw4Lane {
|
||||
let mut material_population = sink.materials;
|
||||
|
||||
let stage = progress.begin_scoped(StageId::Images, "common_mp", None);
|
||||
let (inline, mut plan) = crate::material_images::plan_material_color_maps(
|
||||
let (inline, mut plan) = asset_material::material_images::plan_material_color_maps(
|
||||
path,
|
||||
&mut material_population,
|
||||
&stage,
|
||||
@@ -1108,7 +1123,7 @@ impl ZoneLane for Iw4Lane {
|
||||
inline.missing,
|
||||
inline.unsupported
|
||||
));
|
||||
let tracer_inline = crate::material_images::plan_color_or_2d_for_keys(
|
||||
let tracer_inline = asset_material::material_images::plan_color_or_2d_for_keys(
|
||||
&mut plan,
|
||||
&mut material_population,
|
||||
sink.tracers.material_keys(),
|
||||
@@ -1118,7 +1133,7 @@ impl ZoneLane for Iw4Lane {
|
||||
report.push(format!(
|
||||
"common_mp tracer beam images: {tracer_inline} in-zone TS_COLOR_MAP/TS_2D decoded, rest claimed"
|
||||
));
|
||||
let fx_inline = crate::material_images::plan_color_or_2d_for_keys(
|
||||
let fx_inline = asset_material::material_images::plan_color_or_2d_for_keys(
|
||||
&mut plan,
|
||||
&mut material_population,
|
||||
sink.fx.unique_material_keys(),
|
||||
@@ -1138,7 +1153,7 @@ impl ZoneLane for Iw4Lane {
|
||||
let unique: std::collections::BTreeSet<String> =
|
||||
sink.tracers.named_materials().map(str::to_owned).collect();
|
||||
for name in unique {
|
||||
let bind = crate::fx_material_bind_name(&name);
|
||||
let bind = asset_game::material_bind_name(&name);
|
||||
let twins: Vec<&str> = material_population
|
||||
.materials
|
||||
.iter()
|
||||
@@ -1211,6 +1226,7 @@ impl ZoneLane for Iw4Lane {
|
||||
xmodel_walk: sink.models.walk_census(),
|
||||
s1_common_bytes,
|
||||
teamsets: std::collections::HashMap::new(),
|
||||
scripts: sink.scripts,
|
||||
film_visions: sink.film_visions,
|
||||
}
|
||||
} else {
|
||||
@@ -1246,6 +1262,7 @@ impl ZoneLane for Iw4Lane {
|
||||
xmodel_walk: sink.models.walk_census(),
|
||||
s1_common_bytes,
|
||||
teamsets: std::collections::HashMap::new(),
|
||||
scripts: sink.scripts,
|
||||
film_visions: sink.film_visions,
|
||||
}
|
||||
}
|
||||
@@ -1256,7 +1273,7 @@ impl ZoneLane for Iw4Lane {
|
||||
path: &Path,
|
||||
image: &ZoneImage,
|
||||
progress: &LoadProgress,
|
||||
material_seed: crate::MaterialCatalog,
|
||||
material_seed: asset_material::MaterialCatalog,
|
||||
) -> MaterialPopulation {
|
||||
let zone_name = path.file_stem().map_or_else(
|
||||
|| "startup".to_owned(),
|
||||
@@ -1289,8 +1306,8 @@ impl ZoneLane for Iw4Lane {
|
||||
None,
|
||||
));
|
||||
sink.seed_materials(material_seed);
|
||||
sink.set_capture_zone(crate::ZoneOwner::intern(&zone_name));
|
||||
sink.set_capture_ns(crate::AssetNamespace::Iw4);
|
||||
sink.set_capture_zone(asset_core::ZoneOwner::intern(&zone_name));
|
||||
sink.set_capture_ns(asset_core::AssetNamespace::Iw4);
|
||||
sink.sound = asset_audio::ZoneSoundCapture::claim_common(
|
||||
path,
|
||||
asset_audio::ZoneGame::Iw4,
|
||||
@@ -1320,10 +1337,11 @@ impl ZoneLane for Iw4Lane {
|
||||
));
|
||||
MaterialPopulation {
|
||||
walked: sink.walked,
|
||||
light_defs: crate::capture_light_defs(&stream, &sink.materials),
|
||||
light_defs: asset_world::capture_light_defs(&stream, &sink.materials),
|
||||
materials: sink.materials,
|
||||
report,
|
||||
cac_tables: sink.stats_tables.into_values().collect(),
|
||||
scripts: sink.scripts,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1337,9 +1355,9 @@ fn push_mapents_key_census(report: &mut Vec<String>, stream: &fastfile_iw4::Zone
|
||||
|
||||
fn decode_map_material_images(
|
||||
path: &Path,
|
||||
catalog: &mut crate::MaterialCatalog,
|
||||
stage: crate::progress::StageHandle,
|
||||
fx_material_keys: Vec<crate::MaterialKey>,
|
||||
catalog: &mut asset_material::MaterialCatalog,
|
||||
stage: asset_transport::progress::StageHandle,
|
||||
fx_material_keys: Vec<asset_core::MaterialKey>,
|
||||
glass_names: Vec<String>,
|
||||
) -> Vec<String> {
|
||||
let mut report = Vec::new();
|
||||
@@ -1359,11 +1377,11 @@ fn decode_map_material_images(
|
||||
Err(error) => report.push(format!("IWD material-map gap: {error}")),
|
||||
}
|
||||
let mut keys = fx_material_keys;
|
||||
keys.extend(glass_names.iter().map(|name| crate::MaterialKey {
|
||||
namespace: crate::AssetNamespace::Iw4,
|
||||
keys.extend(glass_names.iter().map(|name| asset_core::MaterialKey {
|
||||
namespace: asset_core::AssetNamespace::Iw4,
|
||||
name: name.clone(),
|
||||
}));
|
||||
match crate::material_images::decode_color_or_2d_for_keys(
|
||||
match asset_material::material_images::decode_color_or_2d_for_keys(
|
||||
path,
|
||||
catalog,
|
||||
keys,
|
||||
|
||||
@@ -5,13 +5,16 @@ use super::{
|
||||
ZoneLane, ZoneWalkSink,
|
||||
};
|
||||
use crate::lane_capability::{LaneStatus, PreparedCapability};
|
||||
use crate::progress::{LoadProgress, StageId};
|
||||
use crate::session_load::{PreparedWorld, WorldDrawPolicy};
|
||||
use crate::{
|
||||
BodyMeshBuild, Iw5ZoneMemory, MASK_PLAYER_SOLID, OwnedLightGrid, XAnimBuild, ZoneGame,
|
||||
ZoneImage, attach_iw5_static_models, build_iw5_clip_collision, build_iw5_world_draw,
|
||||
decode_reflection_probe_cubemap, dm_spawn_points_iw5, intermission_view_iw5,
|
||||
minimap_corners_iw5,
|
||||
use crate::session_load::PreparedWorld;
|
||||
use asset_anim::XAnimBuild;
|
||||
use asset_core::ZoneGame;
|
||||
use asset_material::decode_reflection_probe_cubemap;
|
||||
use asset_model::{BodyMeshBuild, OwnedLightGrid};
|
||||
use asset_transport::progress::{LoadProgress, StageId};
|
||||
use asset_transport::{Iw5ZoneMemory, ZoneImage};
|
||||
use asset_world::{
|
||||
MASK_PLAYER_SOLID, WorldDrawPolicy, attach_iw5_static_models, build_iw5_clip_collision,
|
||||
build_iw5_world_draw, dm_spawn_points_iw5, intermission_view_iw5, minimap_corners_iw5,
|
||||
};
|
||||
|
||||
pub struct Iw5Lane;
|
||||
@@ -47,17 +50,29 @@ impl Iw5Lane {
|
||||
{
|
||||
return;
|
||||
}
|
||||
match crate::find_zone_for_tree(path, "code_post_gfx_mp") {
|
||||
match asset_transport::find_zone_for_tree(path, "code_post_gfx_mp") {
|
||||
Ok(found) => {
|
||||
let (arena, table) = crate::load_iw5_team_sources(&found.path);
|
||||
let (arena, table) = asset_game::load_iw5_team_sources(&found.path);
|
||||
let stem = path.file_stem().and_then(|s| s.to_str()).unwrap_or("");
|
||||
if let Some(table) = table.as_ref() {
|
||||
loaded.facts.team_settings = crate::team_settings_for_zone(
|
||||
loaded.facts.team_settings = asset_game::team_settings_for_zone(
|
||||
table,
|
||||
crate::AssetNamespace::Iw5,
|
||||
asset_core::AssetNamespace::Iw5,
|
||||
arena.as_deref(),
|
||||
stem,
|
||||
);
|
||||
let settings = &loaded.facts.team_settings;
|
||||
loaded.facts.objective_visuals =
|
||||
asset_game::ObjectiveVisuals::from_faction_table(
|
||||
table,
|
||||
Some(
|
||||
settings
|
||||
.allies_charset
|
||||
.as_deref()
|
||||
.unwrap_or(IW5_DEFAULT_ALLIES),
|
||||
),
|
||||
Some(settings.axis_charset.as_deref().unwrap_or(IW5_DEFAULT_AXIS)),
|
||||
);
|
||||
}
|
||||
match (
|
||||
loaded.facts.team_settings.allies.as_ref(),
|
||||
@@ -77,8 +92,26 @@ impl Iw5Lane {
|
||||
}
|
||||
}
|
||||
|
||||
fn finish_loaded(path: &Path, mut loaded: LoadedWorld) -> LoadedWorld {
|
||||
fn finish_loaded(
|
||||
scripts: Option<&(String, Option<String>)>,
|
||||
path: &Path,
|
||||
mut loaded: LoadedWorld,
|
||||
) -> LoadedWorld {
|
||||
Self::stamp_map_tree_team_settings(path, &mut loaded);
|
||||
if let Some((entities, stand_in)) = scripts {
|
||||
loaded.scripts.set_entities(entities.clone());
|
||||
let map = path
|
||||
.file_stem()
|
||||
.map(|s| s.to_string_lossy().to_ascii_lowercase());
|
||||
if let (Some(stand_in), Some(map)) = (stand_in, map) {
|
||||
loaded
|
||||
.scripts
|
||||
.insert_source(&format!("maps/mp/{map}"), stand_in.clone());
|
||||
loaded.report.push(format!(
|
||||
"map script: maps/mp/{map} written from the zone's declarations"
|
||||
));
|
||||
}
|
||||
}
|
||||
loaded
|
||||
}
|
||||
}
|
||||
@@ -97,11 +130,11 @@ impl ZoneLane for Iw5Lane {
|
||||
path: &Path,
|
||||
image: &ZoneImage,
|
||||
progress: &LoadProgress,
|
||||
_shared_surfaces: asset_model::SharedXModelSurfaces,
|
||||
material_seed: crate::MaterialCatalog,
|
||||
shared_surfaces: asset_model::SharedXModelSurfaces,
|
||||
material_seed: asset_material::MaterialCatalog,
|
||||
_common_film_visions: &mut std::collections::BTreeMap<
|
||||
String,
|
||||
Result<crate::FilmVision, crate::FilmVisionParseError>,
|
||||
Result<asset_world::FilmVision, asset_world::FilmVisionParseError>,
|
||||
>,
|
||||
) -> LoadedWorld {
|
||||
let mut report = vec![format!("game: IW5 ({})", path.display())];
|
||||
@@ -114,6 +147,7 @@ impl ZoneLane for Iw5Lane {
|
||||
Err(e) => {
|
||||
stage.fail();
|
||||
return Self::finish_loaded(
|
||||
None,
|
||||
path,
|
||||
LoadedWorld::with_gap(
|
||||
WorldDrawPolicy::iw5(),
|
||||
@@ -126,7 +160,7 @@ impl ZoneLane for Iw5Lane {
|
||||
};
|
||||
|
||||
let stage = progress.begin_scoped(StageId::MapAssets, "memory", None);
|
||||
report.push(crate::zone::xfile_arena_row(
|
||||
report.push(asset_transport::xfile_arena_row(
|
||||
"zone arenas map",
|
||||
&header.block_size,
|
||||
fastfile_iw5::XFILE_BLOCK_TEMP,
|
||||
@@ -141,6 +175,7 @@ impl ZoneLane for Iw5Lane {
|
||||
Err(e) => {
|
||||
stage.fail();
|
||||
return Self::finish_loaded(
|
||||
None,
|
||||
path,
|
||||
LoadedWorld::with_gap(
|
||||
WorldDrawPolicy::iw5(),
|
||||
@@ -156,8 +191,9 @@ impl ZoneLane for Iw5Lane {
|
||||
let mut sink = ZoneWalkSink::default();
|
||||
let seeded_techsets = material_seed.technique_set_facts().to_vec();
|
||||
sink.seed_materials(material_seed);
|
||||
sink.set_capture_zone(crate::ZoneOwner::from_zone_path(path));
|
||||
sink.set_capture_ns(crate::AssetNamespace::Iw5);
|
||||
sink.map_xmodels.shared_surfaces = shared_surfaces;
|
||||
sink.set_capture_zone(asset_core::ZoneOwner::from_zone_path(path));
|
||||
sink.set_capture_ns(asset_core::AssetNamespace::Iw5);
|
||||
sink.sound = Some(asset_audio::ZoneSoundCapture::for_map(
|
||||
path,
|
||||
asset_audio::ZoneGame::Iw5,
|
||||
@@ -179,6 +215,13 @@ impl ZoneLane for Iw5Lane {
|
||||
let exp_fog = sink.exp_fog.take();
|
||||
let script_sound = std::mem::take(&mut sink.script_sound).finish();
|
||||
let createart_name = sink.createart_name.take();
|
||||
let scripts = asset_world::map_ents_entity_string_iw5(&stream)
|
||||
.map(asset_world::iw5_entity_string_named)
|
||||
.map(|entities| {
|
||||
let stand_in =
|
||||
(!sink.iw5_map.is_empty()).then(|| sink.iw5_map.map_script(&entities));
|
||||
(entities, stand_in)
|
||||
});
|
||||
report.push(match &exp_fog {
|
||||
Some(fog) => format!(
|
||||
"createart fog: READY start={:.3} half={:.3} maxOpacity={:.3} sun={} name={}",
|
||||
@@ -242,6 +285,12 @@ impl ZoneLane for Iw5Lane {
|
||||
match build_iw5_clip_collision(&stream, geometry) {
|
||||
Ok(mut clip) => {
|
||||
attach_iw5_static_models(&stream, geometry, &sink.xmodel_coll, &mut clip);
|
||||
clip.trigger_models = asset_world::trigger_models_iw5(&stream);
|
||||
report.push(format!(
|
||||
"trigger models: {} ({} with hulls)",
|
||||
clip.trigger_models.len(),
|
||||
clip.trigger_models.iter().filter(|h| !h.is_empty()).count()
|
||||
));
|
||||
let solid = clip
|
||||
.brushes
|
||||
.iter()
|
||||
@@ -282,6 +331,7 @@ impl ZoneLane for Iw5Lane {
|
||||
report.push("bodies: empty (no GfxWorld; XModel bone capture not reached)".into());
|
||||
let dm_spawns = dm_spawn_points_iw5(&stream);
|
||||
return Self::finish_loaded(
|
||||
scripts.as_ref(),
|
||||
path,
|
||||
LoadedWorld {
|
||||
sound: map_sound,
|
||||
@@ -349,9 +399,9 @@ impl ZoneLane for Iw5Lane {
|
||||
"script_brushmodel: {} *N placements (SP_script_brushmodel, not DrawInst)",
|
||||
map_models.script_brush_models.len()
|
||||
));
|
||||
let crate::PreparedMapModels {
|
||||
let asset_world::PreparedMapModels {
|
||||
static_draw:
|
||||
crate::StaticModelDraw {
|
||||
asset_world::StaticModelDraw {
|
||||
meshes: static_model_meshes,
|
||||
placements: static_model_instances,
|
||||
..
|
||||
@@ -359,14 +409,14 @@ impl ZoneLane for Iw5Lane {
|
||||
scene_assets: map_xmodel_scene_assets,
|
||||
script_instances: script_model_instances,
|
||||
script_brush_models,
|
||||
map_use_triggers,
|
||||
flag_descriptors,
|
||||
script_structs,
|
||||
..
|
||||
} = map_models;
|
||||
let intermission_view = intermission_view_iw5(&stream);
|
||||
let minimap_corners = minimap_corners_iw5(&stream);
|
||||
let north_yaw = crate::worldspawn_north_yaw_iw5(&stream);
|
||||
let north_yaw = asset_world::worldspawn_north_yaw_iw5(&stream);
|
||||
let airstrike_height = asset_world::airstrike_height_iw5(&stream);
|
||||
let dm_spawns = dm_spawn_points_iw5(&stream);
|
||||
report.push(format!(
|
||||
"ffa spawns: {} mp_dm_spawn* ({} start)",
|
||||
@@ -485,7 +535,7 @@ impl ZoneLane for Iw5Lane {
|
||||
)),
|
||||
}
|
||||
let smodel_lighting_samples = {
|
||||
use crate::model_lighting::{
|
||||
use asset_model::model_lighting::{
|
||||
build_smodel_lighting_samples_with_sight, census_lit_fragment_tiles,
|
||||
collect_iw5_smodel_lighting_origins,
|
||||
};
|
||||
@@ -583,6 +633,7 @@ impl ZoneLane for Iw5Lane {
|
||||
}
|
||||
report.push(format!("map xanims: {}", map_xanims.len()));
|
||||
Self::finish_loaded(
|
||||
scripts.as_ref(),
|
||||
path,
|
||||
LoadedWorld {
|
||||
sound: map_sound,
|
||||
@@ -596,7 +647,6 @@ impl ZoneLane for Iw5Lane {
|
||||
map_xmodel_scene_assets,
|
||||
script_model_instances,
|
||||
script_brush_models,
|
||||
map_use_triggers,
|
||||
flag_descriptors,
|
||||
script_structs,
|
||||
intermission_view,
|
||||
@@ -617,6 +667,7 @@ impl ZoneLane for Iw5Lane {
|
||||
script_sound: script_sound.clone(),
|
||||
minimap_corners,
|
||||
north_yaw,
|
||||
airstrike_height,
|
||||
compass,
|
||||
..Default::default()
|
||||
},
|
||||
@@ -629,6 +680,7 @@ impl ZoneLane for Iw5Lane {
|
||||
report.push(format!("world draw: {e}"));
|
||||
let dm_spawns = dm_spawn_points_iw5(&stream);
|
||||
Self::finish_loaded(
|
||||
scripts.as_ref(),
|
||||
path,
|
||||
LoadedWorld {
|
||||
sound: map_sound,
|
||||
@@ -662,7 +714,7 @@ impl ZoneLane for Iw5Lane {
|
||||
image: &ZoneImage,
|
||||
progress: &LoadProgress,
|
||||
decode_color_maps: bool,
|
||||
material_seed: crate::MaterialCatalog,
|
||||
material_seed: asset_material::MaterialCatalog,
|
||||
) -> CommonCensus {
|
||||
let header = match image.iw5_header() {
|
||||
Ok(header) => header,
|
||||
@@ -685,8 +737,8 @@ impl ZoneLane for Iw5Lane {
|
||||
};
|
||||
let mut sink = CommonWalkSink::default();
|
||||
sink.seed_materials(material_seed);
|
||||
sink.set_capture_zone(crate::ZoneOwner::from_zone_path(path));
|
||||
sink.set_capture_ns(crate::AssetNamespace::Iw5);
|
||||
sink.set_capture_zone(asset_core::ZoneOwner::from_zone_path(path));
|
||||
sink.set_capture_ns(asset_core::AssetNamespace::Iw5);
|
||||
sink.sound = asset_audio::ZoneSoundCapture::claim_common(
|
||||
path,
|
||||
asset_audio::ZoneGame::Iw5,
|
||||
@@ -710,7 +762,7 @@ impl ZoneLane for Iw5Lane {
|
||||
sink.weapons.resolve_reticles(&sink.materials);
|
||||
let captured = sink.weapons.len();
|
||||
let mut weapons = sink.weapons.into_build();
|
||||
weapons.stamp_namespace(crate::AssetNamespace::Iw5);
|
||||
weapons.stamp_namespace(asset_core::AssetNamespace::Iw5);
|
||||
weapons.apply_stats_tables(sink.stats_tables.values());
|
||||
weapons.resolve_sz_xanim_edges(&sink.xanims);
|
||||
weapons.resolve_fpv_mesh_edges(&sink.fpv_meshes);
|
||||
@@ -722,7 +774,7 @@ impl ZoneLane for Iw5Lane {
|
||||
));
|
||||
let gun_named = weapons.gun_xmodel_count();
|
||||
report.push(format!(
|
||||
"common_mp weapons: {captured} captures → {} unique catalog ids (sorted; not retail bg_weaponIndex); {gun_named} with gunXModel[0]; {} with szXAnims[IDLE]; {} with any szXAnims slot",
|
||||
"common_mp weapons: {captured} captures -> {} unique catalog ids (sorted); {gun_named} with gunXModel[0]; {} with szXAnims[IDLE]; {} with any szXAnims slot",
|
||||
weapons.len(),
|
||||
weapons.idle_anim_count(),
|
||||
weapons.sz_xanims_count()
|
||||
@@ -745,7 +797,7 @@ impl ZoneLane for Iw5Lane {
|
||||
sink.materials.images.len(),
|
||||
sink.materials.capture_gaps
|
||||
));
|
||||
let light_defs = crate::capture_iw5_light_defs(&stream, &sink.materials);
|
||||
let light_defs = asset_world::capture_iw5_light_defs(&stream, &sink.materials);
|
||||
report.push(format!(
|
||||
"GfxLightDef common_mp: table={} bodies={} recorded={}",
|
||||
sink.light_def_table,
|
||||
@@ -757,7 +809,7 @@ impl ZoneLane for Iw5Lane {
|
||||
let mut pending_images = None;
|
||||
if decode_color_maps {
|
||||
let stage = progress.begin_scoped(StageId::Images, "common_mp", None);
|
||||
let (inline, plan) = crate::material_images::plan_material_color_maps(
|
||||
let (inline, plan) = asset_material::material_images::plan_material_color_maps(
|
||||
path,
|
||||
&mut materials,
|
||||
&stage,
|
||||
@@ -785,6 +837,8 @@ impl ZoneLane for Iw5Lane {
|
||||
world_weapons: sink.world_weapons,
|
||||
xanims: sink.xanims,
|
||||
light_defs,
|
||||
scene_models: sink.scene_models,
|
||||
shared_surfaces: sink.shared_surfaces,
|
||||
report,
|
||||
..Default::default()
|
||||
}
|
||||
@@ -795,7 +849,7 @@ impl ZoneLane for Iw5Lane {
|
||||
path: &Path,
|
||||
image: &ZoneImage,
|
||||
progress: &LoadProgress,
|
||||
material_seed: crate::MaterialCatalog,
|
||||
material_seed: asset_material::MaterialCatalog,
|
||||
) -> MaterialPopulation {
|
||||
let _ = progress;
|
||||
let zone_name = path.file_stem().map_or_else(
|
||||
@@ -825,8 +879,8 @@ impl ZoneLane for Iw5Lane {
|
||||
};
|
||||
let mut sink = MaterialPopulationSink::default();
|
||||
sink.seed_materials(material_seed);
|
||||
sink.set_capture_zone(crate::ZoneOwner::from_zone_path(path));
|
||||
sink.set_capture_ns(crate::AssetNamespace::Iw5);
|
||||
sink.set_capture_zone(asset_core::ZoneOwner::from_zone_path(path));
|
||||
sink.set_capture_ns(asset_core::AssetNamespace::Iw5);
|
||||
sink.sound = asset_audio::ZoneSoundCapture::claim_common(
|
||||
path,
|
||||
asset_audio::ZoneGame::Iw5,
|
||||
@@ -857,6 +911,10 @@ impl ZoneLane for Iw5Lane {
|
||||
materials: sink.materials,
|
||||
report,
|
||||
cac_tables: sink.stats_tables.into_values().collect(),
|
||||
scripts: sink.scripts,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const IW5_DEFAULT_ALLIES: &str = "sas_urban";
|
||||
const IW5_DEFAULT_AXIS: &str = "opforce_henchmen";
|
||||
|
||||
@@ -12,11 +12,14 @@ pub(crate) use helpers::{
|
||||
pub(crate) use sink::{CommonWalkSink, MaterialPopulationSink, ZoneWalkSink};
|
||||
|
||||
use crate::{
|
||||
BodyMeshBuild, FpvMeshBuild, WeaponBuild, WorldWeaponBuild, XAnimBuild, ZoneGame, ZoneImage,
|
||||
lane_capability::{LaneStatus, PreparedCapability},
|
||||
progress::LoadProgress,
|
||||
session_load::{PreparedWorld, WorldDrawPolicy},
|
||||
};
|
||||
use asset_anim::XAnimBuild;
|
||||
use asset_core::ZoneGame;
|
||||
use asset_game::WeaponBuild;
|
||||
use asset_model::{BodyMeshBuild, FpvMeshBuild, WorldWeaponBuild};
|
||||
use asset_transport::{LoadProgress, ZoneImage};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct LaneGap {
|
||||
@@ -27,12 +30,13 @@ pub struct LaneGap {
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct LoadedWorld {
|
||||
pub scripts: crate::ScriptSources,
|
||||
pub world: PreparedWorld,
|
||||
/// What the map zone itself captured. The local material indices in
|
||||
/// `world` are indices into this pool until the match finalizes one.
|
||||
pub materials: crate::MaterialCatalog,
|
||||
pub collision: Option<crate::ClipCollision>,
|
||||
pub spawns: Vec<crate::SpawnPoint>,
|
||||
pub materials: asset_material::MaterialCatalog,
|
||||
pub collision: Option<asset_world::ClipCollision>,
|
||||
pub spawns: Vec<asset_world::SpawnPoint>,
|
||||
pub bodies: BodyMeshBuild,
|
||||
pub fpv_meshes: FpvMeshBuild,
|
||||
pub xanims: XAnimBuild,
|
||||
@@ -84,26 +88,27 @@ impl LoadedWorld {
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct CommonCensus {
|
||||
pub scene_models: crate::MapXModelSceneCatalog,
|
||||
pub scripts: crate::ScriptSources,
|
||||
pub scene_models: asset_world::MapXModelSceneCatalog,
|
||||
pub shared_surfaces: asset_model::SharedXModelSurfaces,
|
||||
pub weapons: WeaponBuild,
|
||||
|
||||
pub cac_tables: Vec<crate::CapturedStringTable>,
|
||||
pub cac_tables: Vec<asset_game::CapturedStringTable>,
|
||||
pub fpv: FpvMeshBuild,
|
||||
pub world_weapons: WorldWeaponBuild,
|
||||
|
||||
pub projectile_meshes: crate::ProjectileMeshBuild,
|
||||
pub projectile_meshes: asset_model::ProjectileMeshBuild,
|
||||
pub xanims: XAnimBuild,
|
||||
pub player_anim_sources: crate::PlayerAnimSources,
|
||||
pub fx: crate::FxCatalog,
|
||||
pub fx_models: crate::FxModelCatalog,
|
||||
pub player_anim_sources: asset_anim::PlayerAnimSources,
|
||||
pub fx: asset_game::FxCatalog,
|
||||
pub fx_models: asset_game::FxModelCatalog,
|
||||
|
||||
pub tracers: crate::TracerCatalog,
|
||||
pub impact_fx: Option<crate::OwnedFxImpactTable>,
|
||||
pub tracers: asset_game::TracerCatalog,
|
||||
pub impact_fx: Option<asset_game::OwnedFxImpactTable>,
|
||||
|
||||
pub material_population: crate::MaterialCatalog,
|
||||
pub material_population: asset_material::MaterialCatalog,
|
||||
|
||||
pub light_defs: Vec<crate::CapturedLightDef>,
|
||||
pub light_defs: Vec<asset_world::CapturedLightDef>,
|
||||
pub report: Vec<String>,
|
||||
|
||||
pub pen_table: weapon_iw4::PenetrationDepthTable,
|
||||
@@ -116,29 +121,33 @@ pub struct CommonCensus {
|
||||
/// themselves do not outlive it.
|
||||
pub s1_common_bytes: usize,
|
||||
|
||||
pub teamsets: std::collections::HashMap<String, crate::MapTeamSettings>,
|
||||
pub film_visions:
|
||||
std::collections::BTreeMap<String, Result<crate::FilmVision, crate::FilmVisionParseError>>,
|
||||
pub teamsets: std::collections::HashMap<String, asset_game::MapTeamSettings>,
|
||||
pub film_visions: std::collections::BTreeMap<
|
||||
String,
|
||||
Result<asset_world::FilmVision, asset_world::FilmVisionParseError>,
|
||||
>,
|
||||
|
||||
pub pending_images: Option<crate::material_images::ImageDemandPlan>,
|
||||
pub pending_images: Option<asset_material::material_images::ImageDemandPlan>,
|
||||
}
|
||||
|
||||
pub struct MaterialPopulation {
|
||||
pub materials: crate::MaterialCatalog,
|
||||
pub light_defs: Vec<crate::CapturedLightDef>,
|
||||
pub materials: asset_material::MaterialCatalog,
|
||||
pub light_defs: Vec<asset_world::CapturedLightDef>,
|
||||
pub walked: usize,
|
||||
pub report: Vec<String>,
|
||||
pub cac_tables: Vec<crate::CapturedStringTable>,
|
||||
pub cac_tables: Vec<asset_game::CapturedStringTable>,
|
||||
pub scripts: crate::ScriptSources,
|
||||
}
|
||||
|
||||
impl Default for MaterialPopulation {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
materials: crate::MaterialCatalog::default(),
|
||||
materials: asset_material::MaterialCatalog::default(),
|
||||
light_defs: Vec::new(),
|
||||
walked: 0,
|
||||
report: Vec::new(),
|
||||
cac_tables: Vec::new(),
|
||||
scripts: crate::ScriptSources::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -155,10 +164,10 @@ pub trait ZoneLane: Send + Sync {
|
||||
|
||||
shared_surfaces: asset_model::SharedXModelSurfaces,
|
||||
|
||||
material_seed: crate::MaterialCatalog,
|
||||
material_seed: asset_material::MaterialCatalog,
|
||||
common_film_visions: &mut std::collections::BTreeMap<
|
||||
String,
|
||||
Result<crate::FilmVision, crate::FilmVisionParseError>,
|
||||
Result<asset_world::FilmVision, asset_world::FilmVisionParseError>,
|
||||
>,
|
||||
) -> LoadedWorld;
|
||||
|
||||
@@ -168,7 +177,7 @@ pub trait ZoneLane: Send + Sync {
|
||||
image: &ZoneImage,
|
||||
progress: &LoadProgress,
|
||||
decode_color_maps: bool,
|
||||
material_seed: crate::MaterialCatalog,
|
||||
material_seed: asset_material::MaterialCatalog,
|
||||
) -> CommonCensus;
|
||||
|
||||
fn load_material_population(
|
||||
@@ -176,7 +185,7 @@ pub trait ZoneLane: Send + Sync {
|
||||
path: &Path,
|
||||
image: &ZoneImage,
|
||||
progress: &LoadProgress,
|
||||
material_seed: crate::MaterialCatalog,
|
||||
material_seed: asset_material::MaterialCatalog,
|
||||
) -> MaterialPopulation;
|
||||
}
|
||||
|
||||
|
||||
+209
-77
@@ -8,7 +8,7 @@ use fastfile_iw4::{
|
||||
use super::helpers::MapXModelCatalog;
|
||||
|
||||
fn is_cac_table(name: &str) -> bool {
|
||||
crate::is_stats_table_name(name)
|
||||
asset_game::is_stats_table_name(name)
|
||||
|| name.eq_ignore_ascii_case("mp/attachmentTable.csv")
|
||||
|| name.eq_ignore_ascii_case("mp/attachmentCombos.csv")
|
||||
}
|
||||
@@ -16,7 +16,7 @@ fn is_cac_table(name: &str) -> bool {
|
||||
fn iw5_cac_table(
|
||||
s: &fastfile_iw5::ZoneStream<'_>,
|
||||
header: fastfile_iw5::Ptr,
|
||||
) -> Option<crate::CapturedStringTable> {
|
||||
) -> Option<asset_game::CapturedStringTable> {
|
||||
let name = match s.ptr_at(header, 0).ok() {
|
||||
Some(fastfile_iw5::ZonePtr::Offset(p)) => {
|
||||
s.cstr(s.resolve_alias(p)).ok().unwrap_or("").to_owned()
|
||||
@@ -32,7 +32,7 @@ fn iw5_cac_table(
|
||||
let arr = match s.ptr_at(header, s.layout(12, 16)).ok() {
|
||||
Some(fastfile_iw5::ZonePtr::Offset(p)) => s.resolve_alias(p),
|
||||
_ => {
|
||||
return Some(crate::CapturedStringTable {
|
||||
return Some(asset_game::CapturedStringTable {
|
||||
name,
|
||||
columns,
|
||||
rows,
|
||||
@@ -52,7 +52,7 @@ fn iw5_cac_table(
|
||||
};
|
||||
cells.push(value);
|
||||
}
|
||||
Some(crate::CapturedStringTable {
|
||||
Some(asset_game::CapturedStringTable {
|
||||
name,
|
||||
columns,
|
||||
rows,
|
||||
@@ -60,26 +60,23 @@ fn iw5_cac_table(
|
||||
})
|
||||
}
|
||||
|
||||
fn t5_cac_table(
|
||||
fn t5_string_table(
|
||||
s: &fastfile_t5::ZoneStream<'_>,
|
||||
header: fastfile_t5::Ptr,
|
||||
) -> Option<crate::CapturedStringTable> {
|
||||
) -> Option<asset_game::CapturedStringTable> {
|
||||
let name = match s.ptr_at(header, 0).ok() {
|
||||
Some(fastfile_t5::ZonePtr::Offset(p)) => {
|
||||
s.cstr(s.resolve_alias(p)).ok().unwrap_or("").to_owned()
|
||||
}
|
||||
_ => return None,
|
||||
};
|
||||
if !is_cac_table(&name) {
|
||||
return None;
|
||||
}
|
||||
let columns = s.i32_at(header, 4).unwrap_or(0).max(0) as usize;
|
||||
let rows = s.i32_at(header, 8).unwrap_or(0).max(0) as usize;
|
||||
let cells_n = columns.saturating_mul(rows);
|
||||
let arr = match s.ptr_at(header, 12).ok() {
|
||||
Some(fastfile_t5::ZonePtr::Offset(p)) => s.resolve_alias(p),
|
||||
_ => {
|
||||
return Some(crate::CapturedStringTable {
|
||||
return Some(asset_game::CapturedStringTable {
|
||||
name,
|
||||
columns,
|
||||
rows,
|
||||
@@ -99,7 +96,7 @@ fn t5_cac_table(
|
||||
};
|
||||
cells.push(value);
|
||||
}
|
||||
Some(crate::CapturedStringTable {
|
||||
Some(asset_game::CapturedStringTable {
|
||||
name,
|
||||
columns,
|
||||
rows,
|
||||
@@ -107,21 +104,18 @@ fn t5_cac_table(
|
||||
})
|
||||
}
|
||||
|
||||
fn iw4_cac_table(s: &ZoneStream<'_>, header: Ptr) -> Option<crate::CapturedStringTable> {
|
||||
fn iw4_string_table(s: &ZoneStream<'_>, header: Ptr) -> Option<asset_game::CapturedStringTable> {
|
||||
let name = match s.ptr_at(header, 0).ok() {
|
||||
Some(ZonePtr::Offset(p)) => s.cstr(s.resolve_alias(p)).ok().unwrap_or("").to_owned(),
|
||||
_ => return None,
|
||||
};
|
||||
if !is_cac_table(&name) {
|
||||
return None;
|
||||
}
|
||||
let columns = s.i32_at(header, s.layout(4, 8)).unwrap_or(0).max(0) as usize;
|
||||
let rows = s.i32_at(header, s.layout(8, 12)).unwrap_or(0).max(0) as usize;
|
||||
let cells_n = columns.saturating_mul(rows);
|
||||
let arr = match s.ptr_at(header, s.layout(12, 16)).ok() {
|
||||
Some(ZonePtr::Offset(p)) => s.resolve_alias(p),
|
||||
_ => {
|
||||
return Some(crate::CapturedStringTable {
|
||||
return Some(asset_game::CapturedStringTable {
|
||||
name,
|
||||
columns,
|
||||
rows,
|
||||
@@ -139,33 +133,37 @@ fn iw4_cac_table(s: &ZoneStream<'_>, header: Ptr) -> Option<crate::CapturedStrin
|
||||
};
|
||||
cells.push(value);
|
||||
}
|
||||
Some(crate::CapturedStringTable {
|
||||
Some(asset_game::CapturedStringTable {
|
||||
name,
|
||||
columns,
|
||||
rows,
|
||||
cells,
|
||||
})
|
||||
}
|
||||
use crate::{
|
||||
BodyMeshBuild, FpvMeshBuild, FxCatalog, ImpactFxCatalog, MaterialCatalog, ModelKind,
|
||||
PreparedXModelWalkCensus, SoldierKit, WeaponCatalog, WorldWeaponBuild, XAnimBuild, ZoneOwner,
|
||||
build_xmodel_mesh, model_kind, progress::StageHandle, soldier_kits,
|
||||
};
|
||||
use crate::PreparedXModelWalkCensus;
|
||||
use asset_anim::XAnimBuild;
|
||||
use asset_core::ZoneOwner;
|
||||
use asset_game::{FxCatalog, ImpactFxCatalog, WeaponCatalog};
|
||||
use asset_material::MaterialCatalog;
|
||||
use asset_model::{BodyMeshBuild, FpvMeshBuild, SoldierKit, WorldWeaponBuild, soldier_kits};
|
||||
use asset_transport::StageHandle;
|
||||
use asset_world::{ModelKind, build_xmodel_mesh, model_kind};
|
||||
|
||||
#[derive(Default)]
|
||||
pub(crate) struct ZoneWalkSink {
|
||||
pub scripts: crate::ScriptSources,
|
||||
pub walked: usize,
|
||||
|
||||
pub stage: Option<StageHandle>,
|
||||
pub models: ModelCensus,
|
||||
pub materials: MaterialCatalog,
|
||||
pub fx: FxCatalog,
|
||||
pub fx_models: crate::FxModelCatalog,
|
||||
pub fx_models: asset_game::FxModelCatalog,
|
||||
pub impact_fx: ImpactFxCatalog,
|
||||
pub tracers: crate::TracerCatalog,
|
||||
pub tracers: asset_game::TracerCatalog,
|
||||
pub map_xmodels: MapXModelCatalog,
|
||||
|
||||
pub phys_presets: crate::PhysPresetCatalog,
|
||||
pub phys_presets: asset_world::PhysPresetCatalog,
|
||||
|
||||
pub fx_glass_def_materials: Vec<(String, String)>,
|
||||
|
||||
@@ -174,17 +172,19 @@ pub(crate) struct ZoneWalkSink {
|
||||
pub fpv_meshes: FpvMeshBuild,
|
||||
pub xanims: XAnimBuild,
|
||||
|
||||
pub xmodel_coll: crate::XModelCollCatalog,
|
||||
pub xmodel_coll: asset_world::XModelCollCatalog,
|
||||
|
||||
pub compass: crate::MapCompassSource,
|
||||
pub compass: asset_world::MapCompassSource,
|
||||
|
||||
pub script_sound: crate::MapScriptSoundSource,
|
||||
pub script_sound: asset_audio::MapScriptSoundSource,
|
||||
pub iw5_map: asset_world::MapDeclarations,
|
||||
|
||||
pub t5_teamset: Option<String>,
|
||||
|
||||
pub exp_fog: Option<crate::ExpFog>,
|
||||
pub exp_fog: Option<asset_world::ExpFog>,
|
||||
|
||||
pub film_visions: BTreeMap<String, Result<crate::FilmVision, crate::FilmVisionParseError>>,
|
||||
pub film_visions:
|
||||
BTreeMap<String, Result<asset_world::FilmVision, asset_world::FilmVisionParseError>>,
|
||||
|
||||
pub createart_name: Option<String>,
|
||||
|
||||
@@ -192,6 +192,8 @@ pub(crate) struct ZoneWalkSink {
|
||||
pub light_def_bodies: usize,
|
||||
strings_t5: fastfile_t5::ScriptStrings,
|
||||
strings_iw5: fastfile_iw5::ScriptStrings,
|
||||
iw5_surfaces:
|
||||
HashMap<fastfile_iw5::Ptr, (Option<fastfile_iw5::Ptr>, Option<fastfile_iw5::Ptr>)>,
|
||||
|
||||
xmodel_names: HashMap<Ptr, Ptr>,
|
||||
xmodel_surfaces: HashMap<Ptr, Ptr>,
|
||||
@@ -202,7 +204,8 @@ pub(crate) struct ZoneWalkSink {
|
||||
|
||||
#[derive(Default)]
|
||||
pub(crate) struct CommonWalkSink {
|
||||
pub scene_models: crate::MapXModelSceneCatalog,
|
||||
pub scripts: crate::ScriptSources,
|
||||
pub scene_models: asset_world::MapXModelSceneCatalog,
|
||||
pub shared_surfaces: asset_model::SharedXModelSurfaces,
|
||||
script_strings: ScriptStrings,
|
||||
pub walked: usize,
|
||||
@@ -213,13 +216,13 @@ pub(crate) struct CommonWalkSink {
|
||||
pub weapons: WeaponCatalog,
|
||||
pub fpv_meshes: FpvMeshBuild,
|
||||
pub world_weapons: WorldWeaponBuild,
|
||||
pub projectile_meshes: crate::ProjectileMeshBuild,
|
||||
pub projectile_meshes: asset_model::ProjectileMeshBuild,
|
||||
pub xanims: XAnimBuild,
|
||||
pub player_anim_sources: crate::PlayerAnimSources,
|
||||
pub player_anim_sources: asset_anim::PlayerAnimSources,
|
||||
pub fx: FxCatalog,
|
||||
pub fx_models: crate::FxModelCatalog,
|
||||
pub fx_models: asset_game::FxModelCatalog,
|
||||
pub impact_fx: ImpactFxCatalog,
|
||||
pub tracers: crate::TracerCatalog,
|
||||
pub tracers: asset_game::TracerCatalog,
|
||||
|
||||
xmodel_names: HashMap<Ptr, Ptr>,
|
||||
xmodel_surfaces: HashMap<Ptr, Ptr>,
|
||||
@@ -227,6 +230,8 @@ pub(crate) struct CommonWalkSink {
|
||||
strings_t5: fastfile_t5::ScriptStrings,
|
||||
xmodel_names_t5: HashMap<fastfile_t5::Ptr, fastfile_t5::Ptr>,
|
||||
strings_iw5: fastfile_iw5::ScriptStrings,
|
||||
iw5_surfaces:
|
||||
HashMap<fastfile_iw5::Ptr, (Option<fastfile_iw5::Ptr>, Option<fastfile_iw5::Ptr>)>,
|
||||
xmodel_names_iw5: HashMap<fastfile_iw5::Ptr, fastfile_iw5::Ptr>,
|
||||
fx_names_iw5: HashMap<fastfile_iw5::Ptr, String>,
|
||||
fx_aliases_iw5: HashMap<fastfile_iw5::Ptr, fastfile_iw5::Ptr>,
|
||||
@@ -238,11 +243,12 @@ pub(crate) struct CommonWalkSink {
|
||||
pub pen_table: Option<weapon_iw4::PenetrationDepthTable>,
|
||||
pub lochit_table: Option<[f32; weapon_iw4::HITLOC_COUNT]>,
|
||||
|
||||
pub teamsets: HashMap<String, crate::MapTeamSettings>,
|
||||
pub teamsets: HashMap<String, asset_game::MapTeamSettings>,
|
||||
|
||||
pub stats_tables: BTreeMap<String, crate::CapturedStringTable>,
|
||||
pub stats_tables: BTreeMap<String, asset_game::CapturedStringTable>,
|
||||
|
||||
pub film_visions: BTreeMap<String, Result<crate::FilmVision, crate::FilmVisionParseError>>,
|
||||
pub film_visions:
|
||||
BTreeMap<String, Result<asset_world::FilmVision, asset_world::FilmVisionParseError>>,
|
||||
|
||||
pub sound: Option<asset_audio::ZoneSoundCapture>,
|
||||
}
|
||||
@@ -277,7 +283,7 @@ impl ZoneWalkSink {
|
||||
self.fpv_meshes.set_capture_zone(zone);
|
||||
}
|
||||
|
||||
pub(crate) fn set_capture_ns(&mut self, ns: crate::AssetNamespace) {
|
||||
pub(crate) fn set_capture_ns(&mut self, ns: asset_core::AssetNamespace) {
|
||||
self.materials.set_capture_ns(ns);
|
||||
self.xanims.set_capture_ns(ns);
|
||||
self.fpv_meshes.set_capture_ns(ns);
|
||||
@@ -318,7 +324,7 @@ impl CommonWalkSink {
|
||||
self.world_weapons.set_capture_zone(zone);
|
||||
}
|
||||
|
||||
pub(crate) fn set_capture_ns(&mut self, ns: crate::AssetNamespace) {
|
||||
pub(crate) fn set_capture_ns(&mut self, ns: asset_core::AssetNamespace) {
|
||||
self.materials.set_capture_ns(ns);
|
||||
self.xanims.set_capture_ns(ns);
|
||||
self.fpv_meshes.set_capture_ns(ns);
|
||||
@@ -330,7 +336,7 @@ impl CommonWalkSink {
|
||||
self.fx_models.set_capture_ns(ns);
|
||||
}
|
||||
|
||||
fn keep_stats_table(&mut self, table: crate::CapturedStringTable) {
|
||||
fn keep_stats_table(&mut self, table: asset_game::CapturedStringTable) {
|
||||
if is_cac_table(&table.name) {
|
||||
self.stats_tables.insert(table.name.clone(), table);
|
||||
}
|
||||
@@ -362,6 +368,22 @@ impl fastfile_iw5::AssetSink for ZoneWalkSink {
|
||||
}
|
||||
|
||||
impl fastfile_iw5::AssetLinkSink for ZoneWalkSink {
|
||||
fn remember_xmodel_surfaces(
|
||||
&mut self,
|
||||
slot: fastfile_iw5::Ptr,
|
||||
surfaces: Option<fastfile_iw5::Ptr>,
|
||||
name: Option<fastfile_iw5::Ptr>,
|
||||
) {
|
||||
self.iw5_surfaces.insert(slot, (surfaces, name));
|
||||
}
|
||||
|
||||
fn xmodel_surfaces(
|
||||
&self,
|
||||
slot: fastfile_iw5::Ptr,
|
||||
) -> (Option<fastfile_iw5::Ptr>, Option<fastfile_iw5::Ptr>) {
|
||||
self.iw5_surfaces.get(&slot).copied().unwrap_or_default()
|
||||
}
|
||||
|
||||
fn loaded(
|
||||
&mut self,
|
||||
stream: &fastfile_iw5::ZoneStream<'_>,
|
||||
@@ -410,6 +432,11 @@ impl fastfile_iw5::AssetLinkSink for ZoneWalkSink {
|
||||
sound.iw5_alias(ty, slot, target);
|
||||
}
|
||||
self.materials.iw5_alias(ty, slot, target);
|
||||
if ty == fastfile_iw5::AssetType::XModelSurfs {
|
||||
if let Some(surfaces) = self.iw5_surfaces.get(&target).copied() {
|
||||
self.iw5_surfaces.insert(slot, surfaces);
|
||||
}
|
||||
}
|
||||
if ty == fastfile_iw5::AssetType::XModel {
|
||||
self.map_xmodels.alias(
|
||||
Ptr {
|
||||
@@ -433,6 +460,8 @@ impl fastfile_iw5::AssetLinkSink for ZoneWalkSink {
|
||||
bytecode: &[u8],
|
||||
) -> fastfile_iw5::Result<()> {
|
||||
self.script_sound.capture_iw5(name, stack, bytecode);
|
||||
self.compass.capture_iw5(name, stack);
|
||||
self.iw5_map.capture(name, stack);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -446,11 +475,11 @@ impl fastfile_iw5::AssetLinkSink for ZoneWalkSink {
|
||||
sound.raw_file(name, data, zlib_compressed);
|
||||
}
|
||||
self.compass.capture(name, data, zlib_compressed);
|
||||
if crate::is_createart_source(name) {
|
||||
if asset_world::is_createart_source(name) {
|
||||
self.createart_name = Some(name.to_owned());
|
||||
}
|
||||
if let Some(fog) = crate::parse_createart_rawfile(name, data, zlib_compressed) {
|
||||
let fog_file = crate::is_createart_fog_file(name);
|
||||
if let Some(fog) = asset_world::parse_createart_rawfile(name, data, zlib_compressed) {
|
||||
let fog_file = asset_world::is_createart_fog_file(name);
|
||||
if self.exp_fog.is_none() || fog_file {
|
||||
self.exp_fog = Some(fog);
|
||||
self.createart_name = Some(name.to_owned());
|
||||
@@ -500,6 +529,22 @@ impl fastfile_iw5::AssetSink for CommonWalkSink {
|
||||
}
|
||||
|
||||
impl fastfile_iw5::AssetLinkSink for CommonWalkSink {
|
||||
fn remember_xmodel_surfaces(
|
||||
&mut self,
|
||||
slot: fastfile_iw5::Ptr,
|
||||
surfaces: Option<fastfile_iw5::Ptr>,
|
||||
name: Option<fastfile_iw5::Ptr>,
|
||||
) {
|
||||
self.iw5_surfaces.insert(slot, (surfaces, name));
|
||||
}
|
||||
|
||||
fn xmodel_surfaces(
|
||||
&self,
|
||||
slot: fastfile_iw5::Ptr,
|
||||
) -> (Option<fastfile_iw5::Ptr>, Option<fastfile_iw5::Ptr>) {
|
||||
self.iw5_surfaces.get(&slot).copied().unwrap_or_default()
|
||||
}
|
||||
|
||||
fn capture_fx(
|
||||
&mut self,
|
||||
stream: &fastfile_iw5::ZoneStream<'_>,
|
||||
@@ -555,6 +600,27 @@ impl fastfile_iw5::AssetLinkSink for CommonWalkSink {
|
||||
.capture_iw5(stream, &self.strings_iw5, &self.materials);
|
||||
self.world_weapons
|
||||
.capture_iw5(stream, &self.strings_iw5, &self.materials);
|
||||
if let Some(geometry) = stream.latest_xmodel()
|
||||
&& let Some(name) = geometry.name.and_then(|p| stream.cstr(p).ok())
|
||||
{
|
||||
let asset = asset_model::capture_xmodel_skel_iw5(
|
||||
stream,
|
||||
&self.strings_iw5,
|
||||
geometry,
|
||||
&self.materials,
|
||||
)
|
||||
.map(|skel| {
|
||||
let skel = std::sync::Arc::new(skel);
|
||||
self.shared_surfaces
|
||||
.retain_iw5(stream, geometry, skel.clone());
|
||||
asset_world::MapXModelSceneAsset::Iw5(skel)
|
||||
})
|
||||
.unwrap_or(asset_world::MapXModelSceneAsset::Unavailable {
|
||||
reason: "IW5 common XModel skeleton capture failed",
|
||||
});
|
||||
self.scene_models
|
||||
.insert(asset_world::MapXModelAssetKey(name.to_owned()), asset);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -579,6 +645,11 @@ impl fastfile_iw5::AssetLinkSink for CommonWalkSink {
|
||||
sound.iw5_alias(ty, slot, target);
|
||||
}
|
||||
self.materials.iw5_alias(ty, slot, target);
|
||||
if ty == fastfile_iw5::AssetType::XModelSurfs {
|
||||
if let Some(surfaces) = self.iw5_surfaces.get(&target).copied() {
|
||||
self.iw5_surfaces.insert(slot, surfaces);
|
||||
}
|
||||
}
|
||||
if ty == fastfile_iw5::AssetType::Fx {
|
||||
self.fx_aliases_iw5.insert(slot, target);
|
||||
}
|
||||
@@ -682,6 +753,22 @@ impl fastfile_t5::AssetLinkSink for CommonWalkSink {
|
||||
.capture_t5(stream, &self.strings_t5, &self.materials);
|
||||
self.projectile_meshes
|
||||
.capture_t5(stream, &self.strings_t5, &self.materials);
|
||||
if let Some(geometry) = stream.latest_xmodel()
|
||||
&& let Some(name) = geometry.name.and_then(|p| stream.cstr(p).ok())
|
||||
{
|
||||
let asset = asset_model::capture_xmodel_skel_t5(
|
||||
stream,
|
||||
&self.strings_t5,
|
||||
geometry,
|
||||
&self.materials,
|
||||
)
|
||||
.map(|skel| asset_world::MapXModelSceneAsset::T5(std::sync::Arc::new(skel)))
|
||||
.unwrap_or(asset_world::MapXModelSceneAsset::Unavailable {
|
||||
reason: "T5 common XModel skeleton capture failed",
|
||||
});
|
||||
self.scene_models
|
||||
.insert(asset_world::MapXModelAssetKey(name.to_owned()), asset);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -786,10 +873,11 @@ impl fastfile_t5::AssetLinkSink for CommonWalkSink {
|
||||
sound.raw_file(name, data, zlib_compressed);
|
||||
}
|
||||
if let Some((key, icons)) =
|
||||
crate::t5_settings_from_teamset_rawfile(name, data, zlib_compressed)
|
||||
asset_game::t5_settings_from_teamset_rawfile(name, data, zlib_compressed)
|
||||
{
|
||||
self.teamsets.insert(key, icons);
|
||||
}
|
||||
self.scripts.capture(name, data, zlib_compressed);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -798,8 +886,11 @@ impl fastfile_t5::AssetLinkSink for CommonWalkSink {
|
||||
s: &fastfile_t5::ZoneStream<'_>,
|
||||
header: fastfile_t5::Ptr,
|
||||
) -> fastfile_t5::Result<()> {
|
||||
if let Some(table) = t5_cac_table(s, header) {
|
||||
self.keep_stats_table(table);
|
||||
if let Some(table) = t5_string_table(s, header) {
|
||||
self.scripts.capture_table(&table);
|
||||
if is_cac_table(&table.name) {
|
||||
self.keep_stats_table(table);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -918,19 +1009,31 @@ impl fastfile_t5::AssetLinkSink for ZoneWalkSink {
|
||||
if let Some(text) = asset_world::decode_rawfile_text(data, zlib_compressed) {
|
||||
self.script_sound.capture(name, text.as_bytes(), false);
|
||||
}
|
||||
if crate::is_createart_source(name) {
|
||||
if asset_world::is_createart_source(name) {
|
||||
self.createart_name = Some(name.to_owned());
|
||||
}
|
||||
if let Some(fog) = crate::parse_createart_rawfile(name, data, zlib_compressed) {
|
||||
let fog_file = crate::is_createart_fog_file(name);
|
||||
if let Some(fog) = asset_world::parse_createart_rawfile(name, data, zlib_compressed) {
|
||||
let fog_file = asset_world::is_createart_fog_file(name);
|
||||
if self.exp_fog.is_none() || fog_file {
|
||||
self.exp_fog = Some(fog);
|
||||
self.createart_name = Some(name.to_owned());
|
||||
}
|
||||
}
|
||||
if let Some(teamset) = crate::t5_teamset_from_rawfile(name, data, zlib_compressed) {
|
||||
if let Some(teamset) = asset_game::t5_teamset_from_rawfile(name, data, zlib_compressed) {
|
||||
self.t5_teamset = Some(teamset);
|
||||
}
|
||||
self.scripts.capture(name, data, zlib_compressed);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn capture_string_table(
|
||||
&mut self,
|
||||
s: &fastfile_t5::ZoneStream<'_>,
|
||||
header: fastfile_t5::Ptr,
|
||||
) -> fastfile_t5::Result<()> {
|
||||
if let Some(table) = t5_string_table(s, header) {
|
||||
self.scripts.capture_table(&table);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1085,25 +1188,37 @@ impl AssetLinkSink for ZoneWalkSink {
|
||||
self.materials.linked_asset_name(slot)
|
||||
}
|
||||
|
||||
fn capture_string_table(
|
||||
&mut self,
|
||||
s: &ZoneStream<'_>,
|
||||
header: Ptr,
|
||||
) -> fastfile_iw4::Result<()> {
|
||||
if let Some(table) = iw4_string_table(s, header) {
|
||||
self.scripts.capture_table(&table);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn capture_raw_file(
|
||||
&mut self,
|
||||
name: &str,
|
||||
data: &[u8],
|
||||
zlib_compressed: bool,
|
||||
) -> fastfile_iw4::Result<()> {
|
||||
self.scripts.capture(name, data, zlib_compressed);
|
||||
if let Some(sound) = self.sound.as_mut() {
|
||||
sound.raw_file(name, data, zlib_compressed);
|
||||
}
|
||||
self.compass.capture(name, data, zlib_compressed);
|
||||
self.script_sound.capture(name, data, zlib_compressed);
|
||||
if crate::is_createart_source(name) {
|
||||
if asset_world::is_createart_source(name) {
|
||||
self.createart_name = Some(name.to_owned());
|
||||
}
|
||||
if let Some(fog) = crate::parse_createart_rawfile(name, data, zlib_compressed) {
|
||||
if let Some(fog) = asset_world::parse_createart_rawfile(name, data, zlib_compressed) {
|
||||
self.exp_fog = Some(fog);
|
||||
self.createart_name = Some(name.to_owned());
|
||||
}
|
||||
match crate::parse_film_vision_rawfile(name, data, zlib_compressed) {
|
||||
match asset_world::parse_film_vision_rawfile(name, data, zlib_compressed) {
|
||||
Ok(Some(vision)) => {
|
||||
self.film_visions
|
||||
.insert(name.replace('\\', "/").to_ascii_lowercase(), Ok(vision));
|
||||
@@ -1224,9 +1339,6 @@ impl AssetSink for CommonWalkSink {
|
||||
self.light_def_bodies += 1;
|
||||
}
|
||||
}
|
||||
if loaded && ty == AssetType::Weapon {
|
||||
self.weapons.capture(s);
|
||||
}
|
||||
self.asset_walked();
|
||||
Ok(())
|
||||
}
|
||||
@@ -1244,6 +1356,12 @@ impl AssetLinkSink for CommonWalkSink {
|
||||
sound.iw4_loaded(stream, ty, slot, insert_slot);
|
||||
}
|
||||
self.materials.loaded(stream, ty, slot, insert_slot)?;
|
||||
if ty == AssetType::Weapon {
|
||||
self.weapons.capture(stream);
|
||||
}
|
||||
if ty == AssetType::Vehicle {
|
||||
self.weapons.capture_vehicle(stream);
|
||||
}
|
||||
if ty == AssetType::Fx {
|
||||
self.fx.note_loaded(slot, insert_slot);
|
||||
}
|
||||
@@ -1253,23 +1371,22 @@ impl AssetLinkSink for CommonWalkSink {
|
||||
if ty == AssetType::XModel {
|
||||
if let Some(geometry) = stream.xmodel()
|
||||
&& let Some(name) = geometry.name.and_then(|p| stream.cstr(p).ok())
|
||||
&& matches!(name, "prop_flag_neutral" | "prop_suitcase_bomb")
|
||||
{
|
||||
let asset = crate::capture_xmodel_skel(
|
||||
let asset = asset_model::capture_xmodel_skel(
|
||||
stream,
|
||||
&self.script_strings,
|
||||
geometry,
|
||||
Some(&self.materials),
|
||||
)
|
||||
.map(|skel| crate::MapXModelSceneAsset::Iw4(std::sync::Arc::new(skel)))
|
||||
.unwrap_or(crate::MapXModelSceneAsset::Unavailable {
|
||||
reason: "common objective XModel skeleton capture failed",
|
||||
.map(|skel| asset_world::MapXModelSceneAsset::Iw4(std::sync::Arc::new(skel)))
|
||||
.unwrap_or(asset_world::MapXModelSceneAsset::Unavailable {
|
||||
reason: "common XModel skeleton capture failed",
|
||||
});
|
||||
if let crate::MapXModelSceneAsset::Iw4(skel) = &asset {
|
||||
if let asset_world::MapXModelSceneAsset::Iw4(skel) = &asset {
|
||||
self.shared_surfaces.retain(stream, geometry, skel.clone());
|
||||
}
|
||||
self.scene_models
|
||||
.insert(crate::MapXModelAssetKey(name.to_owned()), asset);
|
||||
.insert(asset_world::MapXModelAssetKey(name.to_owned()), asset);
|
||||
}
|
||||
self.fpv_meshes.capture(stream, &self.materials);
|
||||
self.world_weapons.capture(stream, &self.materials);
|
||||
@@ -1318,18 +1435,19 @@ impl AssetLinkSink for CommonWalkSink {
|
||||
data: &[u8],
|
||||
zlib_compressed: bool,
|
||||
) -> fastfile_iw4::Result<()> {
|
||||
self.scripts.capture(name, data, zlib_compressed);
|
||||
if let Some(sound) = self.sound.as_mut() {
|
||||
sound.raw_file(name, data, zlib_compressed);
|
||||
}
|
||||
self.player_anim_sources
|
||||
.capture(name, data, zlib_compressed);
|
||||
if let Some(table) = crate::capture_pen_table(name, data, zlib_compressed) {
|
||||
if let Some(table) = asset_game::capture_pen_table(name, data, zlib_compressed) {
|
||||
self.pen_table = Some(table);
|
||||
}
|
||||
if let Some(table) = crate::capture_lochit_table(name, data, zlib_compressed) {
|
||||
if let Some(table) = asset_game::capture_lochit_table(name, data, zlib_compressed) {
|
||||
self.lochit_table = Some(table);
|
||||
}
|
||||
match crate::parse_film_vision_rawfile(name, data, zlib_compressed) {
|
||||
match asset_world::parse_film_vision_rawfile(name, data, zlib_compressed) {
|
||||
Ok(Some(vision)) => {
|
||||
self.film_visions
|
||||
.insert(name.replace('\\', "/").to_ascii_lowercase(), Ok(vision));
|
||||
@@ -1348,8 +1466,11 @@ impl AssetLinkSink for CommonWalkSink {
|
||||
s: &ZoneStream<'_>,
|
||||
header: Ptr,
|
||||
) -> fastfile_iw4::Result<()> {
|
||||
if let Some(table) = iw4_cac_table(s, header) {
|
||||
self.keep_stats_table(table);
|
||||
if let Some(table) = iw4_string_table(s, header) {
|
||||
self.scripts.capture_table(&table);
|
||||
if is_cac_table(&table.name) {
|
||||
self.keep_stats_table(table);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -1413,7 +1534,8 @@ pub(crate) struct MaterialPopulationSink {
|
||||
pub stage: Option<StageHandle>,
|
||||
pub materials: MaterialCatalog,
|
||||
|
||||
pub stats_tables: BTreeMap<String, crate::CapturedStringTable>,
|
||||
pub stats_tables: BTreeMap<String, asset_game::CapturedStringTable>,
|
||||
pub scripts: crate::ScriptSources,
|
||||
|
||||
pub sound: Option<asset_audio::ZoneSoundCapture>,
|
||||
}
|
||||
@@ -1442,11 +1564,11 @@ impl MaterialPopulationSink {
|
||||
self.materials.set_capture_zone(zone);
|
||||
}
|
||||
|
||||
pub(crate) fn set_capture_ns(&mut self, ns: crate::AssetNamespace) {
|
||||
pub(crate) fn set_capture_ns(&mut self, ns: asset_core::AssetNamespace) {
|
||||
self.materials.set_capture_ns(ns);
|
||||
}
|
||||
|
||||
fn keep_stats_table(&mut self, table: Option<crate::CapturedStringTable>) {
|
||||
fn keep_stats_table(&mut self, table: Option<asset_game::CapturedStringTable>) {
|
||||
if let Some(table) = table {
|
||||
self.stats_tables.insert(table.name.clone(), table);
|
||||
}
|
||||
@@ -1509,6 +1631,7 @@ impl AssetLinkSink for MaterialPopulationSink {
|
||||
if let Some(sound) = self.sound.as_mut() {
|
||||
sound.raw_file(name, data, zlib_compressed);
|
||||
}
|
||||
self.scripts.capture(name, data, zlib_compressed);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1517,7 +1640,11 @@ impl AssetLinkSink for MaterialPopulationSink {
|
||||
s: &ZoneStream<'_>,
|
||||
header: Ptr,
|
||||
) -> fastfile_iw4::Result<()> {
|
||||
self.keep_stats_table(iw4_cac_table(s, header));
|
||||
let table = iw4_string_table(s, header);
|
||||
if let Some(table) = &table {
|
||||
self.scripts.capture_table(table);
|
||||
}
|
||||
self.keep_stats_table(table.filter(|t| is_cac_table(&t.name)));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1677,6 +1804,7 @@ impl fastfile_t5::AssetLinkSink for MaterialPopulationSink {
|
||||
if let Some(sound) = self.sound.as_mut() {
|
||||
sound.raw_file(name, data, zlib_compressed);
|
||||
}
|
||||
self.scripts.capture(name, data, zlib_compressed);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1685,7 +1813,11 @@ impl fastfile_t5::AssetLinkSink for MaterialPopulationSink {
|
||||
s: &fastfile_t5::ZoneStream<'_>,
|
||||
header: fastfile_t5::Ptr,
|
||||
) -> fastfile_t5::Result<()> {
|
||||
self.keep_stats_table(t5_cac_table(s, header));
|
||||
let table = t5_string_table(s, header);
|
||||
if let Some(table) = &table {
|
||||
self.scripts.capture_table(table);
|
||||
}
|
||||
self.keep_stats_table(table.filter(|t| is_cac_table(&t.name)));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1792,8 +1924,8 @@ impl ModelCensus {
|
||||
}
|
||||
|
||||
fn capture_tracer_named(
|
||||
tracers: &mut crate::TracerCatalog,
|
||||
materials: &crate::MaterialCatalog,
|
||||
tracers: &mut asset_game::TracerCatalog,
|
||||
materials: &asset_material::MaterialCatalog,
|
||||
s: &ZoneStream<'_>,
|
||||
geometry: fastfile_iw4::TracerDefGeometry,
|
||||
) -> fastfile_iw4::Result<()> {
|
||||
@@ -1809,7 +1941,7 @@ fn capture_tracer_named(
|
||||
|
||||
fn tracer_material_name(
|
||||
s: &ZoneStream<'_>,
|
||||
materials: &crate::MaterialCatalog,
|
||||
materials: &asset_material::MaterialCatalog,
|
||||
geometry: fastfile_iw4::TracerDefGeometry,
|
||||
) -> Option<String> {
|
||||
if let Some(ptr) = geometry.material_name {
|
||||
|
||||
@@ -5,11 +5,13 @@ use super::{
|
||||
ZoneLane, ZoneWalkSink,
|
||||
};
|
||||
use crate::lane_capability::{LaneStatus, PreparedCapability};
|
||||
use crate::progress::{LoadProgress, StageId};
|
||||
use crate::session_load::{PreparedWorld, WorldDrawPolicy};
|
||||
use crate::{
|
||||
MASK_PLAYER_SOLID, T5ZoneMemory, ZoneGame, ZoneImage, build_t5_clip_collision,
|
||||
build_t5_world_draw, decode_material_color_maps, decode_reflection_probe_cubemap,
|
||||
use crate::session_load::PreparedWorld;
|
||||
use asset_core::ZoneGame;
|
||||
use asset_material::{decode_material_color_maps, decode_reflection_probe_cubemap};
|
||||
use asset_transport::progress::{LoadProgress, StageId};
|
||||
use asset_transport::{T5ZoneMemory, ZoneImage};
|
||||
use asset_world::{
|
||||
MASK_PLAYER_SOLID, WorldDrawPolicy, build_t5_clip_collision, build_t5_world_draw,
|
||||
dm_spawn_points_t5, intermission_view_t5, minimap_corners_t5,
|
||||
};
|
||||
|
||||
@@ -57,10 +59,10 @@ impl ZoneLane for T5Lane {
|
||||
image: &ZoneImage,
|
||||
progress: &LoadProgress,
|
||||
_shared_surfaces: asset_model::SharedXModelSurfaces,
|
||||
material_seed: crate::MaterialCatalog,
|
||||
material_seed: asset_material::MaterialCatalog,
|
||||
_common_film_visions: &mut std::collections::BTreeMap<
|
||||
String,
|
||||
Result<crate::FilmVision, crate::FilmVisionParseError>,
|
||||
Result<asset_world::FilmVision, asset_world::FilmVisionParseError>,
|
||||
>,
|
||||
) -> LoadedWorld {
|
||||
let mut report = vec![format!("game: T5 ({})", path.display())];
|
||||
@@ -82,7 +84,7 @@ impl ZoneLane for T5Lane {
|
||||
};
|
||||
|
||||
let stage = progress.begin_scoped(StageId::MapAssets, "memory", None);
|
||||
report.push(crate::zone::xfile_arena_row(
|
||||
report.push(asset_transport::xfile_arena_row(
|
||||
"zone arenas map",
|
||||
&header.block_size,
|
||||
fastfile_t5::XFILE_BLOCK_TEMP,
|
||||
@@ -109,8 +111,8 @@ impl ZoneLane for T5Lane {
|
||||
let mut sink = ZoneWalkSink::default();
|
||||
let seeded_techsets = material_seed.technique_set_facts().to_vec();
|
||||
sink.seed_materials(material_seed);
|
||||
sink.set_capture_zone(crate::ZoneOwner::from_zone_path(path));
|
||||
sink.set_capture_ns(crate::AssetNamespace::T5);
|
||||
sink.set_capture_zone(asset_core::ZoneOwner::from_zone_path(path));
|
||||
sink.set_capture_ns(asset_core::AssetNamespace::T5);
|
||||
sink.sound = Some(asset_audio::ZoneSoundCapture::for_map(
|
||||
path,
|
||||
asset_audio::ZoneGame::T5,
|
||||
@@ -134,9 +136,7 @@ impl ZoneLane for T5Lane {
|
||||
stream.unsettled_offsets()
|
||||
));
|
||||
let runtime_overrun = stream.block_overrun(fastfile_t5::XFILE_BLOCK_RUNTIME as u8);
|
||||
report.push(format!(
|
||||
"retail block overrun: runtime +{runtime_overrun} bytes"
|
||||
));
|
||||
report.push(format!("block overrun: runtime +{runtime_overrun} bytes"));
|
||||
|
||||
let clip = if let Some(geometry) = stream.clip_map() {
|
||||
report.push(format!(
|
||||
@@ -195,6 +195,10 @@ impl ZoneLane for T5Lane {
|
||||
let createart_name = sink.createart_name.clone();
|
||||
let t5_teamset = sink.t5_teamset.clone();
|
||||
let script_sound = std::mem::take(&mut sink.script_sound).finish();
|
||||
let mut scripts = std::mem::take(&mut sink.scripts);
|
||||
if let Some(entities) = asset_world::map_ents_entity_string_t5(&stream) {
|
||||
scripts.set_entities(entities.to_owned());
|
||||
}
|
||||
match (&createart_name, exp_fog) {
|
||||
(Some(name), Some(fog)) => report.push(format!(
|
||||
"t5 createart: {name} fog=ready start={:.1} half={:.1}",
|
||||
@@ -261,6 +265,7 @@ impl ZoneLane for T5Lane {
|
||||
report.push("no GfxWorld retained — nothing to draw".into());
|
||||
let dm_spawns = dm_spawn_points_t5(&stream);
|
||||
let mut loaded = LoadedWorld {
|
||||
scripts,
|
||||
sound: map_sound,
|
||||
world: PreparedWorld {
|
||||
policy: WorldDrawPolicy::t5(),
|
||||
@@ -309,14 +314,7 @@ impl ZoneLane for T5Lane {
|
||||
draw.sky_model.is_some()
|
||||
));
|
||||
}
|
||||
let mut map_models =
|
||||
super::build_t5_static_model_draw(&stream, geometry, map_xmodels);
|
||||
if let Some(clip) = &clip {
|
||||
asset_world::capture_brush_trigger_hulls(
|
||||
&mut map_models.map_use_triggers,
|
||||
clip,
|
||||
);
|
||||
}
|
||||
let map_models = super::build_t5_static_model_draw(&stream, geometry, map_xmodels);
|
||||
if let Some(error) = map_models.static_error.as_ref() {
|
||||
report.push(format!("static models: {error}"));
|
||||
}
|
||||
@@ -336,9 +334,9 @@ impl ZoneLane for T5Lane {
|
||||
"script_brushmodel: {} *N placements (SP_script_brushmodel, not DrawInst)",
|
||||
map_models.script_brush_models.len()
|
||||
));
|
||||
let crate::PreparedMapModels {
|
||||
let asset_world::PreparedMapModels {
|
||||
static_draw:
|
||||
crate::StaticModelDraw {
|
||||
asset_world::StaticModelDraw {
|
||||
meshes: static_model_meshes,
|
||||
placements: static_model_instances,
|
||||
..
|
||||
@@ -346,14 +344,13 @@ impl ZoneLane for T5Lane {
|
||||
scene_assets: map_xmodel_scene_assets,
|
||||
script_instances: script_model_instances,
|
||||
script_brush_models,
|
||||
map_use_triggers,
|
||||
flag_descriptors,
|
||||
script_structs,
|
||||
..
|
||||
} = map_models;
|
||||
let intermission_view = intermission_view_t5(&stream);
|
||||
let minimap_corners = minimap_corners_t5(&stream);
|
||||
let north_yaw = crate::worldspawn_north_yaw_t5(&stream);
|
||||
let north_yaw = asset_world::worldspawn_north_yaw_t5(&stream);
|
||||
let dm_spawns = dm_spawn_points_t5(&stream);
|
||||
report.push(format!(
|
||||
"ffa spawns: {} mp_dm_spawn* ({} start)",
|
||||
@@ -458,7 +455,7 @@ impl ZoneLane for T5Lane {
|
||||
draw.dpvs.cleared_boxes
|
||||
));
|
||||
let smodel_lighting_samples = {
|
||||
use crate::model_lighting::{
|
||||
use asset_model::model_lighting::{
|
||||
OwnedLightGrid, build_smodel_lighting_samples_with_sight,
|
||||
census_lit_fragment_tiles, collect_t5_smodel_lighting_origins,
|
||||
};
|
||||
@@ -550,6 +547,7 @@ impl ZoneLane for T5Lane {
|
||||
let min = draw.stats.min;
|
||||
let max = draw.stats.max;
|
||||
LoadedWorld {
|
||||
scripts,
|
||||
sound: map_sound,
|
||||
materials: map_materials,
|
||||
world: PreparedWorld {
|
||||
@@ -560,10 +558,9 @@ impl ZoneLane for T5Lane {
|
||||
map_xmodel_scene_assets,
|
||||
script_model_instances,
|
||||
script_brush_models,
|
||||
map_use_triggers,
|
||||
flag_descriptors,
|
||||
script_structs,
|
||||
dyn_ents: crate::DynEntCatalog::default(),
|
||||
dyn_ents: asset_world::DynEntCatalog::default(),
|
||||
smodel_lighting_samples,
|
||||
light_grid,
|
||||
fx: sink.fx,
|
||||
@@ -603,6 +600,7 @@ impl ZoneLane for T5Lane {
|
||||
report.push(format!("T5 world mesh: {e}"));
|
||||
let dm_spawns = dm_spawn_points_t5(&stream);
|
||||
let mut loaded = LoadedWorld {
|
||||
scripts,
|
||||
sound: map_sound,
|
||||
world: PreparedWorld {
|
||||
policy: WorldDrawPolicy::t5(),
|
||||
@@ -638,7 +636,7 @@ impl ZoneLane for T5Lane {
|
||||
image: &ZoneImage,
|
||||
progress: &LoadProgress,
|
||||
decode_color_maps: bool,
|
||||
material_seed: crate::MaterialCatalog,
|
||||
material_seed: asset_material::MaterialCatalog,
|
||||
) -> CommonCensus {
|
||||
let header = match image.t5_header() {
|
||||
Ok(header) => header,
|
||||
@@ -661,8 +659,8 @@ impl ZoneLane for T5Lane {
|
||||
};
|
||||
let mut sink = CommonWalkSink::default();
|
||||
sink.seed_materials(material_seed);
|
||||
sink.set_capture_zone(crate::ZoneOwner::from_zone_path(path));
|
||||
sink.set_capture_ns(crate::AssetNamespace::T5);
|
||||
sink.set_capture_zone(asset_core::ZoneOwner::from_zone_path(path));
|
||||
sink.set_capture_ns(asset_core::AssetNamespace::T5);
|
||||
sink.sound = asset_audio::ZoneSoundCapture::claim_common(
|
||||
path,
|
||||
asset_audio::ZoneGame::T5,
|
||||
@@ -694,7 +692,7 @@ impl ZoneLane for T5Lane {
|
||||
|
||||
sink.weapons.resolve_reticles(&sink.materials);
|
||||
sink.weapons
|
||||
.resolve_combat_fx(&sink.fx, &crate::TracerCatalog::default());
|
||||
.resolve_combat_fx(&sink.fx, &asset_game::TracerCatalog::default());
|
||||
let leftover_fx = sink.fx.len();
|
||||
let leftover_fx_gaps = sink.fx.capture_gaps;
|
||||
for key in sink.weapons.rocket_model_hints() {
|
||||
@@ -706,7 +704,7 @@ impl ZoneLane for T5Lane {
|
||||
let projectile_keys = sink.weapons.projectile_model_hints();
|
||||
sink.projectile_meshes.keep_referenced(&projectile_keys);
|
||||
let mut weapons = sink.weapons.into_build();
|
||||
weapons.stamp_namespace(crate::AssetNamespace::T5);
|
||||
weapons.stamp_namespace(asset_core::AssetNamespace::T5);
|
||||
weapons.apply_stats_tables(sink.stats_tables.values());
|
||||
weapons.resolve_sz_xanim_edges(&sink.xanims);
|
||||
weapons.resolve_fpv_mesh_edges(&sink.fpv_meshes);
|
||||
@@ -718,7 +716,7 @@ impl ZoneLane for T5Lane {
|
||||
));
|
||||
let gun_named = weapons.gun_xmodel_count();
|
||||
report.push(format!(
|
||||
"common_mp weapons: {captured} captures → {} unique catalog ids (sorted; not retail bg_weaponIndex); {gun_named} with gunXModel[0]",
|
||||
"common_mp weapons: {captured} captures → {} unique catalog ids (sorted); {gun_named} with gunXModel[0]",
|
||||
weapons.len()
|
||||
));
|
||||
report.push(format!(
|
||||
@@ -740,7 +738,7 @@ impl ZoneLane for T5Lane {
|
||||
let mut pending_images = None;
|
||||
if decode_color_maps {
|
||||
let stage = progress.begin_scoped(StageId::Images, "common_mp", None);
|
||||
let (inline, plan) = crate::material_images::plan_material_color_maps(
|
||||
let (inline, plan) = asset_material::material_images::plan_material_color_maps(
|
||||
path,
|
||||
&mut materials,
|
||||
&stage,
|
||||
@@ -777,6 +775,7 @@ impl ZoneLane for T5Lane {
|
||||
fx_models: sink.fx_models,
|
||||
report,
|
||||
teamsets: sink.teamsets,
|
||||
scene_models: sink.scene_models,
|
||||
film_visions: std::collections::BTreeMap::new(),
|
||||
..Default::default()
|
||||
}
|
||||
@@ -787,7 +786,7 @@ impl ZoneLane for T5Lane {
|
||||
path: &Path,
|
||||
image: &ZoneImage,
|
||||
progress: &LoadProgress,
|
||||
material_seed: crate::MaterialCatalog,
|
||||
material_seed: asset_material::MaterialCatalog,
|
||||
) -> MaterialPopulation {
|
||||
let _ = progress;
|
||||
let zone_name = path.file_stem().map_or_else(
|
||||
@@ -817,8 +816,8 @@ impl ZoneLane for T5Lane {
|
||||
};
|
||||
let mut sink = MaterialPopulationSink::default();
|
||||
sink.seed_materials(material_seed);
|
||||
sink.set_capture_zone(crate::ZoneOwner::from_zone_path(path));
|
||||
sink.set_capture_ns(crate::AssetNamespace::T5);
|
||||
sink.set_capture_zone(asset_core::ZoneOwner::from_zone_path(path));
|
||||
sink.set_capture_ns(asset_core::AssetNamespace::T5);
|
||||
sink.sound = asset_audio::ZoneSoundCapture::claim_common(
|
||||
path,
|
||||
asset_audio::ZoneGame::T5,
|
||||
@@ -849,6 +848,7 @@ impl ZoneLane for T5Lane {
|
||||
materials: sink.materials,
|
||||
report,
|
||||
cac_tables: sink.stats_tables.into_values().collect(),
|
||||
scripts: sink.scripts,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::ZoneGame;
|
||||
use crate::lane::lane;
|
||||
use asset_core::ZoneGame;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum LaneStatus {
|
||||
|
||||
+8
-471
@@ -1,196 +1,19 @@
|
||||
mod animtree {
|
||||
pub use asset_anim::*;
|
||||
}
|
||||
mod arena {
|
||||
pub use asset_game::arena::*;
|
||||
}
|
||||
mod artifact_cache;
|
||||
mod asset_graph;
|
||||
mod attachment_hide {
|
||||
pub use asset_game::*;
|
||||
}
|
||||
mod body_catalog {
|
||||
pub use asset_model::*;
|
||||
}
|
||||
mod cac_stats {
|
||||
pub use asset_game::*;
|
||||
}
|
||||
mod clip_collision {
|
||||
pub use asset_world::*;
|
||||
}
|
||||
mod clip_scheduler {
|
||||
pub use asset_anim::*;
|
||||
}
|
||||
mod compass_map {
|
||||
pub use asset_world::*;
|
||||
}
|
||||
mod createart {
|
||||
pub use asset_world::*;
|
||||
}
|
||||
mod createfx {
|
||||
pub use asset_audio::*;
|
||||
}
|
||||
mod discover;
|
||||
mod ent_channel {
|
||||
pub use asset_audio::*;
|
||||
}
|
||||
mod map_script_sound {
|
||||
pub use asset_audio::*;
|
||||
}
|
||||
mod playeranim_parse {
|
||||
pub use asset_anim::*;
|
||||
}
|
||||
pub mod dobj {
|
||||
pub use xmodel_runtime::*;
|
||||
}
|
||||
mod dyn_ents {
|
||||
pub use asset_world::*;
|
||||
}
|
||||
mod fpv_catalog {
|
||||
pub use asset_model::*;
|
||||
}
|
||||
mod fx_catalog {
|
||||
pub use asset_game::*;
|
||||
}
|
||||
mod fx_model_catalog {
|
||||
pub use asset_game::*;
|
||||
}
|
||||
mod glass_catalog {
|
||||
pub use asset_world::*;
|
||||
}
|
||||
mod gltf_export;
|
||||
mod impact_fx_catalog {
|
||||
pub use asset_game::*;
|
||||
}
|
||||
mod iw5_tech_map {
|
||||
pub use asset_material::iw5_tech_map::*;
|
||||
}
|
||||
mod iwd;
|
||||
mod lane;
|
||||
mod lane_capability;
|
||||
pub mod loading_screen;
|
||||
mod localize {
|
||||
pub use asset_game::*;
|
||||
}
|
||||
mod map_entities {
|
||||
pub use asset_world::*;
|
||||
}
|
||||
mod map_load_process;
|
||||
mod map_scripts;
|
||||
mod match_load;
|
||||
mod material_catalog {
|
||||
pub use asset_material::material_catalog::*;
|
||||
}
|
||||
mod material_draw {
|
||||
pub use asset_material::material_draw::*;
|
||||
}
|
||||
mod material_images {
|
||||
pub use asset_material::material_images::*;
|
||||
}
|
||||
mod menu_catalog {
|
||||
pub use asset_game::*;
|
||||
}
|
||||
pub mod model_lighting {
|
||||
pub use asset_model::*;
|
||||
}
|
||||
mod model_mesh {
|
||||
pub use asset_world::model_mesh::*;
|
||||
}
|
||||
mod model_skel {
|
||||
pub use asset_model::*;
|
||||
}
|
||||
mod penetration {
|
||||
pub use asset_game::*;
|
||||
}
|
||||
pub mod plugin;
|
||||
pub mod prepared;
|
||||
pub mod load_jobs {
|
||||
pub use asset_transport::load_jobs::*;
|
||||
}
|
||||
pub mod progress {
|
||||
pub use asset_transport::progress::*;
|
||||
}
|
||||
mod projectile_mesh_catalog {
|
||||
pub use asset_model::*;
|
||||
}
|
||||
mod map_load_process;
|
||||
pub mod session_load;
|
||||
mod soldiers {
|
||||
pub use asset_model::*;
|
||||
}
|
||||
mod sound_catalog {
|
||||
pub use asset_audio::*;
|
||||
}
|
||||
mod sound_load {
|
||||
pub use asset_audio::*;
|
||||
}
|
||||
mod sound_load_iw5 {
|
||||
pub use asset_audio::*;
|
||||
}
|
||||
mod sound_load_t5 {
|
||||
pub use asset_audio::*;
|
||||
}
|
||||
mod sound_wma_t5 {
|
||||
pub use asset_audio::*;
|
||||
}
|
||||
mod t5_code_remap {
|
||||
pub use asset_material::t5_code_remap::*;
|
||||
}
|
||||
pub mod t5_tech_map {
|
||||
pub use asset_material::t5_tech_map::*;
|
||||
}
|
||||
mod teardown;
|
||||
mod tracer_catalog {
|
||||
pub use asset_game::*;
|
||||
}
|
||||
mod weapon_anim_dispatch {
|
||||
pub use asset_game::*;
|
||||
}
|
||||
mod weapon_animations {
|
||||
pub use asset_game::*;
|
||||
}
|
||||
mod weapon_catalog {
|
||||
pub use asset_game::*;
|
||||
}
|
||||
mod world_draw {
|
||||
pub use asset_world::world_draw::*;
|
||||
}
|
||||
mod world_iw5 {
|
||||
pub use asset_world::world_iw5::*;
|
||||
}
|
||||
mod world_mesh {
|
||||
pub use asset_world::world_mesh::*;
|
||||
}
|
||||
mod world_t5 {
|
||||
pub use asset_world::world_t5::*;
|
||||
}
|
||||
mod world_weapon_catalog {
|
||||
pub use asset_model::*;
|
||||
}
|
||||
mod xanim_catalog {
|
||||
pub use asset_anim::*;
|
||||
}
|
||||
mod xanim_clip {
|
||||
pub use xmodel_runtime::*;
|
||||
}
|
||||
mod zone;
|
||||
|
||||
pub use anim_iw4::PartBits;
|
||||
pub use animtree::{
|
||||
AnimTreeDefinitionError, AnimTreeNodeRuntime, AtrCompileError, CompiledAnimNode,
|
||||
CompiledAnimTreeDefinition, DObjAnimTreeRuntime, MULTIPLAYER_ANIMTREE_PATH,
|
||||
PLAYERANIM_SCRIPT_PATH, PLAYERANIM_TYPES_PATH, PlayerAnimLeafBinds, PlayerAnimSources,
|
||||
};
|
||||
pub use arena::{
|
||||
ArenaCharsets, FACTION_ICON_COL, MapTeamSettings, SessionTeamSettings, arena_charsets,
|
||||
load_iw5_team_sources, parse_arena, read_basemaps_arena, read_iwd_named,
|
||||
t5_settings_from_teamset_gsc, t5_settings_from_teamset_rawfile, t5_teamset_from_map_gsc,
|
||||
t5_teamset_from_rawfile, t5_teamset_key_from_rawfile, team_settings, team_settings_for_zone,
|
||||
};
|
||||
pub use artifact_cache::{cache_flight, cache_get, cache_put, fnv1a64, fnv1a64_more};
|
||||
pub use asset_core::{
|
||||
AssetKey, AssetKeyError, AssetKind, AssetNamespace, AssetRef, AssetRefCensus, BoundTarget,
|
||||
CatalogIndex, IndexSpace, MaterialKey, WalkLocalMaterialIndex, ZoneGame, ZoneOwner,
|
||||
bound_zone_names,
|
||||
};
|
||||
pub(crate) use asset_graph::stamp_match_destructible_death;
|
||||
pub use asset_graph::{
|
||||
AssetEdge, AssetEdgeCensus, AssetEdgeReason, AssetGraphCensus, DESTRUCTIBLE_DEATH_HINTS,
|
||||
@@ -201,318 +24,32 @@ pub use asset_graph::{
|
||||
TracerIndex, TracerSpace, WorldWeaponIndex, WorldWeaponSpace, XAnimIndex, XAnimSpace,
|
||||
resolve_after_absorb,
|
||||
};
|
||||
pub use asset_iw4::{
|
||||
D3DCMP_ALWAYS, D3DCMP_EQUAL, D3DCMP_LESS, D3DCMP_LESSEQUAL, GFXS1_DEPTHTEST_DISABLE,
|
||||
GFXS1_DEPTHTEST_LESSEQUAL, GFXS1_DEPTHWRITE, GFXS1_POLYGON_OFFSET_MASK,
|
||||
GFXS1_POLYGON_OFFSET_SHADOWMAP_LEVEL, GFXS1_POLYGON_OFFSET_SHIFT, POLYGON_OFFSET_BIAS_TO_D3D,
|
||||
R_POLYGON_OFFSET_BIAS_DEFAULT, R_POLYGON_OFFSET_SCALE_DEFAULT, S_DEPTH_TEST_TABLE,
|
||||
SM_POLYGON_OFFSET_BIAS_DEFAULT, SM_POLYGON_OFFSET_SCALE_DEFAULT, SND_CURVE_DEFAULT_ASSET_NAME,
|
||||
SND_ENTCHANNEL_FILE, SndAliasFlags, SndAliasSampleKind, d3d_depth_bias_to_wgpu_constant,
|
||||
depth_state_from_state_bits, depth_test_enable, depth_write_enable, polygon_offset_d3d,
|
||||
polygon_offset_d3d_defaults, polygon_offset_level, polygon_offset_wgpu_defaults,
|
||||
size::{WEAP_ANIM_IDLE, WEAPON_ANIM_COUNT, weap_anim},
|
||||
snd_attenuate,
|
||||
};
|
||||
pub use asset_world::{
|
||||
FilmVision, FilmVisionParseError, MaterialSortTrigger, SurfaceCastsSunShadow, WorldCapture,
|
||||
parse_film_vision_rawfile, world_capture_from_casters,
|
||||
};
|
||||
pub use attachment_hide::{bone_has_hidden_ancestor, effective_hide_tags, surface_visible};
|
||||
pub use body_catalog::{BODY_SPINE_BONES, BodyMeshBuild, BodyMeshCatalog, BodyMeshEntry};
|
||||
pub use cac_stats::{
|
||||
CacAuthoredCategory, CacPerkRow, CacPerkSlot, CacStatBar, CacWeaponFact, CacWeaponPreview,
|
||||
PERK_ICON_COL, PERK_NAME_COL, PERK_REF_COL, PERK_SLOT_COL, STATS_GROUP_COL, STATS_IMAGE_COL,
|
||||
STATS_NAME_COL, STATS_REF_COL, cac_category_from_item_group, is_perk_table_name,
|
||||
is_stats_table_name, item_group_for_weapon, iw4_fallback_item_group, perk_row, perk_rows,
|
||||
primary_sniper_keys, weapon_preview,
|
||||
};
|
||||
pub use clip_collision::{
|
||||
ClipBrush, ClipCollision, ClipCollisionError, ClipMapMaterial, ClipPlacedStaticModel,
|
||||
ClipSweepHit, MASK_PLAYER_SOLID, XModelCollCatalog, attach_iw5_static_models,
|
||||
attach_static_models, build_clip_collision, build_iw5_clip_collision, build_t5_clip_collision,
|
||||
gate_leafbrush_index_fits,
|
||||
};
|
||||
pub use clip_scheduler::{ActiveAnim, ClipScheduler, ClipSchedulerError};
|
||||
pub use compass_map::{
|
||||
MapCompassDeclaration, MapCompassSource, compass_max_range, setup_minimap_material,
|
||||
};
|
||||
pub use createart::{
|
||||
ExpFog, SunFog, decode_rawfile_text, is_createart_fog_file, is_createart_source,
|
||||
parse_createart_rawfile, parse_set_exp_fog, parse_set_vol_fog, parse_vision_set_fog,
|
||||
};
|
||||
pub use createfx::{
|
||||
CreateFxLoopSound, CreateFxOneshot, CreateFxOneshotEmitters, apply_createfx_effect_aliases,
|
||||
parse_createfx_effect_aliases, parse_createfx_loop_sounds, parse_createfx_oneshots,
|
||||
};
|
||||
pub use discover::{
|
||||
GamesRoot, ZoneFile, ensure_artifacts_dir, find_common_mp_for_envelope,
|
||||
find_common_mp_for_zone, find_localized_common_mp_for_zone, find_runtime_common_mp,
|
||||
find_runtime_zone, find_zone_file, find_zone_file_version, find_zone_for_tree,
|
||||
games_content_report, games_root_from_env, games_root_report, group_mp_maps, list_mp_maps,
|
||||
load_dotenv, map_load_title, peek_zone_version, split_zone_key, zone_game_for_path,
|
||||
};
|
||||
pub use dobj::{
|
||||
AIM_PITCH_CLAMP_RAD, AnimInstance, Attach, DObj, DObjError, ModelPoseSrc, TP_HEAD_ATTACH_TAG,
|
||||
TP_WEAPON_ATTACH_TAGS, apply_aim_pitches, apply_legs_yaw, build_body_head_weapon_dobj,
|
||||
build_body_weapon_dobj, pitch_spine_bone, tp_head_attach_tag, tp_weapon_attach_tag, yaw_bone,
|
||||
};
|
||||
pub use dyn_ents::{
|
||||
DYNENT_DRAW_BRUSH, DYNENT_DRAW_MODEL, DynEntCatalog, DynEntDef, DynEntDefScalars,
|
||||
DynEntDrawType, DynEntProps, DynEntType, OwnedPhysPreset, PhysPresetCatalog,
|
||||
RETAIL_DYN_ENT_PROPS, build_dyn_ent_catalog, parse_dyn_ent_def_scalars, retail_dyn_ent_props,
|
||||
};
|
||||
pub use ent_channel::{EntChannel, parse_ent_channel_file};
|
||||
pub use fastfile_iw4::GlyphCapture;
|
||||
pub use fpv_catalog::{
|
||||
FpvHands, FpvMeshBuild, FpvMeshCatalog, FpvMeshEntry, FpvMeshKey, FpvMountPlan, PoseStats,
|
||||
TagViewBind, VIEWHANDS_NAME, VIEWHANDS_NAME_T5,
|
||||
};
|
||||
pub use fx_catalog::{
|
||||
FxBankSound, FxCatalog, FxChildEdge, FxDefinitions, FxElemMaterial, FxElemMaterialReason,
|
||||
FxElemModelEdge, FxName, OwnedFxEffectDef, OwnedFxElemDef, OwnedFxSparkFountainDef,
|
||||
OwnedFxTrailDef, OwnedFxVisual, alias_fx_color_map_stubs, elem_type as fx_elem_type,
|
||||
fx_body_namespace, fx_color_decoded_in_catalog, fx_material_bind_name, insert_fx_color_image,
|
||||
lookup_fx_color_image,
|
||||
};
|
||||
pub use fx_model_catalog::{FxModelCatalog, FxModelEntry};
|
||||
pub use glass_catalog::{FxGlassReset, GlassZoneCensus, build_fx_glass_reset, build_glass_census};
|
||||
pub use gltf_export::{GltfExportSummary, export_prepared_world_gltf};
|
||||
pub use impact_fx_catalog::{ImpactFxCatalog, OwnedFxImpactEntry, OwnedFxImpactTable};
|
||||
pub use iw5_tech_map::{
|
||||
IW5_CODE_COLOR_SATURATION_B, IW5_CODE_COLOR_SATURATION_G, IW5_CODE_COLOR_SATURATION_R,
|
||||
IW5_CODE_EYEOFFSET, IW5_NAME_MATCH_IW4_SLOT, IW5_TECHNIQUE_TYPE_COUNT,
|
||||
IW5_TECHNIQUE_TYPE_NAMES, LEFTOVER_IW5_CODE_BASE, iw5_slot_to_iw4, leftover_iw5_code_bank,
|
||||
leftover_iw5_slots, leftover_selector_census, remap_code_const_index, remap_code_texture_index,
|
||||
remap_occupancy_bits, remap_pass_count_by_slot, remap_shader_arg_type, remap_state_bits_entry,
|
||||
remap_technique_flags_by_slot,
|
||||
};
|
||||
pub use iwd::{IwdSoundIndex, NamespaceSoundIwd, NamespaceTree, NamespaceTrees};
|
||||
pub use lane::{CommonCensus, LANE_GAPS, LaneGap, LoadedWorld, ZoneLane, lane};
|
||||
pub use lane_capability::{LaneStatus, PreparedCapability, lane_status};
|
||||
pub use lighting_iw4::MODEL_LIGHTING_TILE_BYTES;
|
||||
pub use loading_screen::{LoadingPreviewSource, LoadingScreen};
|
||||
pub use localize::{
|
||||
LocalizeCatalog, MP_LOCALIZED_ZONES, decode_localized_text, load_localize_catalog,
|
||||
load_localize_catalog_in_lane, load_localize_catalog_iw5, load_localize_catalog_t5,
|
||||
load_mp_localized_strings,
|
||||
};
|
||||
pub use map_entities::{
|
||||
FlagDescriptor, IntermissionView, MapEntsKeyCensus, MapScriptStruct, MapUseTrigger,
|
||||
MinimapCorners, ScriptBrushModelLink, ScriptBrushModelPlacement, ScriptModelId,
|
||||
ScriptModelPlacement, SpawnPoint, census_entity_string_keys, dm_spawn_points,
|
||||
dm_spawn_points_iw5, dm_spawn_points_t5, exploding_prop_machine, flag_descriptors,
|
||||
flag_descriptors_iw5, flag_descriptors_t5, intermission_view, intermission_view_iw5,
|
||||
intermission_view_t5, map_ents_entity_string, map_script_structs, map_script_structs_iw5,
|
||||
map_script_structs_t5, map_use_triggers, map_use_triggers_iw5, map_use_triggers_t5,
|
||||
minimap_corners, minimap_corners_iw5, minimap_corners_t5, parse_flag_descriptors,
|
||||
parse_map_script_structs, parse_map_use_triggers, script_brush_model_placements,
|
||||
script_brush_model_placements_iw5, script_brush_model_placements_t5, script_model_placements,
|
||||
script_model_placements_iw5, script_model_placements_t5, worldspawn_north_yaw,
|
||||
worldspawn_north_yaw_iw5, worldspawn_north_yaw_t5,
|
||||
};
|
||||
pub use map_script_sound::{
|
||||
MapScriptSoundFacts, MapScriptSoundSource, SessionMapScriptSound, ambient_play_alias,
|
||||
game_nested_string_assignment, game_string_assignment,
|
||||
};
|
||||
pub use map_load_process::MapLoadProcess;
|
||||
pub use match_load::{
|
||||
MapLoadApproval, MatchLoadAbort, MatchLoadAccepted, MatchLoadBusy, MatchLoadDispatch,
|
||||
MatchLoadRequest, PreparedMatchReady, PreparedMatchSound,
|
||||
};
|
||||
pub use material_catalog::{
|
||||
AssetPointerIdentity, AssetRefDumpCensus, AuthoredImage, AuthoredMaterial, AuthoredShader,
|
||||
AuthoredVertexDecl, CrossGameReason, CrossGameTechsetResolution, ImageVariantId,
|
||||
MaterialCatalog, MaterialDefinitions, MaterialImageMemory, MaterialTextureBinding,
|
||||
OwnedMaterialPass, OwnedShaderArgument, OwnedShaderRef, OwnedTechnique, OwnedTechniqueGraph,
|
||||
ShaderSourceCensus, T5TechniqueOccupancy, TS_2D, TS_COLOR_MAP, TS_DETAIL_MAP, TS_FUNCTION,
|
||||
TS_NORMAL_MAP, TS_SPECULAR_MAP, TS_T5_COLOR0_MAP, TS_T5_COLOR15_MAP, TS_T5_THROW_MAP,
|
||||
TS_WATER_MAP, TechniqueSetFacts, TechniqueTable, TechsetKey, TechsetResolve,
|
||||
VertexDeclStreamCensus, t5_feature_token_stripped,
|
||||
};
|
||||
pub use material_draw::{
|
||||
AlphaTest, ColorMapTransform, D3DCULL_CCW, D3DCULL_CW, D3DCULL_NONE, D3DRS_CULLMODE,
|
||||
GFXS0_ATEST_DISABLE, GFXS0_ATEST_GE_128, GFXS0_ATEST_GT_0, GFXS0_ATEST_LT_128,
|
||||
GFXS0_ATEST_MASK, GFXS0_ATEST_SHIFT, GFXS0_CULL_BACK, GFXS0_CULL_FRONT, GFXS0_CULL_MASK,
|
||||
GFXS0_CULL_NONE, GFXS0_CULL_SHIFT, GFXS0_SRGBWRITEENABLE, Gfxs0AlphaTest, MaterialCullFace,
|
||||
MaterialDrawMode, S_ALPHA_TEST_TABLE, S_CULL_TABLE, alpha_test_cutoff_from_state_bits,
|
||||
alpha_test_from_state_bits, cull_face_from_state_bits, d3d_cull_mode_from_state_bits,
|
||||
material_constant_name, srgb_write_enable_from_state_bits,
|
||||
};
|
||||
pub use material_images::{
|
||||
ImageWorkingSet, MaterialImageStats, cached_iwd_main_dirs, decode_catalog_images_from_iwd,
|
||||
decode_dxt5nm_xy, decode_in_zone_builtin_images, decode_map_preview,
|
||||
decode_material_color_maps, decode_menu_background, decode_reflection_probe_cubemap,
|
||||
decode_ui_image, decode_ui_image_from_main, decode_zone_image_rgba, game_main_for_zone,
|
||||
iwd_entry_reads, iwd_read_cost, last_image_working_set, mip_cache_cost, retail_lightmap_bake,
|
||||
retail_lit_color, sampler_from_iw4, shared_payload_copy_cost, shared_variant_census,
|
||||
};
|
||||
pub use menu_catalog::{
|
||||
CapturedStringTable, FontDef, HUD_CHROME_MENUS, HudMaterialPlan, HudMaterialTextureBinding,
|
||||
ITEM_TYPE_BUTTON, ITEM_TYPE_TEXT, MenuCatalog, MenuDef, MenuItem, MenuRect, MenuSetLocalVar,
|
||||
UI_MENU_ZONES, ZoneUiImage, load_menu_catalog, load_ui_menu_catalog, ui_games_root,
|
||||
};
|
||||
pub use model_lighting::{
|
||||
BlockedReason, GridView, LitFragmentTileCensus, OwnedLightGrid, SampledLighting,
|
||||
SmodelLightingCensus, SmodelLightingSample, TileChromaSpan, build_smodel_lighting_samples,
|
||||
build_smodel_lighting_samples_from_world, build_smodel_lighting_samples_with_sight,
|
||||
census_lit_fragment_tiles, collect_iw5_smodel_lighting_origins,
|
||||
collect_smodel_lighting_origins, collect_t5_smodel_lighting_origins, grid_view_from_geometry,
|
||||
lit_fragment_mid_grey_from_tile, lit_fragment_white_from_tile,
|
||||
lit_fragment_white_from_tile_normal, lit_fragment_white_from_tile_texel,
|
||||
lit_fragment_white_sun_add, mean_tile_rgba01, packed_lighting_for_origins, sample_light_grid,
|
||||
sample_light_grid_with_lookup_fallback, sample_light_grid_with_sight, tile_chroma_span,
|
||||
tile_corners_match_compress,
|
||||
};
|
||||
pub use model_mesh::{
|
||||
MapXModelAssetKey, MapXModelMaterialCensus, MapXModelSceneAsset, MapXModelSceneCatalog,
|
||||
ModelColorCensus, ModelKind, ModelMesh, ModelMeshError, ModelSurfaceDraw,
|
||||
OwnedXRigidVertListCollision, OwnedXSurfaceCollisionTree, PreparedMapModels,
|
||||
RetailPackedVertexPayload, RetailXSurfaceCollisionPayload, ScriptModelMetadata,
|
||||
ScriptModelSceneInstance, StaticModelDraw, StaticModelDrawError, StaticModelInstance,
|
||||
StaticModelPlacement, SurfaceColorCensus, VertexColorStats, build_iw5_static_model_instances,
|
||||
build_iw5_xmodel_mesh, build_static_model_instances, build_t5_static_model_instances,
|
||||
build_t5_xmodel_mesh, build_xmodel_mesh, census_mesh_vertex_colors, census_model_mesh,
|
||||
census_rgba_f32, census_skel_vertex_colors, format_vertex_color_stats, model_kind,
|
||||
};
|
||||
pub use model_skel::{
|
||||
BoneBind, BoneCollision, FpvSkel, ModelLodSelector, ModelSkel, VertSkin, capture_body_skel,
|
||||
capture_body_skel_iw5, capture_body_skel_t5, capture_fpv_skel, capture_fpv_skel_iw5,
|
||||
capture_fpv_skel_t5, capture_world_weapon_skel, capture_xmodel_skel, capture_xmodel_skel_iw5,
|
||||
capture_xmodel_skel_t5, dobj_has_lod_for_dist, lod_surface_range, t5_lod, xmodel_lod_for_dist,
|
||||
};
|
||||
pub use penetration::{
|
||||
LochitTableError, PenTableError, capture_lochit_table, capture_pen_table,
|
||||
parse_lochit_info_string, parse_pen_table_info_string, parse_pen_table_rawfile,
|
||||
};
|
||||
pub use playeranim_parse::{
|
||||
ParsedAnimCommand, ParsedAnimCondition, ParsedAnimItem, ParsedPlayerAnimScript,
|
||||
PlayerAnimParseError,
|
||||
};
|
||||
pub use plugin::AssetPlugin;
|
||||
|
||||
pub use asset_game::{
|
||||
AttachmentChoice, AttachmentOption, ConfigurationRefusal, FamilyKey, FamilySlot, LoadoutRules,
|
||||
ResolvedConfiguration, WeaponFamilies, WeaponFamily, WeaponSelection,
|
||||
};
|
||||
pub use map_load_process::MapLoadProcess;
|
||||
pub use prepared::{
|
||||
MapFacts, MatchMaterials, MatchType10SoundHints, PreparedBodies, PreparedBodyClips,
|
||||
PreparedDestructibleDeath, PreparedFpvMeshes, PreparedGaps, PreparedLocalizedStrings,
|
||||
PreparedMap, PreparedProjectileMeshes, PreparedWeapons, PreparedWorldWeapons, PreparedXAnims,
|
||||
PreparedXModelWalkCensus, SessionCompass,
|
||||
};
|
||||
pub use progress::{
|
||||
LoadLaneTiming, LoadProgress, LoadSnapshot, StageEnd, StageHandle, StageId, StageKey,
|
||||
StageOutcome, StageScope, StageSnapshot, WorkCount, peak_resident_bytes,
|
||||
process_resident_bytes,
|
||||
};
|
||||
pub use projectile_mesh_catalog::{
|
||||
ProjectileMeshBuild, ProjectileMeshCatalog, ProjectileMeshEntry, ProjectileMeshKey,
|
||||
};
|
||||
pub use session_load::{
|
||||
MatchLoadOutcome, MatchMaterialSeed, PreparedMatch, PreparedWorld, ShellCommon,
|
||||
WorldDrawPolicy, apply_match_material_map, load_match_material_catalog,
|
||||
load_match_material_seed, load_pool, load_prepared_match, load_shell_common, load_workers,
|
||||
publish_process_cpus,
|
||||
apply_match_material_map, load_match_material_catalog, load_match_material_seed, load_pool,
|
||||
load_prepared_match, load_shell_common, load_workers, publish_process_cpus,
|
||||
};
|
||||
pub use soldiers::{
|
||||
SoldierKit, SoldierKits, arms_for_body, body_has_tp_attach_bones, ffa_assignment_is_axis,
|
||||
is_arms_model, is_body_model, kit_assignment_is_axis, soldier_kits,
|
||||
};
|
||||
pub use sound_catalog::{
|
||||
CapturedAlias, CapturedSndCurve, CapturedSound, LoadedSoundEdge, LoadedSoundEdgeReason,
|
||||
LoadedSoundPcm, MSS_PCM, PickLoadedOutcome, PickedSound, SoundAliasKey, SoundCatalog,
|
||||
lerp_range, pick_weighted_variant_index, snd_advance_lcg, snd_unit_random,
|
||||
};
|
||||
pub use sound_load::{
|
||||
LoadedSoundBank, SoundSources, compose_sound_bank, gather_sound_sources, load_mp_sound_bank,
|
||||
load_sound_catalog, namespace_for_zone,
|
||||
};
|
||||
pub use sound_load_iw5::load_sound_catalog_iw5;
|
||||
pub use sound_load_t5::load_sound_catalog_t5;
|
||||
pub use sound_wma_t5::{
|
||||
T5_WMA, XwmaClip, XwmaDecodeCost, decode_t5_xwma, decode_t5_xwma_batch, xwma_decode_cost,
|
||||
};
|
||||
pub use t5_code_remap::{
|
||||
IW4_CUSTOM_SAMPLER_DEST, LEFTOVER_T5_CODE_BASE, T5_CODE_CUSTOMWIND_CENTER,
|
||||
T5_CODE_CUSTOMWIND_SPRING, T5_CODE_EXTRA_CAM_PARAM, T5_CODE_EYEOFFSET, T5_CODE_FOG,
|
||||
T5_CODE_FOG_COLOR, T5_CODE_FOG2, T5_CODE_GENERIC_PARAM0, T5_CODE_GENERIC_PARAM1,
|
||||
T5_CODE_GENERIC_PARAM4, T5_CODE_GENERIC_PARAM5, T5_CODE_GENERIC_PARAM6,
|
||||
T5_CODE_GRASS_WIND_FORCE0, T5_CODE_HDRCONTROL_0, T5_CODE_HDRCONTROL_1, T5_CODE_HERO_LIGHTING_B,
|
||||
T5_CODE_HERO_LIGHTING_G, T5_CODE_HERO_LIGHTING_R, T5_CODE_LIGHT_ATTENUATION,
|
||||
T5_CODE_LIGHT_CONE_CONTROL1, T5_CODE_LIGHT_CONE_CONTROL2, T5_CODE_LIGHT_FALLOFF_A,
|
||||
T5_CODE_LIGHT_FALLOFF_B, T5_CODE_LIGHT_HERO_SCALE, T5_CODE_LIGHT_SPOT_AABB,
|
||||
T5_CODE_LIGHT_SPOT_COOKIE_SLIDE, T5_CODE_LIGHT_SPOT_MATRIX0, T5_CODE_LIGHT_SPOT_MATRIX1,
|
||||
T5_CODE_LIGHT_SPOT_MATRIX2, T5_CODE_LIGHT_SPOT_MATRIX3, T5_CODE_POSTFX_CONTROL0,
|
||||
T5_CODE_POSTFX_CONTROL6, T5_CODE_SKY_COLOR_MULTIPLIER, T5_CODE_SKY_TRANSITION,
|
||||
T5_CODE_SUN_DIFFUSE, T5_CODE_SUN_FOG, T5_CODE_SUN_FOG_COLOR, T5_CODE_SUN_FOG_DIR,
|
||||
T5_CODE_SUN_POSITION, T5_CODE_SUN_SPECULAR, T5_CODE_TREECANOPY_PARMS,
|
||||
T5_CODE_VARIANT_WIND_SPRING_0, T5_CODE_VARIANT_WIND_SPRING_15, T5_CODE_VPOS1_TO_WORLD,
|
||||
T5_CODE_VPOSX_TO_WORLD, T5_CODE_VPOSY_TO_WORLD, T5_CODE_WIND_DIRECTION, T5_CUSTOM_SAMPLER_DEST,
|
||||
leftover_t5_code_bank, remap_code_const_index as remap_t5_code_const_index,
|
||||
remap_code_texture_index as remap_t5_code_texture_index, remap_t5_code_const_source,
|
||||
remap_t5_custom_sampler_flags, t5_code_const_name,
|
||||
};
|
||||
pub use t5_tech_map::{
|
||||
IW4_TECHNIQUE_TYPE_COUNT, IW4_TECHNIQUE_TYPE_NAMES, OccupancyProjectionCensus,
|
||||
T5_NAME_MATCH_IW4_SLOT, T5_TECHNIQUE_TYPE_COUNT, T5_TECHNIQUE_TYPE_NAMES, T5OccupancyRemapGap,
|
||||
T5SlotProjection, census_occupancy, leftover_t5_selector_census, leftover_t5_slots,
|
||||
occupancy_to_technique_table, project_t5_slot, remap_t5_state_bits_entry,
|
||||
remap_t5_technique_flags, t5_slot_to_iw4,
|
||||
};
|
||||
pub use tracer_catalog::{
|
||||
OwnedTracerDef, TracerCatalog, TracerDefinitions, TracerMaterial, TracerMaterialReason,
|
||||
};
|
||||
pub use weapon_anim_dispatch::{
|
||||
ACTION_GOAL_TIME_SECS, ACTIVE_GOAL_WEIGHT, ANIM_RATE_TABLE, AnimRateOffsets,
|
||||
IDLE_INTERRUPT_GOAL_TIME_SECS, INACTIVE_GOAL_WEIGHT, WEAP_ANIM_EVENT_MASK, WEAPON_ANIM_SLOTS,
|
||||
known_complete_rate_timer_offset, known_rate_timer_offset, playback_rate,
|
||||
slot_for_weap_anim_event, slot_uses_native_rate, weap_anim_extra,
|
||||
};
|
||||
pub use weapon_animations::{AdsOverlayConvention, WeaponAnimSlot, WeaponAnimations};
|
||||
pub use weapon_catalog::{
|
||||
CacOffhandBucket, CatalogWeapon, Iw5ConfigurationCandidate, LinkedNotetrackAction,
|
||||
NotetrackConvention, T5_NOTE_RUMBLE_PREFIX, T5_NOTE_SOUND_PREFIX, UnknownWeaponName,
|
||||
WeaponBodyFacts, WeaponBuild, WeaponCatalog, WeaponCombatFx, WeaponDependencyGap,
|
||||
WeaponHudMaterialEdges, WeaponKickFacts, WeaponProjectileFx, WeaponRegistry,
|
||||
WeaponReticleAssets, WeaponSoundAliases, WeaponSoundSlot, WeaponSwayFacts, cac_offhand_bucket,
|
||||
gsc_weapon_script_name, overlay_name_is_hud_iris, t5_inline_note_alias,
|
||||
};
|
||||
pub use weapon_catalog::{
|
||||
FpvAssembly, FpvAssemblyCensus, FpvAssemblyPart, FpvAssemblyTags, FpvClipTracks, FpvPartRole,
|
||||
FpvSideAssemblies,
|
||||
};
|
||||
pub use world_draw::{
|
||||
CapturedLightDef, DpvsWorldData, GfxBrushModelBounds, GfxBrushModelSurfs, OwnedPortal,
|
||||
ResolvedLightDef, RetailWorldVertexPayload, SunEffectsCapture, WorldBatch, WorldDraw,
|
||||
WorldLightRegionHull, WorldLightmap, WorldLightmapGap, WorldPrimaryLight, WorldReflectionProbe,
|
||||
WorldShadowGeometry, brush_model_vertex_centroid, build_world_draw,
|
||||
};
|
||||
pub(crate) use world_draw::{
|
||||
capture_light_defs, resolve_named_light_def, resolve_outdoor_image,
|
||||
resolve_primary_light_attenuation,
|
||||
};
|
||||
pub use world_iw5::build_iw5_world_draw;
|
||||
pub(crate) use world_iw5::capture_iw5_light_defs;
|
||||
pub use world_mesh::{
|
||||
WorldMeshError, WorldMeshStats, build_world_mesh, bumped_world_normal, half_to_f32,
|
||||
unpack_packed_tex_coords,
|
||||
};
|
||||
pub use world_t5::{build_t5_world_draw, build_t5_world_mesh};
|
||||
pub use world_weapon_catalog::{
|
||||
WorldWeaponBuild, WorldWeaponCatalog, WorldWeaponEntry, WorldWeaponKey,
|
||||
};
|
||||
pub use xanim_catalog::{CapturedXAnim, XAnimBuild, XAnimCatalog, XAnimKey};
|
||||
pub use xanim_clip::{
|
||||
AnimClip, ClipError, ClipNotify, FrameIndices, Keyed, RawXAnimParts, Rotation, SampledTrack,
|
||||
Track, Translation,
|
||||
};
|
||||
pub use zone::{
|
||||
Iw5ZoneMemory, T5ZoneMemory, ZoneImage, ZoneMemory, ZoneOpenError, open_zone, open_zone_shared,
|
||||
parse_zone_image,
|
||||
};
|
||||
|
||||
pub use asset_material::vertex_layout::{T5_WORLD_LAYER_HOST_STRIDE, VertexLayoutFamily};
|
||||
|
||||
pub use asset_material::{material_alpha_test, t5_smodel_camera_emits};
|
||||
|
||||
pub mod image_handles;
|
||||
|
||||
mod script_sources;
|
||||
pub use script_sources::{ScriptSources, ScriptTable};
|
||||
|
||||
@@ -3,7 +3,7 @@ use std::time::{Duration, Instant};
|
||||
|
||||
use bevy::prelude::Resource;
|
||||
|
||||
use crate::progress::LoadProgress;
|
||||
use asset_transport::progress::LoadProgress;
|
||||
|
||||
#[derive(Resource)]
|
||||
pub struct LoadingScreen {
|
||||
|
||||
@@ -10,7 +10,7 @@ use std::time::Instant;
|
||||
|
||||
use bevy::prelude::Resource;
|
||||
|
||||
use crate::progress::{LoadProgress, StageHandle, StageId};
|
||||
use asset_transport::progress::{LoadProgress, StageHandle, StageId};
|
||||
|
||||
#[derive(Resource)]
|
||||
pub struct MapLoadProcess {
|
||||
@@ -76,7 +76,7 @@ impl MapLoadProcess {
|
||||
let installed_ms = self.requested_at.elapsed().as_secs_f32() * 1000.0;
|
||||
let trim_ms = diag::release_freed_heap().as_secs_f32() * 1000.0;
|
||||
let with_trim_ms = self.requested_at.elapsed().as_secs_f32() * 1000.0;
|
||||
let rss = crate::process_resident_bytes()
|
||||
let rss = asset_transport::process_resident_bytes()
|
||||
.map(|bytes| format!(" rss_mib={}", bytes >> 20))
|
||||
.unwrap_or_default();
|
||||
let heap = diag::process_live_heap_bytes()
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
pub(crate) const RADIATION: &str = r#"main()
|
||||
{
|
||||
waittillframeend;
|
||||
level.radiation_busy = true;
|
||||
level.radiation_open = false;
|
||||
level.radiation_doors = [];
|
||||
level.radiation_doors[0] = getent("big_door1_clip", "targetname");
|
||||
level.radiation_doors[1] = getent("big_door2_clip", "targetname");
|
||||
switches = [];
|
||||
switches[0] = getent("switch_trigger1", "targetname");
|
||||
switches[1] = getent("switch_trigger2", "targetname");
|
||||
if (!isdefined(level.radiation_doors[0]) || !isdefined(level.radiation_doors[1])
|
||||
|| !isdefined(switches[0]) || !isdefined(switches[1]))
|
||||
return;
|
||||
models = [];
|
||||
models[0] = getent("big_door1", "targetname");
|
||||
models[1] = getent("big_door2", "targetname");
|
||||
for (i = 0; i < 2; i++)
|
||||
{
|
||||
if (isdefined(models[i]))
|
||||
models[i] linkto(level.radiation_doors[i]);
|
||||
switches[i] usetriggerrequirelookat();
|
||||
switches[i] setcursorhint("HINT_ACTIVATE");
|
||||
switches[i] sethintstring(&"MP_HOLD_TO_OPERATE_DOORS");
|
||||
switches[i] thread watch_switch();
|
||||
}
|
||||
level.radiation_switches = switches;
|
||||
if (!isdefined(level.gameended) || !level.gameended)
|
||||
{
|
||||
level waittill("prematch_over");
|
||||
level.radiation_busy = false;
|
||||
thread operate_doors();
|
||||
}
|
||||
}
|
||||
|
||||
watch_switch()
|
||||
{
|
||||
for (;;)
|
||||
{
|
||||
self waittill("trigger", player);
|
||||
if (!level.radiation_busy)
|
||||
{
|
||||
if (isdefined(player))
|
||||
player playsound("t5:evt_hydraulic_switch");
|
||||
thread operate_doors();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
operate_doors()
|
||||
{
|
||||
if (level.radiation_busy)
|
||||
return;
|
||||
level.radiation_busy = true;
|
||||
for (i = 0; i < 2; i++)
|
||||
level.radiation_switches[i] makeunusable();
|
||||
turn = 123;
|
||||
if (level.radiation_open)
|
||||
turn = -turn;
|
||||
duration = getdvarfloat("scr_d1_time");
|
||||
if (duration <= 0)
|
||||
duration = 8;
|
||||
cooldown = getdvarfloat("scr_door_cooldown");
|
||||
if (cooldown <= 0)
|
||||
cooldown = 20;
|
||||
level.radiation_doors[0] rotateroll(turn, duration, duration * .6, duration * .4);
|
||||
level.radiation_doors[1] rotateroll(-turn, duration, duration * .7, duration * .3);
|
||||
level.radiation_doors[0] playloopsound("evt_hydraulic_loop");
|
||||
level.radiation_doors[0] playsound("t5:evt_hydraulic_start");
|
||||
level.radiation_doors[0] waittill("rotatedone");
|
||||
level.radiation_doors[0] stoploopsound();
|
||||
level.radiation_open = !level.radiation_open;
|
||||
if (level.radiation_open)
|
||||
level.radiation_doors[0] playsound("t5:evt_hydraulic_open");
|
||||
else
|
||||
level.radiation_doors[0] playsound("t5:evt_hydraulic_close");
|
||||
wait cooldown;
|
||||
level.radiation_busy = false;
|
||||
for (i = 0; i < 2; i++)
|
||||
level.radiation_switches[i] makeusable();
|
||||
}
|
||||
"#;
|
||||
@@ -5,8 +5,8 @@ use bevy::tasks::{Task, futures_lite::future};
|
||||
use frame::{ClientSet, LaunchIdentity, MapLoadApproved, MapLoadFailed};
|
||||
|
||||
use crate::map_load_process::MapLoadProcess;
|
||||
use crate::progress::LoadProgress;
|
||||
use crate::session_load::{MatchLoadOutcome, PreparedMatch, load_prepared_match};
|
||||
use asset_transport::progress::LoadProgress;
|
||||
|
||||
#[derive(SystemSet, Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct MapLoadApproval;
|
||||
@@ -175,15 +175,15 @@ fn approve_map_load(
|
||||
);
|
||||
continue;
|
||||
}
|
||||
let games = crate::GamesRoot(identity.games_root.clone());
|
||||
let found = crate::find_zone_file(&games, &request.zone);
|
||||
let games = asset_transport::GamesRoot(identity.games_root.clone());
|
||||
let found = asset_transport::find_zone_file(&games, &request.zone);
|
||||
let zone = found
|
||||
.as_ref()
|
||||
.map(|zone| zone.zone_name.clone())
|
||||
.unwrap_or_else(|_| request.zone.clone());
|
||||
let zone_ff = found.map(|zone| zone.path);
|
||||
let common_mp = match &zone_ff {
|
||||
Ok(path) => crate::find_runtime_common_mp(&games, path).map(|zone| zone.path),
|
||||
Ok(path) => asset_transport::find_runtime_common_mp(&games, path).map(|zone| zone.path),
|
||||
Err(error) => Err(error.clone()),
|
||||
};
|
||||
let error = zone_ff
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
use bevy::prelude::Resource;
|
||||
|
||||
use crate::{
|
||||
asset_graph::DestructibleDeathRow, body_catalog::BodyMeshCatalog, fpv_catalog::FpvMeshCatalog,
|
||||
weapon_catalog::WeaponRegistry, world_weapon_catalog::WorldWeaponCatalog,
|
||||
xanim_catalog::XAnimCatalog,
|
||||
};
|
||||
use crate::asset_graph::DestructibleDeathRow;
|
||||
use asset_anim::XAnimCatalog;
|
||||
use asset_game::WeaponRegistry;
|
||||
use asset_model::{BodyMeshCatalog, FpvMeshCatalog, WorldWeaponCatalog};
|
||||
|
||||
/// The one material population a prepared match owns, and the map-zone-local
|
||||
/// index space that resolves into it.
|
||||
@@ -13,7 +12,7 @@ use crate::{
|
||||
/// nothing else. The pool itself is never nested inside an optional product.
|
||||
#[derive(Clone, Default)]
|
||||
pub struct MatchMaterials {
|
||||
pub population: std::sync::Arc<crate::MaterialDefinitions>,
|
||||
pub population: std::sync::Arc<asset_material::MaterialDefinitions>,
|
||||
|
||||
pub common_profile_id: u64,
|
||||
|
||||
@@ -32,25 +31,29 @@ pub struct PreparedGaps {
|
||||
/// moved from there to its single owner in [`PreparedMap`].
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub struct MapFacts {
|
||||
pub minimap_corners: Option<crate::MinimapCorners>,
|
||||
pub minimap_corners: Option<asset_world::MinimapCorners>,
|
||||
|
||||
pub north_yaw: Option<f32>,
|
||||
|
||||
pub compass: crate::MapCompassDeclaration,
|
||||
pub airstrike_height: Option<f32>,
|
||||
|
||||
pub script_sound: crate::MapScriptSoundFacts,
|
||||
pub compass: asset_world::MapCompassDeclaration,
|
||||
|
||||
pub team_settings: crate::MapTeamSettings,
|
||||
pub script_sound: asset_audio::MapScriptSoundFacts,
|
||||
|
||||
pub team_settings: asset_game::MapTeamSettings,
|
||||
|
||||
pub t5_teamset: Option<String>,
|
||||
|
||||
pub objective_visuals: asset_game::ObjectiveVisuals,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub struct PreparedMap {
|
||||
pub zone: String,
|
||||
|
||||
pub namespace: Option<crate::AssetNamespace>,
|
||||
pub spawns: Vec<crate::SpawnPoint>,
|
||||
pub namespace: Option<asset_core::AssetNamespace>,
|
||||
pub spawns: Vec<asset_world::SpawnPoint>,
|
||||
|
||||
pub facts: MapFacts,
|
||||
pub gaps: PreparedGaps,
|
||||
@@ -72,7 +75,7 @@ pub struct PreparedBodies(pub std::sync::Arc<BodyMeshCatalog>);
|
||||
pub struct PreparedWorldWeapons(pub WorldWeaponCatalog);
|
||||
|
||||
#[derive(Clone, Debug, Default, Resource)]
|
||||
pub struct PreparedProjectileMeshes(pub crate::ProjectileMeshCatalog);
|
||||
pub struct PreparedProjectileMeshes(pub asset_model::ProjectileMeshCatalog);
|
||||
|
||||
#[derive(Clone, Debug, Default, Resource)]
|
||||
pub struct PreparedXModelWalkCensus {
|
||||
@@ -114,15 +117,15 @@ pub struct PreparedXAnims(pub XAnimCatalog);
|
||||
pub struct PreparedDestructibleDeath(pub Vec<DestructibleDeathRow>);
|
||||
|
||||
#[derive(Clone, Debug, Default, Resource)]
|
||||
pub struct PreparedLocalizedStrings(pub crate::LocalizeCatalog);
|
||||
pub struct PreparedLocalizedStrings(pub asset_game::LocalizeCatalog);
|
||||
|
||||
#[derive(Clone, Debug, Default, Resource)]
|
||||
pub struct SessionCompass {
|
||||
pub corners: Option<crate::MinimapCorners>,
|
||||
pub corners: Option<asset_world::MinimapCorners>,
|
||||
|
||||
pub north_yaw: Option<f32>,
|
||||
|
||||
pub declaration: crate::MapCompassDeclaration,
|
||||
pub declaration: asset_world::MapCompassDeclaration,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, Resource, PartialEq, Eq)]
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct ScriptSources {
|
||||
sources: BTreeMap<String, Result<Vec<u8>, String>>,
|
||||
tables: BTreeMap<String, ScriptTable>,
|
||||
entities: Option<String>,
|
||||
configs: BTreeMap<String, String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
||||
pub struct ScriptTable {
|
||||
pub columns: usize,
|
||||
pub rows: usize,
|
||||
pub cells: Vec<String>,
|
||||
}
|
||||
|
||||
fn normalize(name: &str) -> String {
|
||||
name.replace('\\', "/").to_ascii_lowercase()
|
||||
}
|
||||
|
||||
impl ScriptSources {
|
||||
pub fn read(&self, module: &str) -> Result<Vec<u8>, String> {
|
||||
self.sources
|
||||
.get(module)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| Err(format!("missing script asset {module}.gsc")))
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.sources.len()
|
||||
}
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.sources.is_empty()
|
||||
}
|
||||
|
||||
pub(crate) fn asset_names(&self) -> BTreeSet<String> {
|
||||
let mut names = BTreeSet::new();
|
||||
for source in self.sources.values().filter_map(|s| s.as_ref().ok()) {
|
||||
quoted_names(source, &mut names);
|
||||
}
|
||||
if let Some(entities) = &self.entities {
|
||||
quoted_names(entities.as_bytes(), &mut names);
|
||||
}
|
||||
for table in self.tables.values() {
|
||||
names.extend(table.cells.iter().cloned());
|
||||
}
|
||||
names
|
||||
}
|
||||
|
||||
pub fn tables(&self) -> &BTreeMap<String, ScriptTable> {
|
||||
&self.tables
|
||||
}
|
||||
|
||||
pub fn entities(&self) -> Option<&str> {
|
||||
self.entities.as_deref()
|
||||
}
|
||||
|
||||
pub fn config(&self, name: &str) -> Option<&str> {
|
||||
self.configs.get(&normalize(name)).map(String::as_str)
|
||||
}
|
||||
|
||||
pub fn shocks(&self) -> impl Iterator<Item = (&str, &str)> {
|
||||
self.configs.iter().filter_map(|(path, text)| {
|
||||
let name = path.strip_prefix("shock/")?.strip_suffix(".shock")?;
|
||||
Some((name, text.as_str()))
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn capture(&mut self, name: &str, data: &[u8], compressed: bool) {
|
||||
let name = normalize(name);
|
||||
if name.ends_with(".cfg") || name.ends_with(".shock") || name == "radiant/keys.txt" {
|
||||
if let Some(text) = asset_world::decode_rawfile_text(data, compressed) {
|
||||
self.configs.insert(name, text);
|
||||
}
|
||||
return;
|
||||
}
|
||||
let Some(module) = name.strip_suffix(".gsc") else {
|
||||
return;
|
||||
};
|
||||
let source = if compressed {
|
||||
asset_transport::inflate_zlib(data).map_err(|e| e.to_string())
|
||||
} else {
|
||||
Ok(asset_world::decode_packed_rawfile(data).unwrap_or_else(|| data.to_vec()))
|
||||
}
|
||||
.map(|mut bytes| {
|
||||
while bytes.last() == Some(&0) {
|
||||
bytes.pop();
|
||||
}
|
||||
bytes
|
||||
});
|
||||
self.sources.insert(module.to_owned(), source);
|
||||
}
|
||||
|
||||
pub(crate) fn capture_table(&mut self, table: &asset_game::CapturedStringTable) {
|
||||
self.tables.insert(
|
||||
normalize(&table.name),
|
||||
ScriptTable {
|
||||
columns: table.columns,
|
||||
rows: table.rows,
|
||||
cells: table.cells.clone(),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
pub(crate) fn insert_source(&mut self, module: &str, source: String) {
|
||||
self.sources
|
||||
.insert(normalize(module), Ok(source.into_bytes()));
|
||||
}
|
||||
|
||||
pub(crate) fn set_entities(&mut self, entities: String) {
|
||||
self.entities = Some(entities);
|
||||
}
|
||||
|
||||
pub(crate) fn overlay(&mut self, other: Self) {
|
||||
self.sources.extend(other.sources);
|
||||
self.tables.extend(other.tables);
|
||||
self.configs.extend(other.configs);
|
||||
if other.entities.is_some() {
|
||||
self.entities = other.entities;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn quoted_names(bytes: &[u8], names: &mut BTreeSet<String>) {
|
||||
let mut at = 0;
|
||||
while at < bytes.len() {
|
||||
match (bytes[at], bytes.get(at + 1).copied()) {
|
||||
(b'/', Some(b'/')) => {
|
||||
while at < bytes.len() && bytes[at] != b'\n' {
|
||||
at += 1;
|
||||
}
|
||||
}
|
||||
(b'/', Some(b'*')) => {
|
||||
at += 2;
|
||||
while at + 1 < bytes.len() && &bytes[at..at + 2] != b"*/" {
|
||||
at += 1;
|
||||
}
|
||||
at = (at + 2).min(bytes.len());
|
||||
}
|
||||
(b'"', _) => {
|
||||
at += 1;
|
||||
let start = at;
|
||||
while at < bytes.len() && bytes[at] != b'"' {
|
||||
if bytes[at] == b'\\' && at + 1 < bytes.len() {
|
||||
at += 1;
|
||||
}
|
||||
at += 1;
|
||||
}
|
||||
if let Ok(name) = std::str::from_utf8(&bytes[start..at]) {
|
||||
names.insert(name.to_owned());
|
||||
}
|
||||
at += 1;
|
||||
}
|
||||
_ => at += 1,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,7 @@ impl CommonKey {
|
||||
}
|
||||
}
|
||||
|
||||
fn shell(games: &crate::GamesRoot, report: &mut Vec<String>) -> Self {
|
||||
fn shell(games: &asset_transport::GamesRoot, report: &mut Vec<String>) -> Self {
|
||||
let runtime = match find_common_mp_for_envelope(games, fastfile_iw4::ZONE_VERSION_PC) {
|
||||
Ok(found) => Some(found.path),
|
||||
Err(error) => {
|
||||
@@ -63,31 +63,38 @@ pub(super) struct CommonCounts {
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(super) struct CommonProducts {
|
||||
pub(super) scripts: crate::ScriptSources,
|
||||
pub(super) t5_scene_models: asset_world::MapXModelSceneCatalog,
|
||||
pub(super) material_seed: MaterialCatalog,
|
||||
pub(super) shared_surfaces: asset_model::SharedXModelSurfaces,
|
||||
pub(super) scene_models: crate::MapXModelSceneCatalog,
|
||||
pub(super) light_defs: Vec<crate::CapturedLightDef>,
|
||||
pub(super) scene_models: asset_world::MapXModelSceneCatalog,
|
||||
pub(super) light_defs: Vec<asset_world::CapturedLightDef>,
|
||||
pub(super) pen_table: weapon_iw4::PenetrationDepthTable,
|
||||
pub(super) pen_table_loaded: bool,
|
||||
pub(super) lochit_table: Option<[f32; weapon_iw4::HITLOC_COUNT]>,
|
||||
pub(super) tracers: crate::TracerCatalog,
|
||||
pub(super) tracers: asset_game::TracerCatalog,
|
||||
pub(super) xmodel_walk: crate::PreparedXModelWalkCensus,
|
||||
pub(super) s1_common_bytes: usize,
|
||||
pub(super) teamsets: std::collections::HashMap<String, crate::MapTeamSettings>,
|
||||
pub(super) film_visions:
|
||||
std::collections::BTreeMap<String, Result<crate::FilmVision, crate::FilmVisionParseError>>,
|
||||
pub(super) teamsets: std::collections::HashMap<String, asset_game::MapTeamSettings>,
|
||||
pub(super) film_visions: std::collections::BTreeMap<
|
||||
String,
|
||||
Result<asset_world::FilmVision, asset_world::FilmVisionParseError>,
|
||||
>,
|
||||
pub(super) weapons: WeaponBuild,
|
||||
pub(super) fpv_meshes: FpvMeshBuild,
|
||||
pub(super) world_weapons: WorldWeaponBuild,
|
||||
pub(super) projectile_meshes: ProjectileMeshBuild,
|
||||
pub(super) xanims: XAnimBuild,
|
||||
pub(super) player_anim_sources: crate::PlayerAnimSources,
|
||||
pub(super) player_anim_sources: asset_anim::PlayerAnimSources,
|
||||
pub(super) fx: FxCatalog,
|
||||
pub(super) fx_models: crate::FxModelCatalog,
|
||||
pub(super) impact_fx: Option<crate::OwnedFxImpactTable>,
|
||||
pub(super) fx_models: asset_game::FxModelCatalog,
|
||||
pub(super) impact_fx: Option<asset_game::OwnedFxImpactTable>,
|
||||
pub(super) t5_xanims: XAnimBuild,
|
||||
pub(super) t5_fx: FxCatalog,
|
||||
pub(super) t5_impact_fx: Option<asset_game::OwnedFxImpactTable>,
|
||||
pub(super) iw5_materials: MaterialCatalog,
|
||||
pub(super) iw5_scene_models: asset_world::MapXModelSceneCatalog,
|
||||
pub(super) iw5_shared_surfaces: asset_model::SharedXModelSurfaces,
|
||||
pub(super) strings: LocalizeCatalog,
|
||||
pub(super) counts: CommonCounts,
|
||||
pub(super) report: Vec<String>,
|
||||
@@ -97,7 +104,7 @@ pub(super) struct CommonProducts {
|
||||
pub(super) struct KeptImages {
|
||||
pub(super) label: &'static str,
|
||||
pub(super) namespace: &'static str,
|
||||
pub(super) batch: crate::material_images::DecodedImageBatch,
|
||||
pub(super) batch: asset_material::material_images::DecodedImageBatch,
|
||||
pub(super) job: load_jobs::Job,
|
||||
}
|
||||
|
||||
@@ -107,8 +114,8 @@ pub struct CommonSet {
|
||||
pub(super) products: CommonProducts,
|
||||
donor_images: async_lock::OnceCell<Vec<KeptImages>>,
|
||||
pub(super) fpv_plan: Option<ImageDemandPlan>,
|
||||
pub(super) retained: std::sync::Mutex<crate::material_images::PayloadRetention>,
|
||||
pub(super) cac_tables: Vec<(crate::AssetNamespace, crate::CapturedStringTable)>,
|
||||
pub(super) retained: std::sync::Mutex<asset_material::material_images::PayloadRetention>,
|
||||
pub(super) cac_tables: Vec<(asset_core::AssetNamespace, asset_game::CapturedStringTable)>,
|
||||
pub(super) prepared_ms: f32,
|
||||
pub(super) ready_at: std::time::Instant,
|
||||
}
|
||||
@@ -122,7 +129,7 @@ impl CommonSet {
|
||||
self.donor_images.get().map_or(0, Vec::len)
|
||||
}
|
||||
|
||||
pub(super) fn retain(&self, batch: &crate::material_images::DecodedImageBatch) -> u64 {
|
||||
pub(super) fn retain(&self, batch: &asset_material::material_images::DecodedImageBatch) -> u64 {
|
||||
self.retained
|
||||
.lock()
|
||||
.unwrap_or_else(|poison| poison.into_inner())
|
||||
@@ -219,11 +226,12 @@ pub(super) async fn ensure_common(key: CommonKey) -> (Arc<CommonSet>, &'static s
|
||||
|
||||
pub struct ShellCommon {
|
||||
pub weapons: WeaponRegistry,
|
||||
pub tables: Vec<(crate::AssetNamespace, crate::CapturedStringTable)>,
|
||||
pub strings: LocalizeCatalog,
|
||||
pub tables: Vec<(asset_core::AssetNamespace, asset_game::CapturedStringTable)>,
|
||||
pub report: Vec<String>,
|
||||
}
|
||||
|
||||
pub async fn load_shell_common(games: crate::GamesRoot) -> ShellCommon {
|
||||
pub async fn load_shell_common(games: asset_transport::GamesRoot) -> ShellCommon {
|
||||
let mut report = Vec::new();
|
||||
let key = CommonKey::shell(&games, &mut report);
|
||||
let (common, reach) = ensure_common(key).await;
|
||||
@@ -232,9 +240,9 @@ pub async fn load_shell_common(games: crate::GamesRoot) -> ShellCommon {
|
||||
"CAC: {reach} common set {}; weapons={} (iw4={} iw5={} t5={}) tables={}",
|
||||
common.key,
|
||||
weapons.len(),
|
||||
weapons.namespace_count(crate::AssetNamespace::Iw4),
|
||||
weapons.namespace_count(crate::AssetNamespace::Iw5),
|
||||
weapons.namespace_count(crate::AssetNamespace::T5),
|
||||
weapons.namespace_count(asset_core::AssetNamespace::Iw4),
|
||||
weapons.namespace_count(asset_core::AssetNamespace::Iw5),
|
||||
weapons.namespace_count(asset_core::AssetNamespace::T5),
|
||||
common.cac_tables.len(),
|
||||
));
|
||||
for (namespace, table) in &common.cac_tables {
|
||||
@@ -247,6 +255,7 @@ pub async fn load_shell_common(games: crate::GamesRoot) -> ShellCommon {
|
||||
}
|
||||
ShellCommon {
|
||||
weapons,
|
||||
strings: common.products.strings.clone(),
|
||||
tables: common.cac_tables.clone(),
|
||||
report,
|
||||
}
|
||||
@@ -267,9 +276,9 @@ async fn prepare_common(key: CommonKey) -> Arc<CommonSet> {
|
||||
let mut donor_report = Vec::new();
|
||||
let material_donor = key.foreign.clone();
|
||||
let weapon_donor = resolve_iw5_weapon_donor(anchor.as_deref(), &mut donor_report);
|
||||
let iw5_stats_walk = weapon_donor.clone().map(|donor| {
|
||||
let iw5_startup_walk = weapon_donor.clone().map(|donor| {
|
||||
let progress = progress.clone();
|
||||
pool.spawn(async move { walk_iw5_stats_tables(&donor, &progress) })
|
||||
pool.spawn(async move { walk_iw5_startup(&donor, &progress) })
|
||||
});
|
||||
let shared_donor = match (&material_donor, &weapon_donor) {
|
||||
(Some(material), Some(weapons)) if material == weapons => Some(material.clone()),
|
||||
@@ -345,7 +354,8 @@ async fn prepare_common(key: CommonKey) -> Arc<CommonSet> {
|
||||
})
|
||||
};
|
||||
|
||||
let (material_seed, mut common_report, iw4_stats, startup_light_defs) = startup_walk.await;
|
||||
let (material_seed, mut common_report, iw4_stats, startup_light_defs, startup_scripts) =
|
||||
startup_walk.await;
|
||||
let startup_count = material_seed.materials.len();
|
||||
|
||||
let t5_weapon_walk = {
|
||||
@@ -363,8 +373,10 @@ async fn prepare_common(key: CommonKey) -> Arc<CommonSet> {
|
||||
mut material_seed,
|
||||
xanims: t5_xanims,
|
||||
fx: t5_fx,
|
||||
impact_fx: t5_impact_fx,
|
||||
projectiles: t5_projectiles,
|
||||
teamsets: t5_teamsets,
|
||||
scene_models: t5_scene_models,
|
||||
images: t5_images,
|
||||
stats_tables: t5_stats,
|
||||
report: t5_report,
|
||||
@@ -396,19 +408,24 @@ async fn prepare_common(key: CommonKey) -> Arc<CommonSet> {
|
||||
common_report.append(&mut foreign_report);
|
||||
|
||||
let foreign_count = foreign_materials.materials.len();
|
||||
material_seed.absorb_asset_population(foreign_materials);
|
||||
let foreign_linked = material_seed.absorb_asset_population(foreign_materials);
|
||||
let mut shared_bundle = shared_bundle;
|
||||
if let Some(bundle) = &mut shared_bundle {
|
||||
bundle.scene_models.remap_walk_materials(&foreign_linked);
|
||||
}
|
||||
|
||||
let mut shared_surfaces = asset_model::SharedXModelSurfaces::default();
|
||||
let mut common_scene_models = crate::MapXModelSceneCatalog::default();
|
||||
let mut common_scene_models = asset_world::MapXModelSceneCatalog::default();
|
||||
let mut common_light_defs = startup_light_defs;
|
||||
let mut common_pen_table = weapon_iw4::PenetrationDepthTable::empty();
|
||||
let mut common_pen_loaded = false;
|
||||
let mut common_lochit_table = None;
|
||||
|
||||
let mut common_tracers = crate::TracerCatalog::default();
|
||||
let mut common_tracers = asset_game::TracerCatalog::default();
|
||||
let mut xmodel_walk = crate::PreparedXModelWalkCensus::default();
|
||||
let mut s1_common_bytes = 0;
|
||||
let mut teamsets = t5_teamsets;
|
||||
let mut scripts = startup_scripts;
|
||||
let mut common_film_visions = std::collections::BTreeMap::new();
|
||||
let mut fpv_plan = None;
|
||||
let mut iw4_census_stats = Vec::new();
|
||||
@@ -443,6 +460,12 @@ async fn prepare_common(key: CommonKey) -> Arc<CommonSet> {
|
||||
s1_common_bytes = census.s1_common_bytes;
|
||||
teamsets.extend(census.teamsets);
|
||||
common_film_visions = census.film_visions;
|
||||
// patch_mp loads after common_mp, so its scripts win.
|
||||
scripts = {
|
||||
let mut ordered = census.scripts;
|
||||
ordered.overlay(std::mem::take(&mut scripts));
|
||||
ordered
|
||||
};
|
||||
iw4_census_stats = census.cac_tables;
|
||||
(
|
||||
census.weapons,
|
||||
@@ -459,27 +482,27 @@ async fn prepare_common(key: CommonKey) -> Arc<CommonSet> {
|
||||
)
|
||||
}
|
||||
Some((_, Err(error))) => (
|
||||
crate::WeaponBuild::default(),
|
||||
asset_game::WeaponBuild::default(),
|
||||
FpvMeshBuild::default(),
|
||||
WorldWeaponBuild::default(),
|
||||
ProjectileMeshBuild::default(),
|
||||
XAnimBuild::default(),
|
||||
crate::PlayerAnimSources::default(),
|
||||
crate::FxCatalog::default(),
|
||||
crate::FxModelCatalog::default(),
|
||||
asset_anim::PlayerAnimSources::default(),
|
||||
asset_game::FxCatalog::default(),
|
||||
asset_game::FxModelCatalog::default(),
|
||||
None,
|
||||
material_seed,
|
||||
vec![format!("common_mp models: open zone: {error}")],
|
||||
),
|
||||
None => (
|
||||
crate::WeaponBuild::default(),
|
||||
asset_game::WeaponBuild::default(),
|
||||
FpvMeshBuild::default(),
|
||||
WorldWeaponBuild::default(),
|
||||
ProjectileMeshBuild::default(),
|
||||
XAnimBuild::default(),
|
||||
crate::PlayerAnimSources::default(),
|
||||
crate::FxCatalog::default(),
|
||||
crate::FxModelCatalog::default(),
|
||||
asset_anim::PlayerAnimSources::default(),
|
||||
asset_game::FxCatalog::default(),
|
||||
asset_game::FxModelCatalog::default(),
|
||||
None,
|
||||
material_seed,
|
||||
Vec::new(),
|
||||
@@ -509,12 +532,17 @@ async fn prepare_common(key: CommonKey) -> Arc<CommonSet> {
|
||||
xanims: iw5_xanims,
|
||||
materials: iw5_materials,
|
||||
stats_tables: iw5_census_stats,
|
||||
scene_models: iw5_scene_models,
|
||||
shared_surfaces: iw5_shared_surfaces,
|
||||
} = bundle;
|
||||
let iw5_stats = match iw5_stats_walk {
|
||||
let mut material_seed = material_seed;
|
||||
let iw5_stats = match iw5_startup_walk {
|
||||
Some(task) => {
|
||||
let (tables, report) = task.await;
|
||||
common_report.extend(report);
|
||||
tables
|
||||
let population = task.await;
|
||||
common_report.extend(population.report);
|
||||
common_light_defs.extend(population.light_defs);
|
||||
material_seed.absorb_asset_population(population.materials);
|
||||
population.cac_tables
|
||||
}
|
||||
None => Vec::new(),
|
||||
};
|
||||
@@ -574,7 +602,7 @@ async fn prepare_common(key: CommonKey) -> Arc<CommonSet> {
|
||||
common_report.push(format!(
|
||||
"t5 weapon absorb: donor={t5_ids} unique={t5_absorbed} fpv=+{t5_fpv_added}/{t5_fpv_n} world_guns=+{t5_world_added}; registry now {} (t5={})",
|
||||
weapons.len(),
|
||||
weapons.namespace_count(crate::AssetNamespace::T5)
|
||||
weapons.namespace_count(asset_core::AssetNamespace::T5)
|
||||
));
|
||||
common_report.push(format!(
|
||||
"FPV generation: common={fpv_common_n} iw5_keys={iw5_fpv_added} t5={t5_fpv_n} t5_keys={t5_fpv_added} collide={} merged={}",
|
||||
@@ -582,10 +610,14 @@ async fn prepare_common(key: CommonKey) -> Arc<CommonSet> {
|
||||
fpv_meshes.len()
|
||||
));
|
||||
|
||||
let cac_tables: Vec<(crate::AssetNamespace, crate::CapturedStringTable)> = [
|
||||
(crate::AssetNamespace::Iw4, iw4_stats, iw4_census_stats),
|
||||
(crate::AssetNamespace::Iw5, iw5_stats, iw5_census_stats),
|
||||
(crate::AssetNamespace::T5, t5_code_stats, t5_census_stats),
|
||||
let cac_tables: Vec<(asset_core::AssetNamespace, asset_game::CapturedStringTable)> = [
|
||||
(asset_core::AssetNamespace::Iw4, iw4_stats, iw4_census_stats),
|
||||
(asset_core::AssetNamespace::Iw5, iw5_stats, iw5_census_stats),
|
||||
(
|
||||
asset_core::AssetNamespace::T5,
|
||||
t5_code_stats,
|
||||
t5_census_stats,
|
||||
),
|
||||
]
|
||||
.into_iter()
|
||||
.flat_map(|(namespace, code, common)| {
|
||||
@@ -598,7 +630,7 @@ 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_fpv_hands(&fpv_meshes, &asset_model::BodyMeshCatalog::default());
|
||||
weapons.resolve_world_model_edges(&world_weapons);
|
||||
|
||||
common_report.push(format!(
|
||||
@@ -627,7 +659,6 @@ async fn prepare_common(key: CommonKey) -> Arc<CommonSet> {
|
||||
"common set: {key} prepared in {prepared_ms:.0}ms; donor images still decoding"
|
||||
);
|
||||
|
||||
let mut material_seed = material_seed;
|
||||
material_seed.mark_images_common_owned();
|
||||
let mut iw5_materials = iw5_materials;
|
||||
iw5_materials.mark_images_common_owned();
|
||||
@@ -635,6 +666,8 @@ async fn prepare_common(key: CommonKey) -> Arc<CommonSet> {
|
||||
id: NEXT_COMMON_PROFILE_ID.fetch_add(1, std::sync::atomic::Ordering::Relaxed),
|
||||
key,
|
||||
products: CommonProducts {
|
||||
scripts,
|
||||
t5_scene_models,
|
||||
material_seed,
|
||||
shared_surfaces,
|
||||
scene_models: common_scene_models,
|
||||
@@ -658,7 +691,10 @@ async fn prepare_common(key: CommonKey) -> Arc<CommonSet> {
|
||||
impact_fx: common_impact,
|
||||
t5_xanims,
|
||||
t5_fx,
|
||||
t5_impact_fx,
|
||||
iw5_materials,
|
||||
iw5_scene_models,
|
||||
iw5_shared_surfaces,
|
||||
strings,
|
||||
counts: CommonCounts {
|
||||
startup_count,
|
||||
@@ -718,24 +754,24 @@ async fn prepare_common(key: CommonKey) -> Arc<CommonSet> {
|
||||
set
|
||||
}
|
||||
|
||||
fn walk_iw5_stats_tables(
|
||||
donor: &Path,
|
||||
progress: &LoadProgress,
|
||||
) -> (Vec<crate::CapturedStringTable>, Vec<String>) {
|
||||
fn walk_iw5_startup(donor: &Path, progress: &LoadProgress) -> crate::lane::MaterialPopulation {
|
||||
let found = match find_zone_for_tree(donor, "code_post_gfx_mp") {
|
||||
Ok(found) => found,
|
||||
Err(error) => {
|
||||
return (
|
||||
Vec::new(),
|
||||
vec![format!("CAC iw5 code_post_gfx_mp: {error}")],
|
||||
);
|
||||
return crate::lane::MaterialPopulation {
|
||||
report: vec![format!("IW5 startup code_post_gfx_mp: {error}")],
|
||||
..Default::default()
|
||||
};
|
||||
}
|
||||
};
|
||||
let population = walk_population_file(&found.path, progress, MaterialCatalog::default());
|
||||
let line = format!(
|
||||
"CAC iw5: {} tables={}",
|
||||
let mut population = walk_population_file(&found.path, progress, MaterialCatalog::default());
|
||||
population.report.push(format!(
|
||||
"IW5 startup: {} tables={} materials={} images={} light_defs={}",
|
||||
found.path.display(),
|
||||
population.cac_tables.len()
|
||||
);
|
||||
(population.cac_tables, vec![line])
|
||||
population.cac_tables.len(),
|
||||
population.materials.materials.len(),
|
||||
population.materials.images.len(),
|
||||
population.light_defs.len()
|
||||
));
|
||||
population
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ pub fn load_match_material_seed(
|
||||
let root = games_root_from_env()?;
|
||||
let runtime = find_zone_file_version(&root, "common_mp", fastfile_iw4::ZONE_VERSION_PC)?;
|
||||
|
||||
let (catalog, mut report, _, _) = bevy::tasks::futures_lite::future::block_on(
|
||||
let (catalog, mut report, _, _, _) = bevy::tasks::futures_lite::future::block_on(
|
||||
walk_startup_material_zones(Some(&runtime.path), progress),
|
||||
);
|
||||
let catalog =
|
||||
@@ -153,7 +153,7 @@ fn leftover_walk_t5_startup_materials(
|
||||
progress: &LoadProgress,
|
||||
) -> (
|
||||
MaterialCatalog,
|
||||
Vec<crate::CapturedStringTable>,
|
||||
Vec<asset_game::CapturedStringTable>,
|
||||
Vec<String>,
|
||||
) {
|
||||
let mut report = Vec::new();
|
||||
@@ -164,7 +164,7 @@ fn leftover_walk_t5_startup_materials(
|
||||
return (MaterialCatalog::default(), Vec::new(), report);
|
||||
}
|
||||
};
|
||||
const ZONES: [&str; 2] = ["code_post_gfx_mp", "localized_code_post_gfx_mp"];
|
||||
const ZONES: [&str; 3] = ["code_post_gfx_mp", "localized_code_post_gfx_mp", "patch_mp"];
|
||||
let mut seed = MaterialCatalog::default();
|
||||
let mut stats = Vec::new();
|
||||
for zone in ZONES {
|
||||
@@ -333,9 +333,9 @@ fn capture_common_zone(
|
||||
}
|
||||
|
||||
pub(super) fn merge_image_batch(
|
||||
global: &mut crate::MaterialDefinitions,
|
||||
global: &mut asset_material::MaterialDefinitions,
|
||||
label: &str,
|
||||
batch: crate::material_images::DecodedImageBatch,
|
||||
batch: asset_material::material_images::DecodedImageBatch,
|
||||
job: load_jobs::Job,
|
||||
report: &mut Vec<String>,
|
||||
) {
|
||||
@@ -405,11 +405,19 @@ pub(super) fn merge_image_batch(
|
||||
/// there. The walk that discovers the demand hands both over itself.
|
||||
pub(super) struct PendingImages {
|
||||
pub(super) job: load_jobs::Job,
|
||||
pub(super) task: bevy::tasks::Task<(&'static str, crate::material_images::DecodedImageBatch)>,
|
||||
pub(super) task: bevy::tasks::Task<(
|
||||
&'static str,
|
||||
asset_material::material_images::DecodedImageBatch,
|
||||
)>,
|
||||
}
|
||||
|
||||
impl PendingImages {
|
||||
pub(super) async fn join(self) -> (&'static str, crate::material_images::DecodedImageBatch) {
|
||||
pub(super) async fn join(
|
||||
self,
|
||||
) -> (
|
||||
&'static str,
|
||||
asset_material::material_images::DecodedImageBatch,
|
||||
) {
|
||||
let done = self.task.await;
|
||||
self.job.joined();
|
||||
done
|
||||
@@ -484,7 +492,7 @@ impl HeldImagePlan {
|
||||
/// rather than an empty one that does.
|
||||
pub(super) fn prune_then_enqueue(
|
||||
mut self,
|
||||
catalog: &mut crate::MaterialDefinitions,
|
||||
catalog: &mut asset_material::MaterialDefinitions,
|
||||
progress: &LoadProgress,
|
||||
report: &mut Vec<String>,
|
||||
) -> Option<PendingImages> {
|
||||
@@ -539,7 +547,9 @@ pub(super) struct Iw5WeaponBundle {
|
||||
pub(super) xanims: XAnimBuild,
|
||||
|
||||
pub(super) materials: MaterialCatalog,
|
||||
pub(super) stats_tables: Vec<crate::CapturedStringTable>,
|
||||
pub(super) stats_tables: Vec<asset_game::CapturedStringTable>,
|
||||
pub(super) scene_models: asset_world::MapXModelSceneCatalog,
|
||||
pub(super) shared_surfaces: asset_model::SharedXModelSurfaces,
|
||||
}
|
||||
|
||||
pub(super) fn resolve_iw5_weapon_donor(
|
||||
@@ -606,6 +616,8 @@ pub(super) fn walk_iw5_weapon_bundle(
|
||||
xanims: census.xanims,
|
||||
materials: census.material_population,
|
||||
stats_tables: census.cac_tables,
|
||||
scene_models: census.scene_models,
|
||||
shared_surfaces: census.shared_surfaces,
|
||||
},
|
||||
pending_images,
|
||||
report,
|
||||
@@ -655,6 +667,7 @@ pub(super) fn walk_shared_iw5_common(
|
||||
census.world_weapons.len(),
|
||||
census.xanims.len(),
|
||||
));
|
||||
|
||||
(
|
||||
materials,
|
||||
Iw5WeaponBundle {
|
||||
@@ -664,6 +677,8 @@ pub(super) fn walk_shared_iw5_common(
|
||||
xanims: census.xanims,
|
||||
materials: MaterialCatalog::default(),
|
||||
stats_tables: census.cac_tables,
|
||||
scene_models: census.scene_models,
|
||||
shared_surfaces: census.shared_surfaces,
|
||||
},
|
||||
pending_images,
|
||||
report,
|
||||
@@ -674,9 +689,9 @@ pub(super) enum T5CommonPrep {
|
||||
Skip(Vec<String>),
|
||||
Ready {
|
||||
donor: PathBuf,
|
||||
opened: Result<std::sync::Arc<crate::ZoneImage>, String>,
|
||||
opened: Result<std::sync::Arc<asset_transport::ZoneImage>, String>,
|
||||
leftover_startup: MaterialCatalog,
|
||||
leftover_stats: Vec<crate::CapturedStringTable>,
|
||||
leftover_stats: Vec<asset_game::CapturedStringTable>,
|
||||
report: Vec<String>,
|
||||
},
|
||||
}
|
||||
@@ -729,12 +744,14 @@ pub(super) struct T5WeaponCommon {
|
||||
pub(super) material_seed: MaterialCatalog,
|
||||
pub(super) xanims: XAnimBuild,
|
||||
pub(super) fx: FxCatalog,
|
||||
pub(super) projectiles: crate::ProjectileMeshBuild,
|
||||
pub(super) teamsets: std::collections::HashMap<String, crate::MapTeamSettings>,
|
||||
pub(super) impact_fx: Option<asset_game::OwnedFxImpactTable>,
|
||||
pub(super) projectiles: asset_model::ProjectileMeshBuild,
|
||||
pub(super) teamsets: std::collections::HashMap<String, asset_game::MapTeamSettings>,
|
||||
pub(super) scene_models: asset_world::MapXModelSceneCatalog,
|
||||
pub(super) images: Option<PendingImages>,
|
||||
pub(super) stats_tables: (
|
||||
Vec<crate::CapturedStringTable>,
|
||||
Vec<crate::CapturedStringTable>,
|
||||
Vec<asset_game::CapturedStringTable>,
|
||||
Vec<asset_game::CapturedStringTable>,
|
||||
),
|
||||
pub(super) report: Vec<String>,
|
||||
}
|
||||
@@ -742,14 +759,16 @@ pub(super) struct T5WeaponCommon {
|
||||
impl T5WeaponCommon {
|
||||
pub(super) fn empty(material_seed: MaterialCatalog, report: Vec<String>) -> Self {
|
||||
Self {
|
||||
weapons: crate::WeaponBuild::default(),
|
||||
weapons: asset_game::WeaponBuild::default(),
|
||||
fpv: FpvMeshBuild::default(),
|
||||
world_guns: WorldWeaponBuild::default(),
|
||||
material_seed,
|
||||
xanims: XAnimBuild::default(),
|
||||
fx: FxCatalog::default(),
|
||||
projectiles: crate::ProjectileMeshBuild::default(),
|
||||
impact_fx: None,
|
||||
projectiles: asset_model::ProjectileMeshBuild::default(),
|
||||
teamsets: Default::default(),
|
||||
scene_models: Default::default(),
|
||||
images: None,
|
||||
stats_tables: (Vec::new(), Vec::new()),
|
||||
report,
|
||||
@@ -813,8 +832,10 @@ pub(super) fn walk_t5_weapon_common(
|
||||
material_seed: census.material_population,
|
||||
xanims: census.xanims,
|
||||
fx: census.fx,
|
||||
impact_fx: census.impact_fx,
|
||||
projectiles: census.projectile_meshes,
|
||||
teamsets: census.teamsets,
|
||||
scene_models: census.scene_models,
|
||||
images,
|
||||
stats_tables: (leftover_stats, census.cac_tables),
|
||||
report,
|
||||
@@ -827,8 +848,9 @@ pub(super) async fn walk_startup_material_zones(
|
||||
) -> (
|
||||
MaterialCatalog,
|
||||
Vec<String>,
|
||||
Vec<crate::CapturedStringTable>,
|
||||
Vec<crate::CapturedLightDef>,
|
||||
Vec<asset_game::CapturedStringTable>,
|
||||
Vec<asset_world::CapturedLightDef>,
|
||||
crate::ScriptSources,
|
||||
) {
|
||||
const STARTUP_ZONES: [&str; 3] = ["code_post_gfx_mp", "localized_code_post_gfx_mp", "patch_mp"];
|
||||
let Some(map_path) = map_path else {
|
||||
@@ -837,6 +859,7 @@ pub(super) async fn walk_startup_material_zones(
|
||||
Vec::new(),
|
||||
Vec::new(),
|
||||
Vec::new(),
|
||||
crate::ScriptSources::default(),
|
||||
);
|
||||
};
|
||||
let games = games_root_from_env().ok();
|
||||
@@ -872,6 +895,7 @@ pub(super) async fn walk_startup_material_zones(
|
||||
let mut zones_ok = 0usize;
|
||||
let mut stats = Vec::new();
|
||||
let mut light_defs = Vec::new();
|
||||
let mut scripts = crate::ScriptSources::default();
|
||||
for (zone, task) in STARTUP_ZONES.into_iter().zip(opened) {
|
||||
match task.await {
|
||||
Ok((path, image)) => {
|
||||
@@ -891,6 +915,7 @@ pub(super) async fn walk_startup_material_zones(
|
||||
));
|
||||
seed = pop.materials;
|
||||
light_defs.extend(pop.light_defs);
|
||||
scripts.overlay(pop.scripts);
|
||||
if zone == "code_post_gfx_mp" {
|
||||
stats = pop.cac_tables;
|
||||
}
|
||||
@@ -911,13 +936,13 @@ pub(super) async fn walk_startup_material_zones(
|
||||
seed.materials.len(),
|
||||
seed.images.len(),
|
||||
));
|
||||
(seed, report, stats, light_defs)
|
||||
(seed, report, stats, light_defs, scripts)
|
||||
}
|
||||
|
||||
pub(super) fn load_localized_strings_beside(
|
||||
zone_ff: &std::path::Path,
|
||||
report: &mut Vec<String>,
|
||||
stage: &crate::progress::StageHandle,
|
||||
stage: &asset_transport::progress::StageHandle,
|
||||
) -> LocalizeCatalog {
|
||||
let mut catalog = LocalizeCatalog::default();
|
||||
let root = match games_root_from_env() {
|
||||
@@ -927,19 +952,19 @@ pub(super) fn load_localized_strings_beside(
|
||||
return catalog;
|
||||
}
|
||||
};
|
||||
let lanes: [(crate::AssetNamespace, u32, &[&str]); 3] = [
|
||||
let lanes: [(asset_core::AssetNamespace, u32, &[&str]); 3] = [
|
||||
(
|
||||
crate::AssetNamespace::Iw4,
|
||||
asset_core::AssetNamespace::Iw4,
|
||||
fastfile_iw4::ZONE_VERSION_PC,
|
||||
MP_LOCALIZED_ZONES,
|
||||
),
|
||||
(
|
||||
crate::AssetNamespace::T5,
|
||||
asset_core::AssetNamespace::T5,
|
||||
fastfile_t5::ZONE_VERSION_PC,
|
||||
&["code_post_gfx_mp", "common_mp", "ui_mp"],
|
||||
),
|
||||
(
|
||||
crate::AssetNamespace::Iw5,
|
||||
asset_core::AssetNamespace::Iw5,
|
||||
fastfile_iw5::ZONE_VERSION_PC,
|
||||
MP_LOCALIZED_ZONES,
|
||||
),
|
||||
@@ -949,7 +974,7 @@ pub(super) fn load_localized_strings_beside(
|
||||
.and_then(|zone| zone.path.parent()?.file_name()?.to_str().map(str::to_owned));
|
||||
let mut plan = Vec::new();
|
||||
for (namespace, version, names) in lanes {
|
||||
let found: Vec<_> = if namespace == crate::AssetNamespace::T5 {
|
||||
let found: Vec<_> = if namespace == asset_core::AssetNamespace::T5 {
|
||||
match find_zone_file_version(&root, "common_mp", version).and_then(|zone| {
|
||||
asset_transport::discover::find_t5_localized_zones(
|
||||
&zone.path,
|
||||
@@ -966,7 +991,7 @@ pub(super) fn load_localized_strings_beside(
|
||||
names
|
||||
.iter()
|
||||
.filter_map(|name| {
|
||||
if namespace == crate::AssetNamespace::Iw4 {
|
||||
if namespace == asset_core::AssetNamespace::Iw4 {
|
||||
find_runtime_zone(&root, zone_ff, name)
|
||||
} else {
|
||||
find_zone_file_version(&root, name, version)
|
||||
|
||||
@@ -55,9 +55,11 @@ pub(super) async fn walk_prepared_match(
|
||||
|
||||
let cloning = std::time::Instant::now();
|
||||
let CommonProducts {
|
||||
scripts: iw4_scripts,
|
||||
mut t5_scene_models,
|
||||
material_seed,
|
||||
shared_surfaces,
|
||||
scene_models: common_scene_models,
|
||||
scene_models: mut common_scene_models,
|
||||
light_defs: common_light_defs,
|
||||
pen_table: common_pen_table,
|
||||
pen_table_loaded: common_pen_loaded,
|
||||
@@ -78,7 +80,10 @@ pub(super) async fn walk_prepared_match(
|
||||
impact_fx: common_impact,
|
||||
t5_xanims,
|
||||
t5_fx,
|
||||
t5_impact_fx,
|
||||
iw5_materials,
|
||||
mut iw5_scene_models,
|
||||
iw5_shared_surfaces,
|
||||
strings,
|
||||
counts:
|
||||
CommonCounts {
|
||||
@@ -125,6 +130,10 @@ pub(super) async fn walk_prepared_match(
|
||||
let (loaded, map_namespace) = match opened_map {
|
||||
Ok((path, image)) => {
|
||||
let game = image.game;
|
||||
let shared_surfaces = match game {
|
||||
asset_core::ZoneGame::Iw5 => iw5_shared_surfaces,
|
||||
_ => shared_surfaces,
|
||||
};
|
||||
(
|
||||
lane(game).load_world(
|
||||
&path,
|
||||
@@ -134,7 +143,7 @@ pub(super) async fn walk_prepared_match(
|
||||
material_seed,
|
||||
&mut common_film_visions,
|
||||
),
|
||||
Some(crate::AssetNamespace::from_zone_game(game)),
|
||||
Some(asset_core::AssetNamespace::from_zone_game(game)),
|
||||
)
|
||||
}
|
||||
Err(gap) => {
|
||||
@@ -152,6 +161,7 @@ pub(super) async fn walk_prepared_match(
|
||||
};
|
||||
|
||||
let LoadedWorld {
|
||||
scripts: map_scripts,
|
||||
mut world,
|
||||
mut materials,
|
||||
collision: clip,
|
||||
@@ -165,9 +175,6 @@ pub(super) async fn walk_prepared_match(
|
||||
mut report,
|
||||
gaps,
|
||||
} = loaded;
|
||||
world
|
||||
.map_xmodel_scene_assets
|
||||
.absorb_captured(common_scene_models);
|
||||
report.append(&mut common_report);
|
||||
|
||||
if facts.team_settings.allies.is_none() && facts.team_settings.axis.is_none() {
|
||||
@@ -187,6 +194,12 @@ pub(super) async fn walk_prepared_match(
|
||||
.defenders
|
||||
.or_else(|| facts.team_settings.defenders.clone());
|
||||
}
|
||||
if map_namespace == Some(asset_core::AssetNamespace::T5) {
|
||||
facts.objective_visuals = asset_game::ObjectiveVisuals::t5(
|
||||
facts.team_settings.allies_charset.as_deref(),
|
||||
facts.team_settings.axis_charset.as_deref(),
|
||||
);
|
||||
}
|
||||
match (
|
||||
facts.t5_teamset.as_deref(),
|
||||
facts.team_settings.allies.as_ref(),
|
||||
@@ -203,6 +216,23 @@ pub(super) async fn walk_prepared_match(
|
||||
_ => {}
|
||||
}
|
||||
|
||||
let map_scripts = match map_namespace {
|
||||
Some(asset_core::AssetNamespace::T5) => {
|
||||
let scripts = t5_map_under_iw4_rules(&map_scripts, &zone_name, &facts);
|
||||
report.push(format!(
|
||||
"map script: maps/mp/{zone_name} written from the T5 map's declarations; T5 map scripts left out"
|
||||
));
|
||||
scripts
|
||||
}
|
||||
_ => map_scripts,
|
||||
};
|
||||
let mut scripts = iw4_scripts;
|
||||
scripts.overlay(map_scripts);
|
||||
report.push(format!(
|
||||
"GSC source assets: {} (map overrides common_mp)",
|
||||
scripts.len()
|
||||
));
|
||||
|
||||
report.push(format!(
|
||||
"map teams: allies={:?} axis={:?} attackers={:?} defenders={:?}",
|
||||
facts.team_settings.allies_name,
|
||||
@@ -214,7 +244,7 @@ pub(super) async fn walk_prepared_match(
|
||||
report.push(format!(
|
||||
"s1 pool walked: common={s1_common_bytes} map={s1_map_bytes} total={} rss={}",
|
||||
s1_common_bytes.saturating_add(s1_map_bytes),
|
||||
crate::process_resident_bytes().unwrap_or(0),
|
||||
asset_transport::process_resident_bytes().unwrap_or(0),
|
||||
));
|
||||
|
||||
let common_xanim_count = xanims.len();
|
||||
@@ -353,9 +383,28 @@ pub(super) async fn walk_prepared_match(
|
||||
let map_new = pool_mat.saturating_sub(seed_mat);
|
||||
|
||||
let mut global = materials;
|
||||
let provisional_map_ids: Vec<usize> = (0..global.materials.len()).collect();
|
||||
|
||||
let iw5_linked = global.absorb_asset_population_host_materials_win(iw5_materials);
|
||||
let provisional_map_ids: Vec<usize> = (0..global.materials.len()).collect();
|
||||
if iw5_mat_n > 0 {
|
||||
iw5_scene_models.remap_walk_materials(&iw5_linked);
|
||||
}
|
||||
let mut scene_names = scripts.asset_names();
|
||||
scene_names.extend(facts.objective_visuals.model_names().map(str::to_owned));
|
||||
common_scene_models.retain_names(&scene_names);
|
||||
t5_scene_models.retain_names(&scene_names);
|
||||
iw5_scene_models.retain_names(&scene_names);
|
||||
match map_namespace {
|
||||
Some(asset_core::AssetNamespace::T5) => world
|
||||
.map_xmodel_scene_assets
|
||||
.absorb_captured(t5_scene_models),
|
||||
Some(asset_core::AssetNamespace::Iw5) => world
|
||||
.map_xmodel_scene_assets
|
||||
.absorb_captured(iw5_scene_models),
|
||||
_ => {}
|
||||
}
|
||||
world
|
||||
.map_xmodel_scene_assets
|
||||
.absorb_captured(common_scene_models);
|
||||
report.push(format!(
|
||||
"iw5 leftover materials: donor={iw5_mat_n} linked={} pool={}",
|
||||
iw5_linked.len(),
|
||||
@@ -464,7 +513,8 @@ pub(super) async fn walk_prepared_match(
|
||||
}
|
||||
if let Ok(path) = &zone_ff {
|
||||
let stage = progress.begin_scoped(StageId::Images, "merged", None);
|
||||
let decoded = crate::decode_material_color_maps(path, &mut global, &stage, load_pool());
|
||||
let decoded =
|
||||
asset_material::decode_material_color_maps(path, &mut global, &stage, load_pool());
|
||||
stage.finish_from(&decoded);
|
||||
match decoded {
|
||||
Ok(stats) => report.push(format!(
|
||||
@@ -475,15 +525,20 @@ pub(super) async fn walk_prepared_match(
|
||||
}
|
||||
}
|
||||
if let Some(draw) = world.draw.as_mut() {
|
||||
crate::resolve_primary_light_attenuation(draw, &global, &common_light_defs);
|
||||
asset_world::resolve_primary_light_attenuation(draw, &global, &common_light_defs);
|
||||
let dynamic_light_name =
|
||||
(map_namespace == Some(crate::AssetNamespace::Iw4)).then_some("light_dynamic");
|
||||
(map_namespace == Some(asset_core::AssetNamespace::Iw4)).then_some("light_dynamic");
|
||||
let dynamic_light = dynamic_light_name.and_then(|name| {
|
||||
crate::resolve_named_light_def(name, &draw.light_defs, &common_light_defs, &global)
|
||||
asset_world::resolve_named_light_def(
|
||||
name,
|
||||
&draw.light_defs,
|
||||
&common_light_defs,
|
||||
&global,
|
||||
)
|
||||
});
|
||||
let (ordinal, source) = crate::resolve_outdoor_image(
|
||||
let (ordinal, source) = asset_world::resolve_outdoor_image(
|
||||
draw.outdoor_image_name.as_deref(),
|
||||
map_namespace.unwrap_or(crate::AssetNamespace::Iw4),
|
||||
map_namespace.unwrap_or(asset_core::AssetNamespace::Iw4),
|
||||
&global,
|
||||
);
|
||||
draw.outdoor_image = ordinal;
|
||||
@@ -552,7 +607,7 @@ pub(super) async fn walk_prepared_match(
|
||||
requested.iter().map(|(index, _)| *index).collect();
|
||||
let want = want.len();
|
||||
let stage = progress.begin_scoped(StageId::Images, "attenuation", None);
|
||||
let decoded = crate::decode_catalog_images_from_iwd(
|
||||
let decoded = asset_material::decode_catalog_images_from_iwd(
|
||||
path,
|
||||
&mut global,
|
||||
requested,
|
||||
@@ -562,15 +617,20 @@ pub(super) async fn walk_prepared_match(
|
||||
stage.finish_from(&decoded);
|
||||
match decoded {
|
||||
Ok(n) => report.push(format!(
|
||||
"IWD light attenuation: decoded {n} of {want} GfxLightDef images (Image_LoadFromIwi; empty payload is not a host ramp)"
|
||||
"IWD light attenuation: decoded {n} of {want} GfxLightDef images (empty payload is not a host ramp)"
|
||||
)),
|
||||
Err(error) => report.push(format!("IWD light attenuation: {error}")),
|
||||
}
|
||||
}
|
||||
|
||||
crate::resolve_primary_light_attenuation(draw, &global, &common_light_defs);
|
||||
asset_world::resolve_primary_light_attenuation(draw, &global, &common_light_defs);
|
||||
let resolved_dynamic = dynamic_light_name.and_then(|name| {
|
||||
crate::resolve_named_light_def(name, &draw.light_defs, &common_light_defs, &global)
|
||||
asset_world::resolve_named_light_def(
|
||||
name,
|
||||
&draw.light_defs,
|
||||
&common_light_defs,
|
||||
&global,
|
||||
)
|
||||
});
|
||||
let dynamic_decoded = resolved_dynamic
|
||||
.and_then(|light| light.attenuation_image)
|
||||
@@ -596,7 +656,7 @@ pub(super) async fn walk_prepared_match(
|
||||
);
|
||||
}
|
||||
}
|
||||
let builtins = crate::decode_in_zone_builtin_images(&mut global);
|
||||
let builtins = asset_material::decode_in_zone_builtin_images(&mut global);
|
||||
if builtins != 0 {
|
||||
report.push(format!(
|
||||
"in-zone builtin images: decoded {builtins} leftover $ 2D loadDefs after absorb"
|
||||
@@ -604,7 +664,7 @@ pub(super) async fn walk_prepared_match(
|
||||
}
|
||||
if let Ok(path) = &zone_ff {
|
||||
let stage = progress.begin_scoped(StageId::Images, "tracers", None);
|
||||
let decoded = crate::material_images::decode_color_or_2d_for_keys(
|
||||
let decoded = asset_material::material_images::decode_color_or_2d_for_keys(
|
||||
path,
|
||||
&mut global,
|
||||
common_tracers.material_keys(),
|
||||
@@ -624,7 +684,7 @@ pub(super) async fn walk_prepared_match(
|
||||
.map(str::to_owned)
|
||||
.collect();
|
||||
for name in &unique {
|
||||
let bind = crate::fx_material_bind_name(name);
|
||||
let bind = asset_game::material_bind_name(name);
|
||||
let twins: Vec<&str> = global
|
||||
.materials
|
||||
.iter()
|
||||
@@ -731,12 +791,12 @@ pub(super) async fn walk_prepared_match(
|
||||
));
|
||||
report.push(format!(
|
||||
"Material stamp: iw4={} t5={} iw5={} (name-link identity; colliding T5 names are the later-zone row)",
|
||||
materials.namespace_count(crate::AssetNamespace::Iw4),
|
||||
materials.namespace_count(crate::AssetNamespace::T5),
|
||||
materials.namespace_count(crate::AssetNamespace::Iw5),
|
||||
materials.namespace_count(asset_core::AssetNamespace::Iw4),
|
||||
materials.namespace_count(asset_core::AssetNamespace::T5),
|
||||
materials.namespace_count(asset_core::AssetNamespace::Iw5),
|
||||
));
|
||||
report.push(format!(
|
||||
"fx elem material edges after absorb: {} bound ({} unique), {} unresolved (temp={}, catalog_miss={}), {} absent of {} Material* visuals (FxElemDef+0xbc); {} decal mark arms ({} Bound slots, {} unresolved, {} temp, {} array-unpatched)",
|
||||
"fx elem material edges after absorb: {} bound ({} unique), {} unresolved (temp={}, catalog_miss={}), {} absent of {} material visuals; {} decal mark arms ({} Bound slots, {} unresolved, {} temp, {} array-unpatched)",
|
||||
world.fx.material_visual_bound_count(),
|
||||
world.fx.material_visual_unique_bound_count(),
|
||||
world.fx.material_visual_unresolved_count(),
|
||||
@@ -788,15 +848,15 @@ pub(super) async fn walk_prepared_match(
|
||||
.into(),
|
||||
);
|
||||
{
|
||||
let missing: Vec<crate::MaterialKey> = world
|
||||
let missing: Vec<asset_core::MaterialKey> = world
|
||||
.fx
|
||||
.unique_bound_hints()
|
||||
.into_iter()
|
||||
.chain(world.fx.unique_decal_mark_hints())
|
||||
.filter(|(index, _)| !crate::fx_color_decoded_in_catalog(&global, *index))
|
||||
.filter(|(index, _)| !asset_game::color_decoded_in_catalog(&global, *index))
|
||||
.filter_map(|(index, _)| {
|
||||
let material = global.materials.get(index)?;
|
||||
Some(crate::MaterialKey {
|
||||
Some(asset_core::MaterialKey {
|
||||
namespace: material.namespace,
|
||||
name: material.name.as_str().to_owned(),
|
||||
})
|
||||
@@ -805,7 +865,7 @@ pub(super) async fn walk_prepared_match(
|
||||
if !missing.is_empty() {
|
||||
if let Ok(path) = &zone_ff {
|
||||
let stage = progress.begin_scoped(StageId::Images, "fx_elem", None);
|
||||
let decoded = crate::material_images::decode_color_or_2d_for_keys(
|
||||
let decoded = asset_material::material_images::decode_color_or_2d_for_keys(
|
||||
path,
|
||||
&mut global,
|
||||
missing,
|
||||
@@ -825,7 +885,7 @@ pub(super) async fn walk_prepared_match(
|
||||
.fx
|
||||
.unique_bound_hints()
|
||||
.into_iter()
|
||||
.filter(|(index, _)| !crate::fx_color_decoded_in_catalog(&global, *index))
|
||||
.filter(|(index, _)| !asset_game::color_decoded_in_catalog(&global, *index))
|
||||
.map(|(index, hint)| (index, hint.to_owned()))
|
||||
.collect();
|
||||
if !nocolor.is_empty() {
|
||||
@@ -872,7 +932,11 @@ pub(super) async fn walk_prepared_match(
|
||||
}
|
||||
}
|
||||
if world.impact_fx.is_none() {
|
||||
world.impact_fx = common_impact;
|
||||
world.impact_fx = if map_namespace == Some(asset_core::AssetNamespace::T5) {
|
||||
t5_impact_fx.or(common_impact)
|
||||
} else {
|
||||
common_impact
|
||||
};
|
||||
} else if let Some(common_table) = common_impact {
|
||||
report.push(format!(
|
||||
"impactfx: map table kept; common_mp table `{}` discarded",
|
||||
@@ -881,7 +945,8 @@ pub(super) async fn walk_prepared_match(
|
||||
}
|
||||
if let Some(ref table) = world.impact_fx {
|
||||
report.push(format!(
|
||||
"impactfx handoff: `{}` rows={} (fx catalog now {} defs)",
|
||||
"impactfx handoff: {:?}:`{}` rows={} (fx catalog now {} defs)",
|
||||
table.namespace(),
|
||||
table.name,
|
||||
table.row_count(),
|
||||
world.fx.len()
|
||||
@@ -890,7 +955,7 @@ pub(super) async fn walk_prepared_match(
|
||||
report.push("impactfx handoff: missing — combat play_oriented will miss cells".into());
|
||||
}
|
||||
report.push(format!(
|
||||
"tracer catalog handoff: {} named ({} bound, {} unresolved) (CG_SpawnTracer)",
|
||||
"tracer catalog handoff: {} named ({} bound, {} unresolved)",
|
||||
common_tracers.len(),
|
||||
common_tracers.bound_count(),
|
||||
common_tracers.unresolved_count()
|
||||
@@ -916,15 +981,15 @@ pub(super) async fn walk_prepared_match(
|
||||
}
|
||||
|
||||
report.extend(localize_report);
|
||||
let (directory_ms, opens, inflate_ms) = crate::iwd_read_cost();
|
||||
let (directory_ms, opens, inflate_ms) = asset_material::iwd_read_cost();
|
||||
report.push(format!(
|
||||
"IWD read cost: central_dir={directory_ms:.0}ms over {opens} opens, inflate={inflate_ms:.0}ms (summed over worker threads, not wall)"
|
||||
));
|
||||
let (mip_hit, mip_miss, mip_io_ms) = crate::mip_cache_cost();
|
||||
let (mip_hit, mip_miss, mip_io_ms) = asset_material::mip_cache_cost();
|
||||
report.push(format!(
|
||||
"mip cache: hit={mip_hit} miss={mip_miss} io={mip_io_ms:.0}ms"
|
||||
));
|
||||
let (payload_reads, header_reads) = crate::iwd_entry_reads();
|
||||
let (payload_reads, header_reads) = asset_material::iwd_entry_reads();
|
||||
report.push(format!(
|
||||
"IWD entry reads: payload={payload_reads} header-only={header_reads} (a header answers whether an image is a cubemap; a payload read is the whole entry inflated)"
|
||||
));
|
||||
@@ -958,19 +1023,19 @@ pub(super) async fn walk_prepared_match(
|
||||
.defs()
|
||||
.filter_map(|def| def.material.bound_index()),
|
||||
);
|
||||
let mut set = crate::material_images::census_image_working_set(
|
||||
let mut set = asset_material::material_images::census_image_working_set(
|
||||
&global,
|
||||
world_mats,
|
||||
smodel_mats,
|
||||
fpv_mats,
|
||||
fx_mats,
|
||||
);
|
||||
let (probe_n, probe_bytes) = crate::material_images::cpu_image_census(
|
||||
let (probe_n, probe_bytes) = asset_material::material_images::cpu_image_census(
|
||||
world.reflection_probe_images.iter().flatten(),
|
||||
);
|
||||
let (lightmap_n, lightmap_bytes) = match &draw.lightmap {
|
||||
Ok(pages) => {
|
||||
crate::material_images::cpu_image_census(pages.iter().flatten().flat_map(|page| {
|
||||
Ok(pages) => asset_material::material_images::cpu_image_census(
|
||||
pages.iter().flatten().flat_map(|page| {
|
||||
[
|
||||
page.primary_image.as_ref(),
|
||||
page.secondary_image.as_ref(),
|
||||
@@ -980,15 +1045,15 @@ pub(super) async fn walk_prepared_match(
|
||||
]
|
||||
.into_iter()
|
||||
.flatten()
|
||||
}))
|
||||
}
|
||||
}),
|
||||
),
|
||||
Err(_) => (0, 0),
|
||||
};
|
||||
set.probe_n = probe_n;
|
||||
set.probe_bytes = probe_bytes;
|
||||
set.lightmap_n = lightmap_n;
|
||||
set.lightmap_bytes = lightmap_bytes;
|
||||
crate::material_images::store_image_working_set(set);
|
||||
asset_material::material_images::store_image_working_set(set);
|
||||
report.push(format!(
|
||||
"image working set: decoded={} ({:.1}MiB) world-batch={} ({:.1}MiB) smodel={} ({:.1}MiB) fpv={} ({:.1}MiB) probe={} ({:.1}MiB) lightmap={} ({:.1}MiB) fx={} ({:.1}MiB); subsets, not a skipped load",
|
||||
set.decoded_n,
|
||||
@@ -1008,7 +1073,7 @@ pub(super) async fn walk_prepared_match(
|
||||
));
|
||||
}
|
||||
let mut fx = std::mem::take(&mut world.fx).publish();
|
||||
fx.set_map_namespace(map_namespace.unwrap_or(crate::AssetNamespace::Iw4));
|
||||
fx.set_map_namespace(map_namespace.unwrap_or(asset_core::AssetNamespace::Iw4));
|
||||
let xanims = xanims.publish();
|
||||
let destructible_death =
|
||||
crate::stamp_match_destructible_death(&xanims, &world.map_xmodel_scene_assets);
|
||||
@@ -1021,6 +1086,7 @@ pub(super) async fn walk_prepared_match(
|
||||
));
|
||||
}
|
||||
let prepared = PreparedMatch {
|
||||
scripts,
|
||||
fx,
|
||||
world,
|
||||
materials: crate::MatchMaterials {
|
||||
@@ -1050,3 +1116,38 @@ pub(super) async fn walk_prepared_match(
|
||||
};
|
||||
(MatchLoadOutcome::Ready(prepared), Some(common))
|
||||
}
|
||||
|
||||
fn t5_map_under_iw4_rules(
|
||||
map_scripts: &crate::ScriptSources,
|
||||
zone_name: &str,
|
||||
facts: &crate::MapFacts,
|
||||
) -> crate::ScriptSources {
|
||||
let module = format!("maps/mp/{zone_name}");
|
||||
let main = map_scripts
|
||||
.read(&module)
|
||||
.ok()
|
||||
.and_then(|bytes| String::from_utf8(bytes).ok())
|
||||
.unwrap_or_default();
|
||||
let mut declarations = asset_world::MapDeclarations::from_t5_map_main(&main);
|
||||
declarations.set_teams(
|
||||
facts.script_sound.attackers.as_deref().unwrap_or("allies"),
|
||||
facts.script_sound.defenders.as_deref().unwrap_or("axis"),
|
||||
);
|
||||
let entities = map_scripts
|
||||
.entities()
|
||||
.map(asset_world::t5_entities_for_iw4_rules)
|
||||
.unwrap_or_default();
|
||||
let mut scripts = crate::ScriptSources::default();
|
||||
let mut map_main = declarations.map_script(&entities);
|
||||
if zone_name == "mp_radiation" {
|
||||
let end = map_main.rfind('}').expect("generated map main");
|
||||
map_main.insert_str(end, "\tthread iw4l_maps\\radiation::main();\n");
|
||||
scripts.insert_source(
|
||||
"iw4l_maps/radiation",
|
||||
crate::map_scripts::RADIATION.to_owned(),
|
||||
);
|
||||
}
|
||||
scripts.insert_source(&module, map_main);
|
||||
scripts.set_entities(entities);
|
||||
scripts
|
||||
}
|
||||
|
||||
@@ -4,20 +4,27 @@ use std::sync::Arc;
|
||||
use bevy::tasks::{TaskPool, TaskPoolBuilder};
|
||||
|
||||
use crate::{
|
||||
BodyMeshCatalog, ClipCollision, FpvMeshBuild, FpvMeshCatalog, FxCatalog, IntermissionView,
|
||||
LocalizeCatalog, MP_LOCALIZED_ZONES, MaterialCatalog, PreparedGaps, PreparedMap,
|
||||
ProjectileMeshBuild, WeaponBuild, WeaponRegistry, WorldDraw, WorldWeaponBuild,
|
||||
WorldWeaponCatalog, XAnimBuild, XAnimCatalog, find_common_mp_for_envelope,
|
||||
find_common_mp_for_zone, find_runtime_common_mp, find_runtime_zone, find_zone_file_version,
|
||||
find_zone_for_tree, games_root_from_env,
|
||||
PreparedGaps, PreparedMap,
|
||||
lane::{LoadedWorld, lane},
|
||||
lane_capability::PreparedCapability,
|
||||
};
|
||||
use asset_anim::{XAnimBuild, XAnimCatalog};
|
||||
use asset_game::{
|
||||
FxCatalog, LocalizeCatalog, MP_LOCALIZED_ZONES, WeaponBuild, WeaponRegistry,
|
||||
load_localize_catalog_in_lane,
|
||||
material_images::ImageDemandPlan,
|
||||
open_zone_shared, peek_zone_version,
|
||||
progress::{LoadProgress, StageHandle, StageId},
|
||||
};
|
||||
use asset_material::{ImageDemandPlan, MaterialCatalog};
|
||||
use asset_model::{
|
||||
BodyMeshCatalog, FpvMeshBuild, FpvMeshCatalog, ProjectileMeshBuild, WorldWeaponBuild,
|
||||
WorldWeaponCatalog,
|
||||
};
|
||||
use asset_transport::load_jobs::{self, JobKind};
|
||||
use asset_transport::{
|
||||
LoadProgress, StageHandle, StageId, find_common_mp_for_envelope, find_common_mp_for_zone,
|
||||
find_runtime_common_mp, find_runtime_zone, find_zone_file_version, find_zone_for_tree,
|
||||
games_root_from_env, open_zone_shared, peek_zone_version,
|
||||
};
|
||||
use asset_world::{ClipCollision, IntermissionView, WorldDraw};
|
||||
|
||||
pub use asset_world::WorldDrawPolicy;
|
||||
|
||||
@@ -86,44 +93,44 @@ fn set_thread_cpus(_cpus: &[usize]) {}
|
||||
#[derive(Default, Clone)]
|
||||
pub struct PreparedWorld {
|
||||
pub draw: Option<WorldDraw>,
|
||||
pub dynamic_light: Option<crate::ResolvedLightDef>,
|
||||
pub static_model_meshes: Vec<crate::ModelMesh>,
|
||||
pub dynamic_light: Option<asset_world::ResolvedLightDef>,
|
||||
pub static_model_meshes: Vec<asset_world::ModelMesh>,
|
||||
|
||||
pub static_model_instances: Vec<Option<crate::StaticModelPlacement>>,
|
||||
pub static_model_instances: Vec<Option<asset_world::StaticModelPlacement>>,
|
||||
|
||||
pub map_xmodel_scene_assets: crate::MapXModelSceneCatalog,
|
||||
pub map_xmodel_scene_assets: asset_world::MapXModelSceneCatalog,
|
||||
|
||||
pub script_model_instances: Vec<crate::ScriptModelSceneInstance>,
|
||||
pub script_model_instances: Vec<asset_world::ScriptModelSceneInstance>,
|
||||
|
||||
pub script_brush_models: Vec<crate::ScriptBrushModelPlacement>,
|
||||
pub script_brush_models: Vec<asset_world::ScriptBrushModelPlacement>,
|
||||
|
||||
pub map_use_triggers: Vec<crate::MapUseTrigger>,
|
||||
pub flag_descriptors: Vec<asset_world::FlagDescriptor>,
|
||||
|
||||
pub flag_descriptors: Vec<crate::FlagDescriptor>,
|
||||
pub script_structs: Vec<asset_world::MapScriptStruct>,
|
||||
|
||||
pub script_structs: Vec<crate::MapScriptStruct>,
|
||||
pub dyn_ents: asset_world::DynEntCatalog,
|
||||
|
||||
pub dyn_ents: crate::DynEntCatalog,
|
||||
pub smodel_lighting_samples: Vec<asset_model::SmodelLightingSample>,
|
||||
|
||||
pub smodel_lighting_samples: Vec<crate::SmodelLightingSample>,
|
||||
pub light_grid: Option<asset_model::OwnedLightGrid>,
|
||||
|
||||
pub light_grid: Option<crate::OwnedLightGrid>,
|
||||
pub fx: asset_game::FxCatalog,
|
||||
|
||||
pub fx: crate::FxCatalog,
|
||||
pub fx_models: asset_game::FxModelCatalog,
|
||||
|
||||
pub fx_models: crate::FxModelCatalog,
|
||||
pub fx_glass: Option<asset_world::FxGlassReset>,
|
||||
|
||||
pub fx_glass: Option<crate::FxGlassReset>,
|
||||
|
||||
pub impact_fx: Option<crate::OwnedFxImpactTable>,
|
||||
pub impact_fx: Option<asset_game::OwnedFxImpactTable>,
|
||||
pub reflection_probe_images: Vec<Option<bevy::prelude::Image>>,
|
||||
pub intermission_view: Option<IntermissionView>,
|
||||
|
||||
pub exp_fog: Option<crate::ExpFog>,
|
||||
pub exp_fog: Option<asset_world::ExpFog>,
|
||||
|
||||
pub film_vision: Option<crate::FilmVision>,
|
||||
pub film_visions:
|
||||
std::collections::BTreeMap<String, Result<crate::FilmVision, crate::FilmVisionParseError>>,
|
||||
pub film_vision: Option<asset_world::FilmVision>,
|
||||
pub film_visions: std::collections::BTreeMap<
|
||||
String,
|
||||
Result<asset_world::FilmVision, asset_world::FilmVisionParseError>,
|
||||
>,
|
||||
|
||||
pub createart_name: Option<String>,
|
||||
pub min: [f32; 3],
|
||||
@@ -135,9 +142,10 @@ pub struct PreparedWorld {
|
||||
|
||||
#[derive(Default, Clone)]
|
||||
pub struct PreparedMatch {
|
||||
pub scripts: crate::ScriptSources,
|
||||
pub world: PreparedWorld,
|
||||
|
||||
pub fx: crate::FxDefinitions,
|
||||
pub fx: asset_game::FxDefinitions,
|
||||
pub materials: crate::MatchMaterials,
|
||||
pub clip: Option<Arc<ClipCollision>>,
|
||||
pub weapons: Arc<WeaponRegistry>,
|
||||
@@ -145,12 +153,12 @@ pub struct PreparedMatch {
|
||||
pub bodies: Arc<BodyMeshCatalog>,
|
||||
pub world_weapons: WorldWeaponCatalog,
|
||||
|
||||
pub projectile_meshes: crate::ProjectileMeshCatalog,
|
||||
pub projectile_meshes: asset_model::ProjectileMeshCatalog,
|
||||
pub xanims: XAnimCatalog,
|
||||
pub destructible_death: Vec<crate::DestructibleDeathRow>,
|
||||
pub player_anim_sources: crate::PlayerAnimSources,
|
||||
pub player_anim_sources: asset_anim::PlayerAnimSources,
|
||||
|
||||
pub tracers: crate::TracerDefinitions,
|
||||
pub tracers: asset_game::TracerDefinitions,
|
||||
|
||||
pub strings: LocalizeCatalog,
|
||||
pub report: Vec<String>,
|
||||
|
||||
@@ -2,12 +2,15 @@ use bevy::prelude::*;
|
||||
use frame::{ClientSet, MatchTornDown, ReturnedToMenu, SessionSwapApplied};
|
||||
|
||||
use crate::{
|
||||
AssetRefDumpCensus, MapXModelSceneCatalog, MatchType10SoundHints, PlayerAnimSources,
|
||||
PreparedBodies, PreparedBodyClips, PreparedDestructibleDeath, PreparedFpvMeshes,
|
||||
PreparedLocalizedStrings, PreparedProjectileMeshes, PreparedWeapons, PreparedWorldWeapons,
|
||||
PreparedXAnims, PreparedXModelWalkCensus, SessionCompass, SessionMapScriptSound,
|
||||
SessionTeamSettings,
|
||||
MatchType10SoundHints, PreparedBodies, PreparedBodyClips, PreparedDestructibleDeath,
|
||||
PreparedFpvMeshes, PreparedLocalizedStrings, PreparedProjectileMeshes, PreparedWeapons,
|
||||
PreparedWorldWeapons, PreparedXAnims, PreparedXModelWalkCensus, SessionCompass,
|
||||
};
|
||||
use asset_anim::PlayerAnimSources;
|
||||
use asset_audio::SessionMapScriptSound;
|
||||
use asset_game::SessionTeamSettings;
|
||||
use asset_material::AssetRefDumpCensus;
|
||||
use asset_world::MapXModelSceneCatalog;
|
||||
|
||||
pub(crate) fn drop_match_catalogs_on_teardown(
|
||||
mut torn: MessageReader<MatchTornDown>,
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
pub use asset_transport::{
|
||||
Iw5ZoneMemory, T5ZoneMemory, ZoneImage, ZoneMemory, ZoneOpenError, open_zone, open_zone_shared,
|
||||
parse_zone_image, xfile_arena_row,
|
||||
};
|
||||
@@ -8,16 +8,20 @@ description = "Sound aliases and mixing."
|
||||
[dependencies]
|
||||
diag.workspace = true
|
||||
bevy.workspace = true
|
||||
asset_audio.workspace = true
|
||||
asset_core.workspace = true
|
||||
asset_game.workspace = true
|
||||
asset_transport.workspace = true
|
||||
assets.workspace = true
|
||||
asset_iw4.workspace = true
|
||||
weapon_iw4.workspace = true
|
||||
movement_iw4.workspace = true
|
||||
frame.workspace = true
|
||||
entity_iw4.workspace = true
|
||||
net.workspace = true
|
||||
sim.workspace = true
|
||||
hud_iw4 = { path = "../hud_iw4" }
|
||||
playerstate_iw4.workspace = true
|
||||
sound_iw4.workspace = true
|
||||
killcam_iw4.workspace = true
|
||||
gamemode_iw4.workspace = true
|
||||
perf.workspace = true
|
||||
|
||||
@@ -195,8 +195,8 @@ fn build_bank() -> SurfaceBank {
|
||||
names.insert(gear_rattle_alias(gait, local));
|
||||
}
|
||||
}
|
||||
names.insert(mantle_gear_alias(true));
|
||||
names.insert(mantle_gear_alias(false));
|
||||
names.insert(gear_alias(true));
|
||||
names.insert(gear_alias(false));
|
||||
let mut all_names: Vec<&'static str> = names.into_iter().collect();
|
||||
all_names.sort_unstable();
|
||||
SurfaceBank {
|
||||
@@ -282,7 +282,7 @@ pub fn gear_rattle_alias(gait: StepGait, local_player: bool) -> &'static str {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn mantle_gear_alias(local_player: bool) -> &'static str {
|
||||
pub fn gear_alias(local_player: bool) -> &'static str {
|
||||
if local_player {
|
||||
"gear_rattle_plr_mantle"
|
||||
} else {
|
||||
|
||||
+15
-13
@@ -1,11 +1,13 @@
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
use asset_iw4::snd_attenuate;
|
||||
use assets::{
|
||||
AssetNamespace, GamesRoot, LoadedSoundBank, NamespaceSoundIwd, NamespaceTrees, SoundCatalog,
|
||||
compose_sound_bank, gather_sound_sources, namespace_for_zone,
|
||||
use asset_iw4::attenuate;
|
||||
use asset_core::AssetNamespace;
|
||||
use asset_audio::{
|
||||
LoadedSoundBank, SoundCatalog, compose_sound_bank, gather_sound_sources, namespace_for_zone,
|
||||
};
|
||||
use asset_transport::GamesRoot;
|
||||
use assets::{NamespaceSoundIwd, NamespaceTrees};
|
||||
use bevy::{
|
||||
audio::{AudioSink, AudioSinkPlayback, Volume},
|
||||
prelude::*,
|
||||
@@ -121,7 +123,7 @@ pub(crate) fn stop_map_ambient_on_match_end(
|
||||
epoch.bump();
|
||||
stop_map_ambient(&mut commands, &ambient);
|
||||
booted.0 = false;
|
||||
commands.remove_resource::<assets::CreateFxOneshotEmitters>();
|
||||
commands.remove_resource::<asset_audio::CreateFxOneshotEmitters>();
|
||||
|
||||
let compose_is_for_the_incoming_map = !left_session
|
||||
&& compose
|
||||
@@ -148,7 +150,7 @@ pub(crate) fn stop_map_ambient_on_match_end(
|
||||
});
|
||||
});
|
||||
perf::ambient_hold(i64::from(booted.0));
|
||||
diag::info!(Audio, "audio: map ambient stopped (SND_StopAmbient)");
|
||||
diag::info!(Audio, "audio: map ambient stopped");
|
||||
}
|
||||
|
||||
pub(crate) fn start_sound_bank_compose(
|
||||
@@ -193,7 +195,7 @@ pub(crate) fn start_sound_bank_compose(
|
||||
let pool = AsyncComputeTaskPool::get_or_init(TaskPool::default);
|
||||
IwdOpen::Opening(pool.spawn(async move {
|
||||
let mut trees = NamespaceTrees::discover(&games);
|
||||
if let Ok(zone) = assets::find_zone_file(&games, &opened_zone) {
|
||||
if let Ok(zone) = asset_transport::find_zone_file(&games, &opened_zone) {
|
||||
trees.adopt_zone(&zone.path);
|
||||
}
|
||||
NamespaceSoundIwd::open(&trees)
|
||||
@@ -296,7 +298,7 @@ pub(crate) fn install_sound_bank(
|
||||
};
|
||||
};
|
||||
let namespace = namespace_for_zone(&games, &zone);
|
||||
let loaded = assets::find_zone_file(&games, &zone).map(|found| {
|
||||
let loaded = asset_transport::find_zone_file(&games, &zone).map(|found| {
|
||||
let sources = gather_sound_sources(&games, &found.path);
|
||||
let LoadedSoundBank { catalog, gaps, .. } =
|
||||
compose_sound_bank(sources, &zone, namespace, map);
|
||||
@@ -415,7 +417,7 @@ pub(crate) fn boot_map_ambient_once(
|
||||
loading: Option<Res<assets::LoadingScreen>>,
|
||||
bank: Option<Res<SoundBank>>,
|
||||
identity: Option<Res<frame::LaunchIdentity>>,
|
||||
script_sound: Option<Res<assets::SessionMapScriptSound>>,
|
||||
script_sound: Option<Res<asset_audio::SessionMapScriptSound>>,
|
||||
namespace: Option<Res<SoundBankNamespace>>,
|
||||
epoch: Res<MatchEpoch>,
|
||||
clips: Option<Res<crate::ClipStore>>,
|
||||
@@ -587,11 +589,11 @@ fn start_map_ambient_prepared(
|
||||
|
||||
let oneshots = bank.createfx_oneshots(map_ns, map_name);
|
||||
let oneshot_count = oneshots.len();
|
||||
commands.insert_resource(assets::CreateFxOneshotEmitters(oneshots));
|
||||
commands.insert_resource(asset_audio::CreateFxOneshotEmitters(oneshots));
|
||||
if oneshot_count > 0 {
|
||||
diag::info!(
|
||||
Audio,
|
||||
"audio: createfx oneshots {oneshot_count} parsed for {map_name} (host markers; no FX_Register play)"
|
||||
"audio: createfx oneshots {oneshot_count} parsed for {map_name} (host markers)"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -601,7 +603,7 @@ pub(crate) fn emitter_gain(emitter: &MapEmitter, ear_inches: [f32; 3]) -> f32 {
|
||||
if emitter.knots.is_empty() {
|
||||
return 0.0;
|
||||
}
|
||||
let atten = snd_attenuate(&emitter.knots, dist, emitter.dist_min, emitter.dist_max);
|
||||
let atten = attenuate(&emitter.knots, dist, emitter.dist_min, emitter.dist_max);
|
||||
if atten < 0.0 {
|
||||
0.0
|
||||
} else {
|
||||
@@ -625,7 +627,7 @@ pub fn update_map_emitter_gain(
|
||||
return;
|
||||
}
|
||||
if n > 1 {
|
||||
panic!("second listener / amp maxRadius gate not ported");
|
||||
panic!("more than one ambient listener");
|
||||
}
|
||||
let Some(listener) = listeners.iter().next() else {
|
||||
return;
|
||||
|
||||
@@ -5,7 +5,9 @@ use std::sync::mpsc::{Sender, channel};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::Instant;
|
||||
|
||||
use assets::{AssetNamespace, NamespaceSoundIwd, SoundCatalog};
|
||||
use asset_core::AssetNamespace;
|
||||
use asset_audio::SoundCatalog;
|
||||
use assets::NamespaceSoundIwd;
|
||||
use bevy::prelude::*;
|
||||
|
||||
use crate::pcm::{PcmAudio, decode_audio_bytes};
|
||||
@@ -731,7 +733,7 @@ fn prepare_jobs(
|
||||
) -> Vec<Result<PreparedPcm, ClipError>> {
|
||||
let mut out: Vec<Option<Result<PreparedPcm, ClipError>>> = vec![None; jobs.len()];
|
||||
|
||||
let asked: Vec<(usize, &assets::LoadedSoundPcm)> = jobs
|
||||
let asked: Vec<(usize, &asset_audio::LoadedSoundPcm)> = jobs
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(i, job)| match job.key {
|
||||
@@ -743,9 +745,9 @@ fn prepare_jobs(
|
||||
})
|
||||
.collect();
|
||||
if !asked.is_empty() {
|
||||
let clips: Vec<assets::XwmaClip<'_>> = asked
|
||||
let clips: Vec<asset_audio::XwmaClip<'_>> = asked
|
||||
.iter()
|
||||
.map(|(_, sound)| assets::XwmaClip {
|
||||
.map(|(_, sound)| asset_audio::XwmaClip {
|
||||
packets: sound.encoded_bytes(),
|
||||
seek_table: &sound.seek_table,
|
||||
channels: sound.channels().max(0) as u32,
|
||||
@@ -753,7 +755,7 @@ fn prepare_jobs(
|
||||
})
|
||||
.collect();
|
||||
let decode_at = Instant::now();
|
||||
let decoded = assets::decode_t5_xwma_batch(&clips);
|
||||
let decoded = asset_audio::decode_t5_xwma_batch(&clips);
|
||||
note_wall(ClipPath::Xwma, decode_at.elapsed());
|
||||
for ((i, sound), pcm) in asked.into_iter().zip(decoded) {
|
||||
let result = pcm
|
||||
@@ -803,19 +805,19 @@ fn prepare_clip_now(
|
||||
/// Which decoder `prepare_loaded` will reach for, decided the same way it
|
||||
/// decides — the two read the same fields in the same order, so a clip cannot
|
||||
/// be counted under one path and decoded by another.
|
||||
fn loaded_path(sound: &assets::LoadedSoundPcm) -> Option<ClipPath> {
|
||||
fn loaded_path(sound: &asset_audio::LoadedSoundPcm) -> Option<ClipPath> {
|
||||
if sound.t5_adpcm_bytes().is_some() {
|
||||
Some(ClipPath::Adpcm)
|
||||
} else if sound.is_t5_xwma() {
|
||||
Some(ClipPath::Xwma)
|
||||
} else if sound.format() == assets::MSS_PCM {
|
||||
} else if sound.format() == asset_audio::MSS_PCM {
|
||||
Some(ClipPath::Pcm)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn prepare_loaded(sound: &assets::LoadedSoundPcm) -> Result<PreparedPcm, ClipError> {
|
||||
fn prepare_loaded(sound: &asset_audio::LoadedSoundPcm) -> Result<PreparedPcm, ClipError> {
|
||||
if let Some(bytes) = sound.t5_adpcm_bytes() {
|
||||
let channels = u16::try_from(sound.channels().max(1)).map_err(|_| ClipError::Decode)?;
|
||||
let pcm = crate::pcm::t5_stream::decode_adpcm(
|
||||
@@ -833,7 +835,7 @@ fn prepare_loaded(sound: &assets::LoadedSoundPcm) -> Result<PreparedPcm, ClipErr
|
||||
}
|
||||
let decoded;
|
||||
let (bits, bytes) = if sound.is_t5_xwma() {
|
||||
decoded = assets::decode_t5_xwma(
|
||||
decoded = asset_audio::decode_t5_xwma(
|
||||
sound.encoded_bytes(),
|
||||
&sound.seek_table,
|
||||
sound.channels().max(0) as u32,
|
||||
|
||||
@@ -7,7 +7,7 @@ use crate::ambient::{MapAmbient, MapEmitter, SoundBankNamespace};
|
||||
use crate::clip_store::{ClipStore, clip_keys_for_alias};
|
||||
use crate::pcm::PcmAudio;
|
||||
use crate::playback::{MissingAliasGaps, SharedPlayAssets, SoundBank};
|
||||
use assets::AssetNamespace;
|
||||
use asset_core::AssetNamespace;
|
||||
use bevy::prelude::*;
|
||||
use net::PresentedSnapshot;
|
||||
use sim::DestructibleLoopSound;
|
||||
|
||||
@@ -48,7 +48,7 @@ pub fn emit_footstep_on_bob_wrap(
|
||||
snd_ent,
|
||||
});
|
||||
weapon_sounds.write(WeaponSound {
|
||||
namespace: assets::AssetNamespace::Iw4,
|
||||
namespace: asset_core::AssetNamespace::Iw4,
|
||||
alias: gear_rattle_alias(gait, local_player).to_owned(),
|
||||
origin_inches,
|
||||
snd_ent,
|
||||
@@ -62,7 +62,7 @@ pub fn emit_weapon_fire(
|
||||
fire_player: Option<&str>,
|
||||
origin_inches: Option<[f32; 3]>,
|
||||
snd_ent: Option<u32>,
|
||||
namespace: assets::AssetNamespace,
|
||||
namespace: asset_core::AssetNamespace,
|
||||
weapon_sounds: &mut MessageWriter<WeaponSound>,
|
||||
) -> bool {
|
||||
let Some(alias) = select_fire_alias(player_view, fire, fire_player) else {
|
||||
|
||||
@@ -1,22 +1,33 @@
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::sync::Arc;
|
||||
|
||||
use assets::{PreparedWeapons, WeaponSoundSlot};
|
||||
use asset_game::WeaponSoundSlot;
|
||||
use assets::PreparedWeapons;
|
||||
use bevy::prelude::*;
|
||||
use net::{CEntity, EntityEventKind, LocalPresentClient, PresentedSnapshot};
|
||||
use entity_iw4::{EntityEventKind, LOCAL_SOUND_ENTITY};
|
||||
use net::{CEntity, LocalPresentClient, PresentedSnapshot};
|
||||
|
||||
use crate::{
|
||||
Footstep, LandSound, PlayAlias, SoundBank, StepGait, WeaponSound, footstep_aliases,
|
||||
gear_rattle_alias, land_aliases, mantle_gear_alias, snd_ent_from_number,
|
||||
Footstep, LandSound, PlayAlias, SoundBank, StepGait, WeaponSound, ent_from_number,
|
||||
footstep_aliases, gear_alias, gear_rattle_alias, land_aliases,
|
||||
};
|
||||
|
||||
fn selected_alias<'a>(
|
||||
weapons: &assets::WeaponRegistry,
|
||||
bank: &'a assets::SoundCatalog,
|
||||
weapons: &asset_game::WeaponRegistry,
|
||||
bank: &'a asset_audio::SoundCatalog,
|
||||
weapon: u32,
|
||||
event: EntityEventKind,
|
||||
player_view: bool,
|
||||
) -> Option<(assets::AssetNamespace, &'a str)> {
|
||||
knife: bool,
|
||||
) -> Option<(asset_core::AssetNamespace, &'a str)> {
|
||||
let weapon = if matches!(
|
||||
event,
|
||||
EntityEventKind::MELEE_SWIPE | EntityEventKind::MELEE_HIT | EntityEventKind::MELEE_MISS
|
||||
) {
|
||||
weapons.melee_weapon_of(weapon)
|
||||
} else {
|
||||
weapon
|
||||
};
|
||||
let (world, player) = match event {
|
||||
EntityEventKind::ITEM_PICKUP => (WeaponSoundSlot::Pickup, WeaponSoundSlot::PickupPlayer),
|
||||
EntityEventKind::AMMO_PICKUP => (
|
||||
@@ -62,10 +73,10 @@ fn selected_alias<'a>(
|
||||
WeaponSoundSlot::MeleeSwipePlayer,
|
||||
),
|
||||
EntityEventKind::MELEE_HIT => {
|
||||
return weapons.weapon_sound_key(weapon, WeaponSoundSlot::MeleeHit, bank);
|
||||
return melee_impact_alias(weapons, bank, weapon, knife, WeaponSoundSlot::MeleeHit);
|
||||
}
|
||||
EntityEventKind::MELEE_MISS => {
|
||||
return weapons.weapon_sound_key(weapon, WeaponSoundSlot::MeleeMiss, bank);
|
||||
return melee_impact_alias(weapons, bank, weapon, knife, WeaponSoundSlot::MeleeMiss);
|
||||
}
|
||||
_ => return None,
|
||||
};
|
||||
@@ -73,7 +84,42 @@ fn selected_alias<'a>(
|
||||
weapons.weapon_sound_key(weapon, if player_view { player } else { world }, bank)
|
||||
}
|
||||
|
||||
fn cg_entity_event_sound(
|
||||
fn melee_impact_alias<'a>(
|
||||
weapons: &asset_game::WeaponRegistry,
|
||||
bank: &'a asset_audio::SoundCatalog,
|
||||
weapon: u32,
|
||||
knife: bool,
|
||||
slot: WeaponSoundSlot,
|
||||
) -> Option<(asset_core::AssetNamespace, &'a str)> {
|
||||
let ns = weapons.namespace_of(weapon).unwrap_or_default();
|
||||
let t5 = ns == asset_core::AssetNamespace::T5;
|
||||
let hit = slot == WeaponSoundSlot::MeleeHit;
|
||||
let generic = |knife: bool| {
|
||||
let name = match (knife, hit) {
|
||||
(true, true) => "melee_knife_hit_body",
|
||||
(true, false) => "melee_knife_hit_other",
|
||||
(false, true) => "melee_hit",
|
||||
(false, false) => "melee_hit_other",
|
||||
};
|
||||
let name = if t5 {
|
||||
format!("wpn_{name}")
|
||||
} else {
|
||||
name.to_owned()
|
||||
};
|
||||
let order = bank.index_in(ns, &name)?;
|
||||
Some((bank.namespace_of_alias(order), bank.name_at(order)?))
|
||||
};
|
||||
let own = || weapons.weapon_sound_key(weapon, slot, bank);
|
||||
let knife_alias = || knife.then(|| generic(true)).flatten();
|
||||
if t5 {
|
||||
own().or_else(knife_alias)
|
||||
} else {
|
||||
knife_alias().or_else(own)
|
||||
}
|
||||
.or_else(|| generic(false))
|
||||
}
|
||||
|
||||
fn entity_event_sound(
|
||||
sound: On<net::EntityEventSound>,
|
||||
identities: Query<&CEntity>,
|
||||
local: Res<LocalPresentClient>,
|
||||
@@ -89,10 +135,7 @@ fn cg_entity_event_sound(
|
||||
return;
|
||||
}
|
||||
if event == EntityEventKind::SOUND_ALIAS_AS_MASTER {
|
||||
diag::warn!(
|
||||
Audio,
|
||||
"audio: EV_SOUND_ALIAS_AS_MASTER is not ported (typed gap)"
|
||||
);
|
||||
diag::warn!(Audio, "audio: EV_SOUND_ALIAS_AS_MASTER is unsupported");
|
||||
return;
|
||||
}
|
||||
let Ok(identity) = identities.get(sound.entity) else {
|
||||
@@ -169,6 +212,7 @@ fn cg_entity_event_sound(
|
||||
sound.event.payload.weapon,
|
||||
event,
|
||||
player_view,
|
||||
sound.event.payload.event_parm == 1,
|
||||
)
|
||||
}) else {
|
||||
diag::warn!(
|
||||
@@ -181,7 +225,12 @@ fn cg_entity_event_sound(
|
||||
output.write(WeaponSound {
|
||||
namespace,
|
||||
alias: alias.to_owned(),
|
||||
origin_inches: (!player_view).then_some(sound.event.payload.origin),
|
||||
origin_inches: (!player_view
|
||||
|| matches!(
|
||||
event,
|
||||
EntityEventKind::MELEE_HIT | EntityEventKind::MELEE_MISS
|
||||
))
|
||||
.then_some(sound.event.payload.origin),
|
||||
snd_ent: Some(u32::from(identity.number())),
|
||||
});
|
||||
}
|
||||
@@ -200,16 +249,33 @@ fn play_cs_sound_alias(
|
||||
);
|
||||
return;
|
||||
};
|
||||
let local = payload.payload.number == LOCAL_SOUND_ENTITY;
|
||||
diag::event!(
|
||||
Audio,
|
||||
Debug,
|
||||
"cs_sound",
|
||||
"audio: cs sound `{alias}` local={local}"
|
||||
);
|
||||
let (namespace, alias) = alias
|
||||
.split_once(':')
|
||||
.and_then(|(ns, name)| {
|
||||
asset_core::AssetNamespace::parse(ns).map(|ns| (ns, name.to_owned()))
|
||||
})
|
||||
.unwrap_or((asset_core::AssetNamespace::Iw4, alias));
|
||||
play.write(crate::AliasCommand::Play(PlayAlias {
|
||||
namespace: assets::AssetNamespace::Iw4,
|
||||
namespace,
|
||||
alias,
|
||||
fallback: None,
|
||||
origin_inches: Some(payload.payload.origin),
|
||||
snd_ent: snd_ent_from_number(payload.payload.number),
|
||||
origin_inches: (!local).then_some(payload.payload.origin),
|
||||
snd_ent: if local {
|
||||
None
|
||||
} else {
|
||||
ent_from_number(payload.payload.number)
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
fn cg_movement_sound(
|
||||
fn movement_sound(
|
||||
sound: On<net::EntityMovementSound>,
|
||||
identities: Query<&CEntity>,
|
||||
local: Res<LocalPresentClient>,
|
||||
@@ -250,10 +316,10 @@ fn cg_movement_sound(
|
||||
| EntityEventKind::FOOTSTEP_PRONE
|
||||
| EntityEventKind::JUMP => {}
|
||||
EntityEventKind::MANTLE => {
|
||||
let alias = mantle_gear_alias(player_view).to_owned();
|
||||
let fallback = player_view.then(|| mantle_gear_alias(false).to_owned());
|
||||
let alias = gear_alias(player_view).to_owned();
|
||||
let fallback = player_view.then(|| gear_alias(false).to_owned());
|
||||
play.write(crate::AliasCommand::Play(PlayAlias {
|
||||
namespace: assets::AssetNamespace::Iw4,
|
||||
namespace: asset_core::AssetNamespace::Iw4,
|
||||
alias,
|
||||
fallback,
|
||||
origin_inches,
|
||||
@@ -280,7 +346,7 @@ fn cg_movement_sound(
|
||||
snd_ent: Some(u32::from(identity.number())),
|
||||
});
|
||||
gear.write(WeaponSound {
|
||||
namespace: assets::AssetNamespace::Iw4,
|
||||
namespace: asset_core::AssetNamespace::Iw4,
|
||||
alias: gear_rattle_alias(gait, player_view).to_owned(),
|
||||
origin_inches,
|
||||
snd_ent: Some(u32::from(identity.number())),
|
||||
@@ -288,9 +354,9 @@ fn cg_movement_sound(
|
||||
}
|
||||
|
||||
pub(crate) fn register_entity_event_audio(app: &mut App) {
|
||||
app.add_observer(cg_entity_event_sound)
|
||||
.add_observer(cg_movement_sound)
|
||||
.add_observer(cg_grenade_contact);
|
||||
app.add_observer(entity_event_sound)
|
||||
.add_observer(movement_sound)
|
||||
.add_observer(grenade_contact);
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
@@ -307,7 +373,10 @@ struct BoundNotetrack {
|
||||
|
||||
#[derive(Resource, Default)]
|
||||
pub(crate) struct NotetrackSoundTable {
|
||||
owner: Option<(Arc<assets::SoundCatalog>, Arc<assets::WeaponRegistry>)>,
|
||||
owner: Option<(
|
||||
Arc<asset_audio::SoundCatalog>,
|
||||
Arc<asset_game::WeaponRegistry>,
|
||||
)>,
|
||||
actions: HashMap<(u32, String), BoundNotetrack>,
|
||||
reported: HashSet<(u32, String)>,
|
||||
}
|
||||
@@ -315,22 +384,25 @@ pub(crate) struct NotetrackSoundTable {
|
||||
impl NotetrackSoundTable {
|
||||
fn owns(
|
||||
&self,
|
||||
bank: &Arc<assets::SoundCatalog>,
|
||||
weapons: &Arc<assets::WeaponRegistry>,
|
||||
bank: &Arc<asset_audio::SoundCatalog>,
|
||||
weapons: &Arc<asset_game::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 {
|
||||
fn bind(
|
||||
bank: &Arc<asset_audio::SoundCatalog>,
|
||||
weapons: &Arc<asset_game::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);
|
||||
.unwrap_or(asset_core::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) {
|
||||
@@ -448,7 +520,7 @@ pub(crate) fn play_viewmodel_notetrack_messages(
|
||||
fn apply_viewmodel_notetrack(
|
||||
weapon: u32,
|
||||
note: &str,
|
||||
bank: Option<&assets::SoundCatalog>,
|
||||
bank: Option<&asset_audio::SoundCatalog>,
|
||||
table: &mut NotetrackSoundTable,
|
||||
output: &mut MessageWriter<crate::BoundWeaponSound>,
|
||||
rumbles: &mut MessageWriter<crate::rumble::PlayRumble>,
|
||||
@@ -513,7 +585,7 @@ fn apply_viewmodel_notetrack(
|
||||
}
|
||||
}
|
||||
|
||||
fn cg_grenade_contact(
|
||||
fn grenade_contact(
|
||||
contact: On<net::EntityGrenadeContact>,
|
||||
weapons: Option<Res<PreparedWeapons>>,
|
||||
bank: Option<Res<SoundBank>>,
|
||||
@@ -529,7 +601,7 @@ fn cg_grenade_contact(
|
||||
let namespace = weapons
|
||||
.0
|
||||
.namespace_of(payload.weapon)
|
||||
.unwrap_or(assets::AssetNamespace::Iw4);
|
||||
.unwrap_or(asset_core::AssetNamespace::Iw4);
|
||||
Some((namespace, alias))
|
||||
}) else {
|
||||
diag::warn!(
|
||||
@@ -543,6 +615,6 @@ fn cg_grenade_contact(
|
||||
namespace,
|
||||
alias: alias.to_owned(),
|
||||
origin_inches: Some(payload.origin),
|
||||
snd_ent: snd_ent_from_number(payload.number),
|
||||
snd_ent: ent_from_number(payload.number),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use assets::{GamesRoot, NamespaceSoundIwd, NamespaceTrees, SoundCatalog, load_mp_sound_bank};
|
||||
use asset_audio::{SoundCatalog, load_mp_sound_bank};
|
||||
use asset_transport::GamesRoot;
|
||||
use assets::{NamespaceSoundIwd, NamespaceTrees};
|
||||
use bevy::{
|
||||
audio::Volume,
|
||||
prelude::*,
|
||||
@@ -8,7 +10,7 @@ use bevy::{
|
||||
};
|
||||
use frame::{
|
||||
ClientSet, LaunchIdentity, ReturnedToMenu, UiPlayMusic, UiPlaySound, UiStopMusic,
|
||||
register_ui_sound,
|
||||
register_ui_contracts,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
@@ -48,7 +50,7 @@ struct PendingMenuBed {
|
||||
}
|
||||
|
||||
pub(crate) fn register_frontend_audio(app: &mut App) {
|
||||
register_ui_sound(app);
|
||||
register_ui_contracts(app);
|
||||
app.init_resource::<PendingMenuBed>()
|
||||
.add_message::<ReturnedToMenu>()
|
||||
.add_systems(
|
||||
@@ -224,7 +226,7 @@ fn play_ui_sound_messages(
|
||||
&mut shared,
|
||||
&bank.0,
|
||||
iwd.map(|a| a.as_ref()),
|
||||
assets::AssetNamespace::Iw4,
|
||||
asset_core::AssetNamespace::Iw4,
|
||||
&event.alias,
|
||||
None,
|
||||
None,
|
||||
@@ -283,7 +285,7 @@ fn play_ui_music_messages(
|
||||
for (entity, _) in playing.iter() {
|
||||
crate::backend::stop(&mut commands, entity);
|
||||
}
|
||||
let Some(key) = clip_keys_for_alias(&bank.0, assets::AssetNamespace::Iw4, &alias)
|
||||
let Some(key) = clip_keys_for_alias(&bank.0, asset_core::AssetNamespace::Iw4, &alias)
|
||||
.into_iter()
|
||||
.next()
|
||||
else {
|
||||
|
||||
@@ -6,11 +6,9 @@ mod emit;
|
||||
mod entity_events;
|
||||
mod frontend;
|
||||
mod messages;
|
||||
mod objectives;
|
||||
mod pcm;
|
||||
mod playback;
|
||||
mod plugin;
|
||||
pub mod policy;
|
||||
mod rumble;
|
||||
mod shellshock;
|
||||
mod space;
|
||||
@@ -18,9 +16,9 @@ mod start;
|
||||
mod voice;
|
||||
|
||||
pub use aliases::{
|
||||
StepGait, footstep_aliases, gear_rattle_alias, land_aliases, mantle_gear_alias,
|
||||
quiet_surface_alias, select_cg_fire_alias, select_fire_alias, step_prefix,
|
||||
surface_alias_candidates, world_surface_alias,
|
||||
StepGait, footstep_aliases, gear_alias, gear_rattle_alias, land_aliases, quiet_surface_alias,
|
||||
select_cg_fire_alias, select_fire_alias, step_prefix, surface_alias_candidates,
|
||||
world_surface_alias,
|
||||
};
|
||||
pub use ambient::{
|
||||
MAX_ACTIVE_MAP_EMITTERS, MIN_AUDIBLE_EMITTER_GAIN, MapAmbient, MapAmbientBooted, MapEmitter,
|
||||
@@ -32,7 +30,7 @@ pub use frontend::FrontendAudio;
|
||||
pub use match_set::{AudioReady, AudioSilent};
|
||||
pub use messages::{
|
||||
AliasCommand, BoundWeaponSound, Footstep, LandSound, PlayAlias, SND_ENT_LOCAL,
|
||||
ViewmodelNotetracks, WeaponSound, snd_ent_from_number,
|
||||
ViewmodelNotetracks, WeaponSound, ent_from_number,
|
||||
};
|
||||
pub use pcm::{LivePan, LoopingPcmAudio, PcmAudio, decode_audio_bytes};
|
||||
pub use playback::{
|
||||
@@ -40,7 +38,6 @@ pub use playback::{
|
||||
world_oneshot_channel_gains, world_oneshot_pan,
|
||||
};
|
||||
pub use plugin::AudioPlugin;
|
||||
pub use policy::music::ScriptMusicHost;
|
||||
pub use space::{distance_inches, transform_inches};
|
||||
pub use start::{
|
||||
SoundClass, StartDecision, StartDecisions, StartFailure, StartOutcome, SuppressReason,
|
||||
@@ -48,7 +45,7 @@ pub use start::{
|
||||
pub use voice::VoiceOccupancy;
|
||||
|
||||
mod destructible_loops;
|
||||
mod map_doors;
|
||||
mod match_set;
|
||||
mod match_voices;
|
||||
|
||||
mod weapon_lock;
|
||||
|
||||
@@ -1,123 +0,0 @@
|
||||
use crate::ambient::{MapAmbient, MapEmitter};
|
||||
use crate::clip_store::{ClipStore, clip_keys_for_alias};
|
||||
use crate::pcm::PcmAudio;
|
||||
use crate::playback::{MissingAliasGaps, SharedPlayAssets, SoundBank};
|
||||
use assets::AssetNamespace;
|
||||
use bevy::prelude::*;
|
||||
use net::PresentedSnapshot;
|
||||
|
||||
pub(crate) const RADIATION_DOOR_ALIASES: &[&str] = &[
|
||||
"evt_hydraulic_switch",
|
||||
"evt_hydraulic_start",
|
||||
"evt_hydraulic_loop",
|
||||
"evt_hydraulic_open",
|
||||
"evt_hydraulic_close",
|
||||
];
|
||||
|
||||
#[derive(Component)]
|
||||
pub(crate) struct DoorLoop;
|
||||
|
||||
pub(crate) fn update(
|
||||
mut commands: Commands,
|
||||
presented: Res<PresentedSnapshot>,
|
||||
mut playing: Query<(Entity, &mut MapEmitter), With<DoorLoop>>,
|
||||
bank: Option<Res<SoundBank>>,
|
||||
mut clips: Option<ResMut<ClipStore>>,
|
||||
mut pcm: ResMut<Assets<PcmAudio>>,
|
||||
mut shared: ResMut<SharedPlayAssets>,
|
||||
mut gaps: ResMut<MissingAliasGaps>,
|
||||
) {
|
||||
let doors = presented.snapshot().and_then(|s| s.meta.map_doors.as_ref());
|
||||
let elapsed = presented.snapshot().and_then(|s| {
|
||||
doors.and_then(|d| {
|
||||
d.started_at
|
||||
.map(|at| s.tick.0.saturating_mul(50).saturating_sub(at))
|
||||
})
|
||||
});
|
||||
|
||||
let moving = elapsed.is_some_and(|ms| ms < 9000);
|
||||
let fade = elapsed.map_or(0.0, |ms| {
|
||||
if ms < 500 {
|
||||
ms as f32 / 500.0
|
||||
} else if ms < 8000 {
|
||||
1.0
|
||||
} else {
|
||||
((9000.0 - ms as f32) / 1000.0).max(0.0)
|
||||
}
|
||||
});
|
||||
if !moving {
|
||||
for (entity, _) in &playing {
|
||||
commands.entity(entity).try_despawn();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if !playing.is_empty() {
|
||||
if let Some(bank) = bank.as_ref()
|
||||
&& let Some(row) = bank
|
||||
.0
|
||||
.sound_in(AssetNamespace::T5, "evt_hydraulic_loop")
|
||||
.or_else(|| bank.0.sound("evt_hydraulic_loop"))
|
||||
.and_then(|s| s.aliases.first())
|
||||
{
|
||||
for (_, mut emitter) in &mut playing {
|
||||
emitter.base_gain = row.vol_min.max(0.0) * fade;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
let Some(bank) = bank else {
|
||||
return;
|
||||
};
|
||||
let Some(clips) = clips.as_mut() else {
|
||||
return;
|
||||
};
|
||||
let alias = "evt_hydraulic_loop";
|
||||
let key = clip_keys_for_alias(&bank.0, AssetNamespace::T5, alias)
|
||||
.into_iter()
|
||||
.next()
|
||||
.or_else(|| {
|
||||
clip_keys_for_alias(&bank.0, AssetNamespace::Iw4, alias)
|
||||
.into_iter()
|
||||
.next()
|
||||
});
|
||||
let Some(key) = key else {
|
||||
gaps.record(alias);
|
||||
return;
|
||||
};
|
||||
clips.request(key.clone());
|
||||
let Some(audio) = clips.ready(&key) else {
|
||||
return;
|
||||
};
|
||||
let Ok(audio) = audio else {
|
||||
gaps.record(alias);
|
||||
return;
|
||||
};
|
||||
let Some(row) = bank
|
||||
.0
|
||||
.sound_in(AssetNamespace::T5, alias)
|
||||
.or_else(|| bank.0.sound(alias))
|
||||
.and_then(|s| s.aliases.first())
|
||||
else {
|
||||
gaps.record(alias);
|
||||
return;
|
||||
};
|
||||
let Some(curve) = row.volume_falloff.as_ref() else {
|
||||
gaps.record(alias);
|
||||
return;
|
||||
};
|
||||
let origin_inches = doors.expect("moving doors").leaves[0].origin;
|
||||
commands.spawn((
|
||||
DoorLoop,
|
||||
MapAmbient,
|
||||
MapEmitter {
|
||||
origin_inches,
|
||||
dist_min: row.dist_min,
|
||||
dist_max: row.dist_max,
|
||||
knots: shared.intern_curve(&curve.name, &curve.knots),
|
||||
base_gain: row.vol_min.max(0.0) * fade,
|
||||
pcm: pcm.add(audio),
|
||||
live_pan: None,
|
||||
},
|
||||
Transform::from_translation(Vec3::from_array(origin_inches)),
|
||||
));
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
use std::collections::{BTreeSet, HashSet};
|
||||
|
||||
use asset_core::AssetNamespace;
|
||||
use asset_audio::SoundCatalog;
|
||||
use asset_game::WeaponRegistry;
|
||||
use assets::{
|
||||
AssetNamespace, MapLoadProcess, MatchType10SoundHints, PreparedWeapons, SoundCatalog,
|
||||
WeaponRegistry,
|
||||
MapLoadProcess, MatchType10SoundHints, PreparedWeapons,
|
||||
};
|
||||
use bevy::prelude::*;
|
||||
use frame::{ClientSet, LaunchIdentity, MatchTornDown, ReturnedToMenu};
|
||||
@@ -10,7 +12,6 @@ use frame::{ClientSet, LaunchIdentity, MatchTornDown, ReturnedToMenu};
|
||||
use crate::aliases::movement_prepare_names;
|
||||
use crate::ambient::{SoundBankCompose, SoundBankLoadAttempted, SoundBankNamespace};
|
||||
use crate::clip_store::{ClipKey, ClipStore, clip_keys_for_alias};
|
||||
use crate::map_doors::RADIATION_DOOR_ALIASES;
|
||||
use crate::playback::SoundBank;
|
||||
|
||||
#[derive(Resource, Default)]
|
||||
@@ -32,7 +33,7 @@ const MATCH_HUD_PULSE: &[&str] = &["ui_pulse_text_type", "ui_pulse_text_delete"]
|
||||
|
||||
const MENU_CODE: [&str; 2] = ["mouse_over", "mouse_click"];
|
||||
|
||||
const MATCH_CLOCK: &[&str] = &[gamemode_iw4::match_clock::COUNTDOWN_TICK_ALIAS];
|
||||
const MATCH_CLOCK: &[&str] = &["ui_mp_timer_countdown"];
|
||||
|
||||
#[derive(Default)]
|
||||
struct MatchRequests {
|
||||
@@ -48,7 +49,7 @@ struct MatchClipPrep {
|
||||
/// Clips this match still had to convert when the set was queued — not the
|
||||
/// alias count, and not the whole cache.
|
||||
total: usize,
|
||||
stage: Option<assets::StageHandle>,
|
||||
stage: Option<asset_transport::StageHandle>,
|
||||
/// Resident sample bytes this process had already produced when the set
|
||||
/// was queued. The decoders count for the life of the process, so what
|
||||
/// this match cost is the distance from here.
|
||||
@@ -106,8 +107,8 @@ fn queue_match_clips(
|
||||
type10: Option<Res<MatchType10SoundHints>>,
|
||||
bank: Option<Res<SoundBank>>,
|
||||
identity: Option<Res<LaunchIdentity>>,
|
||||
catalog: Option<Res<assets::MenuCatalog>>,
|
||||
script_sound: Option<Res<assets::SessionMapScriptSound>>,
|
||||
catalog: Option<Res<asset_game::MenuCatalog>>,
|
||||
script_sound: Option<Res<asset_audio::SessionMapScriptSound>>,
|
||||
namespace: Option<Res<SoundBankNamespace>>,
|
||||
loading: Option<Res<MapLoadProcess>>,
|
||||
mut prep: ResMut<MatchClipPrep>,
|
||||
@@ -120,7 +121,7 @@ fn queue_match_clips(
|
||||
if silent.is_some() {
|
||||
ready.0 = true;
|
||||
if let Some(loading) = loading.as_ref() {
|
||||
loading.progress.record_skipped(assets::StageId::Audio);
|
||||
loading.progress.record_skipped(asset_transport::StageId::Audio);
|
||||
}
|
||||
diag::info!(Audio, "audio: AudioReady — Silent, no clips prepared");
|
||||
return;
|
||||
@@ -134,7 +135,7 @@ fn queue_match_clips(
|
||||
let Some(clips) = clips.as_mut() else {
|
||||
ready.0 = true;
|
||||
if let Some(loading) = loading.as_ref() {
|
||||
loading.progress.record_skipped(assets::StageId::Audio);
|
||||
loading.progress.record_skipped(asset_transport::StageId::Audio);
|
||||
}
|
||||
diag::info!(
|
||||
Audio,
|
||||
@@ -177,37 +178,14 @@ fn queue_match_clips(
|
||||
request_named(clips, &bank.0, ns, &emitter.soundalias, &mut set);
|
||||
}
|
||||
}
|
||||
let mut destructible_loops: Vec<&str> = Vec::new();
|
||||
for alias in gamemode_iw4::destructible_loop_sound_aliases() {
|
||||
if destructible_loops.contains(&alias) {
|
||||
continue;
|
||||
}
|
||||
destructible_loops.push(alias);
|
||||
aliases += 1;
|
||||
request_named(clips, &bank.0, namespace.namespace, alias, &mut set);
|
||||
}
|
||||
for alias in RADIATION_DOOR_ALIASES {
|
||||
aliases += 1;
|
||||
let ns = match bank.0.index_in(AssetNamespace::T5, alias) {
|
||||
Some(_) => AssetNamespace::T5,
|
||||
None => AssetNamespace::Iw4,
|
||||
};
|
||||
request_named(clips, &bank.0, ns, alias, &mut set);
|
||||
}
|
||||
if let Some(identity) = identity.as_deref() {
|
||||
let (allies, axis) = crate::policy::music::voice_prefixes_for_zone(
|
||||
let (allies, axis) = crate::match_voices::voice_prefixes_for_zone(
|
||||
catalog.as_deref(),
|
||||
Some(bank.as_ref()),
|
||||
identity,
|
||||
);
|
||||
for alias in
|
||||
crate::policy::music::match_script_alias_names(allies.as_deref(), axis.as_deref())
|
||||
{
|
||||
aliases += 1;
|
||||
request_named(clips, &bank.0, AssetNamespace::Iw4, &alias, &mut set);
|
||||
}
|
||||
for alias in
|
||||
crate::policy::music::match_voice_alias_names(allies.as_deref(), axis.as_deref())
|
||||
crate::match_voices::team_voice_aliases(&bank.0, allies.as_deref(), axis.as_deref())
|
||||
{
|
||||
aliases += 1;
|
||||
request_named(clips, &bank.0, AssetNamespace::Iw4, &alias, &mut set);
|
||||
@@ -229,7 +207,6 @@ fn queue_match_clips(
|
||||
.chain(crate::weapon_lock::ALIASES.iter())
|
||||
.chain([&gamemode_iw4::damage_feedback::HIT_ALERT_ALIAS])
|
||||
.chain(MATCH_CLOCK)
|
||||
.chain(crate::objectives::EFFECTS)
|
||||
{
|
||||
aliases += 1;
|
||||
request_named(clips, &bank.0, AssetNamespace::Iw4, alias, &mut set);
|
||||
@@ -255,12 +232,12 @@ fn queue_match_clips(
|
||||
if prep.total == 0 && set.resolved_aliases > 0 {
|
||||
loading
|
||||
.progress
|
||||
.record_reused_scoped(assets::StageId::Audio, "clips");
|
||||
.record_reused_scoped(asset_transport::StageId::Audio, "clips");
|
||||
} else {
|
||||
prep.stage = Some(
|
||||
loading
|
||||
.progress
|
||||
.begin(assets::StageId::Audio, Some(prep.total as u64)),
|
||||
.begin(asset_transport::StageId::Audio, Some(prep.total as u64)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
use asset_core::AssetNamespace;
|
||||
|
||||
pub(crate) fn voice_prefixes_for_zone(
|
||||
catalog: Option<&asset_game::MenuCatalog>,
|
||||
bank: Option<&crate::SoundBank>,
|
||||
identity: &frame::LaunchIdentity,
|
||||
) -> (Option<String>, Option<String>) {
|
||||
let Some(catalog) = catalog else {
|
||||
return (None, None);
|
||||
};
|
||||
let Some(table) = catalog.string_table(gamemode_iw4::FACTION_TABLE) else {
|
||||
return (None, None);
|
||||
};
|
||||
let (allies_cs, axis_cs) = arena_faction_charsets(catalog, bank, identity);
|
||||
let allies = table.lookup_col(&allies_cs, gamemode_iw4::FACTION_VOICE_PREFIX_COL);
|
||||
let axis = table.lookup_col(&axis_cs, gamemode_iw4::FACTION_VOICE_PREFIX_COL);
|
||||
(
|
||||
(!allies.is_empty()).then(|| allies.to_owned()),
|
||||
(!axis.is_empty()).then(|| axis.to_owned()),
|
||||
)
|
||||
}
|
||||
|
||||
fn arena_faction_charsets(
|
||||
catalog: &asset_game::MenuCatalog,
|
||||
bank: Option<&crate::SoundBank>,
|
||||
identity: &frame::LaunchIdentity,
|
||||
) -> (String, String) {
|
||||
let from_ui = catalog.rawfile_text("mp/basemaps.arena").map(str::to_owned);
|
||||
let from_bank = bank.and_then(|b| b.0.rawfile_text("mp/basemaps.arena").map(str::to_owned));
|
||||
let arena_text = from_ui
|
||||
.or(from_bank)
|
||||
.or_else(|| asset_game::read_basemaps_arena(&identity.games_root));
|
||||
let row = arena_text
|
||||
.as_deref()
|
||||
.and_then(|text| asset_game::arena_charsets(text, &identity.zone));
|
||||
let (allies, axis) = row.map_or((None, None), |row| (row.allieschar, row.axischar));
|
||||
(
|
||||
allies
|
||||
.filter(|s| !s.is_empty())
|
||||
.unwrap_or_else(|| gamemode_iw4::DEFAULT_ALLIES_CHARSET.to_owned()),
|
||||
axis.filter(|s| !s.is_empty())
|
||||
.unwrap_or_else(|| gamemode_iw4::DEFAULT_AXIS_CHARSET.to_owned()),
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn team_voice_aliases(
|
||||
bank: &asset_audio::SoundCatalog,
|
||||
allies: Option<&str>,
|
||||
axis: Option<&str>,
|
||||
) -> Vec<String> {
|
||||
let prefixes: Vec<&str> = [allies, axis]
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.chain(["generic_death_"])
|
||||
.collect();
|
||||
(0..bank.sounds.len())
|
||||
.filter(|&index| bank.namespace_of_alias(index) == AssetNamespace::Iw4)
|
||||
.filter_map(|index| bank.name_at(index))
|
||||
.filter(|name| prefixes.iter().any(|prefix| name.starts_with(prefix)))
|
||||
.map(str::to_owned)
|
||||
.collect()
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
use bevy::prelude::*;
|
||||
|
||||
use assets::AssetNamespace;
|
||||
use asset_core::AssetNamespace;
|
||||
|
||||
pub const SND_ENT_LOCAL: u32 = 0;
|
||||
|
||||
@@ -69,6 +69,6 @@ pub struct LandSound {
|
||||
pub snd_ent: Option<u32>,
|
||||
}
|
||||
|
||||
pub fn snd_ent_from_number(number: i32) -> Option<u32> {
|
||||
pub fn ent_from_number(number: i32) -> Option<u32> {
|
||||
u32::try_from(number).ok()
|
||||
}
|
||||
|
||||
@@ -1,144 +0,0 @@
|
||||
use gamemode_iw4::Team;
|
||||
use sim::ObjectiveMatch;
|
||||
use std::collections::HashSet;
|
||||
|
||||
pub(crate) const EFFECTS: &[&str] = &[
|
||||
"mp_bomb_plant",
|
||||
"mp_bomb_defuse",
|
||||
"ui_mp_suitcasebomb_timer",
|
||||
"exp_suitcase_bomb_main",
|
||||
"mp_war_objective_taken",
|
||||
"mp_war_objective_lost",
|
||||
];
|
||||
const SECURING: [&str; 3] = ["securing_a", "securing_b", "securing_c"];
|
||||
const LOSING: [&str; 3] = ["losing_a", "losing_b", "losing_c"];
|
||||
const SECURED: [&str; 3] = ["secured_a", "secured_b", "secured_c"];
|
||||
const ENEMY_HAS: [&str; 3] = ["enemy_has_a", "enemy_has_b", "enemy_has_c"];
|
||||
const LOST: [&str; 3] = ["lost_a", "lost_b", "lost_c"];
|
||||
|
||||
pub(crate) enum Cue {
|
||||
Sound(&'static str),
|
||||
Dialog(&'static str),
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub(crate) struct ObjectiveAudio {
|
||||
previous: Option<ObjectiveMatch>,
|
||||
last_ms: u32,
|
||||
status_ms: [u32; 3],
|
||||
notified: HashSet<u32>,
|
||||
delayed: Vec<(u32, Team, &'static str)>,
|
||||
}
|
||||
|
||||
impl ObjectiveAudio {
|
||||
fn status(&mut self, now: u32, team: Team, line: &'static str, force: bool) {
|
||||
if !matches!(team, Team::Allies | Team::Axis) {
|
||||
return;
|
||||
}
|
||||
if !force && now < self.status_ms[team as usize].saturating_add(5000) {
|
||||
return;
|
||||
}
|
||||
self.status_ms[team as usize] = now;
|
||||
|
||||
self.delayed.push((now.saturating_add(100), team, line));
|
||||
}
|
||||
|
||||
pub(crate) fn collect(&mut self, state: &ObjectiveMatch, now: u32) -> Vec<(Option<Team>, Cue)> {
|
||||
if now < self.last_ms || (state.flags.is_empty() && state.bombs.is_empty()) {
|
||||
*self = Self::default();
|
||||
}
|
||||
self.last_ms = now;
|
||||
let mut out = Vec::new();
|
||||
if let Some(old) = self.previous.take() {
|
||||
for flag in &state.flags {
|
||||
let Some(before) = old.flags.iter().find(|f| f.id == flag.id) else {
|
||||
continue;
|
||||
};
|
||||
let index = match flag.label.as_str() {
|
||||
"A" => 0,
|
||||
"B" => 1,
|
||||
"C" => 2,
|
||||
_ => {
|
||||
diag::warn!(
|
||||
Audio,
|
||||
"DOM dialog: unsupported authored label {}",
|
||||
flag.label
|
||||
);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
if flag.capturing == Team::Free || flag.capturing != before.capturing {
|
||||
self.notified.remove(&flag.id);
|
||||
}
|
||||
|
||||
if flag.capturing != Team::Free
|
||||
&& before.capturing == Team::Free
|
||||
&& flag.owner == Team::Free
|
||||
{
|
||||
self.status(now, flag.capturing, SECURING[index], false);
|
||||
}
|
||||
if flag.progress > 0.05
|
||||
&& flag.progress != before.progress
|
||||
&& self.notified.insert(flag.id)
|
||||
{
|
||||
self.status(now, flag.capturing, SECURING[index], false);
|
||||
self.status(now, flag.owner, LOSING[index], false);
|
||||
}
|
||||
if flag.owner != before.owner && matches!(flag.owner, Team::Allies | Team::Axis) {
|
||||
self.notified.remove(&flag.id);
|
||||
out.push((Some(flag.owner), Cue::Sound("mp_war_objective_taken")));
|
||||
let other = if flag.owner == Team::Allies {
|
||||
Team::Axis
|
||||
} else {
|
||||
Team::Allies
|
||||
};
|
||||
if before.owner != Team::Free {
|
||||
out.push((Some(before.owner), Cue::Sound("mp_war_objective_lost")));
|
||||
}
|
||||
if before.owner != Team::Free
|
||||
&& state.flags.iter().all(|f| f.owner == flag.owner)
|
||||
{
|
||||
self.status(now, flag.owner, "secure_all", false);
|
||||
self.status(now, other, "lost_all", false);
|
||||
} else {
|
||||
self.status(now, flag.owner, SECURED[index], true);
|
||||
self.status(
|
||||
now,
|
||||
other,
|
||||
if before.owner == Team::Free {
|
||||
ENEMY_HAS[index]
|
||||
} else {
|
||||
LOST[index]
|
||||
},
|
||||
true,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
if old.round == state.round {
|
||||
for site in &state.bombs {
|
||||
let Some(before) = old.bombs.iter().find(|b| b.view.id == site.view.id) else {
|
||||
continue;
|
||||
};
|
||||
match (before.planted_at_ms, site.planted_at_ms) {
|
||||
(None, Some(_)) => out.push((None, Cue::Dialog("bomb_planted"))),
|
||||
(Some(_), None) if !site.destroyed => {
|
||||
out.push((None, Cue::Dialog("bomb_defused")))
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
self.delayed.retain(|&(at, team, line)| {
|
||||
if at <= now {
|
||||
out.push((Some(team), Cue::Dialog(line)));
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
});
|
||||
self.previous = Some(state.clone());
|
||||
out
|
||||
}
|
||||
}
|
||||
@@ -3,8 +3,10 @@ use std::f32::consts::FRAC_PI_4;
|
||||
use std::sync::Arc;
|
||||
use std::time::Instant;
|
||||
|
||||
use asset_iw4::{SND_CURVE_MAX_KNOTS, snd_attenuate, snd_has_free_voice};
|
||||
use assets::{AssetNamespace, NamespaceSoundIwd, SoundCatalog, lerp_range, snd_unit_random};
|
||||
use asset_iw4::{SND_CURVE_MAX_KNOTS, attenuate, has_free_voice};
|
||||
use asset_core::AssetNamespace;
|
||||
use asset_audio::{SoundCatalog, lerp_range, unit_random};
|
||||
use assets::NamespaceSoundIwd;
|
||||
use bevy::{
|
||||
audio::{AddAudioSource, AudioSink, AudioSinkPlayback, Volume},
|
||||
prelude::*,
|
||||
@@ -190,11 +192,10 @@ impl Plugin for PlayerSoundPlugin {
|
||||
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
|
||||
.before(crate::ambient::update_map_emitter_gain),
|
||||
crate::ambient::update_map_emitter_gain,
|
||||
snd_update_all_channels,
|
||||
update_all_channels,
|
||||
)
|
||||
.in_set(ClientSet::Effects),
|
||||
)
|
||||
@@ -235,7 +236,7 @@ fn reset_clip_prep_on_match_torn_down(
|
||||
fn listener_pose(listeners: &Query<&Transform, With<AmbientListener>>) -> Option<(Vec3, Vec3)> {
|
||||
let n = listeners.iter().len();
|
||||
if n > 1 {
|
||||
panic!("second listener / amp maxRadius gate not ported");
|
||||
panic!("more than one ambient listener");
|
||||
}
|
||||
listeners
|
||||
.iter()
|
||||
@@ -243,7 +244,7 @@ fn listener_pose(listeners: &Query<&Transform, With<AmbientListener>>) -> Option
|
||||
.map(|t| (t.translation, t.rotation * Vec3::X))
|
||||
}
|
||||
|
||||
fn snd_update_all_channels(
|
||||
fn update_all_channels(
|
||||
listeners: Query<&Transform, With<AmbientListener>>,
|
||||
mut channels: Query<(&Channel3d, &mut AudioSink)>,
|
||||
settings: Res<frame::GameSettings>,
|
||||
@@ -257,7 +258,7 @@ fn snd_update_all_channels(
|
||||
let atten = if ch.knots.is_empty() {
|
||||
0.0
|
||||
} else {
|
||||
let value = snd_attenuate(&ch.knots, dist, ch.dist_min, ch.dist_max);
|
||||
let value = attenuate(&ch.knots, dist, ch.dist_min, ch.dist_max);
|
||||
if value < 0.0 { 0.0 } else { value }
|
||||
};
|
||||
let emitter = Vec3::from_array(ch.origin_inches);
|
||||
@@ -989,7 +990,7 @@ fn prepare_voice(
|
||||
}
|
||||
}
|
||||
let voice_n = occupancy.voice_count(ch);
|
||||
if !snd_has_free_voice(voice_n, 0, info.max_voices) {
|
||||
if !has_free_voice(voice_n, 0, info.max_voices) {
|
||||
return Err(SuppressReason::VoiceLimit);
|
||||
}
|
||||
Ok(())
|
||||
@@ -1014,9 +1015,9 @@ fn track_voice(occupancy: &mut VoiceOccupancy, entity: Entity, lease: Option<Voi
|
||||
}
|
||||
}
|
||||
|
||||
fn streamed_row_volume_pitch(row: &assets::CapturedAlias, rng: &mut u32) -> (f32, f32) {
|
||||
let t_vol = snd_unit_random(rng);
|
||||
let t_pitch = snd_unit_random(rng);
|
||||
fn streamed_row_volume_pitch(row: &asset_audio::CapturedAlias, rng: &mut u32) -> (f32, f32) {
|
||||
let t_vol = unit_random(rng);
|
||||
let t_pitch = unit_random(rng);
|
||||
let volume = if row.vol_min == 0.0 && row.vol_max == 0.0 {
|
||||
1.0
|
||||
} else {
|
||||
@@ -1280,7 +1281,7 @@ fn submit_prepared_oneshot(
|
||||
.with_variant(variant_index);
|
||||
};
|
||||
let knots = shared.intern_curve(&curve.name, &curve.knots);
|
||||
let atten = snd_attenuate(&knots, dist, row.dist_min, row.dist_max);
|
||||
let atten = attenuate(&knots, dist, row.dist_min, row.dist_max);
|
||||
let emitter = Vec3::from_array(pos);
|
||||
let (pan_l, pan_r) = world_oneshot_channel_gains(ear, right, emitter, 1.0);
|
||||
if atten < 0.0 {
|
||||
|
||||
@@ -10,7 +10,6 @@ impl Plugin for AudioPlugin {
|
||||
app.add_plugins(PlayerSoundPlugin);
|
||||
crate::match_set::register(app);
|
||||
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);
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
pub mod music;
|
||||
@@ -1,886 +0,0 @@
|
||||
use std::collections::HashSet;
|
||||
|
||||
use bevy::prelude::*;
|
||||
use frame::{
|
||||
AuthoritySet, GameEnded, GameWin, GameWinner, GlassDestroyed, MatchEndingReason as BusReason,
|
||||
MatchEndingSoon, MatchEndingVerySoon, PrematchDone, RoundSwitchKind, RoundSwitchNotify,
|
||||
RoundWin, SpawnedPlayerNotify,
|
||||
};
|
||||
use killcam_iw4::log::Log;
|
||||
use killcam_iw4::task::Millis;
|
||||
use net::{AuthorityWorld, authority_should_tick};
|
||||
use sound_iw4::{
|
||||
Alias, Broadcast, ClientId, Fleet, INIT, Level, MatchEndingReason as SoundReason,
|
||||
MusicController, MusicStep, Notify, Output, PersTeam, Player, PlayerDialog, Recipe,
|
||||
RoundSwitch, SuspenseMusic, SuspenseStep, Team, TeamScores, VOICE_INFIX, Winner, advance_all,
|
||||
group, leader_dialog, leader_dialog_on_one, leader_dialog_on_one_grouped, play_ffa_game_win,
|
||||
play_spawn_music, play_team_game_win, suspense_len,
|
||||
};
|
||||
|
||||
#[derive(Resource)]
|
||||
pub struct ScriptMusicHost {
|
||||
objectives: crate::objectives::ObjectiveAudio,
|
||||
pub controller: MusicController,
|
||||
pub now_ms: Millis,
|
||||
suspense: Option<SuspenseMusic>,
|
||||
|
||||
rng_state: u64,
|
||||
rng_seeded: bool,
|
||||
players: Vec<Player>,
|
||||
queues: Vec<PlayerDialog>,
|
||||
losing: Vec<ClientId>,
|
||||
allies_prefix: Option<String>,
|
||||
axis_prefix: Option<String>,
|
||||
faction_allies: String,
|
||||
faction_axis: String,
|
||||
prefixes_bound: bool,
|
||||
|
||||
bound_zone: Option<String>,
|
||||
|
||||
prematch_done_flag: bool,
|
||||
|
||||
spawned_once: HashSet<ClientId>,
|
||||
|
||||
/// Only the first side switch of a match is ever spoken.
|
||||
round_switch_done: bool,
|
||||
|
||||
/// The winner line is spoken a share of the round or post-match delay after
|
||||
/// the win, so it outlives the notify frame and only ever fires once.
|
||||
pending_round_dialog: Option<(Millis, Winner)>,
|
||||
round_dialog_done: bool,
|
||||
pending_game_dialog: Option<(Millis, Winner)>,
|
||||
game_dialog_done: bool,
|
||||
|
||||
pub unhandled_glass_destroyed: u32,
|
||||
}
|
||||
|
||||
impl Default for ScriptMusicHost {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
objectives: Default::default(),
|
||||
controller: MusicController::new(),
|
||||
now_ms: 0,
|
||||
suspense: None,
|
||||
rng_state: 0,
|
||||
rng_seeded: false,
|
||||
players: Vec::new(),
|
||||
queues: Vec::new(),
|
||||
losing: Vec::new(),
|
||||
allies_prefix: None,
|
||||
axis_prefix: None,
|
||||
faction_allies: gamemode_iw4::DEFAULT_ALLIES_CHARSET.to_owned(),
|
||||
faction_axis: gamemode_iw4::DEFAULT_AXIS_CHARSET.to_owned(),
|
||||
prefixes_bound: false,
|
||||
bound_zone: None,
|
||||
prematch_done_flag: false,
|
||||
spawned_once: HashSet::new(),
|
||||
round_switch_done: false,
|
||||
pending_round_dialog: None,
|
||||
round_dialog_done: false,
|
||||
pending_game_dialog: None,
|
||||
game_dialog_done: false,
|
||||
unhandled_glass_destroyed: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn register_script_music(app: &mut App) {
|
||||
frame::register_script_notify(app);
|
||||
app.init_resource::<ScriptMusicHost>();
|
||||
|
||||
app.add_systems(
|
||||
FixedUpdate,
|
||||
(bind_faction_prefixes, drain_level_notifies_to_music)
|
||||
.chain()
|
||||
.in_set(AuthoritySet::Bookkeeping)
|
||||
.after(frame::AuthorityBookkeeping)
|
||||
.run_if(authority_should_tick),
|
||||
);
|
||||
}
|
||||
|
||||
fn bind_faction_prefixes(
|
||||
catalog: Option<Res<assets::MenuCatalog>>,
|
||||
bank: Option<Res<crate::SoundBank>>,
|
||||
identity: Option<Res<frame::LaunchIdentity>>,
|
||||
mut host: ResMut<ScriptMusicHost>,
|
||||
) {
|
||||
let Some(identity) = identity.as_ref() else {
|
||||
return;
|
||||
};
|
||||
if identity.zone.is_empty() {
|
||||
return;
|
||||
}
|
||||
if host.prefixes_bound && host.bound_zone.as_deref() == Some(identity.zone.as_str()) {
|
||||
return;
|
||||
}
|
||||
let Some(catalog) = catalog.as_ref() else {
|
||||
return;
|
||||
};
|
||||
let Some(table) = catalog.string_table(gamemode_iw4::FACTION_TABLE) else {
|
||||
if catalog.string_table("mp/splashTable.csv").is_none() {
|
||||
return;
|
||||
}
|
||||
apply_arena_charsets(&mut host, catalog, identity, bank.as_deref());
|
||||
host.prefixes_bound = true;
|
||||
host.bound_zone = Some(identity.zone.clone());
|
||||
host.allies_prefix = None;
|
||||
host.axis_prefix = None;
|
||||
diag::warn!(
|
||||
Audio,
|
||||
"audio: mp/factionTable.csv is unavailable (typed gap)"
|
||||
);
|
||||
return;
|
||||
};
|
||||
apply_arena_charsets(&mut host, catalog, identity, bank.as_deref());
|
||||
host.prefixes_bound = true;
|
||||
host.bound_zone = Some(identity.zone.clone());
|
||||
host.allies_prefix = None;
|
||||
host.axis_prefix = None;
|
||||
let allies = table.lookup_col(&host.faction_allies, gamemode_iw4::FACTION_VOICE_PREFIX_COL);
|
||||
let axis = table.lookup_col(&host.faction_axis, gamemode_iw4::FACTION_VOICE_PREFIX_COL);
|
||||
if !allies.is_empty() {
|
||||
host.allies_prefix = Some(allies.to_owned());
|
||||
}
|
||||
if !axis.is_empty() {
|
||||
host.axis_prefix = Some(axis.to_owned());
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_arena_charsets(
|
||||
host: &mut ScriptMusicHost,
|
||||
catalog: &assets::MenuCatalog,
|
||||
identity: &frame::LaunchIdentity,
|
||||
bank: Option<&crate::SoundBank>,
|
||||
) {
|
||||
let (allies, axis, found) = arena_faction_charsets(catalog, bank, identity);
|
||||
host.faction_allies = allies;
|
||||
host.faction_axis = axis;
|
||||
if !found {
|
||||
diag::warn!(
|
||||
Audio,
|
||||
"audio: map faction charsets are unavailable; retail defaults are in use (typed gap)"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn match_level<'a>(
|
||||
world: &sim::SimWorld,
|
||||
losing: &'a [ClientId],
|
||||
highest: Option<ClientId>,
|
||||
) -> Level<'a> {
|
||||
let kind = world.game_mode_kind();
|
||||
let (roundlimit, rounds_played) = match kind {
|
||||
gamemode_iw4::GameModeKind::Demolition => (
|
||||
gamemode_iw4::dd::ROUND_LIMIT as i32,
|
||||
world.objectives.round.saturating_sub(1) as i32,
|
||||
),
|
||||
gamemode_iw4::GameModeKind::Domination => (gamemode_iw4::dom::ROUND_LIMIT, 0),
|
||||
_ => (gamemode_iw4::ffa::ROUND_LIMIT, 0),
|
||||
};
|
||||
let scores = world.team_scores();
|
||||
Level {
|
||||
splitscreen: false,
|
||||
team_based: kind.is_team(),
|
||||
|
||||
hardcore_mode: false,
|
||||
roundlimit,
|
||||
rounds_played,
|
||||
team_scores: TeamScores {
|
||||
allies: scores.allies,
|
||||
axis: scores.axis,
|
||||
},
|
||||
highest_scoring_player: highest,
|
||||
losing_players: losing,
|
||||
}
|
||||
}
|
||||
|
||||
fn drain_level_notifies_to_music(
|
||||
mut soon: MessageReader<MatchEndingSoon>,
|
||||
mut very_soon: MessageReader<MatchEndingVerySoon>,
|
||||
mut ended: MessageReader<GameEnded>,
|
||||
mut prematch: MessageReader<PrematchDone>,
|
||||
mut game_win: MessageReader<GameWin>,
|
||||
mut round_win: MessageReader<RoundWin>,
|
||||
mut round_switch: MessageReader<RoundSwitchNotify>,
|
||||
mut spawned: MessageReader<SpawnedPlayerNotify>,
|
||||
mut glass_destroyed: MessageReader<GlassDestroyed>,
|
||||
mut host: ResMut<ScriptMusicHost>,
|
||||
mut pending_svc: ResMut<net::PendingSvcSounds>,
|
||||
authority: Res<AuthorityWorld>,
|
||||
clock: Res<net::AuthorityClock>,
|
||||
) {
|
||||
let now = clock.time_ms as Millis;
|
||||
if now < host.now_ms {
|
||||
host.queues.clear();
|
||||
host.objectives = Default::default();
|
||||
host.controller = MusicController::new();
|
||||
host.suspense = None;
|
||||
host.prematch_done_flag = false;
|
||||
host.spawned_once.clear();
|
||||
host.round_switch_done = false;
|
||||
host.pending_round_dialog = None;
|
||||
host.round_dialog_done = false;
|
||||
host.pending_game_dialog = None;
|
||||
host.game_dialog_done = false;
|
||||
host.rng_seeded = false;
|
||||
}
|
||||
sync_roster_from_world(&mut host, &authority);
|
||||
host.now_ms = now;
|
||||
seed_host_rng(&mut host, authority.0.root_seed());
|
||||
|
||||
let soon_msgs: Vec<MatchEndingSoon> = soon.read().copied().collect();
|
||||
let very_msgs: Vec<MatchEndingVerySoon> = very_soon.read().copied().collect();
|
||||
let ended_n = ended.read().count();
|
||||
let prematch_n = prematch.read().count();
|
||||
let win_msgs: Vec<GameWin> = game_win.read().copied().collect();
|
||||
let round_msgs: Vec<RoundWin> = round_win.read().copied().collect();
|
||||
let switch_msgs: Vec<RoundSwitchNotify> = round_switch.read().copied().collect();
|
||||
let spawned_msgs: Vec<SpawnedPlayerNotify> = spawned.read().copied().collect();
|
||||
let _ = glass_destroyed.read().count();
|
||||
let highest = host.players.first().map(|p| p.client);
|
||||
let losing = host.losing.clone();
|
||||
let level = match_level(&authority.0, &losing, highest);
|
||||
let mut out: Log<Output, 32> = Log::new();
|
||||
|
||||
let gametype_line = if !authority.0.objectives.bombs.is_empty() {
|
||||
"demolition"
|
||||
} else if !authority.0.objectives.flags.is_empty() {
|
||||
gamemode_iw4::dom::GAMETYPE_DIALOG_LINE
|
||||
} else {
|
||||
gamemode_iw4::GAMETYPE_DIALOG_LINE
|
||||
};
|
||||
let objective_cues = host.objectives.collect(&authority.0.objectives, now as u32);
|
||||
let before = host.controller.step();
|
||||
let mut controller = host.controller;
|
||||
let ScriptMusicHost {
|
||||
players,
|
||||
queues,
|
||||
suspense,
|
||||
rng_state,
|
||||
prematch_done_flag,
|
||||
spawned_once,
|
||||
round_switch_done,
|
||||
pending_round_dialog,
|
||||
round_dialog_done,
|
||||
pending_game_dialog,
|
||||
game_dialog_done,
|
||||
..
|
||||
} = &mut *host;
|
||||
{
|
||||
let mut fleet = Fleet {
|
||||
players: players.as_slice(),
|
||||
queues: queues.as_mut_slice(),
|
||||
};
|
||||
for _ in 0..ended_n {
|
||||
controller.on_notify(now, Notify::GameEnded, &level, &mut fleet, &mut out);
|
||||
if let Some(s) = suspense.as_mut() {
|
||||
s.on_notify(Notify::GameEnded);
|
||||
}
|
||||
}
|
||||
for _ in 0..prematch_n {
|
||||
*prematch_done_flag = true;
|
||||
controller.on_notify(now, Notify::PrematchDone, &level, &mut fleet, &mut out);
|
||||
fire_intro_for_fleet(
|
||||
now,
|
||||
&level,
|
||||
&mut fleet,
|
||||
spawned_once,
|
||||
&authority.0.objectives,
|
||||
&mut out,
|
||||
);
|
||||
}
|
||||
for msg in spawned_msgs {
|
||||
let Some(player) = fleet
|
||||
.players
|
||||
.iter()
|
||||
.find(|p| u32::from(p.client.0) == msg.client)
|
||||
.copied()
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
if spawned_once.contains(&player.client) {
|
||||
continue;
|
||||
}
|
||||
let team = match player.pers_team {
|
||||
Some(PersTeam::Allies) => Team::Allies,
|
||||
Some(PersTeam::Axis) => Team::Axis,
|
||||
_ => {
|
||||
diag::warn!(Audio, "audio: spawned player has no pers team (typed gap)");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
spawned_once.insert(player.client);
|
||||
play_spawn_music(player.client, team, &mut out);
|
||||
leader_dialog_on_one(
|
||||
now,
|
||||
&level,
|
||||
&mut fleet,
|
||||
player.client,
|
||||
gametype_line,
|
||||
&mut out,
|
||||
);
|
||||
if *prematch_done_flag {
|
||||
fire_intro_on_one(
|
||||
now,
|
||||
&level,
|
||||
&mut fleet,
|
||||
player.client,
|
||||
&authority.0.objectives,
|
||||
&mut out,
|
||||
);
|
||||
}
|
||||
}
|
||||
for msg in win_msgs {
|
||||
let winner = bus_winner(msg.winner);
|
||||
controller.on_notify(now, Notify::GameWin(winner), &level, &mut fleet, &mut out);
|
||||
|
||||
match winner {
|
||||
Winner::Team(team) => play_team_game_win(Some(team), &fleet, &mut out),
|
||||
Winner::Player(client) => play_ffa_game_win(Some(client), &fleet, &mut out),
|
||||
|
||||
Winner::Undefined if level.team_based => {
|
||||
play_team_game_win(None, &fleet, &mut out);
|
||||
}
|
||||
Winner::Undefined => play_ffa_game_win(None, &fleet, &mut out),
|
||||
}
|
||||
|
||||
if !*game_dialog_done {
|
||||
*game_dialog_done = true;
|
||||
*pending_game_dialog = Some((now + killcam_iw4::POST_ROUND_TIME_MS / 2, winner));
|
||||
}
|
||||
}
|
||||
for msg in switch_msgs {
|
||||
let switch = match msg.kind {
|
||||
RoundSwitchKind::Halftime => RoundSwitch::Halftime,
|
||||
RoundSwitchKind::Overtime => RoundSwitch::Overtime,
|
||||
RoundSwitchKind::Other => RoundSwitch::Other,
|
||||
};
|
||||
controller.on_notify(
|
||||
now,
|
||||
Notify::RoundSwitch(switch),
|
||||
&level,
|
||||
&mut fleet,
|
||||
&mut out,
|
||||
);
|
||||
if *round_switch_done {
|
||||
continue;
|
||||
}
|
||||
*round_switch_done = true;
|
||||
let clients: Vec<ClientId> = fleet.players.iter().map(|p| p.client).collect();
|
||||
for client in clients {
|
||||
leader_dialog_on_one(
|
||||
now,
|
||||
&level,
|
||||
&mut fleet,
|
||||
client,
|
||||
switch.dialog_key(),
|
||||
&mut out,
|
||||
);
|
||||
}
|
||||
}
|
||||
for msg in round_msgs {
|
||||
let winner = bus_winner(msg.winner);
|
||||
controller.on_notify(now, Notify::RoundWin(winner), &level, &mut fleet, &mut out);
|
||||
if !*round_dialog_done {
|
||||
*round_dialog_done = true;
|
||||
*pending_round_dialog = Some((now + killcam_iw4::ROUND_END_DELAY_MS / 4, winner));
|
||||
}
|
||||
}
|
||||
if let Some((at, winner)) = *pending_round_dialog {
|
||||
if now >= at {
|
||||
*pending_round_dialog = None;
|
||||
winner_dialog(
|
||||
now,
|
||||
&level,
|
||||
&mut fleet,
|
||||
winner,
|
||||
"round_success",
|
||||
"round_failure",
|
||||
None,
|
||||
&mut out,
|
||||
);
|
||||
}
|
||||
}
|
||||
if let Some((at, winner)) = *pending_game_dialog {
|
||||
if now >= at {
|
||||
*pending_game_dialog = None;
|
||||
winner_dialog(
|
||||
now,
|
||||
&level,
|
||||
&mut fleet,
|
||||
winner,
|
||||
"mission_success",
|
||||
"mission_failure",
|
||||
Some("mission_draw"),
|
||||
&mut out,
|
||||
);
|
||||
}
|
||||
}
|
||||
for msg in soon_msgs {
|
||||
let reason = match msg.reason {
|
||||
BusReason::Time => SoundReason::Time,
|
||||
BusReason::Score => SoundReason::Score,
|
||||
};
|
||||
if reason == SoundReason::Score && fleet_missing_pers_team(&fleet) {
|
||||
diag::warn!(
|
||||
Audio,
|
||||
"audio: score-ending music has no pers team (typed gap)"
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if let Some(s) = suspense.as_mut() {
|
||||
s.on_notify(Notify::MatchEndingSoon(reason));
|
||||
}
|
||||
controller.on_notify(
|
||||
now,
|
||||
Notify::MatchEndingSoon(reason),
|
||||
&level,
|
||||
&mut fleet,
|
||||
&mut out,
|
||||
);
|
||||
}
|
||||
for _ in very_msgs {
|
||||
controller.on_notify(
|
||||
now,
|
||||
Notify::MatchEndingVerySoon,
|
||||
&level,
|
||||
&mut fleet,
|
||||
&mut out,
|
||||
);
|
||||
}
|
||||
if suspense.is_none()
|
||||
&& MusicController::starts_suspense(&level)
|
||||
&& controller.step() == MusicStep::WaitingForSoon
|
||||
{
|
||||
let wait = suspense_wait_ms(rng_state);
|
||||
*suspense = Some(SuspenseMusic::start(now, wait));
|
||||
diag::warn!(
|
||||
Audio,
|
||||
"audio: suspense timing uses the host RNG, not GSC (typed gap)"
|
||||
);
|
||||
}
|
||||
if let Some(s) = suspense.as_mut() {
|
||||
if s.step() == SuspenseStep::Waiting && now >= s.wake_at_ms() {
|
||||
let n = suspense_len().max(1);
|
||||
let track = (next_u32(rng_state) as usize) % n;
|
||||
let next_wait = suspense_wait_ms(rng_state);
|
||||
s.advance(now, &level, &fleet, track, next_wait, &mut out);
|
||||
}
|
||||
}
|
||||
for (team, cue) in objective_cues {
|
||||
let clients: Vec<ClientId> = fleet
|
||||
.players
|
||||
.iter()
|
||||
.filter(|p| {
|
||||
let recipient = match p.pers_team {
|
||||
Some(PersTeam::Allies) => gamemode_iw4::Team::Allies,
|
||||
Some(PersTeam::Axis) => gamemode_iw4::Team::Axis,
|
||||
_ => return false,
|
||||
};
|
||||
team.is_none_or(|t| t == recipient)
|
||||
})
|
||||
.map(|p| p.client)
|
||||
.collect();
|
||||
for client in clients {
|
||||
match cue {
|
||||
crate::objectives::Cue::Dialog(line) => {
|
||||
leader_dialog_on_one(now, &level, &mut fleet, client, line, &mut out)
|
||||
}
|
||||
crate::objectives::Cue::Sound(alias) => {
|
||||
pending_svc.push_alias_u32(u32::from(client.0), false, alias);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
advance_all(now, &mut fleet, &mut out);
|
||||
}
|
||||
|
||||
if before == MusicStep::WaitingForSoon && controller.step() != MusicStep::WaitingForSoon {
|
||||
diag::warn!(
|
||||
Audio,
|
||||
"audio: g_hardcore is unwired; retail default 0 is in use (typed gap)"
|
||||
);
|
||||
}
|
||||
host.controller = controller;
|
||||
let allies = host.allies_prefix.clone();
|
||||
let axis = host.axis_prefix.clone();
|
||||
flush_outputs(&out, &mut pending_svc, allies.as_deref(), axis.as_deref());
|
||||
}
|
||||
|
||||
fn bus_winner(winner: GameWinner) -> Winner {
|
||||
match winner {
|
||||
GameWinner::Allies => Winner::Team(Team::Allies),
|
||||
GameWinner::Axis => Winner::Team(Team::Axis),
|
||||
GameWinner::Player(id) => match u8::try_from(id).ok().map(ClientId) {
|
||||
Some(client) => Winner::Player(client),
|
||||
None => Winner::Undefined,
|
||||
},
|
||||
GameWinner::Undefined => Winner::Undefined,
|
||||
}
|
||||
}
|
||||
|
||||
/// The winning team hears `success`, the other hears `failure`, and a player
|
||||
/// winner says nothing at all. Only the game dialog has a draw line.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn winner_dialog<const N: usize>(
|
||||
now: Millis,
|
||||
level: &Level,
|
||||
fleet: &mut Fleet<'_>,
|
||||
winner: Winner,
|
||||
success: &'static str,
|
||||
failure: &'static str,
|
||||
draw: Option<&'static str>,
|
||||
out: &mut Log<Output, N>,
|
||||
) {
|
||||
match winner {
|
||||
Winner::Team(team) => {
|
||||
leader_dialog(now, level, fleet, Broadcast::to_team(success, team), out);
|
||||
leader_dialog(
|
||||
now,
|
||||
level,
|
||||
fleet,
|
||||
Broadcast::to_team(failure, team.other()),
|
||||
out,
|
||||
);
|
||||
}
|
||||
Winner::Player(_) => {}
|
||||
Winner::Undefined => {
|
||||
if let Some(draw) = draw {
|
||||
leader_dialog(now, level, fleet, Broadcast::everyone(draw), out);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn fire_intro_for_fleet<const N: usize>(
|
||||
now: Millis,
|
||||
level: &Level,
|
||||
fleet: &mut Fleet<'_>,
|
||||
spawned_once: &HashSet<ClientId>,
|
||||
objectives: &sim::ObjectiveMatch,
|
||||
out: &mut Log<Output, N>,
|
||||
) {
|
||||
let clients: Vec<ClientId> = fleet
|
||||
.players
|
||||
.iter()
|
||||
.map(|p| p.client)
|
||||
.filter(|c| spawned_once.contains(c))
|
||||
.collect();
|
||||
for client in clients {
|
||||
fire_intro_on_one(now, level, fleet, client, objectives, out);
|
||||
}
|
||||
}
|
||||
|
||||
fn fire_intro_on_one<const N: usize>(
|
||||
now: Millis,
|
||||
level: &Level,
|
||||
fleet: &mut Fleet<'_>,
|
||||
client: ClientId,
|
||||
objectives: &sim::ObjectiveMatch,
|
||||
out: &mut Log<Output, N>,
|
||||
) {
|
||||
let dialog = intro_dialog_key(fleet, client, objectives);
|
||||
leader_dialog_on_one_grouped(now, level, fleet, client, dialog, group::INTROBOOST, out);
|
||||
}
|
||||
|
||||
fn intro_dialog_key(
|
||||
fleet: &Fleet<'_>,
|
||||
client: ClientId,
|
||||
objectives: &sim::ObjectiveMatch,
|
||||
) -> &'static str {
|
||||
if !objectives.flags.is_empty() {
|
||||
return gamemode_iw4::dom::OBJECTIVE_DIALOG_LINE;
|
||||
}
|
||||
if !objectives.bombs.is_empty() {
|
||||
let attacking = fleet
|
||||
.players
|
||||
.iter()
|
||||
.find(|p| p.client == client)
|
||||
.is_some_and(|p| match p.pers_team {
|
||||
Some(PersTeam::Allies) => objectives.attackers == gamemode_iw4::Team::Allies,
|
||||
Some(PersTeam::Axis) => objectives.attackers == gamemode_iw4::Team::Axis,
|
||||
_ => false,
|
||||
});
|
||||
return if attacking {
|
||||
"obj_destroy"
|
||||
} else {
|
||||
"obj_defend"
|
||||
};
|
||||
}
|
||||
match fleet
|
||||
.players
|
||||
.iter()
|
||||
.find(|p| p.client == client)
|
||||
.and_then(|p| p.pers_team)
|
||||
{
|
||||
Some(PersTeam::Allies) => "offense_obj",
|
||||
_ => "defense_obj",
|
||||
}
|
||||
}
|
||||
|
||||
fn suspense_wait_ms(state: &mut u64) -> Millis {
|
||||
const LO: u32 = 60_000;
|
||||
const SPAN: u32 = 60_000;
|
||||
let u = next_u32(state);
|
||||
(LO + ((u as u64 * SPAN as u64) >> 32) as u32) as Millis
|
||||
}
|
||||
|
||||
fn seed_host_rng(host: &mut ScriptMusicHost, root: u64) {
|
||||
if host.rng_seeded {
|
||||
return;
|
||||
}
|
||||
host.rng_state = root ^ 0x9E37_79B9_7F4A_7C15;
|
||||
host.rng_seeded = true;
|
||||
}
|
||||
|
||||
fn next_u32(state: &mut u64) -> u32 {
|
||||
let mut x = *state;
|
||||
if x == 0 {
|
||||
x = 0x9E37_79B9_7F4A_7C15;
|
||||
}
|
||||
x ^= x >> 12;
|
||||
x ^= x << 25;
|
||||
x ^= x >> 27;
|
||||
*state = x;
|
||||
((x.wrapping_mul(0x2545_F491_4F6C_DD1D)) >> 32) as u32
|
||||
}
|
||||
|
||||
fn fleet_missing_pers_team(fleet: &Fleet<'_>) -> bool {
|
||||
fleet.players.iter().any(|p| p.pers_team.is_none())
|
||||
}
|
||||
|
||||
fn sync_roster_from_world(host: &mut ScriptMusicHost, authority: &AuthorityWorld) {
|
||||
let mut rows: Vec<(u32, i32)> = authority
|
||||
.0
|
||||
.clients_scoreboard()
|
||||
.into_iter()
|
||||
.map(|(id, row)| (id.0, row.score))
|
||||
.collect();
|
||||
rows.sort_by(|a, b| b.1.cmp(&a.1).then(a.0.cmp(&b.0)));
|
||||
|
||||
let mut players = Vec::new();
|
||||
let mut queues = Vec::new();
|
||||
for (id, _) in &rows {
|
||||
let Ok(small) = u8::try_from(*id) else {
|
||||
diag::warn!(
|
||||
Audio,
|
||||
"audio: client id {id} does not fit the music controller"
|
||||
);
|
||||
continue;
|
||||
};
|
||||
let client = ClientId(small);
|
||||
|
||||
let pers_team = authority.0.gsc_pers_team(*id).map(|t| {
|
||||
if t == 0 {
|
||||
PersTeam::Allies
|
||||
} else {
|
||||
PersTeam::Axis
|
||||
}
|
||||
});
|
||||
players.push(Player { client, pers_team });
|
||||
if let Some(existing) = host.queues.iter().find(|q| q.client() == client).cloned() {
|
||||
queues.push(existing);
|
||||
} else {
|
||||
queues.push(PlayerDialog::new(client));
|
||||
}
|
||||
}
|
||||
|
||||
host.spawned_once
|
||||
.retain(|c| players.iter().any(|p| p.client == *c));
|
||||
let winner = players.first().map(|p| p.client);
|
||||
host.losing = players
|
||||
.iter()
|
||||
.map(|p| p.client)
|
||||
.filter(|c| Some(*c) != winner)
|
||||
.collect();
|
||||
host.players = players;
|
||||
host.queues = queues;
|
||||
}
|
||||
|
||||
fn flush_outputs(
|
||||
out: &Log<Output, 32>,
|
||||
pending: &mut net::PendingSvcSounds,
|
||||
allies_prefix: Option<&str>,
|
||||
axis_prefix: Option<&str>,
|
||||
) {
|
||||
for item in out.iter() {
|
||||
match item {
|
||||
Output::PlayLocalSound { client, alias } => {
|
||||
queue_local(pending, client, false, &alias, allies_prefix, axis_prefix);
|
||||
}
|
||||
Output::StopLocalSound { client, alias } => {
|
||||
queue_local(pending, client, true, &alias, allies_prefix, axis_prefix);
|
||||
}
|
||||
Output::EntitySound { .. } => {
|
||||
diag::warn!(
|
||||
Audio,
|
||||
"audio: music host emitted unsupported EntitySound (typed gap)"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn prefix_for<'a>(allies: Option<&'a str>, axis: Option<&'a str>, team: Team) -> Option<&'a str> {
|
||||
match team {
|
||||
Team::Allies => allies,
|
||||
Team::Axis => axis,
|
||||
}
|
||||
}
|
||||
|
||||
fn compose_alias(allies: Option<&str>, axis: Option<&str>, alias: &Alias) -> Option<String> {
|
||||
match alias {
|
||||
Alias::Literal(name) => Some((*name).to_owned()),
|
||||
Alias::TeamMusic { team, suffix } => {
|
||||
prefix_for(allies, axis, *team).map(|p| format!("{p}{suffix}"))
|
||||
}
|
||||
Alias::Voice { team, line } => {
|
||||
prefix_for(allies, axis, *team).map(|p| format!("{p}{VOICE_INFIX}{line}"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn voice_prefixes_for_zone(
|
||||
catalog: Option<&assets::MenuCatalog>,
|
||||
bank: Option<&crate::SoundBank>,
|
||||
identity: &frame::LaunchIdentity,
|
||||
) -> (Option<String>, Option<String>) {
|
||||
let Some(catalog) = catalog else {
|
||||
return (None, None);
|
||||
};
|
||||
let Some(table) = catalog.string_table(gamemode_iw4::FACTION_TABLE) else {
|
||||
return (None, None);
|
||||
};
|
||||
let (allies_cs, axis_cs, _) = arena_faction_charsets(catalog, bank, identity);
|
||||
let allies = table.lookup_col(&allies_cs, gamemode_iw4::FACTION_VOICE_PREFIX_COL);
|
||||
let axis = table.lookup_col(&axis_cs, gamemode_iw4::FACTION_VOICE_PREFIX_COL);
|
||||
(
|
||||
(!allies.is_empty()).then(|| allies.to_owned()),
|
||||
(!axis.is_empty()).then(|| axis.to_owned()),
|
||||
)
|
||||
}
|
||||
|
||||
fn arena_faction_charsets(
|
||||
catalog: &assets::MenuCatalog,
|
||||
bank: Option<&crate::SoundBank>,
|
||||
identity: &frame::LaunchIdentity,
|
||||
) -> (String, String, bool) {
|
||||
let from_ui = catalog.rawfile_text("mp/basemaps.arena").map(str::to_owned);
|
||||
let from_bank = bank.and_then(|b| b.0.rawfile_text("mp/basemaps.arena").map(str::to_owned));
|
||||
let arena_text = from_ui
|
||||
.or(from_bank)
|
||||
.or_else(|| assets::read_basemaps_arena(&identity.games_root));
|
||||
let row = arena_text
|
||||
.as_deref()
|
||||
.and_then(|text| assets::arena_charsets(text, &identity.zone));
|
||||
match row {
|
||||
Some(row) => (
|
||||
row.allieschar
|
||||
.filter(|s| !s.is_empty())
|
||||
.unwrap_or_else(|| gamemode_iw4::DEFAULT_ALLIES_CHARSET.to_owned()),
|
||||
row.axischar
|
||||
.filter(|s| !s.is_empty())
|
||||
.unwrap_or_else(|| gamemode_iw4::DEFAULT_AXIS_CHARSET.to_owned()),
|
||||
true,
|
||||
),
|
||||
None => (
|
||||
gamemode_iw4::DEFAULT_ALLIES_CHARSET.to_owned(),
|
||||
gamemode_iw4::DEFAULT_AXIS_CHARSET.to_owned(),
|
||||
false,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn match_script_alias_names(
|
||||
allies_prefix: Option<&str>,
|
||||
axis_prefix: Option<&str>,
|
||||
) -> Vec<String> {
|
||||
let mut seen = HashSet::new();
|
||||
let mut names = Vec::new();
|
||||
let mut push = |name: String| {
|
||||
if seen.insert(name.clone()) {
|
||||
names.push(name);
|
||||
}
|
||||
};
|
||||
for prefix in [allies_prefix, axis_prefix].into_iter().flatten() {
|
||||
for line in ["demolition", gamemode_iw4::dom::GAMETYPE_DIALOG_LINE] {
|
||||
push(format!("{prefix}{VOICE_INFIX}{line}"));
|
||||
}
|
||||
}
|
||||
for recipe in INIT {
|
||||
match recipe {
|
||||
Recipe::Music { alias, .. } | Recipe::SuspenseTrack { alias } => {
|
||||
if let Some(name) = compose_alias(allies_prefix, axis_prefix, alias) {
|
||||
push(name);
|
||||
}
|
||||
}
|
||||
Recipe::Dialog { line, .. } => {
|
||||
for prefix in [allies_prefix, axis_prefix].into_iter().flatten() {
|
||||
push(format!("{prefix}{VOICE_INFIX}{line}"));
|
||||
}
|
||||
}
|
||||
Recipe::Voice { .. } | Recipe::SuspenseInit => {}
|
||||
}
|
||||
}
|
||||
for prefix in [allies_prefix, axis_prefix].into_iter().flatten() {
|
||||
push(format!(
|
||||
"{prefix}{VOICE_INFIX}{}",
|
||||
gamemode_iw4::GAMETYPE_DIALOG_LINE
|
||||
));
|
||||
}
|
||||
names
|
||||
}
|
||||
|
||||
pub(crate) fn match_voice_alias_names(
|
||||
allies_prefix: Option<&str>,
|
||||
axis_prefix: Option<&str>,
|
||||
) -> Vec<String> {
|
||||
let mut seen = HashSet::new();
|
||||
let mut names = Vec::new();
|
||||
let mut push = |name: String| {
|
||||
if seen.insert(name.clone()) {
|
||||
names.push(name);
|
||||
}
|
||||
};
|
||||
for prefix in [allies_prefix, axis_prefix].into_iter().flatten() {
|
||||
for stem in sound_iw4::BATTLECHATTER_STEMS {
|
||||
push(format!("{prefix}{}{stem}", sound_iw4::BATTLECHATTER_INFIX));
|
||||
}
|
||||
}
|
||||
for n in sound_iw4::DEATH_VOICE_MIN..sound_iw4::DEATH_VOICE_MAX_EXCLUSIVE {
|
||||
push(format!(
|
||||
"generic_death_{}_{n}",
|
||||
sound_iw4::death_voice_nationality(false)
|
||||
));
|
||||
push(format!(
|
||||
"generic_death_{}_{n}",
|
||||
sound_iw4::death_voice_nationality(true)
|
||||
));
|
||||
}
|
||||
names
|
||||
}
|
||||
|
||||
fn queue_local(
|
||||
pending: &mut net::PendingSvcSounds,
|
||||
client: ClientId,
|
||||
stop: bool,
|
||||
alias: &Alias,
|
||||
allies_prefix: Option<&str>,
|
||||
axis_prefix: Option<&str>,
|
||||
) {
|
||||
match alias {
|
||||
Alias::Literal(name) => {
|
||||
pending.push_alias_u32(u32::from(client.0), stop, *name);
|
||||
}
|
||||
Alias::TeamMusic { .. } | Alias::Voice { .. } => {
|
||||
let Some(name) = compose_alias(allies_prefix, axis_prefix, alias) else {
|
||||
diag::warn!(
|
||||
Audio,
|
||||
"audio: team music or voice prefix is unavailable (typed gap)"
|
||||
);
|
||||
return;
|
||||
};
|
||||
pending.push_alias_u32(u32::from(client.0), stop, name);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@ use std::sync::Arc;
|
||||
|
||||
use bevy::input::gamepad::{GamepadRumbleIntensity, GamepadRumbleRequest};
|
||||
use bevy::prelude::*;
|
||||
use net::{CgFrameClock, LocalPresentClient, PresentedSnapshot};
|
||||
use net::{FrameClock, LocalPresentClient, PresentedSnapshot};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct Rumble {
|
||||
@@ -13,8 +13,8 @@ pub(crate) struct Rumble {
|
||||
|
||||
impl Rumble {
|
||||
pub(crate) fn prepare(
|
||||
bank: &assets::SoundCatalog,
|
||||
namespace: assets::AssetNamespace,
|
||||
bank: &asset_audio::SoundCatalog,
|
||||
namespace: asset_core::AssetNamespace,
|
||||
name: &str,
|
||||
) -> Result<Arc<Self>, String> {
|
||||
let text = |name: &str| -> Result<&str, String> {
|
||||
@@ -147,7 +147,7 @@ fn update(
|
||||
presented: Res<PresentedSnapshot>,
|
||||
local: Res<LocalPresentClient>,
|
||||
generation: Res<frame::WorldGeneration>,
|
||||
clock: Res<CgFrameClock>,
|
||||
clock: Res<FrameClock>,
|
||||
gamepads: Query<Entity, With<Gamepad>>,
|
||||
mut state: Local<RumblePlayback>,
|
||||
mut output: MessageWriter<GamepadRumbleRequest>,
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use assets::AssetNamespace;
|
||||
use asset_core::AssetNamespace;
|
||||
use bevy::{audio::Volume, prelude::*};
|
||||
use frame::{AppScreen, LifeEnded, MatchTornDown};
|
||||
use hud_iw4::{shellshock_remaining_ms, shellshock_sound_parms};
|
||||
use net::{CgFrameClock, LocalPresentClient, PresentedSnapshot};
|
||||
use hud_iw4::shellshock_remaining_ms;
|
||||
use net::{FrameClock, LocalPresentClient, PresentedSnapshot};
|
||||
|
||||
use crate::{
|
||||
PlayAlias, SND_ENT_LOCAL,
|
||||
@@ -20,7 +20,7 @@ pub(crate) fn update_shellshock_tinnitus(
|
||||
mut torn: MessageReader<MatchTornDown>,
|
||||
mut died: MessageReader<LifeEnded>,
|
||||
screen: Option<Res<AppScreen>>,
|
||||
cg_clock: Option<Res<CgFrameClock>>,
|
||||
cg_clock: Option<Res<FrameClock>>,
|
||||
presented: Option<Res<PresentedSnapshot>>,
|
||||
local: Option<Res<LocalPresentClient>>,
|
||||
bank: Option<Res<SoundBank>>,
|
||||
@@ -59,10 +59,14 @@ pub(crate) fn update_shellshock_tinnitus(
|
||||
shellshock_remaining_ms(cg_clock.time(), ps.shellshock_time, ps.shellshock_duration)
|
||||
})
|
||||
.unwrap_or(0);
|
||||
let parms = presented
|
||||
.player(local.0)
|
||||
.map(|ps| shellshock_sound_parms(ps.shellshock_index))
|
||||
.unwrap_or_else(|| shellshock_sound_parms(0));
|
||||
let Some(parms) = presented
|
||||
.shellshock(local.0)
|
||||
.map(|shock| shock.sound.clone())
|
||||
else {
|
||||
stop_loop(&mut commands, &playing);
|
||||
*was_active = false;
|
||||
return;
|
||||
};
|
||||
let want = remaining > 0 && parms.affect && alive && !died;
|
||||
if want {
|
||||
ensure_loop(
|
||||
@@ -73,7 +77,7 @@ pub(crate) fn update_shellshock_tinnitus(
|
||||
&mut looping_assets,
|
||||
&mut gaps,
|
||||
epoch.0,
|
||||
parms.loop_alias,
|
||||
&parms.loop_alias,
|
||||
);
|
||||
*was_active = true;
|
||||
return;
|
||||
@@ -81,9 +85,9 @@ pub(crate) fn update_shellshock_tinnitus(
|
||||
if *was_active {
|
||||
stop_loop(&mut commands, &playing);
|
||||
let alias = if died || !alive {
|
||||
parms.abort_alias
|
||||
&parms.abort_alias
|
||||
} else {
|
||||
parms.end_alias
|
||||
&parms.end_alias
|
||||
};
|
||||
play.write(crate::AliasCommand::Play(PlayAlias {
|
||||
namespace: AssetNamespace::Iw4,
|
||||
|
||||
@@ -3,7 +3,7 @@ use std::fmt;
|
||||
|
||||
use bevy::prelude::*;
|
||||
|
||||
use assets::AssetNamespace;
|
||||
use asset_core::AssetNamespace;
|
||||
|
||||
const START_DECISION_CAP: usize = 32;
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use asset_iw4::{SND_ENTCHANNEL_MAX, snd_entity_channel_matches};
|
||||
use asset_iw4::{SND_ENTCHANNEL_MAX, entity_channel_matches};
|
||||
use bevy::prelude::*;
|
||||
|
||||
#[derive(Component, Clone, Copy, Debug)]
|
||||
@@ -51,7 +51,7 @@ impl VoiceOccupancy {
|
||||
let Some(occupant_ent) = lease.snd_ent else {
|
||||
return true;
|
||||
};
|
||||
if snd_entity_channel_matches(occupant_ent, lease.channel, snd_ent, channel) {
|
||||
if entity_channel_matches(occupant_ent, lease.channel, snd_ent, channel) {
|
||||
if let Some(count) = self.counts.get_mut(lease.channel as usize) {
|
||||
*count = (*count - 1).max(0);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use bevy::prelude::*;
|
||||
use net::{CgFrameClock, LocalPresentClient, PresentedSnapshot};
|
||||
use net::{FrameClock, LocalPresentClient, PresentedSnapshot};
|
||||
|
||||
use crate::{AliasCommand, PlayAlias, SND_ENT_LOCAL};
|
||||
|
||||
@@ -26,7 +26,7 @@ fn present(
|
||||
presented: Res<PresentedSnapshot>,
|
||||
local: Res<LocalPresentClient>,
|
||||
generation: Res<frame::WorldGeneration>,
|
||||
clock: Res<CgFrameClock>,
|
||||
clock: Res<FrameClock>,
|
||||
mut cursor: Local<LockAudio>,
|
||||
mut commands: MessageWriter<AliasCommand>,
|
||||
) {
|
||||
@@ -49,7 +49,7 @@ fn present(
|
||||
if cursor.stage != 0 {
|
||||
for alias in ALIASES {
|
||||
commands.write(AliasCommand::Stop {
|
||||
namespace: assets::AssetNamespace::Iw4,
|
||||
namespace: asset_core::AssetNamespace::Iw4,
|
||||
alias: alias.to_owned(),
|
||||
snd_ent: Some(SND_ENT_LOCAL),
|
||||
});
|
||||
@@ -62,7 +62,7 @@ fn present(
|
||||
cursor.last_time = now;
|
||||
if stage != 0 && now >= cursor.next_ping {
|
||||
commands.write(AliasCommand::Play(PlayAlias {
|
||||
namespace: assets::AssetNamespace::Iw4,
|
||||
namespace: asset_core::AssetNamespace::Iw4,
|
||||
alias: ALIASES[usize::from(stage & 2 != 0)].to_owned(),
|
||||
fallback: None,
|
||||
origin_inches: None,
|
||||
|
||||
@@ -20,11 +20,19 @@ bevy-trace = ["bevy/trace", "bevy/trace_chrome"]
|
||||
|
||||
[dependencies]
|
||||
bevy.workspace = true
|
||||
asset_audio.workspace = true
|
||||
asset_core.workspace = true
|
||||
asset_game.workspace = true
|
||||
asset_material.workspace = true
|
||||
asset_transport.workspace = true
|
||||
assets.workspace = true
|
||||
audio.workspace = true
|
||||
console.workspace = true
|
||||
diag.workspace = true
|
||||
frame.workspace = true
|
||||
master_protocol.workspace = true
|
||||
render.workspace = true
|
||||
render_gpu.workspace = true
|
||||
render_frontend.workspace = true
|
||||
sim.workspace = true
|
||||
net.workspace = true
|
||||
|
||||
@@ -11,6 +11,7 @@ pub struct AcceptanceLaunch {
|
||||
pub enum LaunchMode {
|
||||
Menu,
|
||||
Map(String),
|
||||
Serve(String),
|
||||
ExportGltf(String),
|
||||
Play {
|
||||
name: String,
|
||||
@@ -72,7 +73,7 @@ fn parse_acceptance_flag(
|
||||
Ok((out, acceptance))
|
||||
}
|
||||
|
||||
const USAGE: &str = "usage: iw4l [--cmds '<script>'] map <zone> | menu | play <demo>\n iw4l export-gltf <zone>\n iw4l --help";
|
||||
const USAGE: &str = "usage: iw4l [--cmds '<script>'] map <zone> | serve <zone> | menu | play <demo>\n iw4l export-gltf <zone>\n iw4l --help";
|
||||
|
||||
pub fn parse_launch_args(mut args: impl Iterator<Item = String>) -> Result<LaunchMode, String> {
|
||||
match args.next().as_deref() {
|
||||
@@ -83,6 +84,12 @@ pub fn parse_launch_args(mut args: impl Iterator<Item = String>) -> Result<Launc
|
||||
};
|
||||
Ok(LaunchMode::Map(zone))
|
||||
}
|
||||
Some("serve") => {
|
||||
let Some(zone) = args.next().filter(|z| !z.is_empty()) else {
|
||||
return Err("usage: iw4l serve <zone> [--cmds '<script>']".into());
|
||||
};
|
||||
Ok(LaunchMode::Serve(zone))
|
||||
}
|
||||
Some("export-gltf") => {
|
||||
let Some(zone) = args.next().filter(|zone| !zone.is_empty()) else {
|
||||
return Err("usage: iw4l export-gltf <zone>".into());
|
||||
@@ -100,7 +107,7 @@ pub fn parse_launch_args(mut args: impl Iterator<Item = String>) -> Result<Launc
|
||||
}
|
||||
Some("play") => parse_play_args(args),
|
||||
Some(other) => Err(format!(
|
||||
"unknown launch args starting with `{other}` — expected: map <zone> | menu | play <demo> [--cmds '<script>'] | export-gltf <zone>"
|
||||
"unknown launch args starting with `{other}` — expected: map <zone> | serve <zone> | menu | play <demo> [--cmds '<script>'] | export-gltf <zone>"
|
||||
)),
|
||||
None => Err(USAGE.into()),
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use assets::LoadLaneTiming;
|
||||
use asset_transport::LoadLaneTiming;
|
||||
|
||||
use crate::bench::milestones::Milestones;
|
||||
use crate::bench::table::{Align, Table, bytes, items, mib, ms, secs};
|
||||
@@ -49,11 +49,11 @@ pub(crate) fn render(bench: &Milestones, command_start: Option<Instant>, out: &m
|
||||
/// merge had no row for. Only `prepared` is work, so only `prepared` belongs
|
||||
/// next to `decode`.
|
||||
fn image_plans(out: &mut Vec<String>) {
|
||||
let jobs = assets::load_jobs::snapshot();
|
||||
let jobs = asset_transport::load_jobs::snapshot();
|
||||
let plans: Vec<_> = jobs
|
||||
.rows
|
||||
.iter()
|
||||
.filter(|row| row.kind == assets::load_jobs::JobKind::ImageDecode)
|
||||
.filter(|row| row.kind == asset_transport::load_jobs::JobKind::ImageDecode)
|
||||
.collect();
|
||||
if plans.is_empty() {
|
||||
out.push(" image plans: MISS — no image decode job was recorded".to_owned());
|
||||
@@ -102,8 +102,8 @@ fn image_plans(out: &mut Vec<String>) {
|
||||
" {held} of {} plans were held by the decode budget before they began; `budget wait` is that hold and `decode` is the work. The ceiling is a threshold on starting, not a cap: a plan that is let through decodes all of itself, so outstanding bytes can end above it.",
|
||||
plans.len()
|
||||
));
|
||||
let (shared, shared_bytes) = assets::shared_variant_census();
|
||||
let (copied, moved) = assets::shared_payload_copy_cost();
|
||||
let (shared, shared_bytes) = asset_material::shared_variant_census();
|
||||
let (copied, moved) = asset_material::shared_payload_copy_cost();
|
||||
if shared > 0 {
|
||||
out.push(format!(
|
||||
" {shared} payloads ({}) were answered out of another asker's decode instead of being read and decoded a second time. Sharing is keyed on the resolved archive entries and the map type, so a different colour space or sampler no longer costs a decode.",
|
||||
@@ -120,7 +120,7 @@ fn image_plans(out: &mut Vec<String>) {
|
||||
mib(moved),
|
||||
mib(copied),
|
||||
));
|
||||
let (payload_reads, header_reads) = assets::iwd_entry_reads();
|
||||
let (payload_reads, header_reads) = asset_material::iwd_entry_reads();
|
||||
out.push(format!(
|
||||
" {payload_reads} archive entries were inflated whole and {header_reads} only as far as the IWI header, which is where the cubemap question is answered. A 2D image whose mips the prepared cache already holds is a header read and no payload read at all."
|
||||
));
|
||||
@@ -358,7 +358,7 @@ fn audio(out: &mut Vec<String>) {
|
||||
/// took. The artifact cache answers the second run of a zone; the batching is
|
||||
/// what the first run gets.
|
||||
fn xwma(out: &mut Vec<String>) {
|
||||
let cost = assets::xwma_decode_cost();
|
||||
let cost = asset_audio::xwma_decode_cost();
|
||||
if cost.hit + cost.miss + cost.failed == 0 {
|
||||
out.push(
|
||||
" t5 xwma: MISS — no clip reached the external decoder, so the cache answered nothing and nothing was stored."
|
||||
@@ -416,7 +416,7 @@ fn picture(bench: &Milestones, out: &mut Vec<String>) {
|
||||
}
|
||||
match (
|
||||
bench.progress.rss_at_open_bytes(),
|
||||
assets::peak_resident_bytes(),
|
||||
asset_transport::peak_resident_bytes(),
|
||||
) {
|
||||
(Some(open), Some(peak)) => lines.push(format!("rss {} → {}", mib(open), mib(peak))),
|
||||
(_, Some(peak)) => lines.push(format!("rss peak {}", mib(peak))),
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user