Restore native soldiers, barrels, chat and tracers; align weapon unlock admission

This commit is contained in:
vtroc7
2026-10-12 00:00:38 +03:00
parent 08b4224efd
commit efe6db6981
32 changed files with 751 additions and 83 deletions
+33 -9
View File
@@ -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(),
));
}
}
+26
View File
@@ -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));
+25 -9
View File
@@ -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,
+4
View File
@@ -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)
}
+11 -1
View File
@@ -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])> {
+5
View File
@@ -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,
+2 -2
View File
@@ -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}\" );");
}
+74
View File
@@ -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");
}
+6 -2
View File
@@ -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,
});
+1
View File
@@ -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,
+52 -2
View File
@@ -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,
+119 -4
View File
@@ -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(&current_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 -1
View File
@@ -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,
+157
View File
@@ -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()
}
+10 -2
View File
@@ -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,
+29 -3
View File
@@ -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,
)
}
+10 -2
View File
@@ -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(),
+4
View File
@@ -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
+24 -3
View File
@@ -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)));
}
+5 -5
View File
@@ -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
View File
@@ -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>()
+12 -4
View File
@@ -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(
+1 -1
View File
@@ -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,
};
+38 -7
View File
@@ -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;
+28
View File
@@ -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
View File
@@ -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