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:
vtroc7
2026-09-28 18:35:44 +03:00
parent 7af11e5500
commit 38aa9cf741
807 changed files with 50304 additions and 48020 deletions
Generated
+71 -9
View File
@@ -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",
-2
View File
@@ -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" }
+11 -1
View File
@@ -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"
+1 -1
View File
@@ -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())
+6 -6
View File
@@ -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 {
+1 -1
View File
@@ -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
View File
@@ -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;
+4 -1
View File
@@ -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
+1 -1
View File
@@ -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,
+2 -2
View File
@@ -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,
+20 -6
View File
@@ -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
+25 -4
View File
@@ -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,
+140 -19
View File
@@ -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",
+8
View File
@@ -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;
+10 -10
View File
@@ -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,
+7 -7
View File
@@ -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(),
+14
View File
@@ -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)
}
+3 -3
View File
@@ -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 {
+2
View File
@@ -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
+159
View File
@@ -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),
}
}
+55 -27
View File
@@ -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,
}
}
+2 -3
View File
@@ -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)]
+4 -4
View File
@@ -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 {
+1 -17
View File
@@ -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 -24
View File
@@ -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,
+325 -19
View File
@@ -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 {
+150
View File
@@ -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())
}
+3 -2
View File
@@ -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;
+214 -168
View File
@@ -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;
+5 -5
View File
@@ -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,
+3 -3
View File
@@ -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)
}
+5 -5
View File
@@ -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
+4 -7
View File
@@ -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(
+23 -19
View File
@@ -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
}
+76 -15
View File
@@ -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 {
+63 -7
View File
@@ -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> {
+3 -3
View File
@@ -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,
+18 -4
View File
@@ -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)?,
});
}
+55 -2
View File
@@ -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> {
+2 -2
View File
@@ -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;
+6 -6
View File
@@ -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,
+9 -9
View File
@@ -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];
+2
View File
@@ -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::*;
+222
View File
@@ -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
}
+212 -94
View File
@@ -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()
})();
}
}
+75 -17
View File
@@ -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(
+11 -11
View File
@@ -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
+7 -7
View File
@@ -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,
+64 -8
View File
@@ -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,
+13 -14
View File
@@ -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,
-7
View File
@@ -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,
};
+9 -10
View File
@@ -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()
+44 -53
View File
@@ -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,
+64 -46
View File
@@ -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,
+92 -34
View File
@@ -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";
+36 -27
View File
@@ -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
View File
@@ -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 {
+38 -38
View File
@@ -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 -1
View File
@@ -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
View File
@@ -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};
+1 -1
View File
@@ -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 {
+2 -2
View File
@@ -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()
+82
View File
@@ -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();
}
"#;
+4 -4
View File
@@ -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
+19 -16
View File
@@ -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)]
+158
View File
@@ -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,
}
}
}
+94 -58
View File
@@ -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
}
+52 -27
View File
@@ -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)
+145 -44
View File
@@ -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
}
+42 -34
View File
@@ -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>,
+8 -5
View File
@@ -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>,
-4
View File
@@ -1,4 +0,0 @@
pub use asset_transport::{
Iw5ZoneMemory, T5ZoneMemory, ZoneImage, ZoneMemory, ZoneOpenError, open_zone, open_zone_shared,
parse_zone_image, xfile_arena_row,
};
+5 -1
View File
@@ -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
+3 -3
View File
@@ -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
View File
@@ -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;
+11 -9
View File
@@ -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,
+1 -1
View File
@@ -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;
+2 -2
View File
@@ -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 {
+107 -35
View File
@@ -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),
});
}
+7 -5
View File
@@ -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 {
+5 -8
View File
@@ -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;
-123
View File
@@ -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)),
));
}
+14 -37
View File
@@ -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)),
);
}
}
+62
View File
@@ -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()
}
+2 -2
View File
@@ -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()
}
-144
View File
@@ -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
}
}
+13 -12
View File
@@ -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 {
-1
View File
@@ -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
View File
@@ -1 +0,0 @@
pub mod music;
-886
View File
@@ -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);
}
}
}
+4 -4
View File
@@ -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>,
+15 -11
View File
@@ -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,
+1 -1
View File
@@ -3,7 +3,7 @@ use std::fmt;
use bevy::prelude::*;
use assets::AssetNamespace;
use asset_core::AssetNamespace;
const START_DECISION_CAP: usize = 32;
+2 -2
View File
@@ -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);
}
+4 -4
View File
@@ -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,
+8
View File
@@ -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
+9 -2
View File
@@ -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()),
}
+8 -8
View File
@@ -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