mirror of
https://github.com/vladtrc/iw4L
synced 2026-10-26 02:11:39 -04:00
Restore native soldiers, barrels, chat and tracers; align weapon unlock admission
This commit is contained in:
@@ -70,6 +70,18 @@ impl PlayerAnimSources {
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else if family == asset_core::FamilyId::T6 && path == MULTIPLAYER_ANIMTREE_PATH {
|
||||
let decoded = data.get(..4).and_then(|size| {
|
||||
let size = u32::from_le_bytes(size.try_into().ok()?) as usize;
|
||||
asset_transport::inflate_deflate(&data[4..], size)
|
||||
.ok()
|
||||
.filter(|bytes| bytes.len() == size)
|
||||
});
|
||||
let Some(bytes) = decoded else {
|
||||
self.decode_errors.push(path);
|
||||
return;
|
||||
};
|
||||
bytes
|
||||
} else {
|
||||
data.to_vec()
|
||||
};
|
||||
@@ -112,16 +124,23 @@ impl PlayerAnimSources {
|
||||
self.parsed_script = Some(crate::playeranim_parse::parse_player_anim_script(
|
||||
script,
|
||||
tree,
|
||||
self.family == Some(asset_core::AssetNamespace::T5),
|
||||
matches!(
|
||||
self.family,
|
||||
Some(asset_core::FamilyId::T5 | asset_core::FamilyId::T6)
|
||||
),
|
||||
(self.family == Some(asset_core::FamilyId::T6))
|
||||
.then_some(self.playeranim_types.as_deref())
|
||||
.flatten(),
|
||||
));
|
||||
}
|
||||
if self.family == Some(asset_core::AssetNamespace::T5)
|
||||
&& let (Some(Ok(tree)), Some(Ok(script)), Some(atr)) = (
|
||||
self.compiled.as_ref(),
|
||||
self.parsed_script.as_ref(),
|
||||
self.multiplayer_atr.as_deref(),
|
||||
)
|
||||
{
|
||||
if matches!(
|
||||
self.family,
|
||||
Some(asset_core::FamilyId::T5 | asset_core::FamilyId::T6)
|
||||
) && let (Some(Ok(tree)), Some(Ok(script)), Some(atr)) = (
|
||||
self.compiled.as_ref(),
|
||||
self.parsed_script.as_ref(),
|
||||
self.multiplayer_atr.as_deref(),
|
||||
) {
|
||||
self.compiled = Some(crate::atr_compile::compile_selected_multiplayer(
|
||||
atr, tree, script,
|
||||
));
|
||||
@@ -129,7 +148,12 @@ impl PlayerAnimSources {
|
||||
&& let Some(script) = self.playeranim_script.as_deref()
|
||||
{
|
||||
self.parsed_script = Some(crate::playeranim_parse::parse_player_anim_script(
|
||||
script, tree, true,
|
||||
script,
|
||||
tree,
|
||||
true,
|
||||
(self.family == Some(asset_core::FamilyId::T6))
|
||||
.then_some(self.playeranim_types.as_deref())
|
||||
.flatten(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,6 +134,7 @@ pub(crate) fn parse_player_anim_script(
|
||||
script: &[u8],
|
||||
tree: &CompiledAnimTreeDefinition,
|
||||
t5_syntax: bool,
|
||||
player_anim_types: Option<&[u8]>,
|
||||
) -> Result<Arc<ParsedPlayerAnimScript>, PlayerAnimParseError> {
|
||||
let mut parser = AtrParser::new(script);
|
||||
let mut parsed = ParsedPlayerAnimScript {
|
||||
@@ -153,6 +154,24 @@ pub(crate) fn parse_player_anim_script(
|
||||
let mut current_items: Option<Vec<ParsedAnimItem>> = None;
|
||||
let mut current_event_items: Option<Vec<ParsedAnimItem>> = None;
|
||||
let mut aliases: HashMap<(u8, String), u64> = HashMap::new();
|
||||
if let Some(types) = player_anim_types {
|
||||
let mut types = AtrParser::new(types);
|
||||
for index in 0..64 {
|
||||
let name = types.parse(true);
|
||||
if name.is_empty() {
|
||||
break;
|
||||
}
|
||||
if index_ci(&name, anim_iw4::ANIM_PLAYERANIMTYPE_NAMES).is_none() {
|
||||
continue;
|
||||
}
|
||||
for condition in [
|
||||
anim_iw4::ANIM_COND_PLAYERANIMTYPE,
|
||||
anim_iw4::ANIM_COND_PLAYERANIMTYPEPRIMARY,
|
||||
] {
|
||||
aliases.insert((condition, ascii_lower(&name)), 1u64 << index);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
loop {
|
||||
let token = parser.parse(true);
|
||||
@@ -457,6 +476,13 @@ fn resolve_cond_value(
|
||||
if ANIM_COND_IS_BITFLAGS[usize::from(index)] && token.eq_ignore_ascii_case("all") {
|
||||
return Some(ResolvedCondValue::Bits(u64::MAX));
|
||||
}
|
||||
if matches!(
|
||||
index,
|
||||
anim_iw4::ANIM_COND_PLAYERANIMTYPE | anim_iw4::ANIM_COND_PLAYERANIMTYPEPRIMARY
|
||||
) && let Some(bits) = aliases.get(&(index, ascii_lower(token)))
|
||||
{
|
||||
return Some(ResolvedCondValue::Bits(*bits));
|
||||
}
|
||||
let names = anim_cond_value_names(index);
|
||||
if let Some(v) = index_ci(token, names) {
|
||||
return Some(ResolvedCondValue::Bit(v as u8));
|
||||
|
||||
@@ -578,6 +578,7 @@ impl FxCatalog {
|
||||
s: &fastfile_t5::ZoneStream<'_>,
|
||||
geometry: fastfile_t5::FxEffectDefGeometry,
|
||||
materials: &MaterialCatalog,
|
||||
material_name: &impl Fn(fastfile_t5::Ptr) -> Option<String>,
|
||||
) {
|
||||
use fastfile_t5::size as sz_t5;
|
||||
let Some(name_ptr) = geometry.name else {
|
||||
@@ -616,7 +617,7 @@ impl FxCatalog {
|
||||
if let Some(arr) = geometry.elem_defs {
|
||||
for i in 0..geometry.elem_def_count {
|
||||
let elem_ptr = arr.at(i * sz_t5::FX_ELEM_DEF);
|
||||
match leftover_capture_elem_t5(s, elem_ptr, materials) {
|
||||
match leftover_capture_elem_t5(s, elem_ptr, materials, material_name) {
|
||||
Some(elem) => elems.push(elem),
|
||||
None => {
|
||||
self.capture_gaps += 1;
|
||||
@@ -1992,6 +1993,7 @@ fn leftover_capture_elem_t5(
|
||||
s: &fastfile_t5::ZoneStream<'_>,
|
||||
p: fastfile_t5::Ptr,
|
||||
materials: &MaterialCatalog,
|
||||
material_name: &impl Fn(fastfile_t5::Ptr) -> Option<String>,
|
||||
) -> Option<OwnedFxElemDef> {
|
||||
use fastfile_t5::size as sz_t5;
|
||||
let raw = s.slice_at(p, 0, sz_t5::FX_ELEM_DEF).ok()?.to_vec();
|
||||
@@ -2017,7 +2019,7 @@ fn leftover_capture_elem_t5(
|
||||
vis_count,
|
||||
sz_t5::FX_ELEM_VIS_STATE_SAMPLE,
|
||||
);
|
||||
let visuals = leftover_capture_visuals_t5(s, p, &view, materials);
|
||||
let visuals = leftover_capture_visuals_t5(s, p, &view, materials, material_name);
|
||||
let (effect_on_impact, effect_on_impact_hint) = capture_named_child(leftover_read_name_t5(
|
||||
s,
|
||||
p,
|
||||
@@ -2228,6 +2230,7 @@ fn leftover_capture_visuals_t5(
|
||||
p: fastfile_t5::Ptr,
|
||||
view: &FxElemDefView,
|
||||
materials: &MaterialCatalog,
|
||||
material_name: &impl Fn(fastfile_t5::Ptr) -> Option<String>,
|
||||
) -> Vec<OwnedFxVisual> {
|
||||
use fastfile_t5::size as sz_t5;
|
||||
let t = view.elem_type;
|
||||
@@ -2249,8 +2252,8 @@ fn leftover_capture_visuals_t5(
|
||||
.map(|i| {
|
||||
let m = arr.at(i * sz_t5::FX_ELEM_MARK_VISUALS);
|
||||
mark_pair(
|
||||
leftover_resolve_material_t5(s, materials, m.at(0)),
|
||||
leftover_resolve_material_t5(s, materials, m.at(4)),
|
||||
leftover_resolve_material_t5(s, materials, material_name, m.at(0)),
|
||||
leftover_resolve_material_t5(s, materials, material_name, m.at(4)),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
@@ -2278,11 +2281,21 @@ fn leftover_capture_visuals_t5(
|
||||
let arr = s.resolve_alias(arr);
|
||||
return (0..visual_count)
|
||||
.map(|i| {
|
||||
leftover_resolve_material_t5(s, materials, arr.at(i * sz_t5::FX_ELEM_VISUALS))
|
||||
leftover_resolve_material_t5(
|
||||
s,
|
||||
materials,
|
||||
material_name,
|
||||
arr.at(i * sz_t5::FX_ELEM_VISUALS),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
}
|
||||
return vec![leftover_resolve_material_t5(s, materials, vis)];
|
||||
return vec![leftover_resolve_material_t5(
|
||||
s,
|
||||
materials,
|
||||
material_name,
|
||||
vis,
|
||||
)];
|
||||
}
|
||||
if t == elem_type::MODEL {
|
||||
if visual_count > 1 {
|
||||
@@ -2347,6 +2360,7 @@ fn leftover_capture_visuals_t5(
|
||||
fn leftover_resolve_material_t5(
|
||||
s: &fastfile_t5::ZoneStream<'_>,
|
||||
materials: &MaterialCatalog,
|
||||
material_name: &impl Fn(fastfile_t5::Ptr) -> Option<String>,
|
||||
slot: fastfile_t5::Ptr,
|
||||
) -> OwnedFxVisual {
|
||||
let slot_iw4 = t5_ptr_to_iw4(slot);
|
||||
@@ -2357,9 +2371,11 @@ fn leftover_resolve_material_t5(
|
||||
let index = materials
|
||||
.material_index(slot_iw4)
|
||||
.or_else(|| alias.and_then(|target| materials.material_index(target)));
|
||||
let name = index
|
||||
.and_then(|i| materials.materials.get(i.get()).map(|m| m.name.to_string()))
|
||||
.filter(|n| !n.is_empty());
|
||||
let name = material_name(slot).or_else(|| {
|
||||
index
|
||||
.and_then(|i| materials.materials.get(i.get()).map(|m| m.name.to_string()))
|
||||
.filter(|n| !n.is_empty())
|
||||
});
|
||||
OwnedFxVisual::Material {
|
||||
material: FxElemMaterial::unresolved_for(Some(slot_iw4), alias),
|
||||
hint: name,
|
||||
|
||||
@@ -255,6 +255,10 @@ impl TracerDefinitions {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn index_named(&self, namespace: crate::AssetNamespace, name: &str) -> Option<usize> {
|
||||
self.by_key.get(&(namespace, name.to_owned())).copied()
|
||||
}
|
||||
|
||||
pub fn def_at(&self, index: usize) -> Option<&OwnedTracerDef> {
|
||||
self.defs.get(index)
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ mod editor;
|
||||
mod model_linking;
|
||||
pub use editor::{EditorWeaponCatalog, EditorWeaponSelection};
|
||||
mod publication;
|
||||
use catalog_linking::{material_hint_edge, stamp_combat_fx};
|
||||
use catalog_linking::{material_hint_edge, stamp_combat_fx, stamp_mw2_tracer};
|
||||
mod capture_merge;
|
||||
use capture_merge::{
|
||||
apply_leftover_default_anim_overrides, apply_leftover_default_sound_overrides,
|
||||
@@ -2093,6 +2093,13 @@ impl WeaponBuild {
|
||||
pub fn resolve_combat_fx(&mut self, fx: &crate::FxCatalog, tracers: &crate::TracerCatalog) {
|
||||
let n = self.registry.rows.len().min(self.combat_slots.len());
|
||||
for i in 1..n {
|
||||
let category = {
|
||||
let row = &self.registry.rows[i];
|
||||
self.registry
|
||||
.item_groups
|
||||
.get(&(row.namespace, row.name.clone()))
|
||||
.and_then(|group| crate::cac_category_from_item_group(group))
|
||||
};
|
||||
let row = &mut self.registry.rows[i];
|
||||
let Some(namespace) = row.component_namespace(WeaponComponent::Effect) else {
|
||||
row.combat_fx.refuse_preparation();
|
||||
@@ -2105,6 +2112,9 @@ impl WeaponBuild {
|
||||
fx,
|
||||
tracers,
|
||||
);
|
||||
if row.namespace != crate::AssetNamespace::Iw4 {
|
||||
stamp_mw2_tracer(&mut row.combat_fx, &row.facts, category, tracers);
|
||||
}
|
||||
row.preparation.bind_combat_effects(
|
||||
&mut row.combat_fx,
|
||||
self.combat_slots[i],
|
||||
|
||||
@@ -497,3 +497,50 @@ pub(super) fn stamp_combat_fx(
|
||||
};
|
||||
combat.last_shot_eject_pair_authored = slots.last_shot_pair_authored();
|
||||
}
|
||||
|
||||
fn mw2_tracer_name(
|
||||
facts: &WeaponBodyFacts,
|
||||
category: Option<crate::CacAuthoredCategory>,
|
||||
) -> Option<&'static str> {
|
||||
use crate::CacAuthoredCategory as C;
|
||||
if facts.weap_type != weapon_iw4::WEAPTYPE_BULLET
|
||||
|| facts.weap_class == weapon_iw4::WEAPCLASS_TURRET
|
||||
{
|
||||
return None;
|
||||
}
|
||||
Some(match category {
|
||||
Some(C::Assault) => "assaultrifle",
|
||||
Some(C::Smg | C::MachinePistol) => "smg",
|
||||
Some(C::Lmg) => "lmg",
|
||||
Some(C::Sniper) => "sniper",
|
||||
Some(C::Shotgun) => "shotgun",
|
||||
Some(C::Pistol) => "pistol",
|
||||
_ => match facts.weap_class {
|
||||
0 => "assaultrifle",
|
||||
1 => "sniper",
|
||||
2 => "lmg",
|
||||
3 => "smg",
|
||||
weapon_iw4::WEAPCLASS_SPREAD => "shotgun",
|
||||
weapon_iw4::WEAPCLASS_PISTOL => "pistol",
|
||||
_ => return None,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn stamp_mw2_tracer(
|
||||
combat: &mut WeaponCombatFx,
|
||||
facts: &WeaponBodyFacts,
|
||||
category: Option<crate::CacAuthoredCategory>,
|
||||
tracers: &crate::TracerCatalog,
|
||||
) {
|
||||
let Some(name) = mw2_tracer_name(facts, category) else {
|
||||
combat.tracer_hint = None;
|
||||
combat.tracer = AssetEdge::Absent;
|
||||
return;
|
||||
};
|
||||
combat.tracer_hint = Some(name.to_owned());
|
||||
combat.tracer = match tracers.index_named(crate::AssetNamespace::Iw4, name) {
|
||||
Some(index) => AssetEdge::bind_order(index, tracers.zone_of(index)),
|
||||
None => AssetEdge::Unresolved(AssetEdgeReason::CatalogMiss),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -172,7 +172,12 @@ impl WeaponRegistry {
|
||||
&& table.name.eq_ignore_ascii_case("mp/perkTable.csv")
|
||||
&& table.lookup_row_in_col(1, name).is_some()
|
||||
});
|
||||
return if native_perk {
|
||||
let native_weapon = infinity
|
||||
&& self
|
||||
.families
|
||||
.find(&crate::FamilyKey::new(namespace, name))
|
||||
.is_some();
|
||||
return if native_perk || native_weapon {
|
||||
Ok(Default::default())
|
||||
} else {
|
||||
Err("unlock.missing_item")
|
||||
@@ -231,17 +236,7 @@ impl WeaponRegistry {
|
||||
.describe_configuration(id)
|
||||
.ok_or("unlock.missing_family")?;
|
||||
let family = selection.family.as_ref().ok_or("unlock.missing_family")?;
|
||||
match self.item_unlock_requirement(family.namespace, &family.base) {
|
||||
Err("unlock.missing_item")
|
||||
if matches!(
|
||||
family.namespace,
|
||||
crate::AssetNamespace::Iw4 | crate::AssetNamespace::Iw5
|
||||
) =>
|
||||
{
|
||||
Ok(Default::default())
|
||||
}
|
||||
requirement => requirement,
|
||||
}
|
||||
self.item_unlock_requirement(family.namespace, &family.base)
|
||||
}
|
||||
|
||||
pub fn vehicle_compass(&self) -> impl Iterator<Item = (&str, &[String; 2], [i32; 2])> {
|
||||
|
||||
@@ -522,6 +522,11 @@ pub fn inflate_zlib(data: &[u8]) -> Result<Vec<u8>, String> {
|
||||
.map_err(|error| format!("zlib inflate: {error:?}"))
|
||||
}
|
||||
|
||||
pub fn inflate_deflate(data: &[u8], limit: usize) -> Result<Vec<u8>, String> {
|
||||
miniz_oxide::inflate::decompress_to_vec_with_limit(data, limit)
|
||||
.map_err(|error| format!("deflate inflate: {error:?}"))
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct IwdSoundIndex {
|
||||
dir: PathBuf,
|
||||
|
||||
@@ -26,8 +26,8 @@ pub use discover::{
|
||||
pub use ipak::{IPak, ipak_name_hash};
|
||||
pub use iwd::{
|
||||
IwdFile, IwdIndex, IwdSoundIndex, cached_iwd_dirs, game_main_for_zone, game_mains_by_root,
|
||||
game_mains_under, inflate_zlib, installation_revision, iwd_entry_reads, iwd_read_cost,
|
||||
read_iwd_named, read_text,
|
||||
game_mains_under, inflate_deflate, inflate_zlib, installation_revision, iwd_entry_reads,
|
||||
iwd_read_cost, read_iwd_named, read_text,
|
||||
};
|
||||
pub use load_jobs::{CacheResult, Job, JobKind};
|
||||
pub use namespace_trees::{NamespaceSoundIwd, NamespaceTree, NamespaceTrees};
|
||||
|
||||
@@ -123,6 +123,12 @@ impl MapDeclarations {
|
||||
);
|
||||
}
|
||||
out.push_str("\n\tmaps\\mp\\_load::main();\n");
|
||||
if entity_blocks(entities)
|
||||
.iter()
|
||||
.any(|entity| entity.get("targetname").map(String::as_str) == Some("explodable_barrel"))
|
||||
{
|
||||
out.push_str("\tiw4l_maps\\barrels::main();\n");
|
||||
}
|
||||
if let Some(compass) = &self.compass {
|
||||
let _ = writeln!(out, "\tmaps\\mp\\_compass::setupMiniMap( \"{compass}\" );");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
main()
|
||||
{
|
||||
barrels = getentarray("explodable_barrel", "targetname");
|
||||
if (!barrels.size)
|
||||
return;
|
||||
level.barrel_effects["ignite"] = loadfx("props/barrel_ignite");
|
||||
level.barrel_effects["fire"] = loadfx("props/barrel_fire_top");
|
||||
level.barrel_effects["explode"] = loadfx("props/barrelexp");
|
||||
precachemodel("com_barrel_piece");
|
||||
precachemodel("com_barrel_piece2");
|
||||
for (i = 0; i < barrels.size; i++)
|
||||
barrels[i] thread watch_damage();
|
||||
}
|
||||
|
||||
watch_damage()
|
||||
{
|
||||
self endon("exploding");
|
||||
self.barrel_damage = 0;
|
||||
self setcandamage(true);
|
||||
for (;;)
|
||||
{
|
||||
self waittill("damage", amount, attacker, direction, point, means);
|
||||
if (means == "MOD_MELEE" || means == "MOD_IMPACT")
|
||||
continue;
|
||||
self.barrel_attacker = attacker;
|
||||
first_hit = self.barrel_damage == 0;
|
||||
self.barrel_damage += amount;
|
||||
if (first_hit)
|
||||
self thread burn(means);
|
||||
}
|
||||
}
|
||||
|
||||
burn(means)
|
||||
{
|
||||
self endon("exploding");
|
||||
up = anglestoup(self.angles);
|
||||
if (means != "MOD_GRENADE_SPLASH" && means != "MOD_GRENADE")
|
||||
{
|
||||
playfx(level.barrel_effects["ignite"], self.origin);
|
||||
fire = playloopedfx(level.barrel_effects["fire"], .05, self.origin + up * 44);
|
||||
while (self.barrel_damage < 150)
|
||||
{
|
||||
self.barrel_damage += 10 + randomfloat(10);
|
||||
wait 1;
|
||||
}
|
||||
fire delete();
|
||||
}
|
||||
self thread explode();
|
||||
}
|
||||
|
||||
explode()
|
||||
{
|
||||
self notify("exploding");
|
||||
self notify("death");
|
||||
self setcandamage(false);
|
||||
self playsound("iw5:explo_metal_rand");
|
||||
playfx(level.barrel_effects["explode"], self.origin + (0, 0, 4));
|
||||
if (isdefined(self.target))
|
||||
{
|
||||
clip = getent(self.target, "targetname");
|
||||
if (isdefined(clip))
|
||||
clip delete();
|
||||
}
|
||||
radius = 250;
|
||||
if (isdefined(self.radius))
|
||||
radius = self.radius;
|
||||
self radiusdamage(self.origin + (0, 0, 30), radius, 250, 1, self.barrel_attacker, "MOD_EXPLOSIVE");
|
||||
physicsexplosionsphere(self.origin + (0, 0, 30), radius, radius / 2, 2);
|
||||
earthquake(.4, .5, self.origin, 512);
|
||||
if (randomint(2))
|
||||
self setmodel("com_barrel_piece");
|
||||
else
|
||||
self setmodel("com_barrel_piece2");
|
||||
}
|
||||
@@ -214,10 +214,14 @@ impl MapXModelCatalog {
|
||||
break_health: stream.f32_at(st, 4).ok()?,
|
||||
max_time: stream.f32_at(st, 8).ok()?,
|
||||
flags: stream.u32_at(st, 12).ok()?,
|
||||
break_effect_looping: break_effect
|
||||
.as_deref()
|
||||
.and_then(|name| fx.get_in(asset_core::FamilyId::T5, name))
|
||||
.is_some_and(|effect| effect.view.msec_looping_life == i32::MAX),
|
||||
break_effect,
|
||||
break_sound: read_string(st, 20)?,
|
||||
break_sound: read_string(st, 20)?.map(|name| format!("t5:{name}")),
|
||||
break_notify: read_string(st, 24)?,
|
||||
loop_sound: read_string(st, 28)?,
|
||||
loop_sound: read_string(st, 28)?.map(|name| format!("t5:{name}")),
|
||||
has_phys_preset: stream.ptr_at(st, 44).ok()? != ZonePtr::Null,
|
||||
spawn_models,
|
||||
});
|
||||
|
||||
@@ -755,6 +755,7 @@ impl ZoneLane for Iw5Lane {
|
||||
fpv: sink.fpv_meshes,
|
||||
world_weapons: sink.world_weapons,
|
||||
xanims: sink.xanims,
|
||||
player_anim_sources: sink.player_anim_sources,
|
||||
light_defs,
|
||||
fx: sink.fx,
|
||||
scene_models: sink.scene_models,
|
||||
|
||||
@@ -165,6 +165,7 @@ pub(crate) struct ZoneWalkSink {
|
||||
pub models: ModelCensus,
|
||||
pub materials: MaterialCatalog,
|
||||
pub fx: FxCatalog,
|
||||
material_names_t5: HashMap<fastfile_t5::Ptr, fastfile_t5::Ptr>,
|
||||
pub fx_models: asset_game::FxModelCatalog,
|
||||
pub impact_fx: ImpactFxCatalog,
|
||||
pub tracers: asset_game::TracerCatalog,
|
||||
@@ -738,6 +739,12 @@ impl fastfile_iw5::AssetLinkSink for CommonWalkSink {
|
||||
if let Some(sound) = self.sound.as_mut() {
|
||||
sound.raw_file(name, data, zlib_compressed);
|
||||
}
|
||||
self.player_anim_sources.capture(
|
||||
asset_core::AssetNamespace::Iw5,
|
||||
name,
|
||||
data,
|
||||
zlib_compressed,
|
||||
);
|
||||
capture_film_vision(&mut self.film_visions, name, data, zlib_compressed);
|
||||
Ok(())
|
||||
}
|
||||
@@ -890,7 +897,17 @@ impl fastfile_t5::AssetLinkSink for CommonWalkSink {
|
||||
s: &fastfile_t5::ZoneStream<'_>,
|
||||
geometry: fastfile_t5::FxEffectDefGeometry,
|
||||
) -> fastfile_t5::Result<()> {
|
||||
self.fx.capture_t5(s, geometry, &self.materials);
|
||||
self.fx.capture_t5(s, geometry, &self.materials, &|slot| {
|
||||
let name = self.material_names_t5.get(&slot).copied().or_else(|| {
|
||||
let fastfile_t5::ZonePtr::Offset(target) = s.ptr_at(slot, 0).ok()? else {
|
||||
return None;
|
||||
};
|
||||
self.material_names_t5
|
||||
.get(&s.resolve_alias(target))
|
||||
.copied()
|
||||
})?;
|
||||
s.cstr(s.resolve_alias(name)).ok().map(str::to_owned)
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1006,7 +1023,17 @@ impl fastfile_t5::AssetLinkSink for ZoneWalkSink {
|
||||
s: &fastfile_t5::ZoneStream<'_>,
|
||||
geometry: fastfile_t5::FxEffectDefGeometry,
|
||||
) -> fastfile_t5::Result<()> {
|
||||
self.fx.capture_t5(s, geometry, &self.materials);
|
||||
self.fx.capture_t5(s, geometry, &self.materials, &|slot| {
|
||||
let name = self.material_names_t5.get(&slot).copied().or_else(|| {
|
||||
let fastfile_t5::ZonePtr::Offset(target) = s.ptr_at(slot, 0).ok()? else {
|
||||
return None;
|
||||
};
|
||||
self.material_names_t5
|
||||
.get(&s.resolve_alias(target))
|
||||
.copied()
|
||||
})?;
|
||||
s.cstr(s.resolve_alias(name)).ok().map(str::to_owned)
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1053,6 +1080,13 @@ impl fastfile_t5::AssetLinkSink for ZoneWalkSink {
|
||||
target: fastfile_t5::Ptr,
|
||||
) -> fastfile_t5::Result<()> {
|
||||
self.materials.t5_alias(ty, slot, target);
|
||||
if ty == fastfile_t5::AssetType::Material {
|
||||
if let Some(name) = self.material_names_t5.get(&target).copied() {
|
||||
self.material_names_t5.insert(slot, name);
|
||||
} else {
|
||||
self.material_names_t5.remove(&slot);
|
||||
}
|
||||
}
|
||||
if ty == fastfile_t5::AssetType::Fx {
|
||||
self.fx.note_alias(t5_iw4_ptr(slot), t5_iw4_ptr(target));
|
||||
}
|
||||
@@ -1071,6 +1105,22 @@ impl fastfile_t5::AssetLinkSink for ZoneWalkSink {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn remember_material_name(
|
||||
&mut self,
|
||||
slot: fastfile_t5::Ptr,
|
||||
insert_slot: Option<fastfile_t5::Ptr>,
|
||||
name: fastfile_t5::Ptr,
|
||||
) {
|
||||
self.material_names_t5.insert(slot, name);
|
||||
if let Some(slot) = insert_slot {
|
||||
self.material_names_t5.insert(slot, name);
|
||||
}
|
||||
}
|
||||
|
||||
fn material_name_ptr(&self, slot: fastfile_t5::Ptr) -> Option<fastfile_t5::Ptr> {
|
||||
self.material_names_t5.get(&slot).copied()
|
||||
}
|
||||
|
||||
fn capture_raw_file(
|
||||
&mut self,
|
||||
name: &str,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
mod camouflage;
|
||||
mod common_compile;
|
||||
mod destructible;
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::path::Path;
|
||||
@@ -53,6 +54,7 @@ struct T6Content {
|
||||
fn t6_script_model_instance(
|
||||
placement: &asset_world::ScriptModelPlacement,
|
||||
scene_assets: &mut asset_world::MapXModelSceneCatalog,
|
||||
definitions: &BTreeMap<String, Arc<xmodel_runtime::T5DestructibleDef>>,
|
||||
) -> asset_world::ScriptModelSceneInstance {
|
||||
use bevy::math::{EulerRot, Quat, Vec3};
|
||||
let current_model = asset_world::MapXModelAssetKey(placement.model.clone());
|
||||
@@ -64,10 +66,20 @@ fn t6_script_model_instance(
|
||||
},
|
||||
);
|
||||
}
|
||||
let definition = definitions.get(&placement.destructible_def).cloned();
|
||||
let mut dobj_state =
|
||||
xmodel_runtime::DObjSemanticState::bind_pose(placement.model.clone(), 1, 1);
|
||||
if let Some(def) = &definition
|
||||
&& placement.model == def.model
|
||||
&& let Some(asset_world::MapXModelSceneAsset::T6(skel)) = scene_assets.get(¤t_model)
|
||||
{
|
||||
let health: Vec<_> = def.pieces.iter().map(|piece| piece.health as i16).collect();
|
||||
dobj_state.hide_part_bits = def.hide_parts(&health, &skel.bone_names);
|
||||
}
|
||||
let [pitch, yaw, roll] = placement.angles.map(f32::to_radians);
|
||||
asset_world::ScriptModelSceneInstance {
|
||||
id: placement.id,
|
||||
dobj_state: xmodel_runtime::DObjSemanticState::bind_pose(placement.model.clone(), 1, 1),
|
||||
dobj_state,
|
||||
current_model,
|
||||
transform: bevy::prelude::Transform {
|
||||
translation: Vec3::from_array(placement.origin),
|
||||
@@ -81,7 +93,7 @@ fn t6_script_model_instance(
|
||||
script_noteworthy: placement.script_noteworthy.clone(),
|
||||
destructible_type: placement.destructible_type.clone(),
|
||||
destructible_def: placement.destructible_def.clone(),
|
||||
t5_destructible: None,
|
||||
t5_destructible: definition,
|
||||
target: placement.target.clone(),
|
||||
script_exploder: placement.script_exploder.clone(),
|
||||
brush_link: placement.brush_link.clone(),
|
||||
@@ -207,6 +219,71 @@ fn capture_xanims(
|
||||
));
|
||||
}
|
||||
|
||||
fn capture_player_animation(
|
||||
load: &fastfile_t6::ZoneLoad,
|
||||
others: &[fastfile_t6::ZoneLoad],
|
||||
xanims: &mut asset_anim::XAnimBuild,
|
||||
report: &mut Vec<String>,
|
||||
) -> asset_anim::PlayerAnimSources {
|
||||
let mut sources = asset_anim::PlayerAnimSources::default();
|
||||
for load in std::iter::once(load).chain(others) {
|
||||
for asset in &load.assets {
|
||||
if asset.ty != fastfile_t6::AssetType::RawFile {
|
||||
continue;
|
||||
}
|
||||
let Some(name) = header_str(load, &asset.header, 0).filter(|name| {
|
||||
[
|
||||
asset_anim::MULTIPLAYER_ANIMTREE_PATH,
|
||||
asset_anim::PLAYERANIM_SCRIPT_PATH,
|
||||
asset_anim::PLAYERANIM_TYPES_PATH,
|
||||
]
|
||||
.contains(name)
|
||||
}) else {
|
||||
continue;
|
||||
};
|
||||
let data = header_u32(&asset.header, 4).and_then(|size| {
|
||||
let ptr = decode_ptr(header_u32(&asset.header, 8)?)?;
|
||||
load.blocks.bytes(ptr, size as usize).ok()
|
||||
});
|
||||
let Some(data) = data else {
|
||||
report.push(format!(
|
||||
"T6 character animation source {name}: invalid raw file"
|
||||
));
|
||||
continue;
|
||||
};
|
||||
sources.capture(asset_core::FamilyId::T6, name, data, false);
|
||||
}
|
||||
}
|
||||
sources.compile();
|
||||
report.push(sources.compile_report_line());
|
||||
report.push(sources.parse_report_line());
|
||||
let mut wanted: std::collections::BTreeSet<_> = sources
|
||||
.compiled()
|
||||
.and_then(|tree| tree.as_ref().ok())
|
||||
.into_iter()
|
||||
.flat_map(|tree| tree.nodes())
|
||||
.filter(|node| node.child_count == 0)
|
||||
.map(|node| node.name.clone())
|
||||
.collect();
|
||||
let total = wanted.len();
|
||||
let mut captured = 0;
|
||||
for load in std::iter::once(load).chain(others) {
|
||||
for asset in &load.assets {
|
||||
if asset.ty == fastfile_t6::AssetType::XAnimParts
|
||||
&& header_str(load, &asset.header, 0)
|
||||
.is_some_and(|name| wanted.remove(&name.to_ascii_lowercase()))
|
||||
&& xanims.capture_xanim_t6(asset_core::FamilyId::T6, "", load, asset)
|
||||
{
|
||||
captured += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
report.push(format!(
|
||||
"T6 character animations: {captured} of {total} clips captured"
|
||||
));
|
||||
sources
|
||||
}
|
||||
|
||||
fn capture_sounds(
|
||||
path: &Path,
|
||||
load: &fastfile_t6::ZoneLoad,
|
||||
@@ -1303,6 +1380,12 @@ fn capture_effects(
|
||||
.map(|&name| name.to_owned())
|
||||
.collect();
|
||||
wanted.insert("maps/mp_maps/fx_mp_exp_bomb".to_owned());
|
||||
for load in zones {
|
||||
match destructible::effect_names(load) {
|
||||
Ok(names) => wanted.extend(names),
|
||||
Err(error) => content.report.push(error),
|
||||
}
|
||||
}
|
||||
loop {
|
||||
let before = content.fx.len();
|
||||
for &load in zones {
|
||||
@@ -1903,6 +1986,8 @@ impl ZoneLane for T6Lane {
|
||||
}
|
||||
let script_placements =
|
||||
asset_world::parse_script_model_placements(entity_string(&load)?);
|
||||
let destructibles = destructible::capture(&load)?;
|
||||
let destructible_models = destructible::model_names(&destructibles);
|
||||
let mut script_xmodels: Vec<asset_model::T6Model> = Vec::new();
|
||||
for asset in &load.assets {
|
||||
let Some(model) = asset_model::T6Model::new(&load, asset) else {
|
||||
@@ -1911,7 +1996,8 @@ impl ZoneLane for T6Lane {
|
||||
let Some(name) = model.name() else {
|
||||
continue;
|
||||
};
|
||||
if script_placements.iter().any(|p| p.model == name)
|
||||
if (script_placements.iter().any(|p| p.model == name)
|
||||
|| destructible_models.contains(name))
|
||||
&& !script_xmodels.iter().any(|seen| seen.name() == Some(name))
|
||||
{
|
||||
script_xmodels.push(model);
|
||||
@@ -2063,7 +2149,13 @@ impl ZoneLane for T6Lane {
|
||||
}
|
||||
let script_model_instances = script_placements
|
||||
.iter()
|
||||
.map(|placement| t6_script_model_instance(placement, &mut map_xmodel_scene_assets))
|
||||
.map(|placement| {
|
||||
t6_script_model_instance(
|
||||
placement,
|
||||
&mut map_xmodel_scene_assets,
|
||||
&destructibles,
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
report.push(format!(
|
||||
"T6 script models: placements={} models={}",
|
||||
@@ -2388,6 +2480,25 @@ impl ZoneLane for T6Lane {
|
||||
None
|
||||
}
|
||||
};
|
||||
let mut effect_content = T6Content {
|
||||
path: path.to_owned(),
|
||||
..Default::default()
|
||||
};
|
||||
capture_effects(&image_loads, &ipaks, &mut effect_content);
|
||||
let mut fx = asset_game::FxCatalog::default();
|
||||
let mut effect_refusals = Vec::new();
|
||||
report.push(common_compile::bind_t6_fx(
|
||||
&mut effect_content,
|
||||
&mut materials,
|
||||
&mut fx,
|
||||
&mut effect_refusals,
|
||||
));
|
||||
report.extend(effect_content.report);
|
||||
report.extend(
|
||||
effect_refusals
|
||||
.into_iter()
|
||||
.map(|refusal| format!("T6 map effects: {refusal:?}")),
|
||||
);
|
||||
let world = crate::session_load::PreparedWorld {
|
||||
min: draw.stats.min,
|
||||
max: draw.stats.max,
|
||||
@@ -2399,6 +2510,7 @@ impl ZoneLane for T6Lane {
|
||||
static_model_instances,
|
||||
map_xmodel_scene_assets,
|
||||
script_model_instances,
|
||||
fx,
|
||||
smodel_lighting_samples,
|
||||
policy: WorldDrawPolicy::t6(),
|
||||
intermission_view: asset_world::parse_intermission_view(&entities),
|
||||
@@ -2506,6 +2618,8 @@ impl ZoneLane for T6Lane {
|
||||
.collect();
|
||||
let mut content = capture_content(path, &load, &others, &ipaks);
|
||||
capture_xanims(&load, &others, &mut content);
|
||||
let player_anim_sources =
|
||||
capture_player_animation(&load, &others, &mut content.xanims, &mut report);
|
||||
let icon_loads: Vec<_> = others.iter().chain(std::iter::once(&load)).collect();
|
||||
let icons = capture_weapon_icons(path, &icon_loads, &ipaks, &mut report);
|
||||
|
||||
@@ -2519,6 +2633,7 @@ impl ZoneLane for T6Lane {
|
||||
));
|
||||
CommonCensus {
|
||||
ui_images: icons,
|
||||
player_anim_sources,
|
||||
weapons,
|
||||
material_population: material_seed,
|
||||
report,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
mod effects;
|
||||
mod models;
|
||||
use effects::bind_t6_fx;
|
||||
pub(super) use effects::bind_t6_fx;
|
||||
use models::bind_t6_content;
|
||||
|
||||
use crate::lane::{
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::lane::CommonDependencyRefusal;
|
||||
use asset_material::MaterialCatalog;
|
||||
|
||||
pub(super) fn bind_t6_fx(
|
||||
pub(in crate::lane::t6) fn bind_t6_fx(
|
||||
content: &mut super::super::T6Content,
|
||||
materials: &mut MaterialCatalog,
|
||||
catalog: &mut asset_game::FxCatalog,
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::sync::Arc;
|
||||
|
||||
use asset_world::world_t6::Reader;
|
||||
use fastfile_t6::{AssetType, LoadedAsset, Ptr, ZoneLoad};
|
||||
use xmodel_runtime::{T5DestructibleDef, T5DestructiblePiece, T5DestructibleStage};
|
||||
|
||||
pub(super) fn capture(load: &ZoneLoad) -> Result<BTreeMap<String, Arc<T5DestructibleDef>>, String> {
|
||||
load.assets
|
||||
.iter()
|
||||
.filter(|asset| asset.ty == AssetType::DestructibleDef)
|
||||
.map(|asset| {
|
||||
let reader = Reader(load);
|
||||
let name =
|
||||
super::header_str(load, &asset.header, 0).ok_or("T6 destructible name missing")?;
|
||||
let model = asset
|
||||
.field(4)
|
||||
.and_then(|index| load.assets.get(index))
|
||||
.and_then(|model| super::header_str(load, &model.header, 0))
|
||||
.ok_or_else(|| format!("T6 destructible {name}: model missing"))?;
|
||||
let count = Reader::word(&asset.header, 12)? as i32;
|
||||
if count <= 0 {
|
||||
return Err(format!("T6 destructible {name}: invalid piece count"));
|
||||
}
|
||||
let address = Reader::ptr(&asset.header, 16)?;
|
||||
reader.bytes(
|
||||
address,
|
||||
(count as usize)
|
||||
.checked_mul(312)
|
||||
.ok_or("T6 destructible pieces overflow")?,
|
||||
)?;
|
||||
let mut pieces = Vec::with_capacity(count as usize);
|
||||
for index in 0..count as u32 {
|
||||
let p = address.at(index * 312);
|
||||
let mut stages = Vec::with_capacity(5);
|
||||
for stage in 0..5 {
|
||||
let st = p.at(stage * 48);
|
||||
let bone = reader.u16(st)?;
|
||||
let show_bone = if bone == 0 {
|
||||
None
|
||||
} else {
|
||||
Some(
|
||||
load.script_string(bone)
|
||||
.ok_or("T6 destructible bone missing")?
|
||||
.to_owned(),
|
||||
)
|
||||
};
|
||||
let mut spawn_models = [None, None, None];
|
||||
for (slot, model) in spawn_models.iter_mut().enumerate() {
|
||||
*model = asset_name(load, asset, st.at(32 + slot as u32 * 4))?;
|
||||
}
|
||||
stages.push(T5DestructibleStage {
|
||||
show_bone,
|
||||
break_health: reader.f32(st.at(4))?,
|
||||
max_time: reader.f32(st.at(8))?,
|
||||
flags: reader.u32(st.at(12))?,
|
||||
break_effect: asset_name(load, asset, st.at(16))?,
|
||||
break_effect_looping: load
|
||||
.asset_in(asset, st.at(16))
|
||||
.and_then(|effect| super::header_u32(&effect.header, 20))
|
||||
.is_some_and(|duration| duration == i32::MAX as u32),
|
||||
break_sound: text(load, st.at(20))?.map(|name| format!("t6:{name}")),
|
||||
break_notify: text(load, st.at(24))?,
|
||||
loop_sound: text(load, st.at(28))?.map(|name| format!("t6:{name}")),
|
||||
spawn_models,
|
||||
has_phys_preset: reader.u32(st.at(44))? != 0,
|
||||
});
|
||||
}
|
||||
let mut hide_bones = [0; 5];
|
||||
for (word, bits) in hide_bones.iter_mut().enumerate() {
|
||||
*bits = reader.u32(p.at(292 + word as u32 * 4))?;
|
||||
}
|
||||
let launch = if let Some(c) = load
|
||||
.blocks
|
||||
.ptr_at(p.at(268))
|
||||
.map_err(|e| format!("T6 destructible constraints: {e:?}"))?
|
||||
{
|
||||
let data = c.at(8);
|
||||
if reader.u32(c.at(4))? > 0 && reader.u32(data.at(4))? == 7 {
|
||||
Some((reader.xyz(data.at(116))?, reader.f32(data.at(112))?))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
pieces.push(T5DestructiblePiece {
|
||||
stages: stages.try_into().unwrap(),
|
||||
parent_piece: reader.bytes(p.at(240), 1)?[0],
|
||||
parent_damage_percent: reader.f32(p.at(244))?,
|
||||
bullet_damage_scale: reader.f32(p.at(248))?,
|
||||
explosive_damage_scale: reader.f32(p.at(252))?,
|
||||
health: reader.u32(p.at(272))? as i32,
|
||||
launch,
|
||||
hide_bones,
|
||||
});
|
||||
}
|
||||
Ok((
|
||||
name.to_owned(),
|
||||
Arc::new(T5DestructibleDef {
|
||||
name: name.to_owned(),
|
||||
model: model.to_owned(),
|
||||
pieces,
|
||||
client_only: Reader::word(&asset.header, 20)? != 0,
|
||||
}),
|
||||
))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(super) fn model_names(
|
||||
definitions: &BTreeMap<String, Arc<T5DestructibleDef>>,
|
||||
) -> BTreeSet<&str> {
|
||||
definitions
|
||||
.values()
|
||||
.flat_map(|def| {
|
||||
std::iter::once(def.model.as_str()).chain(def.pieces.iter().flat_map(|piece| {
|
||||
piece
|
||||
.stages
|
||||
.iter()
|
||||
.flat_map(|stage| stage.spawn_models.iter().filter_map(Option::as_deref))
|
||||
}))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(super) fn effect_names(load: &ZoneLoad) -> Result<BTreeSet<String>, String> {
|
||||
Ok(capture(load)?
|
||||
.values()
|
||||
.flat_map(|def| {
|
||||
def.pieces.iter().flat_map(|piece| {
|
||||
piece
|
||||
.stages
|
||||
.iter()
|
||||
.filter_map(|stage| stage.break_effect.clone())
|
||||
})
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
fn asset_name(load: &ZoneLoad, owner: &LoadedAsset, slot: Ptr) -> Result<Option<String>, String> {
|
||||
if Reader(load).u32(slot)? == 0 {
|
||||
return Ok(None);
|
||||
}
|
||||
load.asset_in(owner, slot)
|
||||
.and_then(|asset| super::header_str(load, &asset.header, 0))
|
||||
.map(|name| Some(name.to_owned()))
|
||||
.ok_or_else(|| "T6 destructible asset reference missing".into())
|
||||
}
|
||||
|
||||
fn text(load: &ZoneLoad, slot: Ptr) -> Result<Option<String>, String> {
|
||||
load.blocks
|
||||
.ptr_at(slot)
|
||||
.map_err(|e| format!("T6 destructible string: {e:?}"))?
|
||||
.map(|p| Reader(load).text(p).map(str::to_owned))
|
||||
.transpose()
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
pub(crate) const BARRELS: &str = include_str!("barrels.gsc.txt");
|
||||
|
||||
pub(crate) const T6_EQUIPMENT: &str = include_str!("t6_equipment.gsc.txt");
|
||||
|
||||
pub(crate) const RADIATION: &str = r#"main()
|
||||
@@ -94,6 +96,10 @@ broken(event, attacker)
|
||||
self thread ignite(attacker);
|
||||
else if (event == "destructible_car_explosion" || event == "destructible_barrel_explosion" || event == "explode")
|
||||
self explode(attacker);
|
||||
else if (event == "explode_sm")
|
||||
self explode(attacker, 150);
|
||||
else if (event == "explode_lg")
|
||||
self explode(attacker, 450);
|
||||
}
|
||||
|
||||
ignite(attacker)
|
||||
@@ -103,14 +109,16 @@ ignite(attacker)
|
||||
self thread explode(attacker);
|
||||
}
|
||||
|
||||
explode(attacker)
|
||||
explode(attacker, radius)
|
||||
{
|
||||
if (!isdefined(radius))
|
||||
radius = 256;
|
||||
if (isdefined(self.wrecked) && self.wrecked)
|
||||
return;
|
||||
self.wrecked = true;
|
||||
self notify("wrecked");
|
||||
self damagepiece(20000, -1, attacker);
|
||||
self radiusdamage(self.origin, 256, 300, 75, attacker, "MOD_EXPLOSIVE");
|
||||
self radiusdamage(self.origin, radius, 300, 75, attacker, "MOD_EXPLOSIVE");
|
||||
earthquake(.5, .5, self.origin, 800);
|
||||
level.globalCarsDestroyed++;
|
||||
}
|
||||
|
||||
@@ -95,7 +95,9 @@ pub(super) struct CommonProducts {
|
||||
pub(super) fx_models: asset_game::FxModelCatalog,
|
||||
pub(super) impact_fx: Option<asset_game::OwnedFxImpactTable>,
|
||||
pub(super) t5_xanims: XAnimBuild,
|
||||
pub(super) iw5_player_anim_sources: asset_anim::PlayerAnimSources,
|
||||
pub(super) t5_player_anim_sources: asset_anim::PlayerAnimSources,
|
||||
pub(super) t6_player_anim_sources: asset_anim::PlayerAnimSources,
|
||||
pub(super) t5_fx: FxCatalog,
|
||||
pub(super) t5_impact_fx: Option<asset_game::OwnedFxImpactTable>,
|
||||
pub(super) iw5_materials: MaterialCatalog,
|
||||
@@ -633,6 +635,7 @@ async fn prepare_common(key: CommonKey) -> Arc<CommonSet> {
|
||||
fpv: iw5_fpv,
|
||||
world_guns: iw5_world_guns,
|
||||
xanims: iw5_xanims,
|
||||
player_anim_sources: iw5_player_anim_sources,
|
||||
materials: iw5_materials,
|
||||
stats_tables: iw5_census_stats,
|
||||
scene_models: iw5_scene_models,
|
||||
@@ -683,7 +686,8 @@ async fn prepare_common(key: CommonKey) -> Arc<CommonSet> {
|
||||
weapons.len(),
|
||||
weapons.namespace_count(asset_core::AssetNamespace::T5)
|
||||
));
|
||||
let (t6_weapons, preparation, t6_tables, t6_report, t6_ui_images) = t6_weapon_walk.await;
|
||||
let (t6_weapons, preparation, t6_tables, t6_report, t6_ui_images, t6_player_anim_sources) =
|
||||
t6_weapon_walk.await;
|
||||
common_report.extend(t6_report);
|
||||
weapons.absorb(t6_weapons);
|
||||
if let Some(compiler) = preparation {
|
||||
@@ -895,6 +899,8 @@ async fn prepare_common(key: CommonKey) -> Arc<CommonSet> {
|
||||
impact_fx: common_impact,
|
||||
t5_xanims,
|
||||
t5_player_anim_sources,
|
||||
iw5_player_anim_sources,
|
||||
t6_player_anim_sources,
|
||||
t5_fx,
|
||||
t5_impact_fx,
|
||||
iw5_materials,
|
||||
|
||||
@@ -545,6 +545,7 @@ pub(super) struct Iw5WeaponBundle {
|
||||
pub(super) fpv: FpvMeshBuild,
|
||||
pub(super) world_guns: WorldWeaponBuild,
|
||||
pub(super) xanims: XAnimBuild,
|
||||
pub(super) player_anim_sources: asset_anim::PlayerAnimSources,
|
||||
|
||||
pub(super) materials: MaterialCatalog,
|
||||
pub(super) stats_tables: Vec<asset_game::CapturedStringTable>,
|
||||
@@ -611,6 +612,7 @@ pub(super) fn walk_iw5_weapon_bundle(
|
||||
fpv: census.fpv,
|
||||
world_guns: census.world_weapons,
|
||||
xanims: census.xanims,
|
||||
player_anim_sources: census.player_anim_sources,
|
||||
materials: census.material_population,
|
||||
stats_tables: census.cac_tables,
|
||||
scene_models: census.scene_models,
|
||||
@@ -672,6 +674,7 @@ pub(super) fn walk_shared_iw5_common(
|
||||
fpv: census.fpv,
|
||||
world_guns: census.world_weapons,
|
||||
xanims: census.xanims,
|
||||
player_anim_sources: census.player_anim_sources,
|
||||
materials: MaterialCatalog::default(),
|
||||
stats_tables: census.cac_tables,
|
||||
scene_models: census.scene_models,
|
||||
@@ -898,20 +901,35 @@ pub(super) fn walk_t6_weapon_bundle(
|
||||
Vec<asset_game::CapturedStringTable>,
|
||||
Vec<String>,
|
||||
Vec<(String, asset_material::material_images::ZoneUiImage)>,
|
||||
asset_anim::PlayerAnimSources,
|
||||
) {
|
||||
let mut report = Vec::new();
|
||||
let root = match games_root_from_env() {
|
||||
Ok(root) => root,
|
||||
Err(error) => {
|
||||
report.push(format!("t6 weapons: {error}"));
|
||||
return (WeaponBuild::default(), None, Vec::new(), report, Vec::new());
|
||||
return (
|
||||
WeaponBuild::default(),
|
||||
None,
|
||||
Vec::new(),
|
||||
report,
|
||||
Vec::new(),
|
||||
Default::default(),
|
||||
);
|
||||
}
|
||||
};
|
||||
let donor = match find_common_mp_for_envelope(&root, fastfile_t6::ZONE_VERSION_PC) {
|
||||
Ok(donor) => donor,
|
||||
Err(error) => {
|
||||
report.push(format!("t6 weapons: {error}"));
|
||||
return (WeaponBuild::default(), None, Vec::new(), report, Vec::new());
|
||||
return (
|
||||
WeaponBuild::default(),
|
||||
None,
|
||||
Vec::new(),
|
||||
report,
|
||||
Vec::new(),
|
||||
Default::default(),
|
||||
);
|
||||
}
|
||||
};
|
||||
let stage = progress.begin_scoped(StageId::CommonAssets, "t6_weapons", None);
|
||||
@@ -924,7 +942,14 @@ pub(super) fn walk_t6_weapon_bundle(
|
||||
"t6 weapons: open {}: {error}",
|
||||
donor.path.display()
|
||||
));
|
||||
return (WeaponBuild::default(), None, Vec::new(), report, Vec::new());
|
||||
return (
|
||||
WeaponBuild::default(),
|
||||
None,
|
||||
Vec::new(),
|
||||
report,
|
||||
Vec::new(),
|
||||
Default::default(),
|
||||
);
|
||||
}
|
||||
};
|
||||
let tables = t6_class_tables(&root, &mut report);
|
||||
@@ -944,6 +969,7 @@ pub(super) fn walk_t6_weapon_bundle(
|
||||
tables,
|
||||
report,
|
||||
census.ui_images,
|
||||
census.player_anim_sources,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -91,6 +91,8 @@ pub(super) async fn walk_prepared_match(
|
||||
impact_fx: common_impact,
|
||||
t5_xanims,
|
||||
t5_player_anim_sources,
|
||||
iw5_player_anim_sources,
|
||||
t6_player_anim_sources,
|
||||
t5_fx,
|
||||
t5_impact_fx,
|
||||
iw5_materials,
|
||||
@@ -185,8 +187,13 @@ pub(super) async fn walk_prepared_match(
|
||||
gaps,
|
||||
} = loaded;
|
||||
world.source_namespace = map_namespace;
|
||||
if map_family == asset_core::FamilyId::T5 {
|
||||
player_anim_sources = t5_player_anim_sources;
|
||||
if player_anim_sources.family() != Some(map_family) {
|
||||
player_anim_sources = match map_family {
|
||||
asset_core::FamilyId::Iw5 => iw5_player_anim_sources,
|
||||
asset_core::FamilyId::T5 => t5_player_anim_sources,
|
||||
asset_core::FamilyId::T6 => t6_player_anim_sources,
|
||||
_ => asset_anim::PlayerAnimSources::default(),
|
||||
};
|
||||
player_anim_sources.compile();
|
||||
report.push(player_anim_sources.compile_report_line());
|
||||
report.push(player_anim_sources.parse_report_line());
|
||||
@@ -283,6 +290,7 @@ pub(super) async fn walk_prepared_match(
|
||||
};
|
||||
let mut scripts = iw4_scripts;
|
||||
scripts.overlay(map_scripts);
|
||||
scripts.insert_source("iw4l_maps/barrels", crate::map_scripts::BARRELS.to_owned());
|
||||
scripts.insert_source(
|
||||
"iw4l_t6/equipment",
|
||||
crate::map_scripts::T6_EQUIPMENT.to_owned(),
|
||||
|
||||
@@ -67,6 +67,10 @@ pub(crate) fn update(
|
||||
});
|
||||
sources.desired.clear();
|
||||
for (row, alias) in speaking {
|
||||
let (ns, alias) = alias
|
||||
.split_once(':')
|
||||
.and_then(|(family, name)| AssetNamespace::parse(family).map(|ns| (ns, name)))
|
||||
.unwrap_or((ns, alias));
|
||||
let owner = row.owner.to_wire();
|
||||
let changed = sources.live.get(&owner).is_none_or(|current| {
|
||||
current.alias != alias || current.namespace != ns || current.emitter != row.snd_ent
|
||||
|
||||
@@ -35,7 +35,10 @@ pub(crate) fn load_user_settings(
|
||||
};
|
||||
persistence.path = Some(path.clone());
|
||||
match fs::read_to_string(&path) {
|
||||
Ok(source) => parse_settings(&source, &mut settings, &mut binds),
|
||||
Ok(source) => {
|
||||
parse_settings(&source, &mut settings, &mut binds);
|
||||
persistence.last_payload = Some(source);
|
||||
}
|
||||
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
|
||||
Err(error) => {
|
||||
// Do not replace settings we could not read with session defaults.
|
||||
@@ -47,7 +50,9 @@ pub(crate) fn load_user_settings(
|
||||
}
|
||||
}
|
||||
settings.sanitize();
|
||||
persistence.last_payload = Some(serialize_settings(&settings, &binds));
|
||||
if persistence.last_payload.is_none() {
|
||||
persistence.last_payload = Some(serialize_settings(&settings, &binds));
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn consume_menu_binding(
|
||||
@@ -308,7 +313,7 @@ fn parse_into<T: std::str::FromStr>(value: &str, slot: &mut T) {
|
||||
fn serialize_settings(settings: &frame::GameSettings, binds: &KeyBinds) -> String {
|
||||
let safe_name = settings.player_name.replace(['\n', '\r', '='], " ");
|
||||
let mut lines = vec![
|
||||
"// IW4L user settings v2".to_owned(),
|
||||
"// IW4L user settings v3".to_owned(),
|
||||
format!(
|
||||
"resolution={}x{}",
|
||||
settings.resolution.width, settings.resolution.height
|
||||
@@ -454,6 +459,22 @@ fn parse_settings(source: &str, settings: &mut frame::GameSettings, binds: &mut
|
||||
warn!("settings bind: {warning}");
|
||||
}
|
||||
}
|
||||
if !source
|
||||
.lines()
|
||||
.any(|line| line.trim() == "// IW4L user settings v3")
|
||||
{
|
||||
for (key, command) in [
|
||||
(KeyCode::KeyT, "chatmodepublic"),
|
||||
(KeyCode::KeyY, "chatmodeteam"),
|
||||
] {
|
||||
if let Some(id) = input_iw4::command_id_lookup(command)
|
||||
&& binds.get(BindButton::Key(key)).is_none()
|
||||
&& !binds.iter().any(|(_, bound)| bound == id)
|
||||
{
|
||||
binds.set(BindButton::Key(key), id);
|
||||
}
|
||||
}
|
||||
}
|
||||
if !binds.has_pad_binds() {
|
||||
binds.apply_pad_layout(usize::from(settings.pad_layout.min(4)));
|
||||
}
|
||||
|
||||
@@ -74,7 +74,7 @@ pub fn beam_segment_count(length: f32, screw_dist: f32) -> u8 {
|
||||
if screw_dist < 1e-5 {
|
||||
return 1;
|
||||
}
|
||||
let n = libm::roundf(length / screw_dist) as i32;
|
||||
let n = (length / screw_dist * 8.0) as i32;
|
||||
n.clamp(1, i32::from(FX_BEAM_MAX_SEGMENTS)) as u8
|
||||
}
|
||||
|
||||
@@ -332,7 +332,7 @@ pub fn beam_generate_verts(
|
||||
let begin_c = beam_sample_color(&beam.colors, 0.0);
|
||||
verts[0] = FxBeamVert {
|
||||
xyz: sub(beam.begin, scale(flat, beam.begin_radius)),
|
||||
uv: [0.0, 0.5],
|
||||
uv: [0.0, 0.0],
|
||||
color: begin_c,
|
||||
};
|
||||
let mut w = 1usize;
|
||||
@@ -349,12 +349,12 @@ pub fn beam_generate_verts(
|
||||
let c = beam_sample_color(&beam.colors, alpha);
|
||||
verts[w] = FxBeamVert {
|
||||
xyz: sub(base, scale(perp, radius)),
|
||||
uv: [alpha, 0.0],
|
||||
uv: [0.0, 1.0],
|
||||
color: c,
|
||||
};
|
||||
verts[w + 1] = FxBeamVert {
|
||||
xyz: add(base, scale(perp, radius)),
|
||||
uv: [alpha, 1.0],
|
||||
uv: [1.0, 0.0],
|
||||
color: c,
|
||||
};
|
||||
w += 2;
|
||||
@@ -362,7 +362,7 @@ pub fn beam_generate_verts(
|
||||
let end_c = beam_sample_color(&beam.colors, 1.0);
|
||||
verts[w] = FxBeamVert {
|
||||
xyz: add(beam.end, scale(flat, beam.end_radius)),
|
||||
uv: [1.0, 0.5],
|
||||
uv: [1.0, 1.0],
|
||||
color: end_c,
|
||||
};
|
||||
let wrote_idx = write_beam_indices(seg_count, indices);
|
||||
|
||||
+1
-10
@@ -61,20 +61,11 @@ fn send_text(
|
||||
.position(|b| *b == 0)
|
||||
.unwrap_or(meta.name.len());
|
||||
let name = clean(&String::from_utf8_lossy(&meta.name[..end]));
|
||||
let state = match meta.lifecycle {
|
||||
ClientLifecycle::Spectating => "(Spectator) ",
|
||||
ClientLifecycle::Dead | ClientLifecycle::RespawnPending => "(Dead) ",
|
||||
_ => "",
|
||||
};
|
||||
let text = clean(text);
|
||||
if text.trim().is_empty() {
|
||||
return false;
|
||||
}
|
||||
let channel = if team_only { "(Team) " } else { "" };
|
||||
let line = format!(
|
||||
"{state}{channel}{name}{}: {text}",
|
||||
crate::HUD_PRINT_ARG_SEPARATOR
|
||||
);
|
||||
let line = format!("{name}{}: {text}", crate::HUD_PRINT_ARG_SEPARATOR);
|
||||
let dead_chat = world
|
||||
.ecs()
|
||||
.get_resource::<crate::script::MatchScript>()
|
||||
|
||||
@@ -320,13 +320,14 @@ pub(crate) fn destructible_effect(
|
||||
name: String,
|
||||
origin: [f32; 3],
|
||||
direction: [f32; 3],
|
||||
) {
|
||||
up: [f32; 3],
|
||||
) -> Option<u64> {
|
||||
let now = super::players::now_ms(world);
|
||||
let Some(mut runtime) = world.get_resource_mut::<RoundScript>() else {
|
||||
return;
|
||||
return None;
|
||||
};
|
||||
let Ok(id) = runtime.create_entity(super::entities::EntityKind::Spawned, "script_model") else {
|
||||
return;
|
||||
return None;
|
||||
};
|
||||
let forward = glam::Vec3::from_array(direction)
|
||||
.try_normalize()
|
||||
@@ -337,12 +338,19 @@ pub(crate) fn destructible_effect(
|
||||
name,
|
||||
origin,
|
||||
forward: forward.to_array(),
|
||||
up: forward.any_orthonormal_vector().to_array(),
|
||||
up,
|
||||
start_ms: Some(now as i32),
|
||||
repeat_ms: 0,
|
||||
cull_distance: 0.0,
|
||||
},
|
||||
);
|
||||
Some(id)
|
||||
}
|
||||
|
||||
pub(crate) fn stop_destructible_effect(world: &mut World, id: u64) {
|
||||
if let Some(mut runtime) = world.get_resource_mut::<RoundScript>() {
|
||||
runtime.delete_entity(id);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn set_destructible_model(
|
||||
|
||||
@@ -53,5 +53,5 @@ pub(crate) use vm::state::{Frame, Thread, ThreadState, Waiter, WaiterKind};
|
||||
|
||||
pub(crate) use host::entity_damage::{
|
||||
destructible_attacker, destructible_callback, destructible_debris, destructible_effect,
|
||||
set_destructible_model,
|
||||
set_destructible_model, stop_destructible_effect,
|
||||
};
|
||||
|
||||
@@ -8,6 +8,7 @@ pub(crate) struct State {
|
||||
definition: Arc<T5DestructibleDef>,
|
||||
health: Vec<i16>,
|
||||
timers: Vec<(usize, usize, f32, i32)>,
|
||||
effects: Vec<(usize, u64)>,
|
||||
initial_model: String,
|
||||
initial_hide_parts: xmodel_runtime::HidePartBits,
|
||||
}
|
||||
@@ -17,6 +18,7 @@ pub fn install(dobj: &mut AuthorityDObjState, definition: Arc<T5DestructibleDef>
|
||||
health: definition.pieces.iter().map(|p| p.health as i16).collect(),
|
||||
definition,
|
||||
timers: Vec::new(),
|
||||
effects: Vec::new(),
|
||||
initial_model: dobj.current_model.clone(),
|
||||
initial_hide_parts: dobj.semantic_state.hide_part_bits,
|
||||
});
|
||||
@@ -178,10 +180,20 @@ fn publish(
|
||||
.as_ref()
|
||||
.and_then(|cap| cap.pose(&dobj.pose_request, dobj.world_from_model).ok());
|
||||
let mut events = Vec::new();
|
||||
let mut effects = Vec::new();
|
||||
let mut stopped = Vec::new();
|
||||
let mut debris = Vec::new();
|
||||
for event in breaks {
|
||||
let piece = &definition.pieces[event.piece];
|
||||
let st = &piece.stages[event.stage];
|
||||
state.effects.retain(|(piece, id)| {
|
||||
if *piece == event.piece {
|
||||
stopped.push(*id);
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
});
|
||||
|
||||
let bone = dobj.capability.as_ref().and_then(|cap| {
|
||||
cap.pose
|
||||
@@ -256,12 +268,17 @@ fn publish(
|
||||
if let Some(notify) = &st.break_notify {
|
||||
callbacks.push(notify.clone());
|
||||
}
|
||||
if let Some(fx) = &st.break_effect {
|
||||
events.push((
|
||||
entity_iw4::EntityEventKind::PLAY_FX,
|
||||
if let Some(fx) = st.break_effect.as_ref().filter(|_| {
|
||||
!st.break_effect_looping
|
||||
|| piece.stage(event.piece, state.health[event.piece]) == Some(event.stage + 1)
|
||||
}) {
|
||||
effects.push((
|
||||
event.piece,
|
||||
st.break_effect_looping,
|
||||
fx.clone(),
|
||||
origin,
|
||||
direction,
|
||||
matrix.z_axis.truncate().normalize().to_array(),
|
||||
));
|
||||
}
|
||||
if let Some(sound) = &st.break_sound {
|
||||
@@ -274,6 +291,9 @@ fn publish(
|
||||
}
|
||||
}
|
||||
update_hide_parts(dobj);
|
||||
for id in stopped {
|
||||
crate::script::stop_destructible_effect(world.ecs(), id);
|
||||
}
|
||||
for (part, offset, half, velocity) in debris {
|
||||
crate::script::destructible_debris(world.ecs(), part, offset, half, velocity);
|
||||
}
|
||||
@@ -309,11 +329,21 @@ fn publish(
|
||||
],
|
||||
);
|
||||
}
|
||||
for (kind, name, origin, direction) in events {
|
||||
if kind == entity_iw4::EntityEventKind::PLAY_FX {
|
||||
crate::script::destructible_effect(world.ecs(), name, origin, direction);
|
||||
continue;
|
||||
for (piece, looping, name, origin, direction, up) in effects {
|
||||
if let Some(id) =
|
||||
crate::script::destructible_effect(world.ecs(), name, origin, direction, up)
|
||||
&& looping
|
||||
&& let Some(state) = world
|
||||
.entity_collision_capabilities_mut()
|
||||
.iter_mut()
|
||||
.find(|row| row.owner == owner)
|
||||
.and_then(|row| row.dobj.as_mut())
|
||||
.and_then(|dobj| dobj.t5_destructible.as_mut())
|
||||
{
|
||||
state.effects.push((piece, id));
|
||||
}
|
||||
}
|
||||
for (kind, name, origin, direction) in events {
|
||||
let index = world.sound_alias_index(&name);
|
||||
world.push_entity_event(
|
||||
tick,
|
||||
@@ -528,6 +558,7 @@ pub(crate) fn restart(world: &mut FrameWorld) {
|
||||
.map(|p| p.health as i16)
|
||||
.collect();
|
||||
state.timers.clear();
|
||||
state.effects.clear();
|
||||
dobj.replace_model(&model, capability);
|
||||
dobj.semantic_state.hide_part_bits = hide;
|
||||
dobj.pose_request.hide_part_bits = hide;
|
||||
|
||||
@@ -88,6 +88,34 @@ pub fn install_frontend_menus(catalog: &mut asset_game::MenuCatalog) -> Result<(
|
||||
{
|
||||
item.choices = crate::CommunityServers::default().choices;
|
||||
}
|
||||
let chat_bindings: Vec<_> = catalog
|
||||
.get("pc_options_actions")
|
||||
.into_iter()
|
||||
.flat_map(|menu| &menu.items)
|
||||
.filter(|item| {
|
||||
matches!(
|
||||
item.name.as_str(),
|
||||
"public_chat_binding"
|
||||
| "public_chat_label"
|
||||
| "team_chat_binding"
|
||||
| "team_chat_label"
|
||||
)
|
||||
})
|
||||
.cloned()
|
||||
.map(|mut item| {
|
||||
item.rect.y = if item.name.starts_with("public_") {
|
||||
48.0
|
||||
} else {
|
||||
68.0
|
||||
};
|
||||
item.vis_exp = "1".into();
|
||||
item.disabled_exp = "0".into();
|
||||
item
|
||||
})
|
||||
.collect();
|
||||
if let Some(menu) = catalog.menus.get_mut("options_multi") {
|
||||
menu.items.extend(chat_bindings);
|
||||
}
|
||||
let slider = catalog
|
||||
.get("pc_options_video")
|
||||
.and_then(|menu| {
|
||||
|
||||
@@ -25,6 +25,7 @@ pub struct T5DestructibleStage {
|
||||
pub max_time: f32,
|
||||
pub flags: u32,
|
||||
pub break_effect: Option<String>,
|
||||
pub break_effect_looping: bool,
|
||||
pub break_sound: Option<String>,
|
||||
pub break_notify: Option<String>,
|
||||
pub loop_sound: Option<String>,
|
||||
|
||||
+4
-2
@@ -48,11 +48,13 @@ A hand-typed `quit`/`exit`/`disconnect` goes further: it jumps **every** hold,
|
||||
`wait 60s` included, and releases it. A `quit` written into `--cmds` does not —
|
||||
it is part of the script and waits its turn.
|
||||
|
||||
Public chat is **T**, team chat is **Y**. Controls → Actions can rebind both;
|
||||
Public chat is **T**, team chat is **Y**. Multiplayer settings and Controls →
|
||||
Actions can rebind both;
|
||||
`bind T chatmodepublic` / `bind Y chatmodeteam` also persist. Enter sends,
|
||||
Escape cancels; arrows, Home/End, Backspace/Delete edit the message.
|
||||
`say <message>` and `say_team <message>` send from the console. Chat lasts
|
||||
eight seconds, shows five recent messages, and uses team colors for names.
|
||||
eight seconds, shows five recent messages, and uses team colors for names with white
|
||||
message text. Lines contain only `nickname: message`, without status prefixes.
|
||||
|
||||
## Verbs and traps
|
||||
|
||||
|
||||
Reference in New Issue
Block a user