diff --git a/crates/analysis/src/completion.rs b/crates/analysis/src/completion.rs index f24bf12..9123f70 100644 --- a/crates/analysis/src/completion.rs +++ b/crates/analysis/src/completion.rs @@ -494,7 +494,7 @@ fn type_snippet_placeholder(ty: &ValueType, n: usize) -> String { ValueType::AsciiString | ValueType::AsciiStringList | ValueType::QuotedString => { format!("${{{n}:Value}}") } - ValueType::W3dModel => format!("${{{n}:Model}}"), + ValueType::W3dModel | ValueType::W3dModelList => format!("${{{n}:Model}}"), ValueType::W3dModelMember => format!("${{{n}:Bone}}"), _ => format!("${{{n}:?}}"), } @@ -678,7 +678,7 @@ fn completions_for_type( })); out } - ValueType::W3dModel | ValueType::W3dModelMember => Vec::new(), + ValueType::W3dModel | ValueType::W3dModelList | ValueType::W3dModelMember => Vec::new(), _ => Vec::new(), } } @@ -791,6 +791,7 @@ fn type_label(ty: &ValueType) -> String { ValueType::BitFlags { value_set } => format!("flags {value_set}"), ValueType::Reference { ref_kind } => format!("ref {ref_kind:?}"), ValueType::W3dModel => "w3d model".into(), + ValueType::W3dModelList => "w3d models".into(), ValueType::W3dModelMember => "w3d model member".into(), ValueType::RandomVariable { .. } => "real real [distribution]".into(), ValueType::RandomKeyframe => "real real frame".into(), @@ -986,6 +987,27 @@ End assert!(out.contains(&"WeaponA".to_string()), "{out:?}"); } + #[test] + fn ocl_model_list_completes_every_position() { + let a = Analyzer::embedded(); + let mut index = WorkspaceIndex::new(); + index.set_file_models( + "models/Good.w3d", + vec![crate::index::ModelAsset { + name: "Good".into(), + members: vec![], + }], + ); + let src = + "ObjectCreationList Debris\n CreateDebris\n ModelNames = First \n End\nEnd\n"; + let offset = src.find("First ").unwrap() + "First ".len(); + let out = complete(&a, &a.parse(src), offset as u32, Some(&index), None) + .into_iter() + .map(|item| item.label) + .collect::>(); + assert!(out.contains(&"Good".to_string()), "{out:?}"); + } + #[test] fn weapon_bone_completions_use_token_positions() { let a = Analyzer::embedded(); diff --git a/crates/analysis/src/diagnostics.rs b/crates/analysis/src/diagnostics.rs index 00fa676..0cc47fb 100644 --- a/crates/analysis/src/diagnostics.rs +++ b/crates/analysis/src/diagnostics.rs @@ -1012,6 +1012,16 @@ impl<'a> Ctx<'a> { } let tokens = field.value_tokens(); match &schema_field.value_type { + ValueType::W3dModelList => { + for tok in &tokens { + self.validate_model_asset_token( + &schema_field.value_type, + tok, + scope_node, + schema_field.model_source.as_ref(), + ); + } + } ValueType::TokenList { tokens: specs } => { let mut i = 0; for spec in specs { @@ -1499,6 +1509,7 @@ impl<'a> Ctx<'a> { | ValueType::QuotedString | ValueType::AsciiStringList | ValueType::W3dModel + | ValueType::W3dModelList | ValueType::W3dModelMember | ValueType::Color | ValueType::Coord2D @@ -2670,6 +2681,30 @@ End ); } + #[test] + fn ocl_object_and_model_lists_validate_every_reference() { + let a = Analyzer::embedded(); + let mut index = WorkspaceIndex::new(); + let objects = a.parse("Object KnownObject\nEnd\n"); + index.set_file( + "objects.ini", + crate::index::definitions_in(&a, &objects, "objects.ini"), + ); + index.set_file_models( + "models/Good.w3d", + vec![crate::index::ModelAsset { + name: "Good".into(), + members: vec![], + }], + ); + let src = "ObjectCreationList Test\n CreateObject\n ObjectNames = KnownObject MissingObject\n End\n CreateDebris\n ModelNames = Good MissingModel\n End\nEnd\n"; + let diags = diagnose(&a, &a.parse(src), Some(&index), Some("ocl.ini")); + assert!(diags.iter().any(|d| d.code == "unresolved-reference" + && &src[d.span.start as usize..d.span.end as usize] == "MissingObject")); + assert!(diags.iter().any(|d| d.code == "unknown-model" + && &src[d.span.start as usize..d.span.end as usize] == "MissingModel")); + } + #[test] fn model_member_strictness_supports_off_compatible_and_strict() { let a = Analyzer::embedded(); diff --git a/crates/analysis/src/index.rs b/crates/analysis/src/index.rs index b516ec5..c12913f 100644 --- a/crates/analysis/src/index.rs +++ b/crates/analysis/src/index.rs @@ -586,10 +586,19 @@ fn collect_object_models(analyzer: &Analyzer, node: &SyntaxNode, out: &mut Vec out.extend( + values + .iter() + .map(|value| value.text().trim_matches('"').to_string()), + ), + ValueType::W3dModel => { + if let Some(value) = values.first() { + out.push(value.text().trim_matches('"').to_string()); + } } + _ => {} } } SyntaxKind::BLOCK | SyntaxKind::MODULE => collect_object_models(analyzer, &child, out), diff --git a/crates/analysis/src/model.rs b/crates/analysis/src/model.rs index 228625f..2750077 100644 --- a/crates/analysis/src/model.rs +++ b/crates/analysis/src/model.rs @@ -144,7 +144,7 @@ pub fn enclosing_scopes<'a>(analyzer: &'a Analyzer, node: &SyntaxNode) -> Vec bool { - matches!(ty, ValueType::W3dModel) + matches!(ty, ValueType::W3dModel | ValueType::W3dModelList) } pub(crate) fn is_model_member_type(ty: &ValueType) -> bool { @@ -253,6 +253,11 @@ fn collect_models(analyzer: &Analyzer, node: &SyntaxNode, out: &mut Vec) .map(|value| value.text().trim_matches('"')) .collect::>(); match &schema_field.value_type { + ValueType::W3dModelList => out.extend( + values + .iter() + .map(|value| value.text().trim_matches('"').to_string()), + ), ValueType::TokenList { .. } | ValueType::OneOf { .. } | ValueType::Prefixed { .. } => { diff --git a/crates/analysis/src/semantic.rs b/crates/analysis/src/semantic.rs index ca56582..52efad7 100644 --- a/crates/analysis/src/semantic.rs +++ b/crates/analysis/src/semantic.rs @@ -249,6 +249,7 @@ fn value_token_kind(tok: &SyntaxToken, ty: Option<&ValueType>) -> SemKind { Some(ValueType::Reference { .. }) | Some(ValueType::ReferenceList { .. }) | Some(ValueType::W3dModel) + | Some(ValueType::W3dModelList) | Some(ValueType::W3dModelMember) => SemKind::Reference, _ => SemKind::StringLit, } diff --git a/crates/analysis/tests/spec/OCLReferences.ini b/crates/analysis/tests/spec/OCLReferences.ini new file mode 100644 index 0000000..3e317ec --- /dev/null +++ b/crates/analysis/tests/spec/OCLReferences.ini @@ -0,0 +1,22 @@ +Object OCLObjectOne +End + +Object OCLObjectTwo +End + +ParticleSystem HulkExplosionTrail +End + +AudioEvent DebrisBigMetal +End + +ObjectCreationList OCL_References + CreateObject + ObjectNames = OCLObjectOne $1NoSuchOCLObject + ParticleSystem = $2NoSuchParticleSystem + End + CreateDebris + ParticleSystem = $3NoSuchDebrisParticleSystem + BounceSound = $4NoSuchBounceSound + End +End diff --git a/crates/analysis/tests/spec/OCLReferences.spec.toml b/crates/analysis/tests/spec/OCLReferences.spec.toml new file mode 100644 index 0000000..cce9267 --- /dev/null +++ b/crates/analysis/tests/spec/OCLReferences.spec.toml @@ -0,0 +1,37 @@ +# ObjectNames is a variadic list of Object references. + +[[diag]] +severity = "warning" +code = "unresolved-reference" +on = "NoSuchOCLObject" + +[[complete]] +at = "$1" +includes = ["OCLObjectOne", "OCLObjectTwo"] + +[[diag]] +severity = "warning" +code = "unresolved-reference" +on = "NoSuchParticleSystem" + +[[diag]] +severity = "warning" +code = "unresolved-reference" +on = "NoSuchDebrisParticleSystem" + +[[diag]] +severity = "warning" +code = "unresolved-reference" +on = "NoSuchBounceSound" + +[[complete]] +at = "$2" +includes = ["HulkExplosionTrail"] + +[[complete]] +at = "$3" +includes = ["HulkExplosionTrail"] + +[[complete]] +at = "$4" +includes = ["DebrisBigMetal"] diff --git a/crates/schema/schema.json b/crates/schema/schema.json index ea21d9f..597ffe5 100644 --- a/crates/schema/schema.json +++ b/crates/schema/schema.json @@ -12544,7 +12544,8 @@ { "name": "ParticleSystem", "value_type": { - "kind": "ascii_string" + "kind": "reference", + "ref_kind": "particle_system" }, "parse_fn": "INI::parseAsciiString" }, @@ -12756,8 +12757,8 @@ { "name": "ObjectNames", "value_type": { - "kind": "unknown", - "parse_fn": "parseDebrisObjectNames" + "kind": "reference_list", + "ref_kind": "object" }, "parse_fn": "parseDebrisObjectNames" }, @@ -12825,7 +12826,8 @@ { "name": "ParticleSystem", "value_type": { - "kind": "ascii_string" + "kind": "reference", + "ref_kind": "particle_system" }, "parse_fn": "INI::parseAsciiString" }, @@ -13030,8 +13032,7 @@ { "name": "ModelNames", "value_type": { - "kind": "unknown", - "parse_fn": "parseDebrisObjectNames" + "kind": "w3d_model_list" }, "parse_fn": "parseDebrisObjectNames" }, @@ -13084,8 +13085,8 @@ { "name": "BounceSound", "value_type": { - "kind": "unknown", - "parse_fn": "INI::parseAudioEventRTS" + "kind": "reference", + "ref_kind": "audio_event" }, "parse_fn": "INI::parseAudioEventRTS" } diff --git a/crates/schema/src/lib.rs b/crates/schema/src/lib.rs index e8c85cd..abe3d33 100644 --- a/crates/schema/src/lib.rs +++ b/crates/schema/src/lib.rs @@ -205,6 +205,8 @@ pub enum ValueType { AsciiStringList, /// A W3D model asset name, backed by indexed `.w3d` files. W3dModel, + /// One or more W3D model asset names. + W3dModelList, /// A bone, subobject, mesh, or other member of a W3D model asset. W3dModelMember, /// `R:r G:g B:b [A:a]` color.