mirror of
https://github.com/vladtrc/iw4L
synced 2026-10-25 18:05:19 -04:00
Restore cross-game class previews and loadout customization
This commit is contained in:
@@ -1553,6 +1553,7 @@ pub struct WeaponRegistry {
|
||||
|
||||
world_catalog_identity: u64,
|
||||
fpv_catalog_identity: u64,
|
||||
loadout_only: bool,
|
||||
|
||||
iw5_attachments: HashMap<String, Iw5ScopeRow>,
|
||||
|
||||
|
||||
@@ -11,6 +11,11 @@ impl WeaponCatalog {
|
||||
}
|
||||
|
||||
impl WeaponBuild {
|
||||
pub fn publish_for_loadout(mut self) -> WeaponRegistry {
|
||||
self.registry.loadout_only = true;
|
||||
self.publish()
|
||||
}
|
||||
|
||||
pub fn publish(self) -> WeaponRegistry {
|
||||
let mut registry = self.registry;
|
||||
registry.revision = mint_weapon_revision();
|
||||
@@ -451,6 +456,7 @@ impl WeaponBuild {
|
||||
rows,
|
||||
world_catalog_identity: 0,
|
||||
fpv_catalog_identity: 0,
|
||||
loadout_only: false,
|
||||
iw5_attachments,
|
||||
configurations: HashMap::new(),
|
||||
by_name: index_of,
|
||||
|
||||
@@ -303,7 +303,7 @@ impl WeaponRegistry {
|
||||
});
|
||||
}
|
||||
}
|
||||
if row.gun_xmodel_edge.is_bound() {
|
||||
if !self.loadout_only && row.gun_xmodel_edge.is_bound() {
|
||||
for (side, hands) in row.fpv_soldiers.iter().enumerate() {
|
||||
if hands.as_ref().is_none_or(|hands| hands.is_err()) {
|
||||
gaps.push(WeaponDependencyGap {
|
||||
|
||||
@@ -11,6 +11,10 @@ pub mod iw5_tech_map;
|
||||
pub mod material_catalog;
|
||||
pub mod material_draw;
|
||||
pub mod material_images;
|
||||
mod ui_material_images;
|
||||
pub use ui_material_images::{
|
||||
retain_ui_material_images, retain_ui_preview_fallback, ui_material_image, ui_preview_fallback,
|
||||
};
|
||||
pub mod t5_code_remap;
|
||||
pub mod t5_tech_map;
|
||||
pub mod t6_techset;
|
||||
|
||||
@@ -59,25 +59,20 @@ fn texture_semantic_decodes_as_normal(semantic: u8) -> Option<bool> {
|
||||
}
|
||||
|
||||
pub fn decode_material_color_maps(
|
||||
zone_ff: &Path,
|
||||
trees: &asset_transport::NamespaceTrees,
|
||||
catalog: &mut MaterialDefinitions,
|
||||
stage: &StageHandle,
|
||||
pool: &TaskPool,
|
||||
) -> Result<MaterialImageStats, String> {
|
||||
let main = game_main_for_zone(zone_ff)?;
|
||||
let index = IwdIndex::open(&main)?;
|
||||
let work = requested_color_map_slots(catalog);
|
||||
let (decoded, archives) =
|
||||
decode_requests_from_trees(&catalog.images, trees, &work, stage, pool)?;
|
||||
let mut stats = MaterialImageStats {
|
||||
archives: index.archive_count(),
|
||||
requested: work.len(),
|
||||
archives,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let work = requested_color_map_slots(catalog);
|
||||
stats.requested = work.len();
|
||||
stage.set_total(work.len() as u64);
|
||||
|
||||
let images = &catalog.images;
|
||||
let decoded = decode_requests_in_parallel(images, index.as_ref(), &work, stage, pool);
|
||||
|
||||
for outcome in decoded {
|
||||
match outcome {
|
||||
ImageOutcome::Decoded {
|
||||
@@ -155,7 +150,7 @@ fn requested_color_map_slots(catalog: &MaterialDefinitions) -> Vec<ImageRequest>
|
||||
}
|
||||
|
||||
pub fn decode_images_for_keys(
|
||||
zone_ff: &Path,
|
||||
trees: &asset_transport::NamespaceTrees,
|
||||
catalog: &mut MaterialDefinitions,
|
||||
keys: impl IntoIterator<Item = crate::MaterialKey>,
|
||||
stage: &StageHandle,
|
||||
@@ -165,11 +160,7 @@ pub fn decode_images_for_keys(
|
||||
if work.is_empty() {
|
||||
return Ok(0);
|
||||
}
|
||||
let main = game_main_for_zone(zone_ff)?;
|
||||
let index = IwdIndex::open(&main)?;
|
||||
stage.set_total(work.len() as u64);
|
||||
let images = &catalog.images;
|
||||
let decoded = decode_requests_in_parallel(images, index.as_ref(), &work, stage, pool);
|
||||
let (decoded, _) = decode_requests_from_trees(&catalog.images, trees, &work, stage, pool)?;
|
||||
let mut n = 0usize;
|
||||
for outcome in decoded {
|
||||
if let ImageOutcome::Decoded {
|
||||
@@ -253,7 +244,7 @@ fn requested_keyed_image_slots(
|
||||
}
|
||||
|
||||
pub fn decode_catalog_images_from_iwd(
|
||||
zone_ff: &Path,
|
||||
trees: &asset_transport::NamespaceTrees,
|
||||
catalog: &mut MaterialDefinitions,
|
||||
images: impl IntoIterator<Item = (usize, u8)>,
|
||||
stage: &StageHandle,
|
||||
@@ -277,11 +268,7 @@ pub fn decode_catalog_images_from_iwd(
|
||||
if work.is_empty() {
|
||||
return Ok(0);
|
||||
}
|
||||
let main = game_main_for_zone(zone_ff)?;
|
||||
let index = IwdIndex::open(&main)?;
|
||||
stage.set_total(work.len() as u64);
|
||||
let catalog_images = &catalog.images;
|
||||
let decoded = decode_requests_in_parallel(catalog_images, index.as_ref(), &work, stage, pool);
|
||||
let (decoded, _) = decode_requests_from_trees(&catalog.images, trees, &work, stage, pool)?;
|
||||
let mut n = 0usize;
|
||||
for outcome in decoded {
|
||||
if let ImageOutcome::Decoded {
|
||||
@@ -368,6 +355,55 @@ enum ImageOutcome {
|
||||
|
||||
type ImageRequest = (usize, (u8, bool, bool, bool));
|
||||
|
||||
fn decode_requests_from_trees(
|
||||
images: &[AuthoredImage],
|
||||
trees: &asset_transport::NamespaceTrees,
|
||||
work: &[ImageRequest],
|
||||
stage: &StageHandle,
|
||||
pool: &TaskPool,
|
||||
) -> Result<(Vec<ImageOutcome>, usize), String> {
|
||||
stage.set_total(work.len() as u64);
|
||||
let mut decoded = Vec::with_capacity(work.len());
|
||||
let mut archives = 0;
|
||||
for &(slot, _) in work {
|
||||
if images.get(slot).is_none() {
|
||||
decoded.push(ImageOutcome::Missing {
|
||||
gap: format!("catalog image index {slot} is out of bounds"),
|
||||
});
|
||||
stage.advance(1);
|
||||
}
|
||||
}
|
||||
for namespace in [
|
||||
crate::AssetNamespace::Iw4,
|
||||
crate::AssetNamespace::T5,
|
||||
crate::AssetNamespace::Iw5,
|
||||
crate::AssetNamespace::T6,
|
||||
] {
|
||||
let requests: Vec<_> = work
|
||||
.iter()
|
||||
.copied()
|
||||
.filter(|(slot, _)| {
|
||||
images
|
||||
.get(*slot)
|
||||
.is_some_and(|image| image.namespace == namespace)
|
||||
})
|
||||
.collect();
|
||||
if requests.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let index = match trees.main_for(namespace) {
|
||||
Some(main) => IwdIndex::open(main)
|
||||
.map_err(|error| format!("{} image archives: {error}", namespace.as_str()))?,
|
||||
None => Arc::new(IwdIndex::empty()),
|
||||
};
|
||||
archives += index.archive_count();
|
||||
decoded.extend(decode_requests_in_parallel(
|
||||
images, &index, &requests, stage, pool,
|
||||
));
|
||||
}
|
||||
Ok((decoded, archives))
|
||||
}
|
||||
|
||||
/// Decode this work on the pool the caller names.
|
||||
///
|
||||
/// The pool is a parameter because the right one is the loader's, and
|
||||
@@ -808,7 +844,18 @@ pub fn decode_ui_image(
|
||||
let Some(main) = trees.main_for(key.namespace) else {
|
||||
return Ok(None);
|
||||
};
|
||||
return decode_ui_image_from_main(main, &key.name);
|
||||
if let Some(image) = crate::ui_material_image(key.namespace, &key.name)
|
||||
&& let Some(decoded) = decode_ui_image_from_main(main, &image)?
|
||||
{
|
||||
return Ok(Some(decoded));
|
||||
}
|
||||
if let Some(decoded) = decode_ui_image_from_main(main, &key.name)? {
|
||||
return Ok(Some(decoded));
|
||||
}
|
||||
if let Some(image) = crate::ui_preview_fallback(key.namespace, &key.name) {
|
||||
return decode_ui_image_from_main(main, &image);
|
||||
}
|
||||
return Ok(None);
|
||||
}
|
||||
let name = crate::AssetRef::bare_name(image_name);
|
||||
for main in ui_decode_mains(games_root) {
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
use std::collections::HashMap;
|
||||
use std::sync::{LazyLock, RwLock};
|
||||
|
||||
use asset_core::AssetNamespace;
|
||||
|
||||
static IMAGES: LazyLock<RwLock<HashMap<(AssetNamespace, String), String>>> =
|
||||
LazyLock::new(Default::default);
|
||||
static PREVIEW_FALLBACKS: LazyLock<RwLock<HashMap<(AssetNamespace, String), String>>> =
|
||||
LazyLock::new(Default::default);
|
||||
|
||||
pub fn retain_ui_preview_fallback(namespace: AssetNamespace, material: &str, image: &str) {
|
||||
PREVIEW_FALLBACKS
|
||||
.write()
|
||||
.unwrap_or_else(|poison| poison.into_inner())
|
||||
.insert((namespace, material.to_ascii_lowercase()), image.to_owned());
|
||||
}
|
||||
|
||||
pub fn ui_preview_fallback(namespace: AssetNamespace, material: &str) -> Option<String> {
|
||||
PREVIEW_FALLBACKS
|
||||
.read()
|
||||
.unwrap_or_else(|poison| poison.into_inner())
|
||||
.get(&(
|
||||
namespace,
|
||||
asset_core::AssetRef::bare_name(material).to_ascii_lowercase(),
|
||||
))
|
||||
.cloned()
|
||||
}
|
||||
|
||||
pub fn retain_ui_material_images(materials: &crate::MaterialCatalog) {
|
||||
let mut images = IMAGES.write().unwrap_or_else(|poison| poison.into_inner());
|
||||
for material in &materials.materials {
|
||||
if let Some(image) = materials.hud_image_name(material) {
|
||||
images.insert(
|
||||
(
|
||||
material.namespace,
|
||||
material.name.as_str().to_ascii_lowercase(),
|
||||
),
|
||||
image.to_owned(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ui_material_image(namespace: AssetNamespace, material: &str) -> Option<String> {
|
||||
IMAGES
|
||||
.read()
|
||||
.unwrap_or_else(|poison| poison.into_inner())
|
||||
.get(&(
|
||||
namespace,
|
||||
asset_core::AssetRef::bare_name(material).to_ascii_lowercase(),
|
||||
))
|
||||
.cloned()
|
||||
}
|
||||
@@ -1237,7 +1237,9 @@ fn decode_map_material_images(
|
||||
glass_names: Vec<String>,
|
||||
) -> Vec<String> {
|
||||
let mut report = Vec::new();
|
||||
match decode_material_color_maps(path, catalog, &stage, crate::session_load::load_pool()) {
|
||||
let mut trees = asset_transport::NamespaceTrees::default();
|
||||
trees.adopt_zone(path);
|
||||
match decode_material_color_maps(&trees, catalog, &stage, crate::session_load::load_pool()) {
|
||||
Ok(stats) => {
|
||||
report.push(format!(
|
||||
"IWD color/normal maps: {}/{} decoded, {} missing, {} unsupported from {} archives",
|
||||
@@ -1258,7 +1260,7 @@ fn decode_map_material_images(
|
||||
name: name.clone(),
|
||||
}));
|
||||
match asset_material::material_images::decode_images_for_keys(
|
||||
path,
|
||||
&trees,
|
||||
catalog,
|
||||
keys,
|
||||
&stage,
|
||||
|
||||
@@ -238,8 +238,10 @@ impl ZoneLane for T5Lane {
|
||||
let map_xmodels = std::mem::take(&mut sink.map_xmodels);
|
||||
let bodies = std::mem::take(&mut sink.bodies);
|
||||
let fpv_meshes = std::mem::take(&mut sink.fpv_meshes);
|
||||
let mut trees = asset_transport::NamespaceTrees::default();
|
||||
trees.adopt_zone(path);
|
||||
match decode_material_color_maps(
|
||||
path,
|
||||
&trees,
|
||||
&mut materials,
|
||||
&stage,
|
||||
crate::session_load::load_pool(),
|
||||
|
||||
@@ -235,7 +235,15 @@ pub async fn load_shell_common(games: asset_transport::GamesRoot) -> ShellCommon
|
||||
let mut report = Vec::new();
|
||||
let key = CommonKey::shell(&games, &mut report);
|
||||
let (common, reach) = ensure_common(key).await;
|
||||
let weapons = common.products.weapons.clone().publish();
|
||||
let weapons = common.products.weapons.clone().publish_for_loadout();
|
||||
for family in weapons.weapon_families().families() {
|
||||
if let Some(id) = family.base
|
||||
&& let Some(image) = weapons.hud_icon_image_of(id)
|
||||
&& !family.image.is_empty()
|
||||
{
|
||||
asset_material::retain_ui_preview_fallback(family.key.namespace, &family.image, image);
|
||||
}
|
||||
}
|
||||
report.push(format!(
|
||||
"CAC: {reach} common set {}; weapons={} (iw4={} iw5={} t5={} t6={}) tables={}",
|
||||
common.key,
|
||||
@@ -774,6 +782,8 @@ async fn prepare_common(key: CommonKey) -> Arc<CommonSet> {
|
||||
material_seed.mark_images_common_owned();
|
||||
let mut iw5_materials = iw5_materials;
|
||||
iw5_materials.mark_images_common_owned();
|
||||
asset_material::retain_ui_material_images(&material_seed);
|
||||
asset_material::retain_ui_material_images(&iw5_materials);
|
||||
let set = Arc::new(CommonSet {
|
||||
id: NEXT_COMMON_PROFILE_ID.fetch_add(1, std::sync::atomic::Ordering::Relaxed),
|
||||
key,
|
||||
|
||||
@@ -7,6 +7,15 @@ pub(super) async fn walk_prepared_match(
|
||||
common_mp: Result<PathBuf, String>,
|
||||
progress: LoadProgress,
|
||||
) -> (MatchLoadOutcome, Option<Arc<CommonSet>>) {
|
||||
let mut image_trees = games_root_from_env()
|
||||
.map(|root| asset_transport::NamespaceTrees::discover(&root))
|
||||
.unwrap_or_default();
|
||||
if let Ok(path) = &common_mp {
|
||||
image_trees.adopt_zone(path);
|
||||
}
|
||||
if let Ok(path) = &zone_ff {
|
||||
image_trees.adopt_zone(path);
|
||||
}
|
||||
let zone_name = zone_ff
|
||||
.as_ref()
|
||||
.ok()
|
||||
@@ -174,6 +183,7 @@ pub(super) async fn walk_prepared_match(
|
||||
} = loaded;
|
||||
world.source_namespace = map_namespace;
|
||||
report.append(&mut common_report);
|
||||
report.extend(image_trees.report_lines());
|
||||
|
||||
if facts.team_settings.allies.is_none() && facts.team_settings.axis.is_none() {
|
||||
if let Some(name) = facts.t5_teamset.as_ref() {
|
||||
@@ -494,16 +504,28 @@ pub(super) async fn walk_prepared_match(
|
||||
));
|
||||
merge_image_batch(&mut global, label, batch, job, &mut report);
|
||||
}
|
||||
if let Ok(path) = &zone_ff {
|
||||
if zone_ff.is_ok() {
|
||||
let stage = progress.begin_scoped(StageId::Images, "merged", None);
|
||||
let decoded =
|
||||
asset_material::decode_material_color_maps(path, &mut global, &stage, load_pool());
|
||||
let decoded = asset_material::decode_material_color_maps(
|
||||
&image_trees,
|
||||
&mut global,
|
||||
&stage,
|
||||
load_pool(),
|
||||
);
|
||||
stage.finish_from(&decoded);
|
||||
match decoded {
|
||||
Ok(stats) => report.push(format!(
|
||||
"merged material images: {}/{} decoded, {} missing, {} unsupported",
|
||||
stats.decoded, stats.requested, stats.missing, stats.unsupported
|
||||
)),
|
||||
Ok(stats) => {
|
||||
report.push(format!(
|
||||
"merged material images: {}/{} decoded, {} missing, {} unsupported",
|
||||
stats.decoded, stats.requested, stats.missing, stats.unsupported
|
||||
));
|
||||
if let Some(gap) = stats.first_gap {
|
||||
report.push(format!("merged material image missing: {gap}"));
|
||||
}
|
||||
if let Some(gap) = stats.first_unsupported {
|
||||
report.push(format!("merged material image unsupported: {gap}"));
|
||||
}
|
||||
}
|
||||
Err(error) => report.push(format!("merged material images gap: {error}")),
|
||||
}
|
||||
}
|
||||
@@ -513,7 +535,7 @@ pub(super) async fn walk_prepared_match(
|
||||
&mut global,
|
||||
&common_light_defs,
|
||||
map_namespace,
|
||||
&zone_ff,
|
||||
&image_trees,
|
||||
&progress,
|
||||
&mut report,
|
||||
);
|
||||
@@ -524,10 +546,10 @@ pub(super) async fn walk_prepared_match(
|
||||
"in-zone builtin images: decoded {builtins} leftover $ 2D loadDefs after absorb"
|
||||
));
|
||||
}
|
||||
if let Ok(path) = &zone_ff {
|
||||
if zone_ff.is_ok() {
|
||||
let stage = progress.begin_scoped(StageId::Images, "tracers", None);
|
||||
let decoded = asset_material::material_images::decode_images_for_keys(
|
||||
path,
|
||||
&image_trees,
|
||||
&mut global,
|
||||
common_tracers.material_keys(),
|
||||
&stage,
|
||||
@@ -648,7 +670,7 @@ pub(super) async fn walk_prepared_match(
|
||||
"fx color maps handoff: n=0 bytes=0 (Bound GPU bind at spawn; no CPU clone sidecar; stub_aliases=0)"
|
||||
.into(),
|
||||
);
|
||||
decode_fx_colour_maps(&world.fx, &mut global, &zone_ff, &progress, &mut report);
|
||||
decode_fx_colour_maps(&world.fx, &mut global, &image_trees, &progress, &mut report);
|
||||
if world.impact_fx.is_none() {
|
||||
world.impact_fx = if map_namespace == Some(asset_core::AssetNamespace::T5) {
|
||||
t5_impact_fx.or(common_impact)
|
||||
@@ -774,7 +796,7 @@ fn resolve_world_lights(
|
||||
global: &mut asset_material::MaterialDefinitions,
|
||||
common_light_defs: &[asset_world::CapturedLightDef],
|
||||
map_namespace: Option<asset_core::AssetNamespace>,
|
||||
zone_ff: &Result<PathBuf, String>,
|
||||
image_trees: &asset_transport::NamespaceTrees,
|
||||
progress: &LoadProgress,
|
||||
report: &mut Vec<String>,
|
||||
) -> Option<asset_world::ResolvedLightDef> {
|
||||
@@ -840,7 +862,7 @@ fn resolve_world_lights(
|
||||
.map(|def| def.attenuation_image_name.as_deref())
|
||||
.collect::<Vec<_>>(),
|
||||
));
|
||||
if let Ok(path) = &zone_ff {
|
||||
{
|
||||
let mut requested: Vec<(usize, u8)> = draw
|
||||
.primary_lights
|
||||
.iter()
|
||||
@@ -856,7 +878,7 @@ fn resolve_world_lights(
|
||||
let want = want.len();
|
||||
let stage = progress.begin_scoped(StageId::Images, "attenuation", None);
|
||||
let decoded = asset_material::decode_catalog_images_from_iwd(
|
||||
path,
|
||||
image_trees,
|
||||
global,
|
||||
requested,
|
||||
&stage,
|
||||
@@ -1030,7 +1052,7 @@ fn link_materials_after_absorb(
|
||||
fn decode_fx_colour_maps(
|
||||
fx: &FxCatalog,
|
||||
global: &mut asset_material::MaterialDefinitions,
|
||||
zone_ff: &Result<PathBuf, String>,
|
||||
image_trees: &asset_transport::NamespaceTrees,
|
||||
progress: &LoadProgress,
|
||||
report: &mut Vec<String>,
|
||||
) {
|
||||
@@ -1048,20 +1070,18 @@ fn decode_fx_colour_maps(
|
||||
})
|
||||
.collect();
|
||||
if !missing.is_empty() {
|
||||
if let Ok(path) = zone_ff {
|
||||
let stage = progress.begin_scoped(StageId::Images, "fx_elem", None);
|
||||
let decoded = asset_material::material_images::decode_images_for_keys(
|
||||
path,
|
||||
global,
|
||||
missing,
|
||||
&stage,
|
||||
load_pool(),
|
||||
);
|
||||
stage.finish_from(&decoded);
|
||||
match decoded {
|
||||
Ok(n) => report.push(format!("fx elem 2d images after absorb: {n} decoded")),
|
||||
Err(error) => report.push(format!("fx elem 2d images after absorb: {error}")),
|
||||
}
|
||||
let stage = progress.begin_scoped(StageId::Images, "fx_elem", None);
|
||||
let decoded = asset_material::material_images::decode_images_for_keys(
|
||||
image_trees,
|
||||
global,
|
||||
missing,
|
||||
&stage,
|
||||
load_pool(),
|
||||
);
|
||||
stage.finish_from(&decoded);
|
||||
match decoded {
|
||||
Ok(n) => report.push(format!("fx elem 2d images after absorb: {n} decoded")),
|
||||
Err(error) => report.push(format!("fx elem 2d images after absorb: {error}")),
|
||||
}
|
||||
}
|
||||
let nocolor: Vec<(usize, String)> = fx
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use asset_core::AssetNamespace;
|
||||
use bevy::prelude::*;
|
||||
use frame::{UiMenuDvars, UiMenuRequest};
|
||||
use ui::classes::display::{category_label, label, localized, preview_image};
|
||||
use ui::classes::display::{category_label, class_name, label, localized, preview_image};
|
||||
use ui::{ClassEditRow, ClassLoadoutCatalog, ClassPickerFolder, SessionClassStore};
|
||||
|
||||
use crate::{CommandSpec, ConsoleCommand, ConsoleRegistry};
|
||||
@@ -197,8 +197,10 @@ pub(crate) fn route(
|
||||
mut menus: MessageWriter<UiMenuRequest>,
|
||||
mut echo: crate::feature_dispatch::ConsoleEcho,
|
||||
) {
|
||||
let (loc, prepared_loc) = loc;
|
||||
let camo_loc = prepared_loc.as_ref().map_or(&*loc, |strings| &strings.0);
|
||||
let (shell_loc, prepared_loc) = loc;
|
||||
let loc = prepared_loc
|
||||
.as_ref()
|
||||
.map_or(&*shell_loc, |strings| &strings.0);
|
||||
for command in events
|
||||
.read()
|
||||
.filter(|command| command.name.starts_with("ui_class_"))
|
||||
@@ -383,7 +385,7 @@ pub(crate) fn route(
|
||||
"menu: class {} {} camouflage = {}",
|
||||
selected + 1,
|
||||
row.label(),
|
||||
localized_camo_label(&catalog, camo_loc, slot.row_value(row), camo)
|
||||
localized_camo_label(&catalog, loc, slot.row_value(row), camo)
|
||||
));
|
||||
} else {
|
||||
echo.write(format!(
|
||||
@@ -452,7 +454,7 @@ pub(crate) fn route(
|
||||
.collect();
|
||||
if name.is_empty() {
|
||||
return Err(localized(
|
||||
&loc,
|
||||
loc,
|
||||
"MENU_IWNET_CREATE_BADNAME",
|
||||
"Enter a class name",
|
||||
));
|
||||
@@ -486,7 +488,7 @@ pub(crate) fn route(
|
||||
store
|
||||
.slots
|
||||
.get(at)
|
||||
.map(|slot| slot.name.clone())
|
||||
.map(|slot| class_name(&slot.name))
|
||||
.unwrap_or_default(),
|
||||
);
|
||||
}
|
||||
@@ -496,13 +498,42 @@ pub(crate) fn route(
|
||||
{
|
||||
dvars.set("ui_class_status", format!("Class unavailable: {reason}"));
|
||||
}
|
||||
dvars.set("ui_class_title", &slot.name);
|
||||
dvars.set("ui_class_title", class_name(&slot.name));
|
||||
dvars.set("ui_class_saved_name", &slot.name);
|
||||
dvars.set("ui_class_index", store.selected.to_string());
|
||||
for (at, row, weapon) in [
|
||||
(0, ClassEditRow::Primary, &slot.primary),
|
||||
(1, ClassEditRow::Secondary, &slot.secondary),
|
||||
] {
|
||||
let caption = localized(loc, row.loc_key().trim_start_matches('@'), row.label());
|
||||
for (kind, key, fallback, available) in [
|
||||
(
|
||||
"attachments",
|
||||
"MENU_ATTACHMENTS_CAPS",
|
||||
"Attachments",
|
||||
!catalog.attachments(row, weapon).is_empty(),
|
||||
),
|
||||
(
|
||||
"camo",
|
||||
"MENU_CAMO_CAPS",
|
||||
"Camouflage",
|
||||
!camo_names(&catalog, weapon).is_empty(),
|
||||
),
|
||||
] {
|
||||
dvars.set(
|
||||
&format!("ui_class_has_{kind}_{at}"),
|
||||
if available { "1" } else { "0" },
|
||||
);
|
||||
dvars.set(
|
||||
&format!("ui_class_{kind}_button_{at}"),
|
||||
format!("{caption}: {}", localized(loc, key, fallback)),
|
||||
);
|
||||
}
|
||||
}
|
||||
for row in ClassEditRow::ALL {
|
||||
dvars.set(
|
||||
&format!("ui_class_value_{}", row.as_u8()),
|
||||
label(slot.row_value(row), &catalog, &loc),
|
||||
label(slot.row_value(row), &catalog, loc),
|
||||
);
|
||||
dvars.set(
|
||||
&format!("ui_class_image_{}", row.as_u8()),
|
||||
@@ -554,14 +585,14 @@ pub(crate) fn route(
|
||||
if key.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
label(&key, &catalog, &loc)
|
||||
label(&key, &catalog, loc)
|
||||
},
|
||||
);
|
||||
}
|
||||
dvars.set(
|
||||
&format!("ui_class_{name}_attachments"),
|
||||
if selected.is_empty() {
|
||||
label("", &catalog, &loc)
|
||||
label("", &catalog, loc)
|
||||
} else {
|
||||
selected
|
||||
.iter()
|
||||
@@ -576,7 +607,7 @@ pub(crate) fn route(
|
||||
}
|
||||
),
|
||||
&catalog,
|
||||
&loc,
|
||||
loc,
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
@@ -616,14 +647,15 @@ pub(crate) fn route(
|
||||
};
|
||||
dvars.set("ui_class_picker_depth", picker_depth.to_string());
|
||||
let mut caption = state.row.map_or_else(
|
||||
|| localized(&loc, "MENU_CLASSES", "Classes"),
|
||||
|row| localized(&loc, row.loc_key(), row.label()),
|
||||
|| localized(loc, "MENU_CLASSES", "Classes"),
|
||||
|row| localized(loc, row.loc_key(), row.label()),
|
||||
);
|
||||
if state.attachments {
|
||||
caption.push_str(" / ");
|
||||
caption.push_str(&localized(&loc, "MENU_ATTACHMENTS_CAPS", "Attachments"));
|
||||
caption.push_str(&localized(loc, "MENU_ATTACHMENTS_CAPS", "Attachments"));
|
||||
} else if state.camo {
|
||||
caption.push_str(" / Camouflage");
|
||||
caption.push_str(" / ");
|
||||
caption.push_str(&localized(loc, "MENU_CAMO_CAPS", "Camouflage"));
|
||||
}
|
||||
dvars.set("ui_class_caption", caption);
|
||||
dvars.set(
|
||||
@@ -637,7 +669,7 @@ pub(crate) fn route(
|
||||
"ui_class_category_label",
|
||||
state
|
||||
.folder
|
||||
.map(|folder| category_label(folder, &loc))
|
||||
.map(|folder| category_label(folder, loc))
|
||||
.unwrap_or_default(),
|
||||
);
|
||||
state.page = state.page.min(choices.len().saturating_sub(1) / PAGE_SIZE);
|
||||
@@ -653,7 +685,7 @@ pub(crate) fn route(
|
||||
&format!("ui_class_category_{at}"),
|
||||
folders
|
||||
.get(at)
|
||||
.map(|folder| category_label(*folder, &loc))
|
||||
.map(|folder| category_label(*folder, loc))
|
||||
.unwrap_or_default(),
|
||||
);
|
||||
dvars.set(
|
||||
@@ -671,7 +703,7 @@ pub(crate) fn route(
|
||||
if selected { "* " } else { "" },
|
||||
localized_camo_label(
|
||||
&catalog,
|
||||
camo_loc,
|
||||
loc,
|
||||
slot.zip(state.row)
|
||||
.map_or("", |(slot, row)| slot.row_value(row)),
|
||||
value
|
||||
@@ -704,26 +736,26 @@ pub(crate) fn route(
|
||||
format!(
|
||||
"{}{}",
|
||||
if selected { "* " } else { "" },
|
||||
label(&key, &catalog, &loc)
|
||||
label(&key, &catalog, loc)
|
||||
)
|
||||
})
|
||||
.unwrap_or_default(),
|
||||
);
|
||||
}
|
||||
let title = if state.camo {
|
||||
"Camouflage".to_owned()
|
||||
localized(loc, "MENU_CAMO_CAPS", "Camouflage")
|
||||
} else if state.attachments {
|
||||
localized(&loc, "MENU_ATTACHMENTS_CAPS", "Attachments")
|
||||
localized(loc, "MENU_ATTACHMENTS_CAPS", "Attachments")
|
||||
} else if let Some(folder) = state.folder {
|
||||
format!(
|
||||
"{} / {}",
|
||||
folder.namespace.as_str().to_uppercase(),
|
||||
category_label(folder, &loc)
|
||||
category_label(folder, loc)
|
||||
)
|
||||
} else {
|
||||
state.row.map_or_else(
|
||||
|| localized(&loc, "MENU_WEAPON_CLASSES_CAPS", "Classes"),
|
||||
|row| localized(&loc, row.loc_key(), row.label()),
|
||||
|| localized(loc, "MENU_WEAPON_CLASSES_CAPS", "Classes"),
|
||||
|row| localized(loc, row.loc_key(), row.label()),
|
||||
)
|
||||
};
|
||||
dvars.set("ui_class_picker_title", title);
|
||||
@@ -786,8 +818,22 @@ pub(crate) fn route(
|
||||
);
|
||||
dvars.set(
|
||||
"ui_class_preview_title",
|
||||
preview
|
||||
.map(|(_, p)| localized(&loc, &p.name_key, &p.reference))
|
||||
choices
|
||||
.get(state.page * PAGE_SIZE + state.hover)
|
||||
.map(|key| {
|
||||
let weapon = store
|
||||
.slots
|
||||
.get(store.selected)
|
||||
.zip(state.row)
|
||||
.map_or("", |(slot, row)| slot.row_value(row));
|
||||
if state.camo {
|
||||
localized_camo_label(&catalog, loc, weapon, key)
|
||||
} else if state.attachments && !key.is_empty() {
|
||||
label(&format!("{weapon}+{key}"), &catalog, loc)
|
||||
} else {
|
||||
label(key, &catalog, loc)
|
||||
}
|
||||
})
|
||||
.unwrap_or_default(),
|
||||
);
|
||||
for (at, bar) in asset_game::CacStatBar::ALL.into_iter().enumerate() {
|
||||
|
||||
@@ -510,17 +510,22 @@ impl HudImages {
|
||||
return Some((width, height, rgba.as_ref().clone()));
|
||||
}
|
||||
let main = self.trees.main_for(ns)?;
|
||||
let mapped = (ns == HUD_CHROME_NAMESPACE)
|
||||
.then(|| {
|
||||
self.material_images
|
||||
.get(&cache_key(name))
|
||||
.map(String::as_str)
|
||||
})
|
||||
.flatten();
|
||||
let authored = asset_material::ui_material_image(ns, name);
|
||||
let fallback = asset_material::ui_preview_fallback(ns, name);
|
||||
let mapped = authored.as_deref().or_else(|| {
|
||||
(ns == HUD_CHROME_NAMESPACE)
|
||||
.then(|| {
|
||||
self.material_images
|
||||
.get(&cache_key(name))
|
||||
.map(String::as_str)
|
||||
})
|
||||
.flatten()
|
||||
});
|
||||
for image_name in mapped
|
||||
.filter(|image| *image != name)
|
||||
.into_iter()
|
||||
.chain(std::iter::once(name))
|
||||
.chain(fallback.as_deref())
|
||||
{
|
||||
match asset_material::decode_ui_image_from_main(main, image_name) {
|
||||
Ok(Some(image)) => return Some(image),
|
||||
|
||||
@@ -460,7 +460,18 @@ fn offhand_stand_in(world: &mut World, weapon: u32) -> Option<u32> {
|
||||
let stand_in = frame
|
||||
.weapon_setup(weapon)
|
||||
.filter(|setup| setup.realm != realm)
|
||||
.and_then(|setup| setup.stand_in.as_deref());
|
||||
.and_then(|setup| {
|
||||
setup.stand_in.as_deref().or_else(|| {
|
||||
(realm == crate::script::Realm::Iw4).then(|| match setup.base.as_str() {
|
||||
"willy_pete" => "smoke_grenade_mp",
|
||||
"tabun_gas" => "concussion_grenade_mp",
|
||||
"sticky_grenade" => "semtex_mp",
|
||||
"hatchet" => "throwingknife_mp",
|
||||
"satchel_charge" | "c4death" => "c4_mp",
|
||||
_ => frame.weapon_script_name(weapon),
|
||||
})
|
||||
})
|
||||
});
|
||||
frame
|
||||
.weapon_index_by_script_name(stand_in.unwrap_or(frame.weapon_script_name(weapon)))
|
||||
.filter(|&named| weapon != 0 && named != weapon)
|
||||
|
||||
@@ -367,7 +367,8 @@
|
||||
"index": 57,
|
||||
"fields": {
|
||||
"text_exp": "op 38 s:75695f636c6173735f646573635f34 op 1",
|
||||
"vis_exp": "1"
|
||||
"vis_exp": "1",
|
||||
"text_scale": 0.25
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -381,7 +382,8 @@
|
||||
"index": 63,
|
||||
"fields": {
|
||||
"text_exp": "op 38 s:75695f636c6173735f646573635f35 op 1",
|
||||
"vis_exp": "1"
|
||||
"vis_exp": "1",
|
||||
"text_scale": 0.25
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -395,7 +397,8 @@
|
||||
"index": 69,
|
||||
"fields": {
|
||||
"text_exp": "op 38 s:75695f636c6173735f646573635f36 op 1",
|
||||
"vis_exp": "1"
|
||||
"vis_exp": "1",
|
||||
"text_scale": 0.25
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -409,14 +412,16 @@
|
||||
"index": 78,
|
||||
"fields": {
|
||||
"text_exp": "op 38 s:75695f636c6173735f76616c75655f32 op 1",
|
||||
"vis_exp": "1"
|
||||
"vis_exp": "1",
|
||||
"text_scale": 0.3
|
||||
}
|
||||
},
|
||||
{
|
||||
"index": 80,
|
||||
"fields": {
|
||||
"text_exp": "op 38 s:75695f636c6173735f76616c75655f33 op 1",
|
||||
"vis_exp": "1"
|
||||
"vis_exp": "1",
|
||||
"text_scale": 0.3
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -624,6 +629,206 @@
|
||||
"text_align_y": 12,
|
||||
"static_flags": 9437184,
|
||||
"vis_exp": "op 7 op 57 s:636c6173735f72656e616d65 op 1 op 14 op 7 op 57 s:636c6173735f7069636b6572 op 1"
|
||||
},
|
||||
{
|
||||
"name": "class_attachments_0",
|
||||
"item_type": 1,
|
||||
"text_exp": "op 38 s:75695f636c6173735f6174746163686d656e74735f627574746f6e5f30 op 1",
|
||||
"rect": {
|
||||
"x": -64,
|
||||
"y": 238,
|
||||
"w": 336,
|
||||
"h": 20,
|
||||
"horz_align": 1,
|
||||
"vert_align": 1
|
||||
},
|
||||
"background": ",menu_button_selection_bar",
|
||||
"style": 1,
|
||||
"fore_color": [
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1
|
||||
],
|
||||
"font_enum": 3,
|
||||
"text_scale": 0.3,
|
||||
"text_align_mode": 6,
|
||||
"text_align_x": -60,
|
||||
"vis_exp": "op 57 s:636c6173735f656469746f72 op 1 op 14 op 35 s:75695f636c6173735f6861735f6174746163686d656e74735f30 op 1",
|
||||
"handlers": {
|
||||
"action": [
|
||||
{
|
||||
"SetLocalVar": {
|
||||
"kind": 5,
|
||||
"name": "ui_class_anchor",
|
||||
"expr": "op 32 op 150 op 17 140 op 1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Script": "play mouse_click; exec \"ui_class_attachments 0\";"
|
||||
}
|
||||
],
|
||||
"focus": [
|
||||
{
|
||||
"Script": "setItemColor self backcolor 0 0 0 1;"
|
||||
}
|
||||
],
|
||||
"leave_focus": [
|
||||
{
|
||||
"Script": "setItemColor self backcolor 0 0 0 0;"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "class_camo_0",
|
||||
"item_type": 1,
|
||||
"text_exp": "op 38 s:75695f636c6173735f63616d6f5f627574746f6e5f30 op 1",
|
||||
"rect": {
|
||||
"x": -64,
|
||||
"y": 258,
|
||||
"w": 336,
|
||||
"h": 20,
|
||||
"horz_align": 1,
|
||||
"vert_align": 1
|
||||
},
|
||||
"background": ",menu_button_selection_bar",
|
||||
"style": 1,
|
||||
"fore_color": [
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1
|
||||
],
|
||||
"font_enum": 3,
|
||||
"text_scale": 0.3,
|
||||
"text_align_mode": 6,
|
||||
"text_align_x": -60,
|
||||
"vis_exp": "op 57 s:636c6173735f656469746f72 op 1 op 14 op 35 s:75695f636c6173735f6861735f63616d6f5f30 op 1",
|
||||
"handlers": {
|
||||
"action": [
|
||||
{
|
||||
"SetLocalVar": {
|
||||
"kind": 5,
|
||||
"name": "ui_class_anchor",
|
||||
"expr": "op 32 op 150 op 17 140 op 1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Script": "play mouse_click; exec \"ui_class_camo 0\";"
|
||||
}
|
||||
],
|
||||
"focus": [
|
||||
{
|
||||
"Script": "setItemColor self backcolor 0 0 0 1;"
|
||||
}
|
||||
],
|
||||
"leave_focus": [
|
||||
{
|
||||
"Script": "setItemColor self backcolor 0 0 0 0;"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "class_attachments_1",
|
||||
"item_type": 1,
|
||||
"text_exp": "op 38 s:75695f636c6173735f6174746163686d656e74735f627574746f6e5f31 op 1",
|
||||
"rect": {
|
||||
"x": -64,
|
||||
"y": 278,
|
||||
"w": 336,
|
||||
"h": 20,
|
||||
"horz_align": 1,
|
||||
"vert_align": 1
|
||||
},
|
||||
"background": ",menu_button_selection_bar",
|
||||
"style": 1,
|
||||
"fore_color": [
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1
|
||||
],
|
||||
"font_enum": 3,
|
||||
"text_scale": 0.3,
|
||||
"text_align_mode": 6,
|
||||
"text_align_x": -60,
|
||||
"vis_exp": "op 57 s:636c6173735f656469746f72 op 1 op 14 op 35 s:75695f636c6173735f6861735f6174746163686d656e74735f31 op 1",
|
||||
"handlers": {
|
||||
"action": [
|
||||
{
|
||||
"SetLocalVar": {
|
||||
"kind": 5,
|
||||
"name": "ui_class_anchor",
|
||||
"expr": "op 32 op 150 op 17 140 op 1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Script": "play mouse_click; exec \"ui_class_attachments 1\";"
|
||||
}
|
||||
],
|
||||
"focus": [
|
||||
{
|
||||
"Script": "setItemColor self backcolor 0 0 0 1;"
|
||||
}
|
||||
],
|
||||
"leave_focus": [
|
||||
{
|
||||
"Script": "setItemColor self backcolor 0 0 0 0;"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "class_camo_1",
|
||||
"item_type": 1,
|
||||
"text_exp": "op 38 s:75695f636c6173735f63616d6f5f627574746f6e5f31 op 1",
|
||||
"rect": {
|
||||
"x": -64,
|
||||
"y": 298,
|
||||
"w": 336,
|
||||
"h": 20,
|
||||
"horz_align": 1,
|
||||
"vert_align": 1
|
||||
},
|
||||
"background": ",menu_button_selection_bar",
|
||||
"style": 1,
|
||||
"fore_color": [
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1
|
||||
],
|
||||
"font_enum": 3,
|
||||
"text_scale": 0.3,
|
||||
"text_align_mode": 6,
|
||||
"text_align_x": -60,
|
||||
"vis_exp": "op 57 s:636c6173735f656469746f72 op 1 op 14 op 35 s:75695f636c6173735f6861735f63616d6f5f31 op 1",
|
||||
"handlers": {
|
||||
"action": [
|
||||
{
|
||||
"SetLocalVar": {
|
||||
"kind": 5,
|
||||
"name": "ui_class_anchor",
|
||||
"expr": "op 32 op 150 op 17 140 op 1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Script": "play mouse_click; exec \"ui_class_camo 1\";"
|
||||
}
|
||||
],
|
||||
"focus": [
|
||||
{
|
||||
"Script": "setItemColor self backcolor 0 0 0 1;"
|
||||
}
|
||||
],
|
||||
"leave_focus": [
|
||||
{
|
||||
"Script": "setItemColor self backcolor 0 0 0 0;"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -3480,7 +3685,12 @@
|
||||
"fields": {
|
||||
"vis_exp": "op 38 s:75695f636c6173735f70726576696577 op 1 op 13 s:",
|
||||
"material_exp": "op 38 s:75695f636c6173735f70726576696577 op 1",
|
||||
"float_exp": [[2, "op 35 s:75695f636c6173735f707265766965775f7769647468 op 1"]]
|
||||
"float_exp": [
|
||||
[
|
||||
2,
|
||||
"op 35 s:75695f636c6173735f707265766965775f7769647468 op 1"
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -6,6 +6,17 @@ pub fn localized(loc: &asset_game::LocalizeCatalog, key: &str, fallback: &str) -
|
||||
.to_owned()
|
||||
}
|
||||
|
||||
pub fn class_name(name: &str) -> String {
|
||||
if frame::showcase_classes()
|
||||
.iter()
|
||||
.any(|preset| preset.name == name)
|
||||
{
|
||||
name.replace('_', " ").to_ascii_uppercase()
|
||||
} else {
|
||||
name.to_owned()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn category_label(folder: ClassPickerFolder, loc: &asset_game::LocalizeCatalog) -> String {
|
||||
folder.category.map_or_else(
|
||||
|| localized(loc, "MENU_EQUIPMENT_CAPS", "Equipment"),
|
||||
@@ -32,7 +43,13 @@ pub fn label(
|
||||
.unwrap_or(presentation.fallback_label())
|
||||
.to_owned();
|
||||
}
|
||||
key.to_owned()
|
||||
if key.contains('+') {
|
||||
asset_game::CacItemPresentation::prepare_attachment(key)
|
||||
} else {
|
||||
asset_game::CacItemPresentation::prepare_weapon(key)
|
||||
}
|
||||
.fallback_label()
|
||||
.to_owned()
|
||||
}
|
||||
|
||||
pub fn preview_image(key: &str, catalog: &ClassLoadoutCatalog) -> String {
|
||||
|
||||
@@ -230,6 +230,7 @@ fn consume_class_select_handoff(
|
||||
mut catalog: ResMut<crate::ClassLoadoutCatalog>,
|
||||
mut store: ResMut<SessionClassStore>,
|
||||
weapons: Option<Res<assets::PreparedWeapons>>,
|
||||
menus: Res<asset_game::MenuCatalog>,
|
||||
) {
|
||||
if !handoff.pending {
|
||||
return;
|
||||
@@ -237,12 +238,25 @@ fn consume_class_select_handoff(
|
||||
let Some(weapons) = weapons else {
|
||||
return;
|
||||
};
|
||||
if catalog.resolver.0.is_none() {
|
||||
*catalog = crate::ClassLoadoutCatalog::from_weapon_registry(weapons.registry().clone());
|
||||
} else {
|
||||
catalog.resolver.0 = Some(weapons.registry().clone());
|
||||
let revision = catalog.revision.wrapping_add(1);
|
||||
let mut updated = crate::ClassLoadoutCatalog::from_weapon_registry(weapons.registry().clone());
|
||||
if let Some(table) = menus.string_table("mp/perkTable.csv") {
|
||||
updated = updated.with_perk_table(table);
|
||||
}
|
||||
catalog.revision = catalog.revision.wrapping_add(1);
|
||||
updated.perks = catalog.perks.clone();
|
||||
updated.deathstreak = catalog.deathstreak.clone();
|
||||
for (key, preview) in &catalog.previews {
|
||||
if asset_core::AssetKey::parse(key).is_err() {
|
||||
updated.previews.insert(key.clone(), preview.clone());
|
||||
} else if let Some(current) = updated.previews.get_mut(key) {
|
||||
if current.desc_key.is_empty() {
|
||||
current.desc_key.clone_from(&preview.desc_key);
|
||||
}
|
||||
current.bars.clone_from(&preview.bars);
|
||||
}
|
||||
}
|
||||
*catalog = updated;
|
||||
catalog.revision = revision;
|
||||
catalog.primary = std::mem::take(&mut handoff.primary);
|
||||
catalog.secondary = std::mem::take(&mut handoff.secondary);
|
||||
catalog.lethal = std::mem::take(&mut handoff.lethal);
|
||||
|
||||
@@ -143,8 +143,10 @@ impl Default for ClassLoadoutCatalog {
|
||||
impl ClassLoadoutCatalog {
|
||||
pub fn from_weapon_registry(registry: std::sync::Arc<asset_game::WeaponRegistry>) -> Self {
|
||||
let families = registry.weapon_families();
|
||||
let offered: std::collections::HashSet<_> =
|
||||
families.offered().map(|family| &family.key).collect();
|
||||
let mut catalog = Self::default();
|
||||
for family in families.offered() {
|
||||
for family in families.families() {
|
||||
let key = family.key.asset_key();
|
||||
catalog
|
||||
.presentation
|
||||
@@ -183,6 +185,9 @@ impl ClassLoadoutCatalog {
|
||||
),
|
||||
);
|
||||
}
|
||||
if !offered.contains(&family.key) {
|
||||
continue;
|
||||
}
|
||||
let offer = frame::CacWeaponOffer {
|
||||
key,
|
||||
item_group: Some(family.item_group.clone()),
|
||||
|
||||
+3
-3
@@ -5,8 +5,8 @@
|
||||
those operations; `configuration/{iw4,iw5,t5,t6}` own source selection rules.
|
||||
|
||||
`WeaponFamilies` normalizes selections and checks attachments and host limits.
|
||||
Resolution and UI toggles return a private registry-issued `WeaponHandle` plus
|
||||
read-only canonical selection. Console/UI use this resolver. Publication iteration
|
||||
Frontend loadout publication defers map-selected hands admission; matches validate soldiers and skeletons. Saved class previews include unavailable captured families.
|
||||
Resolution and UI toggles return a private registry-issued `WeaponHandle` plus read-only canonical selection. Console/UI use this resolver. Publication iteration
|
||||
includes the last valid row; unarmed, unknown and unsupported remain distinct.
|
||||
|
||||
`WeaponRegistry::bind` rejects foreign revisions before row access. Exact clones
|
||||
@@ -47,4 +47,4 @@ Body/tree/script/clip replacement resets animation and preserves corpse occupati
|
||||
|
||||
Publication compiles appearance plans once; `SelectedWeaponAppearance` binds models, overrides and UI metadata.
|
||||
Raw hand/camouflage data stays on `WeaponBuild`; runtime consumers select appearances.
|
||||
Native previews are optional; known IW4 model choices compile their own preview keys.
|
||||
The editor reopens attachments/camouflage; previews retain namespace material/image bindings and fall back to authored HUD icons. IW4 scripts bridge foreign smoke, gas, sticky, knife and C4 equipment names.
|
||||
|
||||
Reference in New Issue
Block a user