From d2c392cd315f21457d44d341ecb9ec5d74bf506a Mon Sep 17 00:00:00 2001 From: ViTeXFTW Date: Sat, 18 Jul 2026 14:46:55 +0200 Subject: [PATCH 1/2] Add ParticleSystem value formats --- crates/analysis/src/completion.rs | 19 ++ crates/analysis/src/diagnostics.rs | 47 +++++ crates/analysis/src/semantic.rs | 14 ++ .../tests/spec/ParticleSystemValues.ini | 7 + .../tests/spec/ParticleSystemValues.spec.toml | 27 +++ crates/schema/schema.json | 193 ++++++++++-------- crates/schema/src/lib.rs | 17 +- 7 files changed, 232 insertions(+), 92 deletions(-) create mode 100644 crates/analysis/tests/spec/ParticleSystemValues.ini create mode 100644 crates/analysis/tests/spec/ParticleSystemValues.spec.toml diff --git a/crates/analysis/src/completion.rs b/crates/analysis/src/completion.rs index 45962e4..f24bf12 100644 --- a/crates/analysis/src/completion.rs +++ b/crates/analysis/src/completion.rs @@ -502,6 +502,9 @@ fn type_snippet_placeholder(ty: &ValueType, n: usize) -> String { fn value_snippet(ty: &ValueType) -> Option { match ty { + ValueType::RandomVariable { .. } => Some("${1:0} ${2:0}$0".into()), + ValueType::RandomKeyframe => Some("${1:0} ${2:0} ${3:0}$0".into()), + ValueType::ColorKeyframe => Some("R:${1:0} G:${2:0} B:${3:0} ${4:0}$0".into()), ValueType::TokenList { tokens } if tokens.len() > 1 => { let mut snippet = tokens .iter() @@ -526,6 +529,19 @@ fn completions_for_type( index: Option<&WorkspaceIndex>, ) -> Vec { match ty { + ValueType::RandomVariable { value_set } if value_index == 2 => completions_for_type( + analyzer, + &ValueType::Enum { + value_set: value_set.clone(), + }, + 0, + current_token, + first_token, + index, + ), + ValueType::RandomVariable { .. } | ValueType::RandomKeyframe | ValueType::ColorKeyframe => { + Vec::new() + } ValueType::OneOf { variants } => { if current_token.is_some() || value_index > 0 { return ty @@ -776,6 +792,9 @@ fn type_label(ty: &ValueType) -> String { ValueType::Reference { ref_kind } => format!("ref {ref_kind:?}"), ValueType::W3dModel => "w3d model".into(), ValueType::W3dModelMember => "w3d model member".into(), + ValueType::RandomVariable { .. } => "real real [distribution]".into(), + ValueType::RandomKeyframe => "real real frame".into(), + ValueType::ColorKeyframe => "R: G: B: frame".into(), ValueType::Prefixed { prefix, value_type } => { format!("{prefix}:{}", type_label(value_type)) } diff --git a/crates/analysis/src/diagnostics.rs b/crates/analysis/src/diagnostics.rs index 3cdf4b1..00fa676 100644 --- a/crates/analysis/src/diagnostics.rs +++ b/crates/analysis/src/diagnostics.rs @@ -1326,6 +1326,50 @@ impl<'a> Ctx<'a> { self.check_reference(*ref_kind, tok); } } + ValueType::RandomVariable { value_set } => { + for tok in tokens.iter().take(2) { + self.check_number(tok, NumKind::Real); + } + if tokens.len() < 2 { + if let Some(key) = field.key() { + self.warning( + &key, + "missing-value", + format!("`{}` expects at least 2 values", key.text()), + ); + } + } + if let Some(distribution) = tokens.get(2) { + self.check_enum_member(value_set, distribution); + } + } + ValueType::RandomKeyframe => { + for tok in tokens.iter().take(2) { + self.check_number(tok, NumKind::Real); + } + if let Some(frame) = tokens.get(2) { + self.check_number(frame, NumKind::UInt); + } else if let Some(key) = field.key() { + self.warning( + &key, + "missing-value", + format!("`{}` expects 3 values", key.text()), + ); + } + } + ValueType::ColorKeyframe => { + if tokens.len() >= 4 { + let (color, frame) = tokens.split_at(tokens.len() - 1); + self.check_axes(field, color, &["R", "G", "B"], None, true); + self.check_number(&frame[0], NumKind::UInt); + } else if let Some(key) = field.key() { + self.warning( + &key, + "missing-value", + format!("`{}` expects a color and frame", key.text()), + ); + } + } // A fixed sequence of typed tokens; each listed token is required // (the engine's parse function calls getNextToken for each). ValueType::TokenList { tokens: specs } => { @@ -1459,6 +1503,9 @@ impl<'a> Ctx<'a> { | ValueType::Color | ValueType::Coord2D | ValueType::Coord3D + | ValueType::RandomVariable { .. } + | ValueType::RandomKeyframe + | ValueType::ColorKeyframe | ValueType::TokenList { .. } | ValueType::OneOf { .. } | ValueType::Unknown { .. } => {} diff --git a/crates/analysis/src/semantic.rs b/crates/analysis/src/semantic.rs index b352f72..81f1bf4 100644 --- a/crates/analysis/src/semantic.rs +++ b/crates/analysis/src/semantic.rs @@ -187,6 +187,17 @@ impl<'a> Sem<'a> { .map(|token| token.text().trim_matches('"')) .collect::>(); for (i, tok) in value_tokens.iter().enumerate() { + if matches!(active_ty, Some(ValueType::RandomVariable { .. })) { + self.set( + tok, + if i == 2 { + SemKind::EnumMember + } else { + SemKind::Number + }, + ); + continue; + } // Token lists classify each position by its own element type. let elem = active_ty.and_then(|ty| ty.token_type_at_input(&input, i)); self.set(tok, value_token_kind(tok, elem)); @@ -230,6 +241,9 @@ fn value_token_kind(tok: &SyntaxToken, ty: Option<&ValueType>) -> SemKind { | Some(ValueType::Velocity) | Some(ValueType::Acceleration) | Some(ValueType::Color) + | Some(ValueType::RandomVariable { .. }) + | Some(ValueType::RandomKeyframe) + | Some(ValueType::ColorKeyframe) | Some(ValueType::Coord2D) | Some(ValueType::Coord3D) => SemKind::Number, Some(ValueType::Reference { .. }) diff --git a/crates/analysis/tests/spec/ParticleSystemValues.ini b/crates/analysis/tests/spec/ParticleSystemValues.ini new file mode 100644 index 0000000..e29c04f --- /dev/null +++ b/crates/analysis/tests/spec/ParticleSystemValues.ini @@ -0,0 +1,7 @@ +ParticleSystem ParticleSystemValues + AngleZ = 0 1 $1 + VelocityDamping = 0 1 invalid + AngularDamping = invalid 1 + Alpha1 = 0 1 invalid + Color1 = R:0 G:0 B:0 invalid +End diff --git a/crates/analysis/tests/spec/ParticleSystemValues.spec.toml b/crates/analysis/tests/spec/ParticleSystemValues.spec.toml new file mode 100644 index 0000000..926619d --- /dev/null +++ b/crates/analysis/tests/spec/ParticleSystemValues.spec.toml @@ -0,0 +1,27 @@ +[[complete]] +at = "$1" +includes = ["CONSTANT", "UNIFORM", "GAUSSIAN", "TRIANGULAR", "LOW_BIAS", "HIGH_BIAS"] + +[[diag]] +severity = "error" +code = "bad-enum" +on = "invalid" +nth = 1 + +[[diag]] +severity = "error" +code = "bad-number" +on = "invalid" +nth = 2 + +[[diag]] +severity = "error" +code = "bad-number" +on = "invalid" +nth = 3 + +[[diag]] +severity = "error" +code = "bad-number" +on = "invalid" +nth = 4 diff --git a/crates/schema/schema.json b/crates/schema/schema.json index 6d4fe35..ea21d9f 100644 --- a/crates/schema/schema.json +++ b/crates/schema/schema.json @@ -5732,24 +5732,24 @@ { "name": "Radius", "value_type": { - "kind": "unknown", - "parse_fn": "INI::parseGameClientRandomVariable" + "kind": "random_variable", + "value_set": "random_distribution" }, "parse_fn": "INI::parseGameClientRandomVariable" }, { "name": "Height", "value_type": { - "kind": "unknown", - "parse_fn": "INI::parseGameClientRandomVariable" + "kind": "random_variable", + "value_set": "random_distribution" }, "parse_fn": "INI::parseGameClientRandomVariable" }, { "name": "InitialDelay", "value_type": { - "kind": "unknown", - "parse_fn": "INI::parseGameClientRandomVariable" + "kind": "random_variable", + "value_set": "random_distribution" }, "parse_fn": "INI::parseGameClientRandomVariable" }, @@ -13886,8 +13886,8 @@ { "name": "AngleX", "value_type": { - "kind": "unknown", - "parse_fn": "INI::parseGameClientRandomVariable" + "kind": "random_variable", + "value_set": "random_distribution" }, "parse_fn": "INI::parseGameClientRandomVariable", "doc": "only compiled with PARTICLE_USE_XY_ROTATION" @@ -13895,8 +13895,8 @@ { "name": "AngleY", "value_type": { - "kind": "unknown", - "parse_fn": "INI::parseGameClientRandomVariable" + "kind": "random_variable", + "value_set": "random_distribution" }, "parse_fn": "INI::parseGameClientRandomVariable", "doc": "only compiled with PARTICLE_USE_XY_ROTATION" @@ -13904,8 +13904,8 @@ { "name": "AngleZ", "value_type": { - "kind": "unknown", - "parse_fn": "INI::parseGameClientRandomVariable" + "kind": "random_variable", + "value_set": "random_distribution" }, "parse_fn": "INI::parseGameClientRandomVariable", "doc": null @@ -13913,8 +13913,8 @@ { "name": "AngularRateX", "value_type": { - "kind": "unknown", - "parse_fn": "INI::parseGameClientRandomVariable" + "kind": "random_variable", + "value_set": "random_distribution" }, "parse_fn": "INI::parseGameClientRandomVariable", "doc": "only compiled with PARTICLE_USE_XY_ROTATION" @@ -13922,8 +13922,8 @@ { "name": "AngularRateY", "value_type": { - "kind": "unknown", - "parse_fn": "INI::parseGameClientRandomVariable" + "kind": "random_variable", + "value_set": "random_distribution" }, "parse_fn": "INI::parseGameClientRandomVariable", "doc": "only compiled with PARTICLE_USE_XY_ROTATION" @@ -13931,8 +13931,8 @@ { "name": "AngularRateZ", "value_type": { - "kind": "unknown", - "parse_fn": "INI::parseGameClientRandomVariable" + "kind": "random_variable", + "value_set": "random_distribution" }, "parse_fn": "INI::parseGameClientRandomVariable", "doc": null @@ -13940,8 +13940,8 @@ { "name": "AngularDamping", "value_type": { - "kind": "unknown", - "parse_fn": "INI::parseGameClientRandomVariable" + "kind": "random_variable", + "value_set": "random_distribution" }, "parse_fn": "INI::parseGameClientRandomVariable", "doc": null @@ -13949,8 +13949,8 @@ { "name": "VelocityDamping", "value_type": { - "kind": "unknown", - "parse_fn": "INI::parseGameClientRandomVariable" + "kind": "random_variable", + "value_set": "random_distribution" }, "parse_fn": "INI::parseGameClientRandomVariable", "doc": null @@ -13992,8 +13992,8 @@ { "name": "Lifetime", "value_type": { - "kind": "unknown", - "parse_fn": "INI::parseGameClientRandomVariable" + "kind": "random_variable", + "value_set": "random_distribution" }, "parse_fn": "INI::parseGameClientRandomVariable", "doc": null @@ -14009,8 +14009,8 @@ { "name": "Size", "value_type": { - "kind": "unknown", - "parse_fn": "INI::parseGameClientRandomVariable" + "kind": "random_variable", + "value_set": "random_distribution" }, "parse_fn": "INI::parseGameClientRandomVariable", "doc": null @@ -14018,8 +14018,8 @@ { "name": "StartSizeRate", "value_type": { - "kind": "unknown", - "parse_fn": "INI::parseGameClientRandomVariable" + "kind": "random_variable", + "value_set": "random_distribution" }, "parse_fn": "INI::parseGameClientRandomVariable", "doc": null @@ -14027,8 +14027,8 @@ { "name": "SizeRate", "value_type": { - "kind": "unknown", - "parse_fn": "INI::parseGameClientRandomVariable" + "kind": "random_variable", + "value_set": "random_distribution" }, "parse_fn": "INI::parseGameClientRandomVariable", "doc": null @@ -14036,8 +14036,8 @@ { "name": "SizeRateDamping", "value_type": { - "kind": "unknown", - "parse_fn": "INI::parseGameClientRandomVariable" + "kind": "random_variable", + "value_set": "random_distribution" }, "parse_fn": "INI::parseGameClientRandomVariable", "doc": null @@ -14045,8 +14045,7 @@ { "name": "Alpha1", "value_type": { - "kind": "unknown", - "parse_fn": "ParticleSystemTemplate::parseRandomKeyframe" + "kind": "random_keyframe" }, "parse_fn": "ParticleSystemTemplate::parseRandomKeyframe", "doc": " " @@ -14054,8 +14053,7 @@ { "name": "Alpha2", "value_type": { - "kind": "unknown", - "parse_fn": "ParticleSystemTemplate::parseRandomKeyframe" + "kind": "random_keyframe" }, "parse_fn": "ParticleSystemTemplate::parseRandomKeyframe", "doc": " " @@ -14063,8 +14061,7 @@ { "name": "Alpha3", "value_type": { - "kind": "unknown", - "parse_fn": "ParticleSystemTemplate::parseRandomKeyframe" + "kind": "random_keyframe" }, "parse_fn": "ParticleSystemTemplate::parseRandomKeyframe", "doc": " " @@ -14072,8 +14069,7 @@ { "name": "Alpha4", "value_type": { - "kind": "unknown", - "parse_fn": "ParticleSystemTemplate::parseRandomKeyframe" + "kind": "random_keyframe" }, "parse_fn": "ParticleSystemTemplate::parseRandomKeyframe", "doc": " " @@ -14081,8 +14077,7 @@ { "name": "Alpha5", "value_type": { - "kind": "unknown", - "parse_fn": "ParticleSystemTemplate::parseRandomKeyframe" + "kind": "random_keyframe" }, "parse_fn": "ParticleSystemTemplate::parseRandomKeyframe", "doc": " " @@ -14090,8 +14085,7 @@ { "name": "Alpha6", "value_type": { - "kind": "unknown", - "parse_fn": "ParticleSystemTemplate::parseRandomKeyframe" + "kind": "random_keyframe" }, "parse_fn": "ParticleSystemTemplate::parseRandomKeyframe", "doc": " " @@ -14099,8 +14093,7 @@ { "name": "Alpha7", "value_type": { - "kind": "unknown", - "parse_fn": "ParticleSystemTemplate::parseRandomKeyframe" + "kind": "random_keyframe" }, "parse_fn": "ParticleSystemTemplate::parseRandomKeyframe", "doc": " " @@ -14108,8 +14101,7 @@ { "name": "Alpha8", "value_type": { - "kind": "unknown", - "parse_fn": "ParticleSystemTemplate::parseRandomKeyframe" + "kind": "random_keyframe" }, "parse_fn": "ParticleSystemTemplate::parseRandomKeyframe", "doc": " " @@ -14117,8 +14109,7 @@ { "name": "Color1", "value_type": { - "kind": "unknown", - "parse_fn": "ParticleSystemTemplate::parseRGBColorKeyframe" + "kind": "color_keyframe" }, "parse_fn": "ParticleSystemTemplate::parseRGBColorKeyframe", "doc": "R:r G:g B:b " @@ -14126,8 +14117,7 @@ { "name": "Color2", "value_type": { - "kind": "unknown", - "parse_fn": "ParticleSystemTemplate::parseRGBColorKeyframe" + "kind": "color_keyframe" }, "parse_fn": "ParticleSystemTemplate::parseRGBColorKeyframe", "doc": "R:r G:g B:b " @@ -14135,8 +14125,7 @@ { "name": "Color3", "value_type": { - "kind": "unknown", - "parse_fn": "ParticleSystemTemplate::parseRGBColorKeyframe" + "kind": "color_keyframe" }, "parse_fn": "ParticleSystemTemplate::parseRGBColorKeyframe", "doc": "R:r G:g B:b " @@ -14144,8 +14133,7 @@ { "name": "Color4", "value_type": { - "kind": "unknown", - "parse_fn": "ParticleSystemTemplate::parseRGBColorKeyframe" + "kind": "color_keyframe" }, "parse_fn": "ParticleSystemTemplate::parseRGBColorKeyframe", "doc": "R:r G:g B:b " @@ -14153,8 +14141,7 @@ { "name": "Color5", "value_type": { - "kind": "unknown", - "parse_fn": "ParticleSystemTemplate::parseRGBColorKeyframe" + "kind": "color_keyframe" }, "parse_fn": "ParticleSystemTemplate::parseRGBColorKeyframe", "doc": "R:r G:g B:b " @@ -14162,8 +14149,7 @@ { "name": "Color6", "value_type": { - "kind": "unknown", - "parse_fn": "ParticleSystemTemplate::parseRGBColorKeyframe" + "kind": "color_keyframe" }, "parse_fn": "ParticleSystemTemplate::parseRGBColorKeyframe", "doc": "R:r G:g B:b " @@ -14171,8 +14157,7 @@ { "name": "Color7", "value_type": { - "kind": "unknown", - "parse_fn": "ParticleSystemTemplate::parseRGBColorKeyframe" + "kind": "color_keyframe" }, "parse_fn": "ParticleSystemTemplate::parseRGBColorKeyframe", "doc": "R:r G:g B:b " @@ -14180,8 +14165,7 @@ { "name": "Color8", "value_type": { - "kind": "unknown", - "parse_fn": "ParticleSystemTemplate::parseRGBColorKeyframe" + "kind": "color_keyframe" }, "parse_fn": "ParticleSystemTemplate::parseRGBColorKeyframe", "doc": "R:r G:g B:b " @@ -14189,8 +14173,8 @@ { "name": "ColorScale", "value_type": { - "kind": "unknown", - "parse_fn": "INI::parseGameClientRandomVariable" + "kind": "random_variable", + "value_set": "random_distribution" }, "parse_fn": "INI::parseGameClientRandomVariable", "doc": null @@ -14198,8 +14182,8 @@ { "name": "BurstDelay", "value_type": { - "kind": "unknown", - "parse_fn": "INI::parseGameClientRandomVariable" + "kind": "random_variable", + "value_set": "random_distribution" }, "parse_fn": "INI::parseGameClientRandomVariable", "doc": null @@ -14207,8 +14191,8 @@ { "name": "BurstCount", "value_type": { - "kind": "unknown", - "parse_fn": "INI::parseGameClientRandomVariable" + "kind": "random_variable", + "value_set": "random_distribution" }, "parse_fn": "INI::parseGameClientRandomVariable", "doc": null @@ -14216,8 +14200,8 @@ { "name": "InitialDelay", "value_type": { - "kind": "unknown", - "parse_fn": "INI::parseGameClientRandomVariable" + "kind": "random_variable", + "value_set": "random_distribution" }, "parse_fn": "INI::parseGameClientRandomVariable", "doc": null @@ -14242,8 +14226,8 @@ { "name": "VelOrthoX", "value_type": { - "kind": "unknown", - "parse_fn": "INI::parseGameClientRandomVariable" + "kind": "random_variable", + "value_set": "random_distribution" }, "parse_fn": "INI::parseGameClientRandomVariable", "doc": null @@ -14251,8 +14235,8 @@ { "name": "VelOrthoY", "value_type": { - "kind": "unknown", - "parse_fn": "INI::parseGameClientRandomVariable" + "kind": "random_variable", + "value_set": "random_distribution" }, "parse_fn": "INI::parseGameClientRandomVariable", "doc": null @@ -14260,8 +14244,8 @@ { "name": "VelOrthoZ", "value_type": { - "kind": "unknown", - "parse_fn": "INI::parseGameClientRandomVariable" + "kind": "random_variable", + "value_set": "random_distribution" }, "parse_fn": "INI::parseGameClientRandomVariable", "doc": null @@ -14269,8 +14253,8 @@ { "name": "VelSpherical", "value_type": { - "kind": "unknown", - "parse_fn": "INI::parseGameClientRandomVariable" + "kind": "random_variable", + "value_set": "random_distribution" }, "parse_fn": "INI::parseGameClientRandomVariable", "doc": null @@ -14278,8 +14262,8 @@ { "name": "VelHemispherical", "value_type": { - "kind": "unknown", - "parse_fn": "INI::parseGameClientRandomVariable" + "kind": "random_variable", + "value_set": "random_distribution" }, "parse_fn": "INI::parseGameClientRandomVariable", "doc": null @@ -14287,8 +14271,8 @@ { "name": "VelCylindricalRadial", "value_type": { - "kind": "unknown", - "parse_fn": "INI::parseGameClientRandomVariable" + "kind": "random_variable", + "value_set": "random_distribution" }, "parse_fn": "INI::parseGameClientRandomVariable", "doc": null @@ -14296,8 +14280,8 @@ { "name": "VelCylindricalNormal", "value_type": { - "kind": "unknown", - "parse_fn": "INI::parseGameClientRandomVariable" + "kind": "random_variable", + "value_set": "random_distribution" }, "parse_fn": "INI::parseGameClientRandomVariable", "doc": null @@ -14305,8 +14289,8 @@ { "name": "VelOutward", "value_type": { - "kind": "unknown", - "parse_fn": "INI::parseGameClientRandomVariable" + "kind": "random_variable", + "value_set": "random_distribution" }, "parse_fn": "INI::parseGameClientRandomVariable", "doc": null @@ -14314,8 +14298,8 @@ { "name": "VelOutwardOther", "value_type": { - "kind": "unknown", - "parse_fn": "INI::parseGameClientRandomVariable" + "kind": "random_variable", + "value_set": "random_distribution" }, "parse_fn": "INI::parseGameClientRandomVariable", "doc": null @@ -68868,6 +68852,35 @@ } ] }, + { + "id": "random_distribution", + "members": [ + { + "name": "CONSTANT", + "value": 0 + }, + { + "name": "UNIFORM", + "value": 1 + }, + { + "name": "GAUSSIAN", + "value": 2 + }, + { + "name": "TRIANGULAR", + "value": 3 + }, + { + "name": "LOW_BIAS", + "value": 4 + }, + { + "name": "HIGH_BIAS", + "value": 5 + } + ] + }, { "id": "particle_priority", "members": [ diff --git a/crates/schema/src/lib.rs b/crates/schema/src/lib.rs index 1b103c1..b5c4ef7 100644 --- a/crates/schema/src/lib.rs +++ b/crates/schema/src/lib.rs @@ -213,6 +213,12 @@ pub enum ValueType { Coord2D, /// `X:x Y:y Z:z` coordinate. Coord3D, + /// Two real bounds followed by an optional distribution name. + RandomVariable { value_set: String }, + /// Two real bounds followed by an unsigned frame number. + RandomKeyframe, + /// `R:r G:g B:b` followed by an unsigned frame number. + ColorKeyframe, /// One name drawn from a value set (an enum). Enum { value_set: String }, /// One or more flag names from a value set, with optional `+`/`-` modifiers @@ -346,7 +352,12 @@ impl ValueType { return active.token_index_at_input(input, index); } let ValueType::TokenList { tokens } = active else { - return Some(0); + return match active { + ValueType::RandomVariable { .. } + | ValueType::RandomKeyframe + | ValueType::ColorKeyframe => Some(index), + _ => Some(0), + }; }; let mut raw = 0; for (logical, ty) in tokens.iter().enumerate() { @@ -694,7 +705,9 @@ mod tests { definitions: &HashSet, ) { match value_type { - ValueType::Enum { value_set } | ValueType::BitFlags { value_set } => assert!( + ValueType::Enum { value_set } + | ValueType::BitFlags { value_set } + | ValueType::RandomVariable { value_set } => assert!( value_sets.contains(value_set.as_str()), "{path} uses missing value set `{value_set}`" ), From d7cd644b705d6f0e736ba17edcb87b9a6ab5f4de Mon Sep 17 00:00:00 2001 From: ViTeXFTW Date: Sat, 18 Jul 2026 14:55:39 +0200 Subject: [PATCH 2/2] Fix ParticleSystem keyframe semantic tokens --- crates/analysis/src/semantic.rs | 13 +++++++++++++ crates/schema/src/lib.rs | 16 +++++++++++++++- 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/crates/analysis/src/semantic.rs b/crates/analysis/src/semantic.rs index 81f1bf4..ca56582 100644 --- a/crates/analysis/src/semantic.rs +++ b/crates/analysis/src/semantic.rs @@ -295,6 +295,19 @@ mod tests { assert!(t.iter().any(|(k, s)| *k == SemKind::Number && s == "100")); } + #[test] + fn classifies_particle_keyframe_values_as_numbers() { + let src = "ParticleSystem Test\n Alpha1 = 0 1 2\n Color1 = R:0 G:1 B:2 3\nEnd\n"; + let t = toks(src); + for value in ["0", "1", "2", "R:0", "G:1", "B:2", "3"] { + assert!( + t.iter() + .any(|(kind, text)| *kind == SemKind::Number && text == value), + "{value} was not classified as a number" + ); + } + } + #[test] fn range_tokens_cover_exactly_the_intersecting_blocks() { let a = Analyzer::embedded(); diff --git a/crates/schema/src/lib.rs b/crates/schema/src/lib.rs index b5c4ef7..e8c85cd 100644 --- a/crates/schema/src/lib.rs +++ b/crates/schema/src/lib.rs @@ -315,7 +315,11 @@ impl ValueType { }; } return match active { - ValueType::BitFlags { .. } | ValueType::ReferenceList { .. } => Some(active), + ValueType::BitFlags { .. } + | ValueType::ReferenceList { .. } + | ValueType::RandomVariable { .. } + | ValueType::RandomKeyframe + | ValueType::ColorKeyframe => Some(active), _ if index == 0 => Some(active), _ => None, }; @@ -562,6 +566,16 @@ mod tests { )); } + #[test] + fn structured_particle_values_type_every_raw_token() { + for (ty, input) in [ + (ValueType::RandomKeyframe, vec!["0", "1", "2"]), + (ValueType::ColorKeyframe, vec!["R:0", "G:0", "B:0", "2"]), + ] { + assert!((0..input.len()).all(|index| ty.token_type_at_input(&input, index).is_some())); + } + } + fn contains(ty: &ValueType, predicate: fn(&ValueType) -> bool) -> bool { predicate(ty) || match ty {