Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,10 @@ For complete `map.ini` and `solo.ini` diagnostics, set
`.big` archives that load before the map. The same setting also enables W3D
model and bone completion and validation.

`zerosyntax.analysis.modelMemberStrictness` controls bone/subobject warnings:
`off`, `compatible` (the default; present in any applicable model), or `strict`
(present in every applicable model).

```json
{
"zerosyntax.baseIniRoots": [
Expand Down
42 changes: 37 additions & 5 deletions crates/analysis/src/completion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ use zerosyntax_syntax::ast::{Block, Field, Module};
use zerosyntax_syntax::{Parse, SyntaxKind, SyntaxNode};

use crate::model::{
is_model_asset_type, is_model_member_type, model_member_ini_name, models_in_scope, scope_schema,
is_model_asset_type, is_model_member_type, model_member_ini_name, models_for_source,
scope_schema,
};
use crate::{Analyzer, WorkspaceIndex};

Expand Down Expand Up @@ -325,9 +326,14 @@ fn field_value_completions(
let mut base = {
let scope = scope_schema(analyzer, scope_node);
if let Some(f) = scope.field(key) {
if let Some(asset_completions) =
model_asset_completions(analyzer, scope_node, &f.value_type, value_index, index)
{
if let Some(asset_completions) = model_asset_completions(
analyzer,
scope_node,
&f.value_type,
value_index,
index,
f.model_source.as_ref(),
) {
asset_completions
} else {
completions_for_type(
Expand Down Expand Up @@ -362,6 +368,7 @@ fn model_asset_completions(
ty: &ValueType,
value_index: usize,
index: Option<&WorkspaceIndex>,
source: Option<&zerosyntax_schema::ModelSource>,
) -> Option<Vec<Completion>> {
let index = index?;
if !index.has_model_assets() {
Expand All @@ -385,7 +392,7 @@ fn model_asset_completions(
return None;
}
let mut seen = std::collections::HashSet::new();
let out = models_in_scope(analyzer, scope_node)
let out = models_for_source(analyzer, scope_node, source, index)
.into_iter()
.flat_map(|model| {
index
Expand Down Expand Up @@ -904,6 +911,31 @@ End
assert!(out.contains(&"Good".to_string()), "{out:?}");
}

#[test]
fn ocl_member_completions_use_transport_models() {
let a = Analyzer::embedded();
let mut index = WorkspaceIndex::new();
index.set_file_models(
"a10.w3d",
vec![crate::index::ModelAsset {
name: "A10".into(),
members: vec!["WeaponA01".into()],
}],
);
index.set_file_object_models(
"objects.ini",
vec![("AmericaJetA10Thunderbolt".into(), vec!["A10".into()])],
);
let src = "ObjectCreationList Strike\n DeliverPayload\n Transport = AmericaJetA10Thunderbolt\n VisibleDropBoneBaseName = \n End\nEnd\n";
let offset =
src.find("VisibleDropBoneBaseName = ").unwrap() + "VisibleDropBoneBaseName = ".len();
let out = complete(&a, &a.parse(src), offset as u32, Some(&index), None)
.into_iter()
.map(|item| item.label)
.collect::<Vec<_>>();
assert!(out.contains(&"WeaponA".to_string()), "{out:?}");
}

#[test]
fn weapon_bone_completions_use_token_positions() {
let a = Analyzer::embedded();
Expand Down
122 changes: 115 additions & 7 deletions crates/analysis/src/diagnostics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,9 @@ use zerosyntax_schema::{RefKind, ValueType};
use zerosyntax_syntax::ast::{Block, Field, Module};
use zerosyntax_syntax::{Parse, SyntaxKind, SyntaxNode, SyntaxToken};

use crate::index::ModelMemberStrictness;
use crate::model::{
is_model_asset_type, is_model_member_type, model_member_matches, models_in_scope,
is_model_asset_type, is_model_member_type, model_member_matches, models_for_source,
module_fits_slot, scope_schema, ScopeSchema,
};
use crate::{Analyzer, Span, WorkspaceIndex};
Expand Down Expand Up @@ -781,12 +782,22 @@ impl<'a> Ctx<'a> {
match &schema_field.value_type {
ValueType::TokenList { tokens: specs } => {
for (spec, tok) in specs.iter().zip(tokens.iter()) {
self.validate_model_asset_token(spec, tok, scope_node);
self.validate_model_asset_token(
spec,
tok,
scope_node,
schema_field.model_source.as_ref(),
);
}
}
ty => {
if let Some(tok) = tokens.first() {
self.validate_model_asset_token(ty, tok, scope_node);
self.validate_model_asset_token(
ty,
tok,
scope_node,
schema_field.model_source.as_ref(),
);
}
}
}
Expand All @@ -797,6 +808,7 @@ impl<'a> Ctx<'a> {
ty: &ValueType,
tok: &SyntaxToken,
scope_node: &SyntaxNode,
source: Option<&zerosyntax_schema::ModelSource>,
) {
let Some(index) = self.index else { return };
let value = unquote(tok.text());
Expand All @@ -816,11 +828,15 @@ impl<'a> Ctx<'a> {
if !is_model_member_type(ty) {
return;
}
let models = models_in_scope(self.analyzer, scope_node);
if index.model_member_strictness() == ModelMemberStrictness::Off {
return;
}
let models = models_for_source(self.analyzer, scope_node, source, index);
if models.is_empty() {
return;
}
let mut checked_any_model = false;
let mut missing = Vec::new();
for model in models {
if !index.is_model_asset(&model) {
continue;
Expand All @@ -830,14 +846,25 @@ impl<'a> Ctx<'a> {
.model_members(&model)
.any(|member| model_member_matches(member, value))
{
return;
if index.model_member_strictness() == ModelMemberStrictness::Compatible {
return;
}
} else {
missing.push(model);
}
}
if checked_any_model {
if checked_any_model && !missing.is_empty() {
self.warning(
tok,
"unknown-model-member",
format!("`{value}` is not a known W3D model bone or subobject"),
format!(
"`{value}` is not a known W3D model bone or subobject{}",
if index.model_member_strictness() == ModelMemberStrictness::Strict {
format!(" in {}", missing.join(", "))
} else {
String::new()
}
),
);
}
}
Expand Down Expand Up @@ -2028,4 +2055,85 @@ End
"{diags:?}"
);
}

#[test]
fn ocl_members_resolve_through_transport_object() {
let a = Analyzer::embedded();
let mut index = WorkspaceIndex::new();
index.set_file_models(
"models/A10.w3d",
vec![crate::index::ModelAsset {
name: "A10".into(),
members: vec!["WeaponA01".into(), "Missile01".into()],
}],
);
let object = "Object AmericaJetA10Thunderbolt\n Draw = W3DModelDraw ModuleTag_Draw\n DefaultConditionState\n Model = A10\n End\n End\nEnd\n";
let object_parse = a.parse(object);
index.set_file(
"objects.ini",
crate::index::definitions_in(&a, &object_parse, "objects.ini"),
);
index.set_file_object_models(
"objects.ini",
crate::index::object_models_in(&a, &object_parse),
);

let src = "ObjectCreationList Strike\n DeliverPayload\n Transport = AmericaJetA10Thunderbolt\n VisibleDropBoneBaseName = WeaponA\n VisibleSubObjectBaseName = Missing\n End\nEnd\n";
let parse = a.parse(src);
let diags = diagnose(&a, &parse, Some(&index), Some("ocl.ini"));
assert!(
!diags.iter().any(|d| d.code == "unknown-model-member"
&& &src[d.span.start as usize..d.span.end as usize] == "WeaponA"),
"{diags:?}"
);
assert!(
diags.iter().any(|d| d.code == "unknown-model-member"
&& &src[d.span.start as usize..d.span.end as usize] == "Missing"),
"{diags:?}"
);

let missing = src.replace("AmericaJetA10Thunderbolt", "MissingTransport");
let diags = diagnose(&a, &a.parse(&missing), Some(&index), Some("ocl.ini"));
assert!(
diags.iter().any(|d| d.code == "unresolved-reference"),
"{diags:?}"
);
assert!(
!diags.iter().any(|d| d.code == "unknown-model-member"),
"{diags:?}"
);
}

#[test]
fn model_member_strictness_supports_off_compatible_and_strict() {
let a = Analyzer::embedded();
let mut index = WorkspaceIndex::new();
index.set_file_models(
"a.w3d",
vec![crate::index::ModelAsset {
name: "A".into(),
members: vec!["Bone01".into()],
}],
);
index.set_file_models(
"b.w3d",
vec![crate::index::ModelAsset {
name: "B".into(),
members: vec![],
}],
);
let src = "Object Tank\n Draw = W3DModelDraw Tag\n DefaultConditionState\n Model = A\n Model = B\n HideSubObject = Bone\n End\n End\nEnd\n";
let parse = a.parse(src);
assert!(!diagnose(&a, &parse, Some(&index), None)
.iter()
.any(|d| d.code == "unknown-model-member"));
index.set_model_member_strictness(ModelMemberStrictness::Strict);
assert!(diagnose(&a, &parse, Some(&index), None)
.iter()
.any(|d| d.code == "unknown-model-member"));
index.set_model_member_strictness(ModelMemberStrictness::Off);
assert!(!diagnose(&a, &parse, Some(&index), None)
.iter()
.any(|d| d.code == "unknown-model-member"));
}
}
Loading