mirror of
https://github.com/vladtrc/iw4L
synced 2026-10-26 02:11:39 -04:00
Add Black Ops II weapon attachments (visible models)
Every attachment a T6 weapon offers, alone or in a compatible pair, is a weapon row (`an94_acog_grip`) built from the base row and the weapon's WeaponAttachmentUnique assets: the attachment's first-person and world models at the weapon's own placement, hidden gun bones, clip overrides, fire sounds and alternate weapon. Stat-only attachments make rows that change nothing shown yet. - fastfile_t6: AttachmentUniqueView; loaded assets record the asset each pointer field named at load time, since walk memory is reused. - Each attachment model is captured per weapon (`model@unique`) with its offset as root rest and the gun bone it hangs from (ModelSkel::mount_tag). - T6 guns hide parts by collapsing bones in skinning: their sights share the body's surfaces. - Bare T6 rows hide what their `none` unique hides. - The full T6 attachment table loads; stats-table names like `acog_sniper` map to `acog`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -44,6 +44,18 @@ pub struct FpvAssembly {
|
||||
pub paired_bones: usize,
|
||||
pub combined_hands: bool,
|
||||
pub tags: FpvAssemblyTags,
|
||||
/// Gun bones whose vertices fold away (see [`FpvHideMode::Bones`]).
|
||||
pub collapsed_bones: Vec<usize>,
|
||||
}
|
||||
|
||||
/// How a gun hides its hide tags. IW4 guns keep hideable parts in
|
||||
/// surfaces of their own and skip those surfaces; a T6 gun's sights share
|
||||
/// its body's surfaces, and T6 folds the hidden bones' vertices away.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
|
||||
pub enum FpvHideMode {
|
||||
#[default]
|
||||
Surfaces,
|
||||
Bones,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -72,6 +84,7 @@ pub struct FpvAssemblyKey {
|
||||
pub rocket: Option<FpvMeshIndex>,
|
||||
pub knife: Option<FpvMeshIndex>,
|
||||
pub hide_tags: Vec<String>,
|
||||
pub hide_mode: FpvHideMode,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
@@ -117,6 +130,7 @@ impl FpvAssembly {
|
||||
rocket: bool,
|
||||
knife: Option<FpvMeshIndex>,
|
||||
hide_tags: &[String],
|
||||
hide_mode: FpvHideMode,
|
||||
) -> Result<Self, FpvAssemblyError> {
|
||||
let pose_of = |model: FpvMeshIndex| -> Result<&ModelPoseSrc, FpvAssemblyError> {
|
||||
catalog
|
||||
@@ -203,18 +217,31 @@ impl FpvAssembly {
|
||||
tracker_light: tag("tag_motion_tracker_fx"),
|
||||
};
|
||||
let paired_bones = specs[0].0.num_bones + specs[1].0.num_bones;
|
||||
let mut collapsed_bones = Vec::new();
|
||||
let parts = parts
|
||||
.into_iter()
|
||||
.zip(&dobj.models)
|
||||
.map(|((model, role, _), slot)| FpvAssemblyPart {
|
||||
model,
|
||||
role,
|
||||
bone_base: slot.base,
|
||||
hide: if role == FpvPartRole::Gun {
|
||||
hide_words(catalog, model, hide_tags)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
.map(|((model, role, _), slot)| {
|
||||
let hide = (role == FpvPartRole::Gun)
|
||||
.then(|| hide_words(catalog, model, hide_tags))
|
||||
.flatten();
|
||||
let hide = match (hide_mode, hide) {
|
||||
(FpvHideMode::Bones, Some(words)) => {
|
||||
collapsed_bones.extend(
|
||||
(0..slot.bone_count)
|
||||
.filter(|&bone| words[bone >> 5] & (0x8000_0000u32 >> (bone & 31)) != 0)
|
||||
.map(|bone| slot.base + bone),
|
||||
);
|
||||
None
|
||||
}
|
||||
(_, hide) => hide,
|
||||
};
|
||||
FpvAssemblyPart {
|
||||
model,
|
||||
role,
|
||||
bone_base: slot.base,
|
||||
hide,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
Ok(Self {
|
||||
@@ -225,6 +252,7 @@ impl FpvAssembly {
|
||||
paired_bones,
|
||||
combined_hands: mounts.secondary_gun.is_some(),
|
||||
tags,
|
||||
collapsed_bones,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -481,6 +481,11 @@ pub struct CatalogWeapon {
|
||||
/// Models a T6 weapon attaches to its first-person and world guns (its
|
||||
/// magazine, a sniper's scope).
|
||||
pub attached_models: [Vec<String>; 2],
|
||||
/// Of those, the magazine (attachment slot 6), which a fast-mag
|
||||
/// attachment replaces.
|
||||
pub t6_clip_models: [Option<String>; 2],
|
||||
/// What each T6 attachment the weapon takes does to it.
|
||||
pub t6_attachments: Vec<T6Attachment>,
|
||||
pub iw5_reload_overrides: Vec<fastfile_iw5::ReloadOverride>,
|
||||
pub iw5_anim_overrides: Vec<LeftoverAnimOverride>,
|
||||
pub iw5_fx_overrides: Vec<Iw5FxOverride>,
|
||||
@@ -1376,6 +1381,8 @@ impl WeaponCatalog {
|
||||
scope_rows: Default::default(),
|
||||
iw5_attachment_slots: std::array::from_fn(|_| None),
|
||||
attached_models: Default::default(),
|
||||
t6_clip_models: Default::default(),
|
||||
t6_attachments: Vec::new(),
|
||||
iw5_reload_overrides: Vec::new(),
|
||||
iw5_anim_overrides: Vec::new(),
|
||||
iw5_fx_overrides: Vec::new(),
|
||||
@@ -2134,6 +2141,8 @@ impl WeaponCatalog {
|
||||
scope_rows,
|
||||
iw5_attachment_slots,
|
||||
attached_models: Default::default(),
|
||||
t6_clip_models: Default::default(),
|
||||
t6_attachments: Vec::new(),
|
||||
iw5_reload_overrides,
|
||||
iw5_anim_overrides: leftover_anim_overrides,
|
||||
iw5_fx_overrides,
|
||||
@@ -2271,6 +2280,8 @@ impl WeaponCatalog {
|
||||
scope_rows: Default::default(),
|
||||
iw5_attachment_slots: std::array::from_fn(|_| None),
|
||||
attached_models: Default::default(),
|
||||
t6_clip_models: Default::default(),
|
||||
t6_attachments: Vec::new(),
|
||||
iw5_reload_overrides: Vec::new(),
|
||||
iw5_anim_overrides: Vec::new(),
|
||||
iw5_fx_overrides: Vec::new(),
|
||||
@@ -2406,6 +2417,15 @@ impl WeaponCatalog {
|
||||
scope_rows: Default::default(),
|
||||
iw5_attachment_slots: std::array::from_fn(|_| None),
|
||||
attached_models: [true, false].map(|view| t6_attached_models(weapon, view)),
|
||||
t6_clip_models: [true, false].map(|view| {
|
||||
weapon
|
||||
.attached_model(T6_CLIP_SLOT, view)
|
||||
.map(|(name, _, _)| t6_model_name(name))
|
||||
}),
|
||||
t6_attachments: weapon
|
||||
.attachment_uniques()
|
||||
.filter_map(|unique| capture_t6_attachment(unique))
|
||||
.collect(),
|
||||
iw5_reload_overrides: Vec::new(),
|
||||
iw5_anim_overrides: Vec::new(),
|
||||
iw5_fx_overrides: Vec::new(),
|
||||
@@ -2777,6 +2797,13 @@ pub const T6_XANIM_PREFIX: &str = "t6_";
|
||||
/// for T5, named with [`T6_XANIM_PREFIX`]. A T6 row keeps these only when it shows its own first-person
|
||||
/// model, see [`WeaponBuild::dress_t6_stand_ins`].
|
||||
fn t6_sz_xanims(w: fastfile_t6::weapon::WeaponView<'_>) -> [Option<String>; WEAPON_ANIM_SLOTS] {
|
||||
t6_sz_xanims_by(|slot| w.xanim(slot))
|
||||
}
|
||||
|
||||
/// [`t6_sz_xanims`] over any T6 `szXAnims` (a weapon's or an attachment's).
|
||||
fn t6_sz_xanims_by<'a>(
|
||||
xanim: impl Fn(u32) -> Option<&'a str>,
|
||||
) -> [Option<String>; WEAPON_ANIM_SLOTS] {
|
||||
use fastfile_t6::weapon::weap_anim as t6_anim;
|
||||
const PAIRS: [(usize, usize); 33] = [
|
||||
(t6_anim::IDLE, weap_anim::IDLE),
|
||||
@@ -2820,8 +2847,7 @@ fn t6_sz_xanims(w: fastfile_t6::weapon::WeaponView<'_>) -> [Option<String>; WEAP
|
||||
for (src, dst) in PAIRS {
|
||||
// `FIRE_INTRO` stands in for `FIRE` only where there is no `FIRE`.
|
||||
if out[dst].is_none() {
|
||||
out[dst] = w
|
||||
.xanim(src as u32)
|
||||
out[dst] = xanim(src as u32)
|
||||
.filter(|name| !name.is_empty())
|
||||
.map(|name| format!("{T6_XANIM_PREFIX}{}", name.to_ascii_lowercase()));
|
||||
}
|
||||
@@ -3590,6 +3616,120 @@ fn capture_t6_sounds(w: fastfile_t6::weapon::WeaponView<'_>) -> WeaponSoundAlias
|
||||
|
||||
/// The models a T6 weapon attaches to its first-person (`view`) or world
|
||||
/// gun, by slot.
|
||||
/// The `attachViewModel` slot T6 weapons hold their magazine in.
|
||||
const T6_CLIP_SLOT: u32 = 6;
|
||||
|
||||
/// One T6 attachment (or authored pair of attachments) as one weapon
|
||||
/// takes it, read from its `WeaponAttachmentUnique`.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct T6Attachment {
|
||||
/// `au_an94_acog`, `au_an94_acog+grip`.
|
||||
pub name: String,
|
||||
/// The attachment table's index of the attachment (of a pair, the one
|
||||
/// it is filed under).
|
||||
pub kind: u32,
|
||||
/// For a pair, `1 << kind` of both; zero for one attachment.
|
||||
pub mask: u32,
|
||||
/// The weapon the attachment switches to (`gl_an94_mp`).
|
||||
pub alt_weapon: Option<String>,
|
||||
/// First-person and world models, named as their placed copies (see
|
||||
/// [`t6_attachment_models`]).
|
||||
pub models: [Vec<String>; 2],
|
||||
/// Gun bones the attachment hides (iron sights under an optic).
|
||||
pub hide_tags: Vec<String>,
|
||||
/// Clips the attachment plays instead of the weapon's, in IW4 slots.
|
||||
pub xanims: [Option<String>; WEAPON_ANIM_SLOTS],
|
||||
pub fire_sound: Option<String>,
|
||||
pub fire_sound_player: Option<String>,
|
||||
/// Removes the weapon's own attached models (a sniper's scope) but its
|
||||
/// magazine.
|
||||
pub disable_base_attachment: bool,
|
||||
/// Removes the weapon's magazine (a fast mag carries its own).
|
||||
pub disable_base_clip: bool,
|
||||
}
|
||||
|
||||
/// A T6 attachment model placed for one weapon: the name its copy is
|
||||
/// captured under, the model it copies, the gun bone it hangs from
|
||||
/// (`None` for the root) and its offset and `(pitch, yaw, roll)` there.
|
||||
pub struct T6AttachmentModel {
|
||||
pub copy: String,
|
||||
pub model: String,
|
||||
pub tag: Option<String>,
|
||||
pub offset: [f32; 3],
|
||||
pub angles: [f32; 3],
|
||||
}
|
||||
|
||||
/// The models `unique` hangs on the first-person (`view`) or world gun.
|
||||
/// The same model sits differently on every weapon, so each weapon's is a
|
||||
/// copy of its own, named `model@unique`.
|
||||
pub fn t6_attachment_models(
|
||||
unique: fastfile_t6::weapon::AttachmentUniqueView<'_>,
|
||||
view: bool,
|
||||
) -> Vec<T6AttachmentModel> {
|
||||
let Some(owner) = unique.name() else {
|
||||
return Vec::new();
|
||||
};
|
||||
unique
|
||||
.models(view)
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.map(|(model, tag, offset, angles)| {
|
||||
let model = t6_model_name(model);
|
||||
T6AttachmentModel {
|
||||
copy: format!("{model}@{owner}"),
|
||||
model,
|
||||
tag: tag.map(str::to_owned),
|
||||
offset,
|
||||
angles,
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn capture_t6_attachment(unique: fastfile_t6::weapon::AttachmentUniqueView<'_>) -> Option<T6Attachment> {
|
||||
use fastfile_t6::weapon::unique as u;
|
||||
let name = unique.name()?.to_owned();
|
||||
Some(T6Attachment {
|
||||
kind: unique.attachment_type(),
|
||||
mask: unique.combined_mask(),
|
||||
alt_weapon: unique.alt_weapon().map(str::to_owned),
|
||||
models: [true, false].map(|view| {
|
||||
t6_attachment_models(unique, view)
|
||||
.into_iter()
|
||||
.map(|model| model.copy)
|
||||
.collect()
|
||||
}),
|
||||
hide_tags: unique.hide_tags().map(str::to_owned).collect(),
|
||||
xanims: t6_sz_xanims_by(|slot| unique.xanim(slot)),
|
||||
fire_sound: unique.sound(u::FIRE_SOUND).map(str::to_owned),
|
||||
fire_sound_player: unique.sound(u::FIRE_SOUND_PLAYER).map(str::to_owned),
|
||||
disable_base_attachment: unique.flag(u::DISABLE_BASE_ATTACHMENT),
|
||||
disable_base_clip: unique.flag(u::DISABLE_BASE_CLIP),
|
||||
name,
|
||||
})
|
||||
}
|
||||
|
||||
/// Every clip a weapon's attachments name, as the zone names them.
|
||||
pub fn t6_attachment_xanim_names(w: fastfile_t6::weapon::WeaponView<'_>) -> Vec<String> {
|
||||
w.attachment_uniques()
|
||||
.flat_map(|unique| {
|
||||
(0..fastfile_t6::weapon::variant::XANIM_COUNT)
|
||||
.filter_map(move |slot| unique.xanim(slot))
|
||||
.map(str::to_ascii_lowercase)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// The fire sounds a weapon's attachments name (a silencer's).
|
||||
pub fn t6_attachment_sound_names(w: fastfile_t6::weapon::WeaponView<'_>) -> Vec<String> {
|
||||
use fastfile_t6::weapon::unique as u;
|
||||
w.attachment_uniques()
|
||||
.flat_map(|unique| [u::FIRE_SOUND, u::FIRE_SOUND_PLAYER].map(|off| unique.sound(off)))
|
||||
.flatten()
|
||||
.map(str::to_owned)
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn t6_attached_models(weapon: fastfile_t6::weapon::WeaponView<'_>, view: bool) -> Vec<String> {
|
||||
(0..fastfile_t6::weapon::variant::ATTACH_MODEL_COUNT)
|
||||
.filter_map(|slot| weapon.attached_model(slot, view))
|
||||
@@ -5493,6 +5633,11 @@ struct WeaponRow {
|
||||
attachment_view_models: Vec<String>,
|
||||
attachment_world_models: Vec<String>,
|
||||
|
||||
/// See [`CatalogWeapon::t6_clip_models`] and
|
||||
/// [`CatalogWeapon::t6_attachments`].
|
||||
t6_clip_models: [Option<String>; 2],
|
||||
t6_attachments: Vec<T6Attachment>,
|
||||
|
||||
iw5_configuration: Option<(u32, Iw5AttachmentSelection)>,
|
||||
prepared_attachments: Vec<String>,
|
||||
|
||||
@@ -5581,6 +5726,8 @@ impl Default for WeaponRow {
|
||||
hide_tags: Vec::new(),
|
||||
attachment_view_models: Vec::new(),
|
||||
attachment_world_models: Vec::new(),
|
||||
t6_clip_models: Default::default(),
|
||||
t6_attachments: Vec::new(),
|
||||
iw5_configuration: None,
|
||||
prepared_attachments: Vec::new(),
|
||||
iw5_attachment_slots: std::array::from_fn(|_| None),
|
||||
@@ -5667,6 +5814,104 @@ pub struct Iw5PreparationCensus {
|
||||
pub refused: Vec<crate::WeaponSelection>,
|
||||
}
|
||||
|
||||
/// `base` wearing the T6 attachments of `kinds`: each one's models,
|
||||
/// hidden gun bones, clips and fire sound, and the clips of the authored
|
||||
/// pair when there is one. An attachment the weapon has no unique for
|
||||
/// (FMJ, fast ADS) changes nothing it shows. `None` for a weapon without
|
||||
/// T6 attachments.
|
||||
fn compose_t6_configuration(base: &WeaponRow, kinds: &[u32], name: String) -> Option<WeaponRow> {
|
||||
if base.t6_attachments.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let singles: Vec<&T6Attachment> = kinds
|
||||
.iter()
|
||||
.filter_map(|&kind| {
|
||||
base.t6_attachments
|
||||
.iter()
|
||||
.find(|attachment| attachment.kind == kind && attachment.mask == 0)
|
||||
})
|
||||
.collect();
|
||||
let mask = kinds.iter().fold(0u32, |mask, &kind| mask | 1u32.checked_shl(kind).unwrap_or(0));
|
||||
let pair = (kinds.len() > 1)
|
||||
.then(|| base.t6_attachments.iter().find(|attachment| attachment.mask == mask))
|
||||
.flatten();
|
||||
let mut row = base.clone();
|
||||
row.name = name;
|
||||
row.t6_attachments = Vec::new();
|
||||
for (side, models) in [&mut row.attachment_view_models, &mut row.attachment_world_models]
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
{
|
||||
let clip = base.t6_clip_models[side].as_ref();
|
||||
let is_clip = |model: &String| clip == Some(model);
|
||||
if singles.iter().any(|a| a.disable_base_attachment) {
|
||||
models.retain(|model| is_clip(model));
|
||||
}
|
||||
if singles.iter().any(|a| a.disable_base_clip) {
|
||||
models.retain(|model| !is_clip(model));
|
||||
}
|
||||
for attachment in &singles {
|
||||
models.extend(attachment.models[side].iter().cloned());
|
||||
}
|
||||
}
|
||||
// An attachment's clip set repeats the weapon's where it changes
|
||||
// nothing; only its own clips replace the weapon's, so one
|
||||
// attachment's set does not undo another's.
|
||||
for attachment in singles.iter().copied().chain(pair) {
|
||||
for (slot, clip) in attachment.xanims.iter().enumerate() {
|
||||
if let Some(clip) = clip
|
||||
&& base.sz_xanims[slot].as_ref() != Some(clip)
|
||||
{
|
||||
row.sz_xanims[slot] = Some(clip.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
for attachment in &singles {
|
||||
row.hide_tags.extend(attachment.hide_tags.iter().cloned());
|
||||
if let Some(sound) = &attachment.fire_sound {
|
||||
row.sounds.fire = Some(sound.clone());
|
||||
}
|
||||
if let Some(sound) = &attachment.fire_sound_player {
|
||||
row.sounds.fire_player = Some(sound.clone());
|
||||
}
|
||||
if let Some(alt) = &attachment.alt_weapon {
|
||||
row.alternate_weapon = Some(alt.clone());
|
||||
}
|
||||
}
|
||||
Some(row)
|
||||
}
|
||||
|
||||
/// What [`WeaponBuild::prepare_t6_configurations`] did.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct T6PreparationCensus {
|
||||
pub prepared: usize,
|
||||
/// Selections of an attachment the T6 table does not name, or of a
|
||||
/// weapon without T6 attachments.
|
||||
pub refused: usize,
|
||||
}
|
||||
|
||||
/// The T6 attachment table's index of each attachment, by name: the
|
||||
/// `eAttachment` a weapon's attachment uniques are filed under.
|
||||
fn t6_attachment_kinds(
|
||||
tables: &[(crate::AssetNamespace, crate::CapturedStringTable)],
|
||||
) -> HashMap<String, u32> {
|
||||
tables
|
||||
.iter()
|
||||
.filter(|(namespace, table)| {
|
||||
*namespace == crate::AssetNamespace::T6
|
||||
&& table.name.eq_ignore_ascii_case("mp/attachmentTable.csv")
|
||||
})
|
||||
.flat_map(|(_, table)| {
|
||||
(1..table.rows as i32)
|
||||
.filter(|&row| table.cell(row, 2) == "attachment")
|
||||
.filter_map(|row| {
|
||||
Some((table.cell(row, 4).to_ascii_lowercase(), table.cell(row, 0).parse().ok()?))
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// The knife and swings a T6 gun without melee clips borrows (see
|
||||
/// [`crate::T6_MELEE_WEAPON`]).
|
||||
#[derive(Clone, Debug)]
|
||||
@@ -5727,6 +5972,69 @@ impl WeaponBuild {
|
||||
self.family_tables = tables;
|
||||
}
|
||||
|
||||
/// A row for every T6 family's attachment and compatible pair of
|
||||
/// attachments: the base row wearing what each attachment's unique
|
||||
/// does to it (see [`compose_t6_configuration`]).
|
||||
pub fn prepare_t6_configurations(&mut self) -> T6PreparationCensus {
|
||||
let families = crate::WeaponFamilies::build(&self.family_tables, &self.registry);
|
||||
let kinds = t6_attachment_kinds(&self.family_tables);
|
||||
let mut census = T6PreparationCensus::default();
|
||||
let mut prepared = Vec::new();
|
||||
for (base_id, selection) in families.candidate_selections(crate::AssetNamespace::T6) {
|
||||
if selection.attachments.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let Some(family) = selection.family.as_ref().and_then(|key| families.family(key)) else {
|
||||
continue;
|
||||
};
|
||||
let attachments = families.normalize(crate::AssetNamespace::T6, &selection.attachments);
|
||||
let name = families.configuration_name(family, &attachments);
|
||||
// Dual wield is a weapon pair of its own (`fiveseven_dw`).
|
||||
if self
|
||||
.registry
|
||||
.by_namespaced
|
||||
.contains_key(&(crate::AssetNamespace::T6, name.clone()))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
let kinds: Option<Vec<u32>> = attachments.iter().map(|a| kinds.get(a).copied()).collect();
|
||||
match kinds.and_then(|kinds| {
|
||||
compose_t6_configuration(&self.registry.rows[base_id as usize], &kinds, name)
|
||||
}) {
|
||||
Some(mut row) => {
|
||||
row.prepared_attachments = attachments.clone();
|
||||
prepared.push((
|
||||
base_id,
|
||||
crate::WeaponSelection {
|
||||
attachments,
|
||||
..selection
|
||||
},
|
||||
row,
|
||||
));
|
||||
}
|
||||
None => census.refused += 1,
|
||||
}
|
||||
}
|
||||
census.prepared = prepared.len();
|
||||
for (base_id, selection, row) in prepared {
|
||||
self.registry
|
||||
.configurations
|
||||
.insert(selection, self.registry.rows.len() as u32);
|
||||
let slots = self
|
||||
.combat_slots
|
||||
.get(base_id as usize)
|
||||
.copied()
|
||||
.unwrap_or_default();
|
||||
self.combat_slots
|
||||
.resize(self.registry.rows.len(), CombatFxSlots::default());
|
||||
self.combat_slots.push(slots);
|
||||
self.registry.rows.push(row);
|
||||
}
|
||||
self.registry.rebuild_name_maps();
|
||||
self.registry.revision = mint_weapon_revision();
|
||||
census
|
||||
}
|
||||
|
||||
pub fn prepare_iw5_configurations(&mut self) -> Iw5PreparationCensus {
|
||||
let families = crate::WeaponFamilies::build(&self.family_tables, &self.registry);
|
||||
let mut census = Iw5PreparationCensus::default();
|
||||
@@ -6060,6 +6368,38 @@ impl WeaponBuild {
|
||||
sounds.notetrack_convention = dressed.sounds.notetrack_convention;
|
||||
}
|
||||
dressed.sounds = sounds;
|
||||
// A gun of its own hides what T6's bare weapon hides (a
|
||||
// sniper's iron sights under its scope), not the stand-in's tags.
|
||||
if own_gun {
|
||||
dressed.hide_tags = own
|
||||
.t6_attachments
|
||||
.iter()
|
||||
.find(|attachment| attachment.kind == 0 && attachment.mask == 0)
|
||||
.map(|bare| bare.hide_tags.clone())
|
||||
.unwrap_or_default();
|
||||
}
|
||||
// The attachments keep what reached the catalogs; their clips
|
||||
// only where the row plays its own.
|
||||
dressed.t6_clip_models = [
|
||||
own.t6_clip_models[0].clone().filter(|name| own_view(name)),
|
||||
own.t6_clip_models[1].clone().filter(|name| own_world(name)),
|
||||
];
|
||||
dressed.t6_attachments = own
|
||||
.t6_attachments
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(|mut attachment| {
|
||||
attachment.models[0].retain(|name| own_gun && own_view(name));
|
||||
attachment.models[1].retain(|name| own_world(name));
|
||||
attachment.xanims = attachment
|
||||
.xanims
|
||||
.map(|name| name.filter(|name| hands.is_some() && own_anim(name)));
|
||||
for sound in [&mut attachment.fire_sound, &mut attachment.fire_sound_player] {
|
||||
*sound = sound.take().filter(|name| own_sound(name));
|
||||
}
|
||||
attachment
|
||||
})
|
||||
.collect();
|
||||
dressed.name = own.name;
|
||||
dressed.namespace = crate::AssetNamespace::T6;
|
||||
dressed.alternate_weapon = own.alternate_weapon;
|
||||
@@ -6433,6 +6773,11 @@ impl WeaponBuild {
|
||||
.chain(&row.sz_xanim_left_edges)
|
||||
.filter_map(|edge| edge.bound_index())
|
||||
.collect();
|
||||
let hide_mode = if row.namespace == crate::AssetNamespace::T6 {
|
||||
crate::FpvHideMode::Bones
|
||||
} else {
|
||||
crate::FpvHideMode::Surfaces
|
||||
};
|
||||
let knife_model = row.knife_xmodel.as_deref().map(|name| {
|
||||
fpv_model_edge(Some(name), row.namespace.content(), fpv)
|
||||
.bound_index()
|
||||
@@ -6453,12 +6798,15 @@ impl WeaponBuild {
|
||||
.flatten(),
|
||||
knife,
|
||||
hide_tags: hide_tags.clone(),
|
||||
hide_mode,
|
||||
};
|
||||
shared
|
||||
.entry(key)
|
||||
.or_insert_with(|| {
|
||||
census.built += 1;
|
||||
crate::FpvAssembly::build(fpv, hands, mounts, rocket, knife, &hide_tags)
|
||||
crate::FpvAssembly::build(
|
||||
fpv, hands, mounts, rocket, knife, &hide_tags, hide_mode,
|
||||
)
|
||||
.map(Arc::new)
|
||||
.map_err(|error| error.to_string())
|
||||
})
|
||||
@@ -6547,8 +6895,13 @@ impl WeaponBuild {
|
||||
.iter()
|
||||
.map(|edge| {
|
||||
if on_gun_root {
|
||||
edge.bound_index()?;
|
||||
return gun?.skel.bone_names.first().cloned();
|
||||
// The gun bone a T6 attachment names, else the root.
|
||||
let attachment = catalog.get_at(edge.bound_index()?)?;
|
||||
let bones = &gun?.skel.bone_names;
|
||||
let tag = attachment.skel.mount_tag.as_deref().and_then(|tag| {
|
||||
bones.iter().find(|bone| bone.eq_ignore_ascii_case(tag))
|
||||
});
|
||||
return tag.or(bones.first()).cloned();
|
||||
}
|
||||
let root = catalog
|
||||
.get_at(edge.bound_index()?)?
|
||||
@@ -6877,6 +7230,8 @@ impl WeaponBuild {
|
||||
hide_tags: entry.hide_tags,
|
||||
attachment_view_models: entry.attached_models[0].clone(),
|
||||
attachment_world_models: entry.attached_models[1].clone(),
|
||||
t6_clip_models: entry.t6_clip_models,
|
||||
t6_attachments: entry.t6_attachments,
|
||||
iw5_configuration: None,
|
||||
prepared_attachments: Vec::new(),
|
||||
iw5_attachment_slots: entry.iw5_attachment_slots,
|
||||
|
||||
@@ -437,6 +437,17 @@ impl WeaponFamilies {
|
||||
.cell(row, 8)
|
||||
.split_ascii_whitespace()
|
||||
.map(str::to_ascii_lowercase)
|
||||
// T6 names an attachment after the per-class asset
|
||||
// that carries it (`acog_sniper`, `reflex_pistol`).
|
||||
.map(|name| match name.rsplit_once('_') {
|
||||
Some((attachment, _))
|
||||
if namespace == AssetNamespace::T6
|
||||
&& !known.attachments.contains_key(&name) =>
|
||||
{
|
||||
attachment.to_owned()
|
||||
}
|
||||
_ => name,
|
||||
})
|
||||
.collect(),
|
||||
),
|
||||
};
|
||||
@@ -558,7 +569,7 @@ impl WeaponFamilies {
|
||||
out
|
||||
}
|
||||
|
||||
fn configuration_name(&self, family: &WeaponFamily, attachments: &[String]) -> String {
|
||||
pub(crate) fn configuration_name(&self, family: &WeaponFamily, attachments: &[String]) -> String {
|
||||
if attachments.is_empty() {
|
||||
return family.key.base.clone();
|
||||
}
|
||||
@@ -607,10 +618,16 @@ impl WeaponFamilies {
|
||||
}
|
||||
|
||||
pub(crate) fn iw5_candidate_selections(&self) -> Vec<(u32, WeaponSelection)> {
|
||||
self.candidate_selections(AssetNamespace::Iw5)
|
||||
}
|
||||
|
||||
/// Every loaded family of `namespace` bare, with each attachment it
|
||||
/// offers, and with each compatible pair of them.
|
||||
pub(crate) fn candidate_selections(&self, namespace: AssetNamespace) -> Vec<(u32, WeaponSelection)> {
|
||||
let mut families: Vec<&WeaponFamily> = self
|
||||
.families
|
||||
.iter()
|
||||
.filter(|family| family.key.namespace == AssetNamespace::Iw5 && family.base.is_some())
|
||||
.filter(|family| family.key.namespace == namespace && family.base.is_some())
|
||||
.collect();
|
||||
families.sort_by_key(|family| family.key.asset_key());
|
||||
let mut out = Vec::new();
|
||||
@@ -630,7 +647,7 @@ impl WeaponFamilies {
|
||||
WeaponSelection::with(family.key.clone(), std::slice::from_ref(name)),
|
||||
));
|
||||
for other in &names[i + 1..] {
|
||||
if self.compatible(AssetNamespace::Iw5, name, other) {
|
||||
if self.compatible(namespace, name, other) {
|
||||
out.push((
|
||||
base,
|
||||
WeaponSelection::with(
|
||||
|
||||
@@ -657,9 +657,16 @@ pub fn plan_fpv_mounts(
|
||||
.iter()
|
||||
.any(|name| name.eq_ignore_ascii_case(root))
|
||||
});
|
||||
// A T6 attachment hangs from the gun bone it names, else the root.
|
||||
let gun_root = || {
|
||||
let tag = skel.mount_tag.as_deref().and_then(|tag| {
|
||||
gun_skel
|
||||
.bone_names
|
||||
.iter()
|
||||
.find(|name| name.eq_ignore_ascii_case(tag))
|
||||
});
|
||||
on_gun_root
|
||||
.then(|| gun_skel.bone_names.first().map(|root| (1, root.as_str())))
|
||||
.then(|| tag.or(gun_skel.bone_names.first()).map(|bone| (1, bone.as_str())))
|
||||
.flatten()
|
||||
};
|
||||
let joint = if on_gun_root {
|
||||
|
||||
@@ -95,6 +95,10 @@ pub struct ModelSkel {
|
||||
|
||||
pub movement_brushes: Vec<xmodel_runtime::ModelMovementBrush>,
|
||||
|
||||
/// The gun bone a T6 attachment copy hangs from (`j_barrel` for a
|
||||
/// silencer); `None` for the gun's root.
|
||||
pub mount_tag: Option<String>,
|
||||
|
||||
pub lod: Option<crate::ModelLodSelector>,
|
||||
|
||||
pub lod_smc: Option<[[u8; 4]; 4]>,
|
||||
@@ -542,6 +546,8 @@ fn capture_model_skel_iw4(
|
||||
coll_lod: geometry.coll_lod,
|
||||
coll_surfs: capture_coll_surfs(stream, geometry),
|
||||
movement_brushes: Vec::new(),
|
||||
|
||||
mount_tag: None,
|
||||
lod: Some(crate::ModelLodSelector::Iw4 {
|
||||
lod_start: geometry.lod_start,
|
||||
num_lods: geometry.num_lods,
|
||||
@@ -1092,6 +1098,8 @@ fn capture_model_skel_t5(
|
||||
coll_lod: geometry.coll_lod,
|
||||
coll_surfs,
|
||||
movement_brushes: capture_movement_brushes_t5(stream, geometry)?,
|
||||
|
||||
mount_tag: None,
|
||||
lod: Some(crate::ModelLodSelector::T5 {
|
||||
num_lods: geometry.num_lods,
|
||||
lod_dist: geometry.lod_dist,
|
||||
@@ -1722,6 +1730,8 @@ fn capture_model_skel_iw5(
|
||||
coll_surfs: Vec::new(),
|
||||
movement_brushes: Vec::new(),
|
||||
|
||||
mount_tag: None,
|
||||
|
||||
lod: None,
|
||||
lod_smc: None,
|
||||
lod_part_bits: None,
|
||||
|
||||
@@ -296,6 +296,7 @@ pub fn capture_model_skel_t6(
|
||||
coll_surfs,
|
||||
// Weapons and arms: nothing the player collides with.
|
||||
movement_brushes: Vec::new(),
|
||||
mount_tag: None,
|
||||
lod: Some(crate::ModelLodSelector::T5 {
|
||||
num_lods: model.h_u16(off::NUM_LODS) as i16,
|
||||
lod_dist,
|
||||
|
||||
@@ -149,6 +149,7 @@ fn capture_xanims(
|
||||
asset_game::t6_stand_in_for(name).is_some() || name == asset_game::T6_MELEE_WEAPON
|
||||
}) {
|
||||
wanted.extend(asset_game::t6_weapon_xanim_names(weapon));
|
||||
wanted.extend(asset_game::t6_attachment_xanim_names(weapon));
|
||||
}
|
||||
}
|
||||
let (total, mut captured) = (wanted.len(), 0usize);
|
||||
@@ -204,6 +205,7 @@ fn capture_sounds(
|
||||
.is_some()
|
||||
{
|
||||
names.extend(asset_game::t6_weapon_sound_names(weapon));
|
||||
names.extend(asset_game::t6_attachment_sound_names(weapon));
|
||||
}
|
||||
}
|
||||
names.extend(content.note_sounds.iter().cloned());
|
||||
@@ -931,6 +933,9 @@ fn capture_content(
|
||||
// Where each attached model (a magazine, a sniper's scope) rests on its
|
||||
// gun's root bone.
|
||||
let mut placements: BTreeMap<String, ([f32; 3], [f32; 3])> = BTreeMap::new();
|
||||
// An attachment model's copy placed for one weapon: the model it
|
||||
// copies and the gun bone it hangs from.
|
||||
let mut copies: BTreeMap<String, (String, Option<String>)> = BTreeMap::new();
|
||||
for asset in &load.assets {
|
||||
let Some(weapon) = WeaponView::new(load, asset) else {
|
||||
continue;
|
||||
@@ -950,6 +955,15 @@ fn capture_content(
|
||||
.or_insert((view, false, stand_in));
|
||||
}
|
||||
}
|
||||
for unique in weapon.attachment_uniques() {
|
||||
for view in [true, false] {
|
||||
for placed in asset_game::t6_attachment_models(unique, view) {
|
||||
placements.insert(placed.copy.clone(), (placed.offset, placed.angles));
|
||||
wanted.entry(placed.copy.clone()).or_insert((view, false, stand_in));
|
||||
copies.insert(placed.copy, (placed.model, placed.tag));
|
||||
}
|
||||
}
|
||||
}
|
||||
for view in [true, false] {
|
||||
for slot in 0..fastfile_t6::weapon::variant::ATTACH_MODEL_COUNT {
|
||||
if let Some((name, offset, angles)) = weapon.attached_model(slot, view) {
|
||||
@@ -973,7 +987,9 @@ fn capture_content(
|
||||
let mut textures = DecodedTextures::new();
|
||||
let mut native_textures = DecodedTextures::new();
|
||||
for (name, (view, hands, stand_in)) in wanted {
|
||||
let Some(&(load, asset)) = models.get(name.as_str()) else {
|
||||
let copy = copies.get(&name);
|
||||
let source = copy.map_or(name.as_str(), |(model, _)| model.as_str());
|
||||
let Some(&(load, asset)) = models.get(source) else {
|
||||
content
|
||||
.report
|
||||
.push(format!("t6 content: {name}: in no zone read"));
|
||||
@@ -994,6 +1010,10 @@ fn capture_content(
|
||||
{
|
||||
pose.root_rest = Some(attachment_rest(offset, angles));
|
||||
}
|
||||
if let Some((_, tag)) = copy {
|
||||
skel.name = name.clone();
|
||||
skel.mount_tag = tag.clone();
|
||||
}
|
||||
let mut surface_materials = Vec::with_capacity(model.surface_count());
|
||||
for surface in 0..model.surface_count() {
|
||||
let material = model
|
||||
|
||||
@@ -739,6 +739,11 @@ async fn prepare_common(key: CommonKey) -> Arc<CommonSet> {
|
||||
|
||||
weapons.set_family_tables(cac_tables.clone());
|
||||
let iw5_prepared = weapons.prepare_iw5_configurations();
|
||||
let t6_prepared = weapons.prepare_t6_configurations();
|
||||
common_report.push(format!(
|
||||
"T6 configurations: prepared={} refused={}",
|
||||
t6_prepared.prepared, t6_prepared.refused
|
||||
));
|
||||
weapons.resolve_fpv_mesh_edges(&fpv_meshes);
|
||||
weapons.resolve_fpv_hands(&fpv_meshes, &asset_model::BodyMeshCatalog::default());
|
||||
weapons.resolve_world_model_edges(&world_weapons);
|
||||
|
||||
@@ -884,7 +884,7 @@ fn t6_class_tables(
|
||||
if asset_game::is_stats_table_name(&table.name) {
|
||||
Some(table)
|
||||
} else if table.name.eq_ignore_ascii_case("mp/attachmentTable.csv") {
|
||||
Some(t6_dual_wield_attachment(table))
|
||||
Some(table)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
@@ -898,29 +898,6 @@ fn t6_class_tables(
|
||||
tables
|
||||
}
|
||||
|
||||
/// The T6 attachment table cut to its header and the dual-wield row. T6
|
||||
/// builds its other attachments onto a gun at runtime, which no weapon row
|
||||
/// here carries; dual wield is a weapon pair of its own (`X_dw_mp` and
|
||||
/// `X_lh_mp`), so it is the one attachment a class can hold.
|
||||
fn t6_dual_wield_attachment(
|
||||
table: asset_game::CapturedStringTable,
|
||||
) -> asset_game::CapturedStringTable {
|
||||
let keep: Vec<i32> = (0..table.rows as i32)
|
||||
.filter(|&row| row == 0 || table.cell(row, 4).eq_ignore_ascii_case("dw"))
|
||||
.collect();
|
||||
let cells = keep
|
||||
.iter()
|
||||
.flat_map(|&row| (0..table.columns as i32).map(move |col| (row, col)))
|
||||
.map(|(row, col)| table.cell(row, col).to_owned())
|
||||
.collect();
|
||||
asset_game::CapturedStringTable {
|
||||
name: table.name.clone(),
|
||||
columns: table.columns,
|
||||
rows: keep.len(),
|
||||
cells,
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn walk_t6_weapon_bundle(
|
||||
progress: &LoadProgress,
|
||||
) -> (
|
||||
|
||||
@@ -187,6 +187,20 @@ pub struct LoadedAsset {
|
||||
/// inline as another asset's dependency.
|
||||
pub list_index: Option<usize>,
|
||||
pub header: Vec<u8>,
|
||||
/// The assets the header's own pointer fields named when it was
|
||||
/// loaded, by field offset. Read these rather than [`ZoneLoad::asset_at`]
|
||||
/// on the header's address, which later loads may reuse.
|
||||
pub fields: Vec<(u32, usize)>,
|
||||
}
|
||||
|
||||
impl LoadedAsset {
|
||||
/// Index (into [`ZoneLoad::assets`]) of the asset field `offset` names.
|
||||
pub fn field(&self, offset: u32) -> Option<usize> {
|
||||
self.fields
|
||||
.iter()
|
||||
.find(|(at, _)| *at == offset)
|
||||
.map(|&(_, index)| index)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ZoneLoad {
|
||||
@@ -290,6 +304,11 @@ struct Walker<'a> {
|
||||
temp_saved: Vec<usize>,
|
||||
assets: Vec<LoadedAsset>,
|
||||
asset_slots: BTreeMap<Ptr, usize>,
|
||||
/// Every slot bound, in order: an asset's own fields are the ones
|
||||
/// bound while it loaded.
|
||||
slot_log: Vec<(Ptr, usize)>,
|
||||
/// Each asset by where its header sits, as of now (memory is reused).
|
||||
addresses: BTreeMap<Ptr, usize>,
|
||||
script_strings: Option<(Ptr, usize)>,
|
||||
}
|
||||
|
||||
@@ -596,7 +615,7 @@ impl<'a> Walker<'a> {
|
||||
let aliased = self.mem.u32_at(target)?;
|
||||
self.mem.write_u32(slot, aliased)?;
|
||||
if let Some(&i) = self.asset_slots.get(&target) {
|
||||
self.asset_slots.insert(slot, i);
|
||||
self.bind_slot(slot, i);
|
||||
}
|
||||
}
|
||||
// Otherwise the offset already is the pointer, in zone encoding.
|
||||
@@ -761,9 +780,15 @@ impl<'a> Walker<'a> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn bind_slot(&mut self, slot: Ptr, index: usize) {
|
||||
self.asset_slots.insert(slot, index);
|
||||
self.slot_log.push((slot, index));
|
||||
}
|
||||
|
||||
/// `Loader_X::Load(&slot)` → `LoadPtr_X(false)`.
|
||||
fn load_asset(&mut self, asset: usize, slot: Ptr, list_index: Option<usize>) -> Result<()> {
|
||||
let schema: &'a AssetSchema = &self.schema.assets[asset];
|
||||
let log_start = self.slot_log.len();
|
||||
let in_temp = schema.root_in_temp;
|
||||
if in_temp {
|
||||
self.push(XFILE_BLOCK_TEMP);
|
||||
@@ -790,24 +815,38 @@ impl<'a> Walker<'a> {
|
||||
|
||||
let ty = asset_type_for(&schema.name).ok_or(WalkError::Schema)?;
|
||||
let header = self.mem.bytes(at, root.size as usize)?.to_vec();
|
||||
let end = at.at(root.size);
|
||||
let mut fields: Vec<(u32, usize)> = Vec::new();
|
||||
for &(field, index) in &self.slot_log[log_start..] {
|
||||
if (at..end).contains(&field) {
|
||||
let offset = field.offset - at.offset;
|
||||
fields.retain(|(o, _)| *o != offset);
|
||||
fields.push((offset, index));
|
||||
}
|
||||
}
|
||||
let index = self.assets.len();
|
||||
self.assets.push(LoadedAsset {
|
||||
ty,
|
||||
list_index,
|
||||
header,
|
||||
fields,
|
||||
});
|
||||
self.asset_slots.insert(slot, index);
|
||||
self.bind_slot(slot, index);
|
||||
self.addresses.insert(at, index);
|
||||
if let Some(ins) = insert_slot {
|
||||
self.mem.write_u32(ins, at.encode())?;
|
||||
self.asset_slots.insert(ins, index);
|
||||
self.bind_slot(ins, index);
|
||||
}
|
||||
} else if in_temp {
|
||||
let target = Ptr::decode(raw).ok_or(WalkError::BadPointer { raw })?;
|
||||
let aliased = self.mem.u32_at(target)?;
|
||||
self.mem.write_u32(slot, aliased)?;
|
||||
if let Some(&i) = self.asset_slots.get(&target) {
|
||||
self.asset_slots.insert(slot, i);
|
||||
self.bind_slot(slot, i);
|
||||
}
|
||||
} else if let Some(&i) = Ptr::decode(raw).and_then(|at| self.addresses.get(&at)) {
|
||||
// A pointer to an asset loaded earlier, left as it is.
|
||||
self.bind_slot(slot, i);
|
||||
}
|
||||
}
|
||||
if in_temp {
|
||||
@@ -848,6 +887,8 @@ pub fn load_zone(
|
||||
temp_saved: Vec::new(),
|
||||
assets: Vec::new(),
|
||||
asset_slots: BTreeMap::new(),
|
||||
slot_log: Vec::new(),
|
||||
addresses: BTreeMap::new(),
|
||||
script_strings: None,
|
||||
};
|
||||
let result = walk_list(&mut w, &mut on_asset);
|
||||
|
||||
@@ -26,6 +26,11 @@ pub mod variant {
|
||||
pub const ATTACH_VIEW_MODEL_ROTATIONS: u32 = 248;
|
||||
pub const ATTACH_WORLD_MODEL_ROTATIONS: u32 = 344;
|
||||
pub const ATTACH_MODEL_COUNT: u32 = 8;
|
||||
/// `WeaponAttachmentUnique** attachmentUniques`: what each attachment
|
||||
/// (and each authored pair, see [`super::unique::COMBINED_MASK`]) does
|
||||
/// to this weapon, [`ATTACHMENT_UNIQUE_COUNT`] slots.
|
||||
pub const ATTACHMENT_UNIQUES: u32 = 28;
|
||||
pub const ATTACHMENT_UNIQUE_COUNT: u32 = 95;
|
||||
pub const CLIP_SIZE: u32 = 476;
|
||||
pub const RELOAD_TIME: u32 = 480;
|
||||
pub const RELOAD_EMPTY_TIME: u32 = 484;
|
||||
@@ -261,6 +266,177 @@ pub mod weap_anim {
|
||||
pub const ADS_DOWN: usize = 86;
|
||||
}
|
||||
|
||||
/// `WeaponAttachmentUnique` (424 bytes): one attachment as one weapon
|
||||
/// carries it.
|
||||
pub mod unique {
|
||||
pub const NAME: u32 = 0;
|
||||
/// `eAttachment`: the attachment table's index (`acog` 1, `gl` 11, …).
|
||||
pub const TYPE: u32 = 4;
|
||||
/// Non-zero for an authored pair: `1 << type` of both attachments.
|
||||
pub const COMBINED_MASK: u32 = 16;
|
||||
pub const ALT_WEAPON_NAME: u32 = 20;
|
||||
/// `unsigned short* hideTags`: 32 script strings.
|
||||
pub const HIDE_TAGS: u32 = 36;
|
||||
pub const HIDE_TAG_COUNT: u32 = 32;
|
||||
pub const VIEW_MODEL: u32 = 40;
|
||||
pub const VIEW_MODEL_ADDITIONAL: u32 = 44;
|
||||
pub const WORLD_MODEL: u32 = 52;
|
||||
pub const WORLD_MODEL_ADDITIONAL: u32 = 56;
|
||||
/// The gun bone the models hang from; empty for the gun's root.
|
||||
pub const VIEW_MODEL_TAG: u32 = 60;
|
||||
pub const WORLD_MODEL_TAG: u32 = 64;
|
||||
pub const VIEW_MODEL_OFFSETS: u32 = 68;
|
||||
pub const WORLD_MODEL_OFFSETS: u32 = 80;
|
||||
pub const VIEW_MODEL_ROTATIONS: u32 = 92;
|
||||
pub const WORLD_MODEL_ROTATIONS: u32 = 104;
|
||||
pub const VIEW_MODEL_ADD_OFFSETS: u32 = 116;
|
||||
pub const WORLD_MODEL_ADD_OFFSETS: u32 = 128;
|
||||
pub const VIEW_MODEL_ADD_ROTATIONS: u32 = 140;
|
||||
pub const WORLD_MODEL_ADD_ROTATIONS: u32 = 152;
|
||||
/// The weapon's own attached optic (or its magazine) is removed.
|
||||
pub const DISABLE_BASE_ATTACHMENT: u32 = 168;
|
||||
pub const DISABLE_BASE_CLIP: u32 = 169;
|
||||
/// `const char** szXAnims`: the weapon's clips with this attachment, by
|
||||
/// [`super::weap_anim`]; an empty name keeps the weapon's own.
|
||||
pub const XANIMS: u32 = 232;
|
||||
pub const FIRE_SOUND: u32 = 256;
|
||||
pub const FIRE_SOUND_PLAYER: u32 = 260;
|
||||
}
|
||||
|
||||
/// An attached model of a [`AttachmentUniqueView`]: its name, the gun bone
|
||||
/// it hangs from (`None` for the root) and its offset and `(pitch, yaw,
|
||||
/// roll)` degrees there.
|
||||
pub type UniqueModel<'z> = (&'z str, Option<&'z str>, [f32; 3], [f32; 3]);
|
||||
|
||||
/// A `WeaponAttachmentUnique` of a finished zone load.
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct AttachmentUniqueView<'z> {
|
||||
load: &'z ZoneLoad,
|
||||
asset: &'z LoadedAsset,
|
||||
}
|
||||
|
||||
impl<'z> AttachmentUniqueView<'z> {
|
||||
fn u32_at(&self, off: u32) -> u32 {
|
||||
let o = off as usize;
|
||||
self.asset
|
||||
.header
|
||||
.get(o..o + 4)
|
||||
.map_or(0, |b| u32::from_le_bytes(b.try_into().unwrap()))
|
||||
}
|
||||
|
||||
fn vec3(&self, off: u32) -> [f32; 3] {
|
||||
core::array::from_fn(|i| f32::from_bits(self.u32_at(off + 4 * i as u32)))
|
||||
}
|
||||
|
||||
fn str_at(&self, off: u32) -> Option<&'z str> {
|
||||
let p = crate::walk::decode_ptr(self.u32_at(off))?;
|
||||
self.load
|
||||
.blocks
|
||||
.cstr(p)
|
||||
.ok()
|
||||
.and_then(|b| core::str::from_utf8(b).ok())
|
||||
.filter(|name| !name.is_empty())
|
||||
}
|
||||
|
||||
pub fn name(&self) -> Option<&'z str> {
|
||||
self.str_at(unique::NAME)
|
||||
}
|
||||
|
||||
pub fn attachment_type(&self) -> u32 {
|
||||
self.u32_at(unique::TYPE)
|
||||
}
|
||||
|
||||
pub fn combined_mask(&self) -> u32 {
|
||||
self.u32_at(unique::COMBINED_MASK)
|
||||
}
|
||||
|
||||
pub fn alt_weapon(&self) -> Option<&'z str> {
|
||||
self.str_at(unique::ALT_WEAPON_NAME)
|
||||
}
|
||||
|
||||
pub fn flag(&self, off: u32) -> bool {
|
||||
self.asset.header.get(off as usize).is_some_and(|&b| b != 0)
|
||||
}
|
||||
|
||||
pub fn sound(&self, off: u32) -> Option<&'z str> {
|
||||
self.str_at(off)
|
||||
}
|
||||
|
||||
/// The asset a model field named when this was loaded.
|
||||
fn model_name(&self, off: u32) -> Option<&'z str> {
|
||||
let model = &self.load.assets[self.asset.field(off)?];
|
||||
let p = crate::walk::decode_ptr(u32::from_le_bytes(model.header.get(0..4)?.try_into().ok()?))?;
|
||||
self.load
|
||||
.blocks
|
||||
.cstr(p)
|
||||
.ok()
|
||||
.and_then(|b| core::str::from_utf8(b).ok())
|
||||
}
|
||||
|
||||
/// The models this attachment hangs on the first-person (`view`) or
|
||||
/// world gun: the main one, then the additional one (a mount).
|
||||
pub fn models(&self, view: bool) -> [Option<UniqueModel<'z>>; 2] {
|
||||
use unique as u;
|
||||
let (model, add, tag, offsets, rotations, add_offsets, add_rotations) = if view {
|
||||
(
|
||||
u::VIEW_MODEL,
|
||||
u::VIEW_MODEL_ADDITIONAL,
|
||||
u::VIEW_MODEL_TAG,
|
||||
u::VIEW_MODEL_OFFSETS,
|
||||
u::VIEW_MODEL_ROTATIONS,
|
||||
u::VIEW_MODEL_ADD_OFFSETS,
|
||||
u::VIEW_MODEL_ADD_ROTATIONS,
|
||||
)
|
||||
} else {
|
||||
(
|
||||
u::WORLD_MODEL,
|
||||
u::WORLD_MODEL_ADDITIONAL,
|
||||
u::WORLD_MODEL_TAG,
|
||||
u::WORLD_MODEL_OFFSETS,
|
||||
u::WORLD_MODEL_ROTATIONS,
|
||||
u::WORLD_MODEL_ADD_OFFSETS,
|
||||
u::WORLD_MODEL_ADD_ROTATIONS,
|
||||
)
|
||||
};
|
||||
let tag = self.str_at(tag);
|
||||
[
|
||||
(model, offsets, rotations),
|
||||
(add, add_offsets, add_rotations),
|
||||
]
|
||||
.map(|(model, offsets, rotations)| {
|
||||
Some((
|
||||
self.model_name(model)?,
|
||||
tag,
|
||||
self.vec3(offsets),
|
||||
self.vec3(rotations),
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
/// The clip in `slot` ([`weap_anim`]), when this attachment names one.
|
||||
pub fn xanim(&self, slot: u32) -> Option<&'z str> {
|
||||
let arr = crate::walk::decode_ptr(self.u32_at(unique::XANIMS))?;
|
||||
let p = self.load.blocks.ptr_at(arr.at(4 * slot)).ok()??;
|
||||
self.load
|
||||
.blocks
|
||||
.cstr(p)
|
||||
.ok()
|
||||
.and_then(|b| core::str::from_utf8(b).ok())
|
||||
.filter(|name| !name.is_empty())
|
||||
}
|
||||
|
||||
/// The gun bones this attachment hides (iron sights under an optic).
|
||||
pub fn hide_tags(&self) -> impl Iterator<Item = &'z str> + 'z {
|
||||
let load = self.load;
|
||||
let arr = crate::walk::decode_ptr(self.u32_at(unique::HIDE_TAGS));
|
||||
(0..unique::HIDE_TAG_COUNT).filter_map(move |i| {
|
||||
let b = load.blocks.bytes(arr?.at(2 * i), 2).ok()?;
|
||||
let id = u16::from_le_bytes([b[0], b[1]]);
|
||||
(id != 0).then(|| load.script_string(id)).flatten()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// `weapType_t`.
|
||||
pub mod weap_type {
|
||||
pub const BULLET: i32 = 0;
|
||||
@@ -342,6 +518,18 @@ impl<'z> WeaponView<'z> {
|
||||
crate::walk::decode_ptr(self.variant_u32(off))
|
||||
}
|
||||
|
||||
/// The variant's attachment uniques: one per attachment it takes, and
|
||||
/// one per authored pair.
|
||||
pub fn attachment_uniques(&self) -> impl Iterator<Item = AttachmentUniqueView<'z>> + 'z {
|
||||
let load = self.load;
|
||||
let arr = self.variant_ptr(variant::ATTACHMENT_UNIQUES);
|
||||
(0..variant::ATTACHMENT_UNIQUE_COUNT).filter_map(move |i| {
|
||||
let asset = load.asset_at(arr?.at(4 * i))?;
|
||||
(asset.ty == crate::AssetType::AttachmentUnique)
|
||||
.then_some(AttachmentUniqueView { load, asset })
|
||||
})
|
||||
}
|
||||
|
||||
pub fn has_def(&self) -> bool {
|
||||
self.def.is_some()
|
||||
}
|
||||
|
||||
@@ -601,7 +601,18 @@ impl PreparedFpvRig {
|
||||
.collect();
|
||||
let assembly = &self.composition.assembly;
|
||||
let world = assembly.dobj.pose(&instances, &self.parts, Mat4::IDENTITY);
|
||||
let skin = assembly.dobj.skin_matrices(&world);
|
||||
let mut skin = assembly.dobj.skin_matrices(&world);
|
||||
// A hidden T6 gun bone takes its vertices to a point.
|
||||
for &bone in &assembly.collapsed_bones {
|
||||
if let (Some(skin), Some(world)) = (skin.get_mut(bone), world.get(bone)) {
|
||||
*skin = Mat4::from_cols(
|
||||
bevy::math::Vec4::ZERO,
|
||||
bevy::math::Vec4::ZERO,
|
||||
bevy::math::Vec4::ZERO,
|
||||
world.w_axis,
|
||||
);
|
||||
}
|
||||
}
|
||||
let eye_from_world = tag_view_to_bevy_camera() * world[assembly.view_bone].inverse();
|
||||
let lens = assembly
|
||||
.camera_bone
|
||||
|
||||
Reference in New Issue
Block a user