mirror of
https://github.com/vladtrc/iw4L
synced 2026-10-25 09:59:40 -04:00
Restore T5 character animation, killfeed colors and killcam audio continuity
This commit is contained in:
@@ -106,10 +106,30 @@ impl PlayerAnimSources {
|
||||
self.multiplayer_atr.as_deref(),
|
||||
self.playeranim_script.as_deref(),
|
||||
));
|
||||
if let Some(Ok(tree)) = self.compiled.as_ref() {
|
||||
if let Some(script) = self.playeranim_script.as_deref() {
|
||||
if let Some(Ok(tree)) = self.compiled.as_ref()
|
||||
&& let Some(script) = self.playeranim_script.as_deref()
|
||||
{
|
||||
self.parsed_script = Some(crate::playeranim_parse::parse_player_anim_script(
|
||||
script,
|
||||
tree,
|
||||
self.family == Some(asset_core::AssetNamespace::T5),
|
||||
));
|
||||
}
|
||||
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(),
|
||||
)
|
||||
{
|
||||
self.compiled = Some(crate::atr_compile::compile_selected_multiplayer(
|
||||
atr, tree, script,
|
||||
));
|
||||
if let Some(Ok(tree)) = self.compiled.as_ref()
|
||||
&& let Some(script) = self.playeranim_script.as_deref()
|
||||
{
|
||||
self.parsed_script = Some(crate::playeranim_parse::parse_player_anim_script(
|
||||
script, tree,
|
||||
script, tree, true,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,8 @@ use crate::animtree::{
|
||||
PLAYERANIM_SCRIPT_PATH,
|
||||
};
|
||||
|
||||
const UNSUPPORTED_BRANCH: u16 = 1 << 15;
|
||||
|
||||
const BG_FIND_ANIM_NAMES: &[&str] = &["torso", "legs", "turning"];
|
||||
|
||||
pub(crate) fn compile_multiplayer(
|
||||
@@ -28,6 +30,54 @@ pub(crate) fn compile_multiplayer(
|
||||
names.insert((*name).to_owned());
|
||||
}
|
||||
harvest_script_anim_names(script, &mut names);
|
||||
compile_named_tree(atr, names)
|
||||
}
|
||||
|
||||
pub(crate) fn compile_selected_multiplayer(
|
||||
atr: &[u8],
|
||||
tree: &CompiledAnimTreeDefinition,
|
||||
script: &crate::ParsedPlayerAnimScript,
|
||||
) -> Result<Arc<CompiledAnimTreeDefinition>, AtrCompileError> {
|
||||
let mut names: HashSet<String> = BG_FIND_ANIM_NAMES
|
||||
.iter()
|
||||
.map(|name| (*name).into())
|
||||
.collect();
|
||||
for (_, _, items) in &script.slots {
|
||||
add_selected_names(&mut names, tree, items);
|
||||
}
|
||||
for (_, items) in &script.events {
|
||||
add_selected_names(&mut names, tree, items);
|
||||
}
|
||||
let tree = compile_named_tree(atr, names)?;
|
||||
if tree.node_count() > usize::from(anim_iw4::PLAYER_ANIM_INDEX_MASK) + 1 {
|
||||
return Err(AtrCompileError::BadToken {
|
||||
offset: 0,
|
||||
message: "character animation tree exceeds packed index capacity".into(),
|
||||
});
|
||||
}
|
||||
Ok(tree)
|
||||
}
|
||||
|
||||
fn add_selected_names(
|
||||
names: &mut HashSet<String>,
|
||||
tree: &CompiledAnimTreeDefinition,
|
||||
items: &[crate::ParsedAnimItem],
|
||||
) {
|
||||
for command in items
|
||||
.iter()
|
||||
.filter(|item| !item.skip)
|
||||
.flat_map(|item| &item.commands)
|
||||
{
|
||||
if let Some(node) = tree.node(command.anim_index) {
|
||||
names.insert(node.name.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn compile_named_tree(
|
||||
atr: &[u8],
|
||||
names: HashSet<String>,
|
||||
) -> Result<Arc<CompiledAnimTreeDefinition>, AtrCompileError> {
|
||||
let script_names = names.len();
|
||||
let mut ignored = 0usize;
|
||||
let children = parse_atr(atr, &names, &mut ignored)?;
|
||||
@@ -114,7 +164,22 @@ fn parse_internal(
|
||||
return Err(AtrCompileError::DuplicateAnimation { name });
|
||||
}
|
||||
let ignore = !complete && !names.contains(&name);
|
||||
let on_line = parser.parse(false);
|
||||
let mut on_line = parser.parse(false);
|
||||
if on_line == "[" {
|
||||
let key = parser.parse(false);
|
||||
let equals = parser.parse(false);
|
||||
let value = parser.parse(false).parse::<f32>();
|
||||
let close = parser.parse(false);
|
||||
if !key.eq_ignore_ascii_case("turn")
|
||||
|| equals != "="
|
||||
|| value.is_err()
|
||||
|| value.is_ok_and(|value| !value.is_finite())
|
||||
|| close != "]"
|
||||
{
|
||||
return Err(parser.bad_token("invalid animation turn annotation"));
|
||||
}
|
||||
on_line = parser.parse(false);
|
||||
}
|
||||
if on_line.is_empty() {
|
||||
current = Some(WorkAnim {
|
||||
name,
|
||||
@@ -130,7 +195,7 @@ fn parse_internal(
|
||||
if on_line != ":" {
|
||||
return Err(parser.bad_token("bad token"));
|
||||
}
|
||||
let flags = parse_properties(parser)?;
|
||||
let flags = parse_properties(parser, ignore)?;
|
||||
let brace = parser.parse(true);
|
||||
if brace != "{" {
|
||||
return Err(
|
||||
@@ -169,6 +234,9 @@ fn parse_internal(
|
||||
if nested_eof {
|
||||
return Err(parser.bad_token("unexpected end of file"));
|
||||
}
|
||||
if !nested.is_empty() && work.flags & UNSUPPORTED_BRANCH != 0 {
|
||||
return Err(parser.bad_token("unsupported property on retained animation branch"));
|
||||
}
|
||||
if nested.is_empty() {
|
||||
*ignored += 1;
|
||||
} else {
|
||||
@@ -239,7 +307,7 @@ fn finish_parent(children: &mut Vec<ParsedAnim>, include_parent: bool, loop_sync
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_properties(parser: &mut AtrParser<'_>) -> Result<u16, AtrCompileError> {
|
||||
fn parse_properties(parser: &mut AtrParser<'_>, ignored: bool) -> Result<u16, AtrCompileError> {
|
||||
let mut flags = 0u16;
|
||||
loop {
|
||||
let token = parser.parse(false);
|
||||
@@ -254,6 +322,12 @@ fn parse_properties(parser: &mut AtrParser<'_>) -> Result<u16, AtrCompileError>
|
||||
Some(1) => flags |= ANIMFLAG_NONLOOPSYNC,
|
||||
Some(2) => flags |= ANIMFLAG_COMPLETE,
|
||||
Some(3) => flags |= ANIMFLAG_ADDITIVE,
|
||||
_ if ignored
|
||||
&& (token.eq_ignore_ascii_case("client")
|
||||
|| token.eq_ignore_ascii_case("separate")) =>
|
||||
{
|
||||
flags |= UNSUPPORTED_BRANCH
|
||||
}
|
||||
_ => return Err(parser.bad_token("unknown anim property")),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,6 +133,7 @@ impl core::fmt::Display for PlayerAnimParseError {
|
||||
pub(crate) fn parse_player_anim_script(
|
||||
script: &[u8],
|
||||
tree: &CompiledAnimTreeDefinition,
|
||||
t5_syntax: bool,
|
||||
) -> Result<Arc<ParsedPlayerAnimScript>, PlayerAnimParseError> {
|
||||
let mut parser = AtrParser::new(script);
|
||||
let mut parsed = ParsedPlayerAnimScript {
|
||||
@@ -167,7 +168,7 @@ pub(crate) fn parse_player_anim_script(
|
||||
}
|
||||
if mode == 0 {
|
||||
if token.eq_ignore_ascii_case("set") {
|
||||
parse_define(&mut parser, &mut aliases)?;
|
||||
parse_define(&mut parser, &mut aliases, t5_syntax)?;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
@@ -207,7 +208,7 @@ pub(crate) fn parse_player_anim_script(
|
||||
}
|
||||
1 => {
|
||||
parser.unget();
|
||||
let item = parse_item(&mut parser, tree, &mut parsed, &aliases)?;
|
||||
let item = parse_item(&mut parser, tree, &mut parsed, &aliases, t5_syntax)?;
|
||||
if let Some(items) = current_event_items.as_mut() {
|
||||
if items.len() >= 128 {
|
||||
return Err(bad(&parser, "exceeded maximum items per script (128)"));
|
||||
@@ -246,7 +247,7 @@ pub(crate) fn parse_player_anim_script(
|
||||
}
|
||||
2 => {
|
||||
parser.unget();
|
||||
let item = parse_item(&mut parser, tree, &mut parsed, &aliases)?;
|
||||
let item = parse_item(&mut parser, tree, &mut parsed, &aliases, t5_syntax)?;
|
||||
if let Some(items) = current_items.as_mut() {
|
||||
if items.len() >= 128 {
|
||||
return Err(bad(&parser, "exceeded maximum items per script (128)"));
|
||||
@@ -293,8 +294,9 @@ fn parse_item(
|
||||
tree: &CompiledAnimTreeDefinition,
|
||||
parsed: &mut ParsedPlayerAnimScript,
|
||||
aliases: &HashMap<(u8, String), u64>,
|
||||
t5_syntax: bool,
|
||||
) -> Result<ParsedAnimItem, PlayerAnimParseError> {
|
||||
let (skip, conditions, raw) = parse_conditions(parser, aliases)?;
|
||||
let (skip, conditions, raw) = parse_conditions(parser, aliases, t5_syntax)?;
|
||||
let brace = parser.parse(true);
|
||||
if brace != "{" {
|
||||
parser.unget();
|
||||
@@ -336,7 +338,20 @@ fn parse_item(
|
||||
);
|
||||
} else if extra.eq_ignore_ascii_case("sound") {
|
||||
let _ = parser.parse(false);
|
||||
} else if extra.eq_ignore_ascii_case("turretanim") {
|
||||
} else if t5_syntax
|
||||
&& (extra.eq_ignore_ascii_case("blendouttime")
|
||||
|| extra.eq_ignore_ascii_case("animrate"))
|
||||
{
|
||||
let value = parser.parse(false).parse::<f32>();
|
||||
if value.is_err() || value.is_ok_and(|value| !value.is_finite()) {
|
||||
return Err(bad(parser, "expected animation modifier value"));
|
||||
}
|
||||
} else if extra.eq_ignore_ascii_case("turretanim")
|
||||
|| (t5_syntax
|
||||
&& (extra.eq_ignore_ascii_case("grenadeanim")
|
||||
|| extra.eq_ignore_ascii_case("weapontimescale")
|
||||
|| extra.eq_ignore_ascii_case("allowmovementinterrupt")))
|
||||
{
|
||||
continue;
|
||||
} else {
|
||||
parser.unget();
|
||||
@@ -355,7 +370,8 @@ fn parse_item(
|
||||
blend_ms,
|
||||
});
|
||||
}
|
||||
None => parsed.unresolved_anims += 1,
|
||||
None if !skip => parsed.unresolved_anims += 1,
|
||||
None => {}
|
||||
}
|
||||
}
|
||||
Ok(ParsedAnimItem {
|
||||
@@ -369,9 +385,14 @@ fn parse_item(
|
||||
fn parse_define(
|
||||
parser: &mut AtrParser<'_>,
|
||||
aliases: &mut HashMap<(u8, String), u64>,
|
||||
t5_syntax: bool,
|
||||
) -> Result<(), PlayerAnimParseError> {
|
||||
let cond = parser.parse(false);
|
||||
let Some(index) = index_ci(&cond, ANIM_COND_NAMES) else {
|
||||
if t5_syntax {
|
||||
while !parser.parse(false).is_empty() {}
|
||||
return Ok(());
|
||||
}
|
||||
return Err(bad(parser, "unknown define condition"));
|
||||
};
|
||||
let alias = ascii_lower(&parser.parse(false));
|
||||
@@ -449,6 +470,7 @@ fn resolve_cond_value(
|
||||
fn parse_conditions(
|
||||
parser: &mut AtrParser<'_>,
|
||||
aliases: &HashMap<(u8, String), u64>,
|
||||
t5_syntax: bool,
|
||||
) -> Result<(bool, Vec<ParsedAnimCondition>, String), PlayerAnimParseError> {
|
||||
let mut skip = false;
|
||||
let mut saw = false;
|
||||
@@ -478,6 +500,33 @@ fn parse_conditions(
|
||||
saw = true;
|
||||
continue;
|
||||
}
|
||||
if t5_syntax
|
||||
&& (token.eq_ignore_ascii_case("stance") || token.eq_ignore_ascii_case("direction"))
|
||||
{
|
||||
if let Some(cond) = current.take() {
|
||||
conditions.push(cond);
|
||||
}
|
||||
let value = parser.parse(false);
|
||||
raw.push(value.clone());
|
||||
saw = true;
|
||||
if let Some(condition) = t5_pose_condition(&token, &value) {
|
||||
conditions.push(condition);
|
||||
if token.eq_ignore_ascii_case("direction")
|
||||
&& (value.eq_ignore_ascii_case("forward")
|
||||
|| value.eq_ignore_ascii_case("backward"))
|
||||
{
|
||||
conditions.push(ParsedAnimCondition {
|
||||
index: anim_iw4::ANIM_COND_STRAFING,
|
||||
bitflags: false,
|
||||
bits: 0,
|
||||
value: 0,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
skip = true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if let Some(cond) = current.as_mut()
|
||||
&& let Some(value) = resolve_cond_value(cond.index, &token, aliases)
|
||||
{
|
||||
@@ -538,6 +587,11 @@ fn parse_conditions(
|
||||
skip = true;
|
||||
continue;
|
||||
}
|
||||
if t5_syntax {
|
||||
skip = true;
|
||||
saw = true;
|
||||
continue;
|
||||
}
|
||||
return Err(bad(parser, "unknown condition token"));
|
||||
}
|
||||
if let Some(cond) = current.take() {
|
||||
@@ -549,6 +603,34 @@ fn parse_conditions(
|
||||
Ok((skip, conditions, raw.join(" ")))
|
||||
}
|
||||
|
||||
fn t5_pose_condition(name: &str, value: &str) -> Option<ParsedAnimCondition> {
|
||||
let mask = |indices: &[u8]| {
|
||||
indices
|
||||
.iter()
|
||||
.fold(0u64, |bits, index| bits | (1u64 << index))
|
||||
};
|
||||
let crouch = mask(&[2, 6, 7, 12, 13, 16, 17]);
|
||||
let prone = mask(&[3, 8, 9]);
|
||||
let backward = mask(&[5, 7, 9, 11, 13]);
|
||||
let (index, bitflags, bits, value) =
|
||||
match (ascii_lower(name).as_str(), ascii_lower(value).as_str()) {
|
||||
("stance", "crouch") => (anim_iw4::ANIM_COND_MOVETYPE, true, crouch, 0),
|
||||
("stance", "prone") => (anim_iw4::ANIM_COND_MOVETYPE, true, prone, 0),
|
||||
("stance", "stand") => (anim_iw4::ANIM_COND_MOVETYPE, true, !(crouch | prone), 0),
|
||||
("direction", "backward") => (anim_iw4::ANIM_COND_MOVETYPE, true, backward, 0),
|
||||
("direction", "forward") => (anim_iw4::ANIM_COND_MOVETYPE, true, !backward, 0),
|
||||
("direction", "left") => (anim_iw4::ANIM_COND_STRAFING, false, 0, 1),
|
||||
("direction", "right") => (anim_iw4::ANIM_COND_STRAFING, false, 0, 2),
|
||||
_ => return None,
|
||||
};
|
||||
Some(ParsedAnimCondition {
|
||||
index,
|
||||
bitflags,
|
||||
bits,
|
||||
value,
|
||||
})
|
||||
}
|
||||
|
||||
fn skip_unknown_block(parser: &mut AtrParser<'_>) {
|
||||
let mut depth = 0i32;
|
||||
loop {
|
||||
|
||||
@@ -946,6 +946,12 @@ impl fastfile_t5::AssetLinkSink for CommonWalkSink {
|
||||
{
|
||||
self.teamsets.insert(key, icons);
|
||||
}
|
||||
self.player_anim_sources.capture(
|
||||
asset_core::AssetNamespace::T5,
|
||||
name,
|
||||
data,
|
||||
zlib_compressed,
|
||||
);
|
||||
self.scripts.capture(name, data, zlib_compressed);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -679,6 +679,7 @@ impl ZoneLane for T5Lane {
|
||||
projectile_meshes: sink.projectile_meshes,
|
||||
world_weapons: sink.world_weapons,
|
||||
xanims: sink.xanims,
|
||||
player_anim_sources: sink.player_anim_sources,
|
||||
fx: sink.fx,
|
||||
impact_fx: sink.impact_fx.take_table(),
|
||||
fx_models: sink.fx_models,
|
||||
|
||||
@@ -95,6 +95,7 @@ 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) t5_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,
|
||||
@@ -468,6 +469,7 @@ async fn prepare_common(key: CommonKey) -> Arc<CommonSet> {
|
||||
world_guns: t5_world_guns,
|
||||
mut material_seed,
|
||||
xanims: t5_xanims,
|
||||
player_anim_sources: t5_player_anim_sources,
|
||||
fx: t5_fx,
|
||||
impact_fx: t5_impact_fx,
|
||||
projectiles: t5_projectiles,
|
||||
@@ -892,6 +894,7 @@ async fn prepare_common(key: CommonKey) -> Arc<CommonSet> {
|
||||
fx_models: common_fx_models,
|
||||
impact_fx: common_impact,
|
||||
t5_xanims,
|
||||
t5_player_anim_sources,
|
||||
t5_fx,
|
||||
t5_impact_fx,
|
||||
iw5_materials,
|
||||
|
||||
@@ -740,6 +740,7 @@ pub(super) struct T5WeaponCommon {
|
||||
pub(super) world_guns: WorldWeaponBuild,
|
||||
pub(super) material_seed: MaterialCatalog,
|
||||
pub(super) xanims: XAnimBuild,
|
||||
pub(super) player_anim_sources: asset_anim::PlayerAnimSources,
|
||||
pub(super) fx: FxCatalog,
|
||||
pub(super) impact_fx: Option<asset_game::OwnedFxImpactTable>,
|
||||
pub(super) projectiles: asset_model::ProjectileMeshBuild,
|
||||
@@ -761,6 +762,7 @@ impl T5WeaponCommon {
|
||||
world_guns: WorldWeaponBuild::default(),
|
||||
material_seed,
|
||||
xanims: XAnimBuild::default(),
|
||||
player_anim_sources: asset_anim::PlayerAnimSources::default(),
|
||||
fx: FxCatalog::default(),
|
||||
impact_fx: None,
|
||||
projectiles: asset_model::ProjectileMeshBuild::default(),
|
||||
@@ -827,6 +829,7 @@ pub(super) fn walk_t5_weapon_common(
|
||||
world_guns: census.world_weapons,
|
||||
material_seed: census.material_population,
|
||||
xanims: census.xanims,
|
||||
player_anim_sources: census.player_anim_sources,
|
||||
fx: census.fx,
|
||||
impact_fx: census.impact_fx,
|
||||
projectiles: census.projectile_meshes,
|
||||
|
||||
@@ -90,6 +90,7 @@ pub(super) async fn walk_prepared_match(
|
||||
fx_models: common_fx_models,
|
||||
impact_fx: common_impact,
|
||||
t5_xanims,
|
||||
t5_player_anim_sources,
|
||||
t5_fx,
|
||||
t5_impact_fx,
|
||||
iw5_materials,
|
||||
@@ -184,6 +185,12 @@ 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;
|
||||
player_anim_sources.compile();
|
||||
report.push(player_anim_sources.compile_report_line());
|
||||
report.push(player_anim_sources.parse_report_line());
|
||||
}
|
||||
report.append(&mut common_report);
|
||||
report.extend(image_trees.report_lines());
|
||||
|
||||
|
||||
@@ -303,18 +303,6 @@ impl AudioRuntime {
|
||||
}
|
||||
|
||||
pub(crate) fn set_event_context(&self, context: Option<crate::event::EventContext>) {
|
||||
let previous = self.event_context.get();
|
||||
if previous
|
||||
.zip(context)
|
||||
.is_some_and(|(old, new)| old.world != new.world || old.timeline != new.timeline)
|
||||
{
|
||||
self.cue_cancellation.cancel(
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
self.shared.match_epoch.load(Ordering::Acquire),
|
||||
);
|
||||
}
|
||||
self.event_context.set(context);
|
||||
}
|
||||
|
||||
|
||||
@@ -58,4 +58,4 @@ pub use score::Score;
|
||||
|
||||
pub use stuck_in_client::{G_PLAYER_COLLISION_EJECT_SPEED_DEFAULT, StuckClient, stuck_in_client};
|
||||
pub use suicide::is_really_alive;
|
||||
pub use teams::{TEAM_COLOR_ENEMY_TEAM, TEAM_COLOR_MY_TEAM};
|
||||
pub use teams::{TEAM_COLOR_ALLIES, TEAM_COLOR_AXIS, TEAM_COLOR_ENEMY_TEAM, TEAM_COLOR_MY_TEAM};
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
pub const TEAM_COLOR_AXIS: &str = ".65 .57 .41";
|
||||
|
||||
pub const TEAM_COLOR_ALLIES: &str = ".6 .64 .69";
|
||||
|
||||
pub const TEAM_COLOR_MY_TEAM: &str = ".6 .8 .6";
|
||||
|
||||
pub const TEAM_COLOR_ENEMY_TEAM: &str = "1 .45 .5";
|
||||
|
||||
+41
-11
@@ -174,20 +174,45 @@ fn snapshot_client_team(presented: &net::PresentedSnapshot, client: i32) -> i32
|
||||
.map_or(0, |meta| meta.client_state_team)
|
||||
}
|
||||
|
||||
fn obituary_name_color(local_team: i32, team: i32) -> [f32; 4] {
|
||||
fn obituary_name_color(local_team: i32, team: i32, dvars: sim::ScriptDvars<'_>) -> [f32; 4] {
|
||||
if !matches!(local_team, 1 | 2) || !matches!(team, 1 | 2) {
|
||||
return [1.0, 1.0, 1.0, 1.0];
|
||||
}
|
||||
let rgb = if team == local_team {
|
||||
gamemode_iw4::TEAM_COLOR_MY_TEAM
|
||||
let (name, default) = if dvars
|
||||
.int("useRelativeTeamColors")
|
||||
.is_some_and(|value| value != 0)
|
||||
{
|
||||
if team == local_team {
|
||||
("g_TeamColor_MyTeam", gamemode_iw4::TEAM_COLOR_MY_TEAM)
|
||||
} else {
|
||||
("g_TeamColor_EnemyTeam", gamemode_iw4::TEAM_COLOR_ENEMY_TEAM)
|
||||
}
|
||||
} else if team == 1 {
|
||||
("g_TeamColor_Axis", gamemode_iw4::TEAM_COLOR_AXIS)
|
||||
} else {
|
||||
gamemode_iw4::TEAM_COLOR_ENEMY_TEAM
|
||||
("g_TeamColor_Allies", gamemode_iw4::TEAM_COLOR_ALLIES)
|
||||
};
|
||||
let mut color = [1.0; 4];
|
||||
for (slot, value) in color.iter_mut().zip(rgb.split_whitespace()) {
|
||||
*slot = value.parse().unwrap_or(1.0);
|
||||
}
|
||||
color
|
||||
let parse = |rgb: &str| {
|
||||
let mut values = rgb.split_whitespace().map(str::parse::<f32>);
|
||||
let rgb = [
|
||||
values.next()?.ok()?,
|
||||
values.next()?.ok()?,
|
||||
values.next()?.ok()?,
|
||||
];
|
||||
(values.next().is_none() && rgb.iter().all(|value| value.is_finite())).then(|| {
|
||||
[
|
||||
rgb[0].clamp(0.0, 1.0),
|
||||
rgb[1].clamp(0.0, 1.0),
|
||||
rgb[2].clamp(0.0, 1.0),
|
||||
1.0,
|
||||
]
|
||||
})
|
||||
};
|
||||
dvars
|
||||
.string(name)
|
||||
.and_then(parse)
|
||||
.or_else(|| parse(default))
|
||||
.unwrap_or([1.0; 4])
|
||||
}
|
||||
|
||||
fn snapshot_client_name(presented: &net::PresentedSnapshot, client: i32) -> String {
|
||||
@@ -405,6 +430,11 @@ pub(crate) fn update_killfeed(
|
||||
}
|
||||
|
||||
let local_team = snapshot_client_team(&presented, local.0.0 as i32);
|
||||
let Some(snapshot) = presented.snapshot() else {
|
||||
hide(&mut pass);
|
||||
return;
|
||||
};
|
||||
let dvars = snapshot.meta.script_dvars(local.0);
|
||||
let Some(item) = catalog
|
||||
.as_ref()
|
||||
.and_then(|c| c.get("hud_fullscreen"))
|
||||
@@ -547,7 +577,7 @@ pub(crate) fn update_killfeed(
|
||||
applied.h,
|
||||
font_material.clone(),
|
||||
attacker.clone(),
|
||||
obituary_name_color(local_team, *attacker_team),
|
||||
obituary_name_color(local_team, *attacker_team, dvars),
|
||||
"killfeed_obituary",
|
||||
));
|
||||
x_virtual += attacker_w + icon_spacing;
|
||||
@@ -611,7 +641,7 @@ pub(crate) fn update_killfeed(
|
||||
applied.h,
|
||||
font_material.clone(),
|
||||
victim.clone(),
|
||||
obituary_name_color(local_team, *victim_team),
|
||||
obituary_name_color(local_team, *victim_team, dvars),
|
||||
"killfeed_obituary",
|
||||
));
|
||||
}
|
||||
|
||||
@@ -7,7 +7,17 @@ use bevy_ecs::prelude::World;
|
||||
use gamemode_iw4::Team;
|
||||
|
||||
const MAX_OBJECTIVES: i32 = 32;
|
||||
const ENGINE_SERVER_INFO: &[&str] = &["ui_bomb_timer", "mapname", "g_gametype", "g_hardcore"];
|
||||
const ENGINE_SERVER_INFO: &[&str] = &[
|
||||
"ui_bomb_timer",
|
||||
"mapname",
|
||||
"g_gametype",
|
||||
"g_hardcore",
|
||||
"userelativeteamcolors",
|
||||
"g_teamcolor_axis",
|
||||
"g_teamcolor_allies",
|
||||
"g_teamcolor_myteam",
|
||||
"g_teamcolor_enemyteam",
|
||||
];
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub(crate) struct ScriptObjective {
|
||||
|
||||
+2
-1
@@ -17,7 +17,8 @@ retain its revision; foreign owners and media/source mismatches refuse before pl
|
||||
T5 layers activate at resolution; others wait for primary preparation. Layers share
|
||||
deadlines, cancellation and lifetime, with independent pitch/failure. Unknown looping
|
||||
uses named one-shot compatibility. [CUES.md](CUES.md) details binding and decode contracts.
|
||||
Killcam worlds retain the live sound registry for round-result commands.
|
||||
Killcam worlds retain the live sound registry for round-result commands. Replay
|
||||
timeline changes retire event cues; music and local announcements keep playing.
|
||||
|
||||
Weapon publication compiles thermal scopes, cue namespaces, melee precedence,
|
||||
knife substitution and breath aliases. Prediction identifies fire occurrences;
|
||||
|
||||
+2
-1
@@ -17,7 +17,8 @@ projectiles, script movers, combat events and script effects.
|
||||
Sound event identities use archived ticks during replay, including entity-ring
|
||||
sounds and viewmodel animation markers. Repeated snapshots do not replay the
|
||||
same event. Entering, rewinding or leaving killcam changes the event timeline
|
||||
and cancels cues from the previous timeline.
|
||||
and cancels event cues from the previous timeline. Music, local announcements
|
||||
and other cues without replay event identities continue across these transitions.
|
||||
|
||||
FX transitions reset active effects, tracers, marks and projectile bolt state.
|
||||
Map and script effects are reconstructed from the presented world. Script FX
|
||||
|
||||
Reference in New Issue
Block a user