diff --git a/crates/analysis/src/completion.rs b/crates/analysis/src/completion.rs index 9123f70..120c78d 100644 --- a/crates/analysis/src/completion.rs +++ b/crates/analysis/src/completion.rs @@ -6,10 +6,11 @@ //! * after `=` -> enum/bitflag members, `Yes`/`No`, module names, or (with the //! workspace index) names of the referenced definition kind. -use zerosyntax_schema::ValueType; +use zerosyntax_schema::{AudioExtension, ValueType}; use zerosyntax_syntax::ast::{Block, Field, Module}; use zerosyntax_syntax::{Parse, SyntaxKind, SyntaxNode}; +use crate::index::AssetKind; use crate::model::{ is_model_asset_type, is_model_member_type, model_member_ini_name, models_for_source, scope_schema, @@ -494,6 +495,10 @@ fn type_snippet_placeholder(ty: &ValueType, n: usize) -> String { ValueType::AsciiString | ValueType::AsciiStringList | ValueType::QuotedString => { format!("${{{n}:Value}}") } + ValueType::AudioFile { .. } => format!("${{{n}:Sound.wav}}"), + ValueType::AudioStemList => format!("${{{n}:Sound}}"), + ValueType::TextureFile => format!("${{{n}:Texture.tga}}"), + ValueType::TextureStem | ValueType::TextureSequenceStem => format!("${{{n}:Texture}}"), ValueType::W3dModel | ValueType::W3dModelList => format!("${{{n}:Model}}"), ValueType::W3dModelMember => format!("${{{n}:Bone}}"), _ => format!("${{{n}:?}}"), @@ -678,11 +683,83 @@ fn completions_for_type( })); out } + ValueType::AudioFile { extension } => asset_completions( + index, + AssetKind::Audio, + "audio file", + |name| match extension { + AudioExtension::Any => Some(name.to_string()), + AudioExtension::Wav if has_extension(name, "wav") => Some(name.to_string()), + AudioExtension::Mp3 if has_extension(name, "mp3") => Some(name.to_string()), + _ => None, + }, + ), + ValueType::AudioStemList => { + asset_completions(index, AssetKind::Audio, "sound stem", |name| { + has_extension(name, "wav").then(|| file_stem(name).to_string()) + }) + } + ValueType::TextureFile => asset_completions(index, AssetKind::Texture, "texture", |name| { + Some(format!("{}.tga", file_stem(name))) + }), + ValueType::TextureStem => asset_completions(index, AssetKind::Texture, "texture", |name| { + Some(file_stem(name).to_string()) + }), + ValueType::TextureSequenceStem => { + asset_completions(index, AssetKind::Texture, "texture", |name| { + let stem = file_stem(name); + if let Some(base) = stem.strip_suffix("0000") { + Some(base.to_string()) + } else if stem + .as_bytes() + .get(stem.len().saturating_sub(4)..) + .is_some_and(|suffix| { + suffix.len() == 4 && suffix.iter().all(u8::is_ascii_digit) + }) + { + None + } else { + Some(stem.to_string()) + } + }) + } ValueType::W3dModel | ValueType::W3dModelList | ValueType::W3dModelMember => Vec::new(), _ => Vec::new(), } } +fn file_stem(name: &str) -> &str { + name.rsplit_once('.').map(|(stem, _)| stem).unwrap_or(name) +} + +fn has_extension(name: &str, extension: &str) -> bool { + name.rsplit_once('.') + .is_some_and(|(_, actual)| actual.eq_ignore_ascii_case(extension)) +} + +fn asset_completions( + index: Option<&WorkspaceIndex>, + kind: AssetKind, + detail: &str, + label: impl Fn(&str) -> Option, +) -> Vec { + let Some(index) = index.filter(|index| index.has_assets(kind)) else { + return Vec::new(); + }; + let mut seen = std::collections::HashSet::new(); + index + .asset_names(kind) + .filter_map(label) + .filter(|label| seen.insert(label.to_ascii_lowercase())) + .map(|label| Completion { + label, + kind: CompletionKind::Reference, + detail: Some(detail.to_string()), + insert: None, + }) + .collect() +} + fn top_level_completions(analyzer: &Analyzer) -> Vec { analyzer .schema() diff --git a/crates/analysis/src/diagnostics.rs b/crates/analysis/src/diagnostics.rs index 0cc47fb..f84c222 100644 --- a/crates/analysis/src/diagnostics.rs +++ b/crates/analysis/src/diagnostics.rs @@ -16,11 +16,11 @@ use std::collections::{HashMap, HashSet}; use std::sync::Arc; -use zerosyntax_schema::{Field as SchemaField, RefKind, ValueType}; +use zerosyntax_schema::{AudioExtension, Field as SchemaField, RefKind, ValueType}; use zerosyntax_syntax::ast::{Block, Field, Module}; use zerosyntax_syntax::{Parse, SyntaxKind, SyntaxNode, SyntaxToken}; -use crate::index::ModelMemberStrictness; +use crate::index::{AssetKind, ModelMemberStrictness}; use crate::model::{ is_model_asset_type, is_model_member_type, model_member_matches, models_for_source, module_fits_slot, scope_schema, ScopeSchema, @@ -96,6 +96,8 @@ pub const KNOWN_CODES: &[&str] = &[ "unresolved-reference", "unknown-model", "unknown-model-member", + "unknown-audio-file", + "unknown-texture", "unknown-suppression", "module-wrong-slot", "duplicate-module-tag", @@ -989,6 +991,7 @@ impl<'a> Ctx<'a> { if let Some(schema_field) = scope.field(name) { self.validate_value(&field, &schema_field.value_type); self.validate_model_asset(&field, schema_field, scope_node); + self.validate_raw_asset(&field, &schema_field.value_type); } else if scope.has_field_schema() && !scope.module_slots().iter().any(|s| s.keyword == name) { @@ -1052,6 +1055,63 @@ impl<'a> Ctx<'a> { } } + fn validate_raw_asset(&mut self, field: &Field, ty: &ValueType) { + let Some(index) = self.index else { return }; + let tokens = field.value_tokens(); + match ty { + ValueType::AudioFile { extension } if index.has_assets(AssetKind::Audio) => { + if let Some(token) = tokens.first() { + let name = unquote(token.text()); + let allowed = match extension { + AudioExtension::Any => { + has_extension(name, "wav") || has_extension(name, "mp3") + } + AudioExtension::Wav => has_extension(name, "wav"), + AudioExtension::Mp3 => has_extension(name, "mp3"), + }; + if !name.eq_ignore_ascii_case("None") + && (!allowed || !index.is_asset(AssetKind::Audio, name)) + { + self.warning( + token, + "unknown-audio-file", + format!("`{name}` is not a known audio file"), + ); + } + } + } + ValueType::AudioStemList if index.has_assets(AssetKind::Audio) => { + for token in tokens { + let name = unquote(token.text()); + if !name.eq_ignore_ascii_case("None") + && !index.is_asset(AssetKind::Audio, &format!("{name}.wav")) + { + self.warning( + &token, + "unknown-audio-file", + format!("`{name}` is not a known WAV sound stem"), + ); + } + } + } + ValueType::TextureFile | ValueType::TextureStem | ValueType::TextureSequenceStem + if index.has_assets(AssetKind::Texture) => + { + if let Some(token) = tokens.first() { + let name = unquote(token.text()); + if !name.eq_ignore_ascii_case("None") && !texture_exists(index, ty, name) { + self.warning( + token, + "unknown-texture", + format!("`{name}` is not a known texture"), + ); + } + } + } + _ => {} + } + } + fn validate_model_asset_token( &mut self, ty: &ValueType, @@ -1511,6 +1571,11 @@ impl<'a> Ctx<'a> { | ValueType::W3dModel | ValueType::W3dModelList | ValueType::W3dModelMember + | ValueType::AudioFile { .. } + | ValueType::AudioStemList + | ValueType::TextureFile + | ValueType::TextureStem + | ValueType::TextureSequenceStem | ValueType::Color | ValueType::Coord2D | ValueType::Coord3D @@ -1831,6 +1896,30 @@ impl<'a> Ctx<'a> { } } +fn has_extension(name: &str, extension: &str) -> bool { + name.rsplit_once('.') + .is_some_and(|(_, actual)| actual.eq_ignore_ascii_case(extension)) +} + +fn texture_exists(index: &WorkspaceIndex, ty: &ValueType, name: &str) -> bool { + let exact = |candidate: &str| index.is_asset(AssetKind::Texture, candidate); + match ty { + ValueType::TextureFile if has_extension(name, "dds") => exact(name), + ValueType::TextureFile if has_extension(name, "tga") => { + exact(name) || exact(&format!("{}.dds", &name[..name.len() - 4])) + } + ValueType::TextureFile => false, + ValueType::TextureStem => exact(&format!("{name}.tga")) || exact(&format!("{name}.dds")), + ValueType::TextureSequenceStem => { + exact(&format!("{name}.tga")) + || exact(&format!("{name}.dds")) + || exact(&format!("{name}0000.tga")) + || exact(&format!("{name}0000.dds")) + } + _ => false, + } +} + enum NumKind { Int, UInt, @@ -2737,4 +2826,39 @@ End .iter() .any(|d| d.code == "unknown-model-member")); } + + #[test] + fn raw_asset_warnings_are_gated_per_kind() { + let a = Analyzer::embedded(); + let src = "DialogEvent Dialog\n Filename = Missing.wav\nEnd\nMappedImage Image\n Texture = Missing.tga\nEnd\n"; + let parse = a.parse(src); + let mut index = WorkspaceIndex::new(); + let codes = |index: &WorkspaceIndex| { + diagnose(&a, &parse, Some(index), None) + .into_iter() + .map(|diagnostic| diagnostic.code) + .collect::>() + }; + assert!(!codes(&index) + .iter() + .any(|code| code.starts_with("unknown-"))); + index.set_file_assets( + "audio", + vec![crate::index::FileAsset { + kind: AssetKind::Audio, + name: "Known.wav".into(), + }], + ); + let audio_only = codes(&index); + assert!(audio_only.contains(&"unknown-audio-file")); + assert!(!audio_only.contains(&"unknown-texture")); + index.set_file_assets( + "texture", + vec![crate::index::FileAsset { + kind: AssetKind::Texture, + name: "Known.dds".into(), + }], + ); + assert!(codes(&index).contains(&"unknown-texture")); + } } diff --git a/crates/analysis/src/index.rs b/crates/analysis/src/index.rs index c12913f..4ccaebb 100644 --- a/crates/analysis/src/index.rs +++ b/crates/analysis/src/index.rs @@ -14,6 +14,18 @@ use zerosyntax_syntax::{Parse, SyntaxKind, SyntaxNode, SyntaxToken}; use crate::model::{scope_schema, ScopeSchema}; use crate::{Analyzer, Span}; +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum AssetKind { + Audio, + Texture, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct FileAsset { + pub kind: AssetKind, + pub name: String, +} + /// Model data discovered from a W3D asset. #[derive(Debug, Clone, PartialEq, Eq)] pub struct ModelAsset { @@ -95,6 +107,8 @@ pub struct WorkspaceIndex { model_assets: HashMap>, /// Reverse map for removing/replacing models contributed by one asset file. file_models: HashMap>, + asset_names: HashMap>>, + file_assets: HashMap>, object_models: HashMap)>>, file_object_models: HashMap)>>, object_parents: HashMap>, @@ -194,6 +208,7 @@ impl WorkspaceIndex { self.remove_entries(file); self.remove_site_entries(file); self.remove_model_entries(file); + self.set_file_assets(file, Vec::new()); self.remove_object_model_entries(file); self.remove_object_parent_entries(file); } @@ -265,6 +280,83 @@ impl WorkspaceIndex { } } + /// Replace raw audio/texture assets contributed by a file, directory entry, + /// or synthetic archive contribution. + pub fn set_file_assets(&mut self, file: &str, assets: Vec) { + let affected = self + .file_assets + .get(file) + .into_iter() + .flatten() + .chain(&assets) + .map(|asset| (asset.kind, asset.name.to_ascii_lowercase())) + .collect::>(); + let before = affected + .iter() + .map(|(kind, name)| ((*kind, name.clone()), self.is_asset(*kind, name))) + .collect::>(); + self.remove_asset_entries(file); + for asset in &assets { + self.asset_names + .entry(asset.kind) + .or_default() + .entry(asset.name.to_ascii_lowercase()) + .or_default() + .push((file.to_string(), asset.name.clone())); + } + if assets.is_empty() { + self.file_assets.remove(file); + } else { + self.file_assets.insert(file.to_string(), assets); + } + if before + .into_iter() + .any(|((kind, name), existed)| existed != self.is_asset(kind, &name)) + { + self.generation += 1; + } + } + + fn remove_asset_entries(&mut self, file: &str) { + let Some(assets) = self.file_assets.remove(file) else { + return; + }; + for asset in assets { + let lower = asset.name.to_ascii_lowercase(); + if let Some(names) = self.asset_names.get_mut(&asset.kind) { + if let Some(contribs) = names.get_mut(&lower) { + contribs.retain(|(source, _)| source != file); + if contribs.is_empty() { + names.remove(&lower); + } + } + if names.is_empty() { + self.asset_names.remove(&asset.kind); + } + } + } + } + + pub fn has_assets(&self, kind: AssetKind) -> bool { + self.asset_names + .get(&kind) + .is_some_and(|names| !names.is_empty()) + } + + pub fn is_asset(&self, kind: AssetKind, name: &str) -> bool { + self.asset_names + .get(&kind) + .is_some_and(|names| names.contains_key(&name.to_ascii_lowercase())) + } + + pub fn asset_names(&self, kind: AssetKind) -> impl Iterator { + self.asset_names + .get(&kind) + .into_iter() + .flat_map(|names| names.values().filter_map(|sources| sources.first())) + .map(|(_, display)| display.as_str()) + } + pub fn set_file_object_models(&mut self, file: &str, objects: Vec<(String, Vec)>) { let normalized = normalize_object_models(&objects); let changed = self.file_object_models.get(file) != Some(&normalized); @@ -883,6 +975,35 @@ mod tests { assert_eq!(idx.models_for_object("child"), vec!["ParentModel"]); } + #[test] + fn raw_assets_are_case_insensitive_and_track_effective_names() { + let audio = |name: &str| FileAsset { + kind: AssetKind::Audio, + name: name.into(), + }; + let mut idx = WorkspaceIndex::new(); + idx.set_file_assets("base", vec![audio("Click.WAV")]); + let first = idx.generation(); + assert!(idx.is_asset(AssetKind::Audio, "click.wav")); + + idx.set_file_assets("mod", vec![audio("CLICK.wav")]); + assert_eq!( + idx.generation(), + first, + "duplicate contributor changes no effective name" + ); + idx.remove_file("base"); + assert_eq!( + idx.generation(), + first, + "removing one duplicate preserves the name" + ); + idx.set_file_assets("mod", vec![audio("Other.wav")]); + assert_ne!(idx.generation(), first); + assert!(!idx.is_asset(AssetKind::Audio, "Click.wav")); + assert!(idx.is_asset(AssetKind::Audio, "OTHER.WAV")); + } + #[test] fn split_prefixed_reference_site_span_excludes_prefix() { let a = Analyzer::embedded(); diff --git a/crates/analysis/src/semantic.rs b/crates/analysis/src/semantic.rs index 52efad7..4a8d91d 100644 --- a/crates/analysis/src/semantic.rs +++ b/crates/analysis/src/semantic.rs @@ -250,7 +250,12 @@ fn value_token_kind(tok: &SyntaxToken, ty: Option<&ValueType>) -> SemKind { | Some(ValueType::ReferenceList { .. }) | Some(ValueType::W3dModel) | Some(ValueType::W3dModelList) - | Some(ValueType::W3dModelMember) => SemKind::Reference, + | Some(ValueType::W3dModelMember) + | Some(ValueType::AudioFile { .. }) + | Some(ValueType::AudioStemList) + | Some(ValueType::TextureFile) + | Some(ValueType::TextureStem) + | Some(ValueType::TextureSequenceStem) => SemKind::Reference, _ => SemKind::StringLit, } } diff --git a/crates/analysis/tests/spec.rs b/crates/analysis/tests/spec.rs index ba96947..9b9076e 100644 --- a/crates/analysis/tests/spec.rs +++ b/crates/analysis/tests/spec.rs @@ -38,7 +38,7 @@ use std::path::{Path, PathBuf}; use zerosyntax_analysis::actions; use zerosyntax_analysis::completion::complete; use zerosyntax_analysis::diagnostics::{diagnose, Severity}; -use zerosyntax_analysis::index::{definitions_in, WorkspaceIndex}; +use zerosyntax_analysis::index::{definitions_in, AssetKind, FileAsset, WorkspaceIndex}; use zerosyntax_analysis::{Analyzer, Span}; use serde::Deserialize; @@ -57,6 +57,10 @@ struct Spec { complete: Vec, #[serde(default)] action: Vec, + #[serde(default)] + audio_assets: Vec, + #[serde(default)] + texture_assets: Vec, } #[derive(Deserialize)] @@ -393,6 +397,20 @@ fn specs_hold() { // the definitions it declares (and only those). let mut index = WorkspaceIndex::new(); index.set_file(&name, definitions_in(&analyzer, &parse, &name)); + index.set_file_assets( + "spec-assets", + spec.audio_assets + .iter() + .map(|name| FileAsset { + kind: AssetKind::Audio, + name: name.clone(), + }) + .chain(spec.texture_assets.iter().map(|name| FileAsset { + kind: AssetKind::Texture, + name: name.clone(), + })) + .collect(), + ); let diags = diagnose(&analyzer, &parse, Some(&index), Some(&name)); if spec.no_errors { diff --git a/crates/analysis/tests/spec/Assets.ini b/crates/analysis/tests/spec/Assets.ini new file mode 100644 index 0000000..eb5c76d --- /dev/null +++ b/crates/analysis/tests/spec/Assets.ini @@ -0,0 +1,37 @@ +AudioEvent Effect + Sounds = $1Click MissingOne MissingTwo +End + +DialogEvent Dialog + Filename = $2Click.wav +End + +MusicTrack Music + Filename = $3Track.mp3 +End + +MappedImage Image + Texture = $4Particle.tga +End + +Object Thing + ShadowTexture = $5Particle + Draw = W3DModelDraw ModuleTag_Draw + TrackMarks = $7Particle.tga + End +End + +MouseCursor CursorDefinition + Texture = $6Cursor +End + +EvaEvent Announcement + SideSounds + Side = America + Sounds = $8Click MissingEva + End +End + +Weather + SnowTexture = MissingTexture.tga +End diff --git a/crates/analysis/tests/spec/Assets.spec.toml b/crates/analysis/tests/spec/Assets.spec.toml new file mode 100644 index 0000000..c798db1 --- /dev/null +++ b/crates/analysis/tests/spec/Assets.spec.toml @@ -0,0 +1,79 @@ +no_errors = true +audio_assets = ["Click.wav", "Track.mp3"] +texture_assets = ["Particle.dds", "Cursor0000.tga", "Cursor0001.tga"] + +[[complete]] +at = "$1" +includes = ["Click"] +excludes = ["Click.wav", "Track"] + +[[complete]] +at = "$2" +includes = ["Click.wav"] +excludes = ["Track.mp3"] + +[[complete]] +at = "$3" +includes = ["Track.mp3"] +excludes = ["Click.wav"] + +[[complete]] +at = "$4" +includes = ["Particle.tga"] + +[[complete]] +at = "$5" +includes = ["Particle"] + +[[complete]] +at = "$6" +includes = ["Cursor"] +excludes = ["Cursor0000", "Cursor0001"] + +[[complete]] +at = "$7" +includes = ["Particle.tga"] + +[[complete]] +at = "$8" +includes = ["Click"] +excludes = ["Click.wav", "Track"] + +[[diag]] +severity = "warning" +code = "unknown-audio-file" +on = "MissingOne" + +[[diag]] +severity = "warning" +code = "unknown-audio-file" +on = "MissingTwo" + +[[diag]] +severity = "warning" +code = "unknown-audio-file" +on = "MissingEva" + +[[diag]] +severity = "warning" +code = "unknown-texture" +on = "MissingTexture.tga" + +[[diag]] +severity = "warning" +code = "unknown-audio-file" +on = "Click.wav" +absent = true + +[[diag]] +severity = "warning" +code = "unknown-texture" +on = "Particle.tga" +absent = true + +[[diag]] +severity = "warning" +code = "unknown-texture" +on = "Particle.tga" +nth = 2 +absent = true diff --git a/crates/schema/schema.json b/crates/schema/schema.json index e9ba736..98ae348 100644 --- a/crates/schema/schema.json +++ b/crates/schema/schema.json @@ -642,7 +642,8 @@ { "name": "Filename", "value_type": { - "kind": "ascii_string" + "kind": "audio_file", + "extension": "any" }, "parse_fn": "INI::parseAsciiString", "doc": null @@ -735,7 +736,7 @@ { "name": "Sounds", "value_type": { - "kind": "ascii_string_list" + "kind": "audio_stem_list" }, "parse_fn": "INI::parseSoundsList", "doc": null @@ -743,7 +744,7 @@ { "name": "SoundsNight", "value_type": { - "kind": "ascii_string_list" + "kind": "audio_stem_list" }, "parse_fn": "INI::parseSoundsList", "doc": null @@ -751,7 +752,7 @@ { "name": "SoundsEvening", "value_type": { - "kind": "ascii_string_list" + "kind": "audio_stem_list" }, "parse_fn": "INI::parseSoundsList", "doc": null @@ -759,7 +760,7 @@ { "name": "SoundsMorning", "value_type": { - "kind": "ascii_string_list" + "kind": "audio_stem_list" }, "parse_fn": "INI::parseSoundsList", "doc": null @@ -767,7 +768,7 @@ { "name": "Attack", "value_type": { - "kind": "ascii_string_list" + "kind": "audio_stem_list" }, "parse_fn": "INI::parseSoundsList", "doc": null @@ -775,7 +776,7 @@ { "name": "Decay", "value_type": { - "kind": "ascii_string_list" + "kind": "audio_stem_list" }, "parse_fn": "INI::parseSoundsList", "doc": null @@ -1206,7 +1207,7 @@ { "name": "Texture", "value_type": { - "kind": "ascii_string" + "kind": "texture_file" }, "parse_fn": "INI::parseAsciiString", "doc": null @@ -1222,7 +1223,7 @@ { "name": "TextureDamaged", "value_type": { - "kind": "ascii_string" + "kind": "texture_file" }, "parse_fn": "INI::parseAsciiString", "doc": null @@ -1238,7 +1239,7 @@ { "name": "TextureReallyDamaged", "value_type": { - "kind": "ascii_string" + "kind": "texture_file" }, "parse_fn": "INI::parseAsciiString", "doc": null @@ -1254,7 +1255,7 @@ { "name": "TextureBroken", "value_type": { - "kind": "ascii_string" + "kind": "texture_file" }, "parse_fn": "INI::parseAsciiString", "doc": null @@ -5151,7 +5152,8 @@ { "name": "Filename", "value_type": { - "kind": "ascii_string" + "kind": "audio_file", + "extension": "wav" }, "parse_fn": "INI::parseAsciiString", "doc": null @@ -5244,7 +5246,7 @@ { "name": "Sounds", "value_type": { - "kind": "ascii_string_list" + "kind": "audio_stem_list" }, "parse_fn": "INI::parseSoundsList", "doc": null @@ -5252,7 +5254,7 @@ { "name": "SoundsNight", "value_type": { - "kind": "ascii_string_list" + "kind": "audio_stem_list" }, "parse_fn": "INI::parseSoundsList", "doc": null @@ -5260,7 +5262,7 @@ { "name": "SoundsEvening", "value_type": { - "kind": "ascii_string_list" + "kind": "audio_stem_list" }, "parse_fn": "INI::parseSoundsList", "doc": null @@ -5268,7 +5270,7 @@ { "name": "SoundsMorning", "value_type": { - "kind": "ascii_string_list" + "kind": "audio_stem_list" }, "parse_fn": "INI::parseSoundsList", "doc": null @@ -5276,7 +5278,7 @@ { "name": "Attack", "value_type": { - "kind": "ascii_string_list" + "kind": "audio_stem_list" }, "parse_fn": "INI::parseSoundsList", "doc": null @@ -5284,7 +5286,7 @@ { "name": "Decay", "value_type": { - "kind": "ascii_string_list" + "kind": "audio_stem_list" }, "parse_fn": "INI::parseSoundsList", "doc": null @@ -5513,7 +5515,25 @@ ], "sub_blocks": [ { - "keyword": "SideSounds" + "keyword": "SideSounds", + "fields": [ + { + "name": "Side", + "value_type": { + "kind": "ascii_string" + }, + "parse_fn": "INI::parseAsciiString", + "doc": null + }, + { + "name": "Sounds", + "value_type": { + "kind": "audio_stem_list" + }, + "parse_fn": "INI::parseSoundsList", + "doc": null + } + ] } ] }, @@ -9499,91 +9519,1802 @@ ], "sub_blocks": [ { - "keyword": "A10StrikeRadiusCursor" - }, - { - "keyword": "AmbulanceRadiusCursor" - }, - { - "keyword": "AmbushRadiusCursor" - }, - { - "keyword": "AnthraxBombRadiusCursor" - }, - { - "keyword": "ArtilleryRadiusCursor" - }, - { - "keyword": "AttackContinueAreaRadiusCursor" - }, - { - "keyword": "AttackDamageAreaRadiusCursor" - }, - { - "keyword": "AttackScatterAreaRadiusCursor" - }, - { - "keyword": "CarpetBombRadiusCursor" + "keyword": "A10StrikeRadiusCursor", + "fields": [ + { + "name": "Texture", + "value_type": { + "kind": "texture_stem" + }, + "parse_fn": "INI::parseAsciiString", + "doc": null + }, + { + "name": "Style", + "value_type": { + "kind": "bit_flags", + "value_set": "shadow_type" + }, + "parse_fn": "INI::parseBitString32", + "doc": null + }, + { + "name": "OpacityMin", + "value_type": { + "kind": "percent" + }, + "parse_fn": "INI::parsePercentToReal", + "doc": null + }, + { + "name": "OpacityMax", + "value_type": { + "kind": "percent" + }, + "parse_fn": "INI::parsePercentToReal", + "doc": null + }, + { + "name": "OpacityThrobTime", + "value_type": { + "kind": "duration" + }, + "parse_fn": "INI::parseDurationUnsignedInt", + "doc": null + }, + { + "name": "Color", + "value_type": { + "kind": "color" + }, + "parse_fn": "INI::parseColorInt", + "doc": null + }, + { + "name": "OnlyVisibleToOwningPlayer", + "value_type": { + "kind": "bool" + }, + "parse_fn": "INI::parseBool", + "doc": null + } + ] }, { - "keyword": "ClearMinesRadiusCursor" + "keyword": "AmbulanceRadiusCursor", + "fields": [ + { + "name": "Texture", + "value_type": { + "kind": "texture_stem" + }, + "parse_fn": "INI::parseAsciiString", + "doc": null + }, + { + "name": "Style", + "value_type": { + "kind": "bit_flags", + "value_set": "shadow_type" + }, + "parse_fn": "INI::parseBitString32", + "doc": null + }, + { + "name": "OpacityMin", + "value_type": { + "kind": "percent" + }, + "parse_fn": "INI::parsePercentToReal", + "doc": null + }, + { + "name": "OpacityMax", + "value_type": { + "kind": "percent" + }, + "parse_fn": "INI::parsePercentToReal", + "doc": null + }, + { + "name": "OpacityThrobTime", + "value_type": { + "kind": "duration" + }, + "parse_fn": "INI::parseDurationUnsignedInt", + "doc": null + }, + { + "name": "Color", + "value_type": { + "kind": "color" + }, + "parse_fn": "INI::parseColorInt", + "doc": null + }, + { + "name": "OnlyVisibleToOwningPlayer", + "value_type": { + "kind": "bool" + }, + "parse_fn": "INI::parseBool", + "doc": null + } + ] }, { - "keyword": "ClusterMinesRadiusCursor" + "keyword": "AmbushRadiusCursor", + "fields": [ + { + "name": "Texture", + "value_type": { + "kind": "texture_stem" + }, + "parse_fn": "INI::parseAsciiString", + "doc": null + }, + { + "name": "Style", + "value_type": { + "kind": "bit_flags", + "value_set": "shadow_type" + }, + "parse_fn": "INI::parseBitString32", + "doc": null + }, + { + "name": "OpacityMin", + "value_type": { + "kind": "percent" + }, + "parse_fn": "INI::parsePercentToReal", + "doc": null + }, + { + "name": "OpacityMax", + "value_type": { + "kind": "percent" + }, + "parse_fn": "INI::parsePercentToReal", + "doc": null + }, + { + "name": "OpacityThrobTime", + "value_type": { + "kind": "duration" + }, + "parse_fn": "INI::parseDurationUnsignedInt", + "doc": null + }, + { + "name": "Color", + "value_type": { + "kind": "color" + }, + "parse_fn": "INI::parseColorInt", + "doc": null + }, + { + "name": "OnlyVisibleToOwningPlayer", + "value_type": { + "kind": "bool" + }, + "parse_fn": "INI::parseBool", + "doc": null + } + ] }, { - "keyword": "DaisyCutterRadiusCursor" + "keyword": "AnthraxBombRadiusCursor", + "fields": [ + { + "name": "Texture", + "value_type": { + "kind": "texture_stem" + }, + "parse_fn": "INI::parseAsciiString", + "doc": null + }, + { + "name": "Style", + "value_type": { + "kind": "bit_flags", + "value_set": "shadow_type" + }, + "parse_fn": "INI::parseBitString32", + "doc": null + }, + { + "name": "OpacityMin", + "value_type": { + "kind": "percent" + }, + "parse_fn": "INI::parsePercentToReal", + "doc": null + }, + { + "name": "OpacityMax", + "value_type": { + "kind": "percent" + }, + "parse_fn": "INI::parsePercentToReal", + "doc": null + }, + { + "name": "OpacityThrobTime", + "value_type": { + "kind": "duration" + }, + "parse_fn": "INI::parseDurationUnsignedInt", + "doc": null + }, + { + "name": "Color", + "value_type": { + "kind": "color" + }, + "parse_fn": "INI::parseColorInt", + "doc": null + }, + { + "name": "OnlyVisibleToOwningPlayer", + "value_type": { + "kind": "bool" + }, + "parse_fn": "INI::parseBool", + "doc": null + } + ] }, { - "keyword": "EMPPulseRadiusCursor" + "keyword": "ArtilleryRadiusCursor", + "fields": [ + { + "name": "Texture", + "value_type": { + "kind": "texture_stem" + }, + "parse_fn": "INI::parseAsciiString", + "doc": null + }, + { + "name": "Style", + "value_type": { + "kind": "bit_flags", + "value_set": "shadow_type" + }, + "parse_fn": "INI::parseBitString32", + "doc": null + }, + { + "name": "OpacityMin", + "value_type": { + "kind": "percent" + }, + "parse_fn": "INI::parsePercentToReal", + "doc": null + }, + { + "name": "OpacityMax", + "value_type": { + "kind": "percent" + }, + "parse_fn": "INI::parsePercentToReal", + "doc": null + }, + { + "name": "OpacityThrobTime", + "value_type": { + "kind": "duration" + }, + "parse_fn": "INI::parseDurationUnsignedInt", + "doc": null + }, + { + "name": "Color", + "value_type": { + "kind": "color" + }, + "parse_fn": "INI::parseColorInt", + "doc": null + }, + { + "name": "OnlyVisibleToOwningPlayer", + "value_type": { + "kind": "bool" + }, + "parse_fn": "INI::parseBool", + "doc": null + } + ] }, { - "keyword": "EmergencyRepairRadiusCursor" + "keyword": "AttackContinueAreaRadiusCursor", + "fields": [ + { + "name": "Texture", + "value_type": { + "kind": "texture_stem" + }, + "parse_fn": "INI::parseAsciiString", + "doc": null + }, + { + "name": "Style", + "value_type": { + "kind": "bit_flags", + "value_set": "shadow_type" + }, + "parse_fn": "INI::parseBitString32", + "doc": null + }, + { + "name": "OpacityMin", + "value_type": { + "kind": "percent" + }, + "parse_fn": "INI::parsePercentToReal", + "doc": null + }, + { + "name": "OpacityMax", + "value_type": { + "kind": "percent" + }, + "parse_fn": "INI::parsePercentToReal", + "doc": null + }, + { + "name": "OpacityThrobTime", + "value_type": { + "kind": "duration" + }, + "parse_fn": "INI::parseDurationUnsignedInt", + "doc": null + }, + { + "name": "Color", + "value_type": { + "kind": "color" + }, + "parse_fn": "INI::parseColorInt", + "doc": null + }, + { + "name": "OnlyVisibleToOwningPlayer", + "value_type": { + "kind": "bool" + }, + "parse_fn": "INI::parseBool", + "doc": null + } + ] }, { - "keyword": "FrenzyRadiusCursor" + "keyword": "AttackDamageAreaRadiusCursor", + "fields": [ + { + "name": "Texture", + "value_type": { + "kind": "texture_stem" + }, + "parse_fn": "INI::parseAsciiString", + "doc": null + }, + { + "name": "Style", + "value_type": { + "kind": "bit_flags", + "value_set": "shadow_type" + }, + "parse_fn": "INI::parseBitString32", + "doc": null + }, + { + "name": "OpacityMin", + "value_type": { + "kind": "percent" + }, + "parse_fn": "INI::parsePercentToReal", + "doc": null + }, + { + "name": "OpacityMax", + "value_type": { + "kind": "percent" + }, + "parse_fn": "INI::parsePercentToReal", + "doc": null + }, + { + "name": "OpacityThrobTime", + "value_type": { + "kind": "duration" + }, + "parse_fn": "INI::parseDurationUnsignedInt", + "doc": null + }, + { + "name": "Color", + "value_type": { + "kind": "color" + }, + "parse_fn": "INI::parseColorInt", + "doc": null + }, + { + "name": "OnlyVisibleToOwningPlayer", + "value_type": { + "kind": "bool" + }, + "parse_fn": "INI::parseBool", + "doc": null + } + ] }, { - "keyword": "FriendlySpecialPowerRadiusCursor" + "keyword": "AttackScatterAreaRadiusCursor", + "fields": [ + { + "name": "Texture", + "value_type": { + "kind": "texture_stem" + }, + "parse_fn": "INI::parseAsciiString", + "doc": null + }, + { + "name": "Style", + "value_type": { + "kind": "bit_flags", + "value_set": "shadow_type" + }, + "parse_fn": "INI::parseBitString32", + "doc": null + }, + { + "name": "OpacityMin", + "value_type": { + "kind": "percent" + }, + "parse_fn": "INI::parsePercentToReal", + "doc": null + }, + { + "name": "OpacityMax", + "value_type": { + "kind": "percent" + }, + "parse_fn": "INI::parsePercentToReal", + "doc": null + }, + { + "name": "OpacityThrobTime", + "value_type": { + "kind": "duration" + }, + "parse_fn": "INI::parseDurationUnsignedInt", + "doc": null + }, + { + "name": "Color", + "value_type": { + "kind": "color" + }, + "parse_fn": "INI::parseColorInt", + "doc": null + }, + { + "name": "OnlyVisibleToOwningPlayer", + "value_type": { + "kind": "bool" + }, + "parse_fn": "INI::parseBool", + "doc": null + } + ] }, { - "keyword": "GuardAreaRadiusCursor" + "keyword": "CarpetBombRadiusCursor", + "fields": [ + { + "name": "Texture", + "value_type": { + "kind": "texture_stem" + }, + "parse_fn": "INI::parseAsciiString", + "doc": null + }, + { + "name": "Style", + "value_type": { + "kind": "bit_flags", + "value_set": "shadow_type" + }, + "parse_fn": "INI::parseBitString32", + "doc": null + }, + { + "name": "OpacityMin", + "value_type": { + "kind": "percent" + }, + "parse_fn": "INI::parsePercentToReal", + "doc": null + }, + { + "name": "OpacityMax", + "value_type": { + "kind": "percent" + }, + "parse_fn": "INI::parsePercentToReal", + "doc": null + }, + { + "name": "OpacityThrobTime", + "value_type": { + "kind": "duration" + }, + "parse_fn": "INI::parseDurationUnsignedInt", + "doc": null + }, + { + "name": "Color", + "value_type": { + "kind": "color" + }, + "parse_fn": "INI::parseColorInt", + "doc": null + }, + { + "name": "OnlyVisibleToOwningPlayer", + "value_type": { + "kind": "bool" + }, + "parse_fn": "INI::parseBool", + "doc": null + } + ] }, { - "keyword": "HelixNapalmBombRadiusCursor" + "keyword": "ClearMinesRadiusCursor", + "fields": [ + { + "name": "Texture", + "value_type": { + "kind": "texture_stem" + }, + "parse_fn": "INI::parseAsciiString", + "doc": null + }, + { + "name": "Style", + "value_type": { + "kind": "bit_flags", + "value_set": "shadow_type" + }, + "parse_fn": "INI::parseBitString32", + "doc": null + }, + { + "name": "OpacityMin", + "value_type": { + "kind": "percent" + }, + "parse_fn": "INI::parsePercentToReal", + "doc": null + }, + { + "name": "OpacityMax", + "value_type": { + "kind": "percent" + }, + "parse_fn": "INI::parsePercentToReal", + "doc": null + }, + { + "name": "OpacityThrobTime", + "value_type": { + "kind": "duration" + }, + "parse_fn": "INI::parseDurationUnsignedInt", + "doc": null + }, + { + "name": "Color", + "value_type": { + "kind": "color" + }, + "parse_fn": "INI::parseColorInt", + "doc": null + }, + { + "name": "OnlyVisibleToOwningPlayer", + "value_type": { + "kind": "bool" + }, + "parse_fn": "INI::parseBool", + "doc": null + } + ] + }, + { + "keyword": "ClusterMinesRadiusCursor", + "fields": [ + { + "name": "Texture", + "value_type": { + "kind": "texture_stem" + }, + "parse_fn": "INI::parseAsciiString", + "doc": null + }, + { + "name": "Style", + "value_type": { + "kind": "bit_flags", + "value_set": "shadow_type" + }, + "parse_fn": "INI::parseBitString32", + "doc": null + }, + { + "name": "OpacityMin", + "value_type": { + "kind": "percent" + }, + "parse_fn": "INI::parsePercentToReal", + "doc": null + }, + { + "name": "OpacityMax", + "value_type": { + "kind": "percent" + }, + "parse_fn": "INI::parsePercentToReal", + "doc": null + }, + { + "name": "OpacityThrobTime", + "value_type": { + "kind": "duration" + }, + "parse_fn": "INI::parseDurationUnsignedInt", + "doc": null + }, + { + "name": "Color", + "value_type": { + "kind": "color" + }, + "parse_fn": "INI::parseColorInt", + "doc": null + }, + { + "name": "OnlyVisibleToOwningPlayer", + "value_type": { + "kind": "bool" + }, + "parse_fn": "INI::parseBool", + "doc": null + } + ] }, { - "keyword": "NapalmStrikeRadiusCursor" + "keyword": "DaisyCutterRadiusCursor", + "fields": [ + { + "name": "Texture", + "value_type": { + "kind": "texture_stem" + }, + "parse_fn": "INI::parseAsciiString", + "doc": null + }, + { + "name": "Style", + "value_type": { + "kind": "bit_flags", + "value_set": "shadow_type" + }, + "parse_fn": "INI::parseBitString32", + "doc": null + }, + { + "name": "OpacityMin", + "value_type": { + "kind": "percent" + }, + "parse_fn": "INI::parsePercentToReal", + "doc": null + }, + { + "name": "OpacityMax", + "value_type": { + "kind": "percent" + }, + "parse_fn": "INI::parsePercentToReal", + "doc": null + }, + { + "name": "OpacityThrobTime", + "value_type": { + "kind": "duration" + }, + "parse_fn": "INI::parseDurationUnsignedInt", + "doc": null + }, + { + "name": "Color", + "value_type": { + "kind": "color" + }, + "parse_fn": "INI::parseColorInt", + "doc": null + }, + { + "name": "OnlyVisibleToOwningPlayer", + "value_type": { + "kind": "bool" + }, + "parse_fn": "INI::parseBool", + "doc": null + } + ] }, { - "keyword": "NuclearMissileRadiusCursor" + "keyword": "EMPPulseRadiusCursor", + "fields": [ + { + "name": "Texture", + "value_type": { + "kind": "texture_stem" + }, + "parse_fn": "INI::parseAsciiString", + "doc": null + }, + { + "name": "Style", + "value_type": { + "kind": "bit_flags", + "value_set": "shadow_type" + }, + "parse_fn": "INI::parseBitString32", + "doc": null + }, + { + "name": "OpacityMin", + "value_type": { + "kind": "percent" + }, + "parse_fn": "INI::parsePercentToReal", + "doc": null + }, + { + "name": "OpacityMax", + "value_type": { + "kind": "percent" + }, + "parse_fn": "INI::parsePercentToReal", + "doc": null + }, + { + "name": "OpacityThrobTime", + "value_type": { + "kind": "duration" + }, + "parse_fn": "INI::parseDurationUnsignedInt", + "doc": null + }, + { + "name": "Color", + "value_type": { + "kind": "color" + }, + "parse_fn": "INI::parseColorInt", + "doc": null + }, + { + "name": "OnlyVisibleToOwningPlayer", + "value_type": { + "kind": "bool" + }, + "parse_fn": "INI::parseBool", + "doc": null + } + ] }, { - "keyword": "OffensiveSpecialPowerRadiusCursor" + "keyword": "EmergencyRepairRadiusCursor", + "fields": [ + { + "name": "Texture", + "value_type": { + "kind": "texture_stem" + }, + "parse_fn": "INI::parseAsciiString", + "doc": null + }, + { + "name": "Style", + "value_type": { + "kind": "bit_flags", + "value_set": "shadow_type" + }, + "parse_fn": "INI::parseBitString32", + "doc": null + }, + { + "name": "OpacityMin", + "value_type": { + "kind": "percent" + }, + "parse_fn": "INI::parsePercentToReal", + "doc": null + }, + { + "name": "OpacityMax", + "value_type": { + "kind": "percent" + }, + "parse_fn": "INI::parsePercentToReal", + "doc": null + }, + { + "name": "OpacityThrobTime", + "value_type": { + "kind": "duration" + }, + "parse_fn": "INI::parseDurationUnsignedInt", + "doc": null + }, + { + "name": "Color", + "value_type": { + "kind": "color" + }, + "parse_fn": "INI::parseColorInt", + "doc": null + }, + { + "name": "OnlyVisibleToOwningPlayer", + "value_type": { + "kind": "bool" + }, + "parse_fn": "INI::parseBool", + "doc": null + } + ] }, { - "keyword": "ParadropRadiusCursor" + "keyword": "FrenzyRadiusCursor", + "fields": [ + { + "name": "Texture", + "value_type": { + "kind": "texture_stem" + }, + "parse_fn": "INI::parseAsciiString", + "doc": null + }, + { + "name": "Style", + "value_type": { + "kind": "bit_flags", + "value_set": "shadow_type" + }, + "parse_fn": "INI::parseBitString32", + "doc": null + }, + { + "name": "OpacityMin", + "value_type": { + "kind": "percent" + }, + "parse_fn": "INI::parsePercentToReal", + "doc": null + }, + { + "name": "OpacityMax", + "value_type": { + "kind": "percent" + }, + "parse_fn": "INI::parsePercentToReal", + "doc": null + }, + { + "name": "OpacityThrobTime", + "value_type": { + "kind": "duration" + }, + "parse_fn": "INI::parseDurationUnsignedInt", + "doc": null + }, + { + "name": "Color", + "value_type": { + "kind": "color" + }, + "parse_fn": "INI::parseColorInt", + "doc": null + }, + { + "name": "OnlyVisibleToOwningPlayer", + "value_type": { + "kind": "bool" + }, + "parse_fn": "INI::parseBool", + "doc": null + } + ] + }, + { + "keyword": "FriendlySpecialPowerRadiusCursor", + "fields": [ + { + "name": "Texture", + "value_type": { + "kind": "texture_stem" + }, + "parse_fn": "INI::parseAsciiString", + "doc": null + }, + { + "name": "Style", + "value_type": { + "kind": "bit_flags", + "value_set": "shadow_type" + }, + "parse_fn": "INI::parseBitString32", + "doc": null + }, + { + "name": "OpacityMin", + "value_type": { + "kind": "percent" + }, + "parse_fn": "INI::parsePercentToReal", + "doc": null + }, + { + "name": "OpacityMax", + "value_type": { + "kind": "percent" + }, + "parse_fn": "INI::parsePercentToReal", + "doc": null + }, + { + "name": "OpacityThrobTime", + "value_type": { + "kind": "duration" + }, + "parse_fn": "INI::parseDurationUnsignedInt", + "doc": null + }, + { + "name": "Color", + "value_type": { + "kind": "color" + }, + "parse_fn": "INI::parseColorInt", + "doc": null + }, + { + "name": "OnlyVisibleToOwningPlayer", + "value_type": { + "kind": "bool" + }, + "parse_fn": "INI::parseBool", + "doc": null + } + ] + }, + { + "keyword": "GuardAreaRadiusCursor", + "fields": [ + { + "name": "Texture", + "value_type": { + "kind": "texture_stem" + }, + "parse_fn": "INI::parseAsciiString", + "doc": null + }, + { + "name": "Style", + "value_type": { + "kind": "bit_flags", + "value_set": "shadow_type" + }, + "parse_fn": "INI::parseBitString32", + "doc": null + }, + { + "name": "OpacityMin", + "value_type": { + "kind": "percent" + }, + "parse_fn": "INI::parsePercentToReal", + "doc": null + }, + { + "name": "OpacityMax", + "value_type": { + "kind": "percent" + }, + "parse_fn": "INI::parsePercentToReal", + "doc": null + }, + { + "name": "OpacityThrobTime", + "value_type": { + "kind": "duration" + }, + "parse_fn": "INI::parseDurationUnsignedInt", + "doc": null + }, + { + "name": "Color", + "value_type": { + "kind": "color" + }, + "parse_fn": "INI::parseColorInt", + "doc": null + }, + { + "name": "OnlyVisibleToOwningPlayer", + "value_type": { + "kind": "bool" + }, + "parse_fn": "INI::parseBool", + "doc": null + } + ] + }, + { + "keyword": "HelixNapalmBombRadiusCursor", + "fields": [ + { + "name": "Texture", + "value_type": { + "kind": "texture_stem" + }, + "parse_fn": "INI::parseAsciiString", + "doc": null + }, + { + "name": "Style", + "value_type": { + "kind": "bit_flags", + "value_set": "shadow_type" + }, + "parse_fn": "INI::parseBitString32", + "doc": null + }, + { + "name": "OpacityMin", + "value_type": { + "kind": "percent" + }, + "parse_fn": "INI::parsePercentToReal", + "doc": null + }, + { + "name": "OpacityMax", + "value_type": { + "kind": "percent" + }, + "parse_fn": "INI::parsePercentToReal", + "doc": null + }, + { + "name": "OpacityThrobTime", + "value_type": { + "kind": "duration" + }, + "parse_fn": "INI::parseDurationUnsignedInt", + "doc": null + }, + { + "name": "Color", + "value_type": { + "kind": "color" + }, + "parse_fn": "INI::parseColorInt", + "doc": null + }, + { + "name": "OnlyVisibleToOwningPlayer", + "value_type": { + "kind": "bool" + }, + "parse_fn": "INI::parseBool", + "doc": null + } + ] + }, + { + "keyword": "NapalmStrikeRadiusCursor", + "fields": [ + { + "name": "Texture", + "value_type": { + "kind": "texture_stem" + }, + "parse_fn": "INI::parseAsciiString", + "doc": null + }, + { + "name": "Style", + "value_type": { + "kind": "bit_flags", + "value_set": "shadow_type" + }, + "parse_fn": "INI::parseBitString32", + "doc": null + }, + { + "name": "OpacityMin", + "value_type": { + "kind": "percent" + }, + "parse_fn": "INI::parsePercentToReal", + "doc": null + }, + { + "name": "OpacityMax", + "value_type": { + "kind": "percent" + }, + "parse_fn": "INI::parsePercentToReal", + "doc": null + }, + { + "name": "OpacityThrobTime", + "value_type": { + "kind": "duration" + }, + "parse_fn": "INI::parseDurationUnsignedInt", + "doc": null + }, + { + "name": "Color", + "value_type": { + "kind": "color" + }, + "parse_fn": "INI::parseColorInt", + "doc": null + }, + { + "name": "OnlyVisibleToOwningPlayer", + "value_type": { + "kind": "bool" + }, + "parse_fn": "INI::parseBool", + "doc": null + } + ] + }, + { + "keyword": "NuclearMissileRadiusCursor", + "fields": [ + { + "name": "Texture", + "value_type": { + "kind": "texture_stem" + }, + "parse_fn": "INI::parseAsciiString", + "doc": null + }, + { + "name": "Style", + "value_type": { + "kind": "bit_flags", + "value_set": "shadow_type" + }, + "parse_fn": "INI::parseBitString32", + "doc": null + }, + { + "name": "OpacityMin", + "value_type": { + "kind": "percent" + }, + "parse_fn": "INI::parsePercentToReal", + "doc": null + }, + { + "name": "OpacityMax", + "value_type": { + "kind": "percent" + }, + "parse_fn": "INI::parsePercentToReal", + "doc": null + }, + { + "name": "OpacityThrobTime", + "value_type": { + "kind": "duration" + }, + "parse_fn": "INI::parseDurationUnsignedInt", + "doc": null + }, + { + "name": "Color", + "value_type": { + "kind": "color" + }, + "parse_fn": "INI::parseColorInt", + "doc": null + }, + { + "name": "OnlyVisibleToOwningPlayer", + "value_type": { + "kind": "bool" + }, + "parse_fn": "INI::parseBool", + "doc": null + } + ] + }, + { + "keyword": "OffensiveSpecialPowerRadiusCursor", + "fields": [ + { + "name": "Texture", + "value_type": { + "kind": "texture_stem" + }, + "parse_fn": "INI::parseAsciiString", + "doc": null + }, + { + "name": "Style", + "value_type": { + "kind": "bit_flags", + "value_set": "shadow_type" + }, + "parse_fn": "INI::parseBitString32", + "doc": null + }, + { + "name": "OpacityMin", + "value_type": { + "kind": "percent" + }, + "parse_fn": "INI::parsePercentToReal", + "doc": null + }, + { + "name": "OpacityMax", + "value_type": { + "kind": "percent" + }, + "parse_fn": "INI::parsePercentToReal", + "doc": null + }, + { + "name": "OpacityThrobTime", + "value_type": { + "kind": "duration" + }, + "parse_fn": "INI::parseDurationUnsignedInt", + "doc": null + }, + { + "name": "Color", + "value_type": { + "kind": "color" + }, + "parse_fn": "INI::parseColorInt", + "doc": null + }, + { + "name": "OnlyVisibleToOwningPlayer", + "value_type": { + "kind": "bool" + }, + "parse_fn": "INI::parseBool", + "doc": null + } + ] + }, + { + "keyword": "ParadropRadiusCursor", + "fields": [ + { + "name": "Texture", + "value_type": { + "kind": "texture_stem" + }, + "parse_fn": "INI::parseAsciiString", + "doc": null + }, + { + "name": "Style", + "value_type": { + "kind": "bit_flags", + "value_set": "shadow_type" + }, + "parse_fn": "INI::parseBitString32", + "doc": null + }, + { + "name": "OpacityMin", + "value_type": { + "kind": "percent" + }, + "parse_fn": "INI::parsePercentToReal", + "doc": null + }, + { + "name": "OpacityMax", + "value_type": { + "kind": "percent" + }, + "parse_fn": "INI::parsePercentToReal", + "doc": null + }, + { + "name": "OpacityThrobTime", + "value_type": { + "kind": "duration" + }, + "parse_fn": "INI::parseDurationUnsignedInt", + "doc": null + }, + { + "name": "Color", + "value_type": { + "kind": "color" + }, + "parse_fn": "INI::parseColorInt", + "doc": null + }, + { + "name": "OnlyVisibleToOwningPlayer", + "value_type": { + "kind": "bool" + }, + "parse_fn": "INI::parseBool", + "doc": null + } + ] }, { - "keyword": "ParticleCannonRadiusCursor" + "keyword": "ParticleCannonRadiusCursor", + "fields": [ + { + "name": "Texture", + "value_type": { + "kind": "texture_stem" + }, + "parse_fn": "INI::parseAsciiString", + "doc": null + }, + { + "name": "Style", + "value_type": { + "kind": "bit_flags", + "value_set": "shadow_type" + }, + "parse_fn": "INI::parseBitString32", + "doc": null + }, + { + "name": "OpacityMin", + "value_type": { + "kind": "percent" + }, + "parse_fn": "INI::parsePercentToReal", + "doc": null + }, + { + "name": "OpacityMax", + "value_type": { + "kind": "percent" + }, + "parse_fn": "INI::parsePercentToReal", + "doc": null + }, + { + "name": "OpacityThrobTime", + "value_type": { + "kind": "duration" + }, + "parse_fn": "INI::parseDurationUnsignedInt", + "doc": null + }, + { + "name": "Color", + "value_type": { + "kind": "color" + }, + "parse_fn": "INI::parseColorInt", + "doc": null + }, + { + "name": "OnlyVisibleToOwningPlayer", + "value_type": { + "kind": "bool" + }, + "parse_fn": "INI::parseBool", + "doc": null + } + ] }, { - "keyword": "RadarRadiusCursor" + "keyword": "RadarRadiusCursor", + "fields": [ + { + "name": "Texture", + "value_type": { + "kind": "texture_stem" + }, + "parse_fn": "INI::parseAsciiString", + "doc": null + }, + { + "name": "Style", + "value_type": { + "kind": "bit_flags", + "value_set": "shadow_type" + }, + "parse_fn": "INI::parseBitString32", + "doc": null + }, + { + "name": "OpacityMin", + "value_type": { + "kind": "percent" + }, + "parse_fn": "INI::parsePercentToReal", + "doc": null + }, + { + "name": "OpacityMax", + "value_type": { + "kind": "percent" + }, + "parse_fn": "INI::parsePercentToReal", + "doc": null + }, + { + "name": "OpacityThrobTime", + "value_type": { + "kind": "duration" + }, + "parse_fn": "INI::parseDurationUnsignedInt", + "doc": null + }, + { + "name": "Color", + "value_type": { + "kind": "color" + }, + "parse_fn": "INI::parseColorInt", + "doc": null + }, + { + "name": "OnlyVisibleToOwningPlayer", + "value_type": { + "kind": "bool" + }, + "parse_fn": "INI::parseBool", + "doc": null + } + ] }, { - "keyword": "ScudStormRadiusCursor" + "keyword": "ScudStormRadiusCursor", + "fields": [ + { + "name": "Texture", + "value_type": { + "kind": "texture_stem" + }, + "parse_fn": "INI::parseAsciiString", + "doc": null + }, + { + "name": "Style", + "value_type": { + "kind": "bit_flags", + "value_set": "shadow_type" + }, + "parse_fn": "INI::parseBitString32", + "doc": null + }, + { + "name": "OpacityMin", + "value_type": { + "kind": "percent" + }, + "parse_fn": "INI::parsePercentToReal", + "doc": null + }, + { + "name": "OpacityMax", + "value_type": { + "kind": "percent" + }, + "parse_fn": "INI::parsePercentToReal", + "doc": null + }, + { + "name": "OpacityThrobTime", + "value_type": { + "kind": "duration" + }, + "parse_fn": "INI::parseDurationUnsignedInt", + "doc": null + }, + { + "name": "Color", + "value_type": { + "kind": "color" + }, + "parse_fn": "INI::parseColorInt", + "doc": null + }, + { + "name": "OnlyVisibleToOwningPlayer", + "value_type": { + "kind": "bool" + }, + "parse_fn": "INI::parseBool", + "doc": null + } + ] }, { - "keyword": "SpectreGunshipRadiusCursor" + "keyword": "SpectreGunshipRadiusCursor", + "fields": [ + { + "name": "Texture", + "value_type": { + "kind": "texture_stem" + }, + "parse_fn": "INI::parseAsciiString", + "doc": null + }, + { + "name": "Style", + "value_type": { + "kind": "bit_flags", + "value_set": "shadow_type" + }, + "parse_fn": "INI::parseBitString32", + "doc": null + }, + { + "name": "OpacityMin", + "value_type": { + "kind": "percent" + }, + "parse_fn": "INI::parsePercentToReal", + "doc": null + }, + { + "name": "OpacityMax", + "value_type": { + "kind": "percent" + }, + "parse_fn": "INI::parsePercentToReal", + "doc": null + }, + { + "name": "OpacityThrobTime", + "value_type": { + "kind": "duration" + }, + "parse_fn": "INI::parseDurationUnsignedInt", + "doc": null + }, + { + "name": "Color", + "value_type": { + "kind": "color" + }, + "parse_fn": "INI::parseColorInt", + "doc": null + }, + { + "name": "OnlyVisibleToOwningPlayer", + "value_type": { + "kind": "bool" + }, + "parse_fn": "INI::parseBool", + "doc": null + } + ] }, { - "keyword": "SpyDroneRadiusCursor" + "keyword": "SpyDroneRadiusCursor", + "fields": [ + { + "name": "Texture", + "value_type": { + "kind": "texture_stem" + }, + "parse_fn": "INI::parseAsciiString", + "doc": null + }, + { + "name": "Style", + "value_type": { + "kind": "bit_flags", + "value_set": "shadow_type" + }, + "parse_fn": "INI::parseBitString32", + "doc": null + }, + { + "name": "OpacityMin", + "value_type": { + "kind": "percent" + }, + "parse_fn": "INI::parsePercentToReal", + "doc": null + }, + { + "name": "OpacityMax", + "value_type": { + "kind": "percent" + }, + "parse_fn": "INI::parsePercentToReal", + "doc": null + }, + { + "name": "OpacityThrobTime", + "value_type": { + "kind": "duration" + }, + "parse_fn": "INI::parseDurationUnsignedInt", + "doc": null + }, + { + "name": "Color", + "value_type": { + "kind": "color" + }, + "parse_fn": "INI::parseColorInt", + "doc": null + }, + { + "name": "OnlyVisibleToOwningPlayer", + "value_type": { + "kind": "bool" + }, + "parse_fn": "INI::parseBool", + "doc": null + } + ] }, { - "keyword": "SpySatelliteRadiusCursor" + "keyword": "SpySatelliteRadiusCursor", + "fields": [ + { + "name": "Texture", + "value_type": { + "kind": "texture_stem" + }, + "parse_fn": "INI::parseAsciiString", + "doc": null + }, + { + "name": "Style", + "value_type": { + "kind": "bit_flags", + "value_set": "shadow_type" + }, + "parse_fn": "INI::parseBitString32", + "doc": null + }, + { + "name": "OpacityMin", + "value_type": { + "kind": "percent" + }, + "parse_fn": "INI::parsePercentToReal", + "doc": null + }, + { + "name": "OpacityMax", + "value_type": { + "kind": "percent" + }, + "parse_fn": "INI::parsePercentToReal", + "doc": null + }, + { + "name": "OpacityThrobTime", + "value_type": { + "kind": "duration" + }, + "parse_fn": "INI::parseDurationUnsignedInt", + "doc": null + }, + { + "name": "Color", + "value_type": { + "kind": "color" + }, + "parse_fn": "INI::parseColorInt", + "doc": null + }, + { + "name": "OnlyVisibleToOwningPlayer", + "value_type": { + "kind": "bool" + }, + "parse_fn": "INI::parseBool", + "doc": null + } + ] }, { - "keyword": "SuperweaponScatterAreaRadiusCursor" + "keyword": "SuperweaponScatterAreaRadiusCursor", + "fields": [ + { + "name": "Texture", + "value_type": { + "kind": "texture_stem" + }, + "parse_fn": "INI::parseAsciiString", + "doc": null + }, + { + "name": "Style", + "value_type": { + "kind": "bit_flags", + "value_set": "shadow_type" + }, + "parse_fn": "INI::parseBitString32", + "doc": null + }, + { + "name": "OpacityMin", + "value_type": { + "kind": "percent" + }, + "parse_fn": "INI::parsePercentToReal", + "doc": null + }, + { + "name": "OpacityMax", + "value_type": { + "kind": "percent" + }, + "parse_fn": "INI::parsePercentToReal", + "doc": null + }, + { + "name": "OpacityThrobTime", + "value_type": { + "kind": "duration" + }, + "parse_fn": "INI::parseDurationUnsignedInt", + "doc": null + }, + { + "name": "Color", + "value_type": { + "kind": "color" + }, + "parse_fn": "INI::parseColorInt", + "doc": null + }, + { + "name": "OnlyVisibleToOwningPlayer", + "value_type": { + "kind": "bool" + }, + "parse_fn": "INI::parseBool", + "doc": null + } + ] } ] }, @@ -10514,7 +12245,7 @@ { "name": "Texture", "value_type": { - "kind": "ascii_string" + "kind": "texture_file" }, "parse_fn": "INI::parseAsciiString", "doc": "Texture page filename (.tga), not a MappedImage reference" @@ -11139,7 +12870,7 @@ { "name": "Texture", "value_type": { - "kind": "ascii_string" + "kind": "texture_sequence_stem" }, "parse_fn": "INI::parseAsciiString", "doc": null @@ -11304,7 +13035,8 @@ { "name": "Filename", "value_type": { - "kind": "ascii_string" + "kind": "audio_file", + "extension": "mp3" }, "parse_fn": "INI::parseAsciiString", "doc": null @@ -11397,7 +13129,7 @@ { "name": "Sounds", "value_type": { - "kind": "ascii_string_list" + "kind": "audio_stem_list" }, "parse_fn": "INI::parseSoundsList", "doc": null @@ -11405,7 +13137,7 @@ { "name": "SoundsNight", "value_type": { - "kind": "ascii_string_list" + "kind": "audio_stem_list" }, "parse_fn": "INI::parseSoundsList", "doc": null @@ -11413,7 +13145,7 @@ { "name": "SoundsEvening", "value_type": { - "kind": "ascii_string_list" + "kind": "audio_stem_list" }, "parse_fn": "INI::parseSoundsList", "doc": null @@ -11421,7 +13153,7 @@ { "name": "SoundsMorning", "value_type": { - "kind": "ascii_string_list" + "kind": "audio_stem_list" }, "parse_fn": "INI::parseSoundsList", "doc": null @@ -11429,7 +13161,7 @@ { "name": "Attack", "value_type": { - "kind": "ascii_string_list" + "kind": "audio_stem_list" }, "parse_fn": "INI::parseSoundsList", "doc": null @@ -11437,7 +13169,7 @@ { "name": "Decay", "value_type": { - "kind": "ascii_string_list" + "kind": "audio_stem_list" }, "parse_fn": "INI::parseSoundsList", "doc": null @@ -11784,7 +13516,7 @@ { "name": "ShadowTexture", "value_type": { - "kind": "ascii_string" + "kind": "texture_stem" }, "parse_fn": "INI::parseAsciiString", "doc": null @@ -13136,12 +14868,12 @@ "keyword": "DeliverPayload", "sub_blocks": [ { - "keyword": "DeliveryDecal", - "fields": [ - { - "name": "Texture", - "value_type": { - "kind": "ascii_string" + "keyword": "DeliveryDecal", + "fields": [ + { + "name": "Texture", + "value_type": { + "kind": "texture_stem" }, "parse_fn": "INI::parseAsciiString" }, @@ -13879,7 +15611,7 @@ { "name": "ParticleName", "value_type": { - "kind": "ascii_string" + "kind": "texture_file" }, "parse_fn": "INI::parseAsciiString", "doc": "particle texture image name" @@ -14914,7 +16646,7 @@ { "name": "Texture", "value_type": { - "kind": "ascii_string" + "kind": "texture_file" }, "parse_fn": "INI::parseAsciiString", "doc": null @@ -15374,7 +17106,7 @@ { "name": "Texture", "value_type": { - "kind": "ascii_string" + "kind": "texture_file" }, "parse_fn": "INI::parseAsciiString", "doc": null @@ -15520,7 +17252,7 @@ { "name": "SkyTexture", "value_type": { - "kind": "ascii_string" + "kind": "texture_file" }, "parse_fn": "INI::parseAsciiString", "doc": null @@ -15528,7 +17260,7 @@ { "name": "WaterTexture", "value_type": { - "kind": "ascii_string" + "kind": "texture_file" }, "parse_fn": "INI::parseAsciiString", "doc": null @@ -15649,7 +17381,7 @@ { "name": "StandingWaterTexture", "value_type": { - "kind": "ascii_string" + "kind": "texture_file" }, "parse_fn": "INI::parseAsciiString", "doc": null @@ -15673,7 +17405,7 @@ { "name": "SkyboxTextureN", "value_type": { - "kind": "ascii_string" + "kind": "texture_file" }, "parse_fn": "INI::parseAsciiString", "doc": null @@ -15681,7 +17413,7 @@ { "name": "SkyboxTextureE", "value_type": { - "kind": "ascii_string" + "kind": "texture_file" }, "parse_fn": "INI::parseAsciiString", "doc": null @@ -15689,7 +17421,7 @@ { "name": "SkyboxTextureS", "value_type": { - "kind": "ascii_string" + "kind": "texture_file" }, "parse_fn": "INI::parseAsciiString", "doc": null @@ -15697,7 +17429,7 @@ { "name": "SkyboxTextureW", "value_type": { - "kind": "ascii_string" + "kind": "texture_file" }, "parse_fn": "INI::parseAsciiString", "doc": null @@ -15705,7 +17437,7 @@ { "name": "SkyboxTextureT", "value_type": { - "kind": "ascii_string" + "kind": "texture_file" }, "parse_fn": "INI::parseAsciiString", "doc": null @@ -16443,7 +18175,7 @@ { "name": "SnowTexture", "value_type": { - "kind": "ascii_string" + "kind": "texture_file" }, "parse_fn": "INI::parseAsciiString", "doc": null @@ -16671,7 +18403,7 @@ { "name": "TrackMarks", "value_type": { - "kind": "ascii_string" + "kind": "texture_file" }, "parse_fn": "parseAsciiStringLC", "doc": null @@ -25422,12 +27154,12 @@ ], "sub_blocks": [ { - "keyword": "DeliveryDecal", - "fields": [ - { - "name": "Texture", - "value_type": { - "kind": "ascii_string" + "keyword": "DeliveryDecal", + "fields": [ + { + "name": "Texture", + "value_type": { + "kind": "texture_stem" }, "parse_fn": "INI::parseAsciiString", "doc": null @@ -27246,12 +28978,12 @@ ], "sub_blocks": [ { - "keyword": "GridDecalTemplate", - "fields": [ - { - "name": "Texture", - "value_type": { - "kind": "ascii_string" + "keyword": "GridDecalTemplate", + "fields": [ + { + "name": "Texture", + "value_type": { + "kind": "texture_stem" }, "parse_fn": "INI::parseAsciiString", "doc": null @@ -35070,12 +36802,12 @@ ], "sub_blocks": [ { - "keyword": "DeliveryDecal", - "fields": [ - { - "name": "Texture", - "value_type": { - "kind": "ascii_string" + "keyword": "DeliveryDecal", + "fields": [ + { + "name": "Texture", + "value_type": { + "kind": "texture_stem" }, "parse_fn": "INI::parseAsciiString", "doc": null @@ -42428,12 +44160,12 @@ ], "sub_blocks": [ { - "keyword": "AttackAreaDecal", - "fields": [ - { - "name": "Texture", - "value_type": { - "kind": "ascii_string" + "keyword": "AttackAreaDecal", + "fields": [ + { + "name": "Texture", + "value_type": { + "kind": "texture_stem" }, "parse_fn": "INI::parseAsciiString", "doc": null @@ -42492,12 +44224,12 @@ "doc": "RadiusDecalTemplate nested field table." }, { - "keyword": "TargetingReticleDecal", - "fields": [ - { - "name": "Texture", - "value_type": { - "kind": "ascii_string" + "keyword": "TargetingReticleDecal", + "fields": [ + { + "name": "Texture", + "value_type": { + "kind": "texture_stem" }, "parse_fn": "INI::parseAsciiString", "doc": null @@ -53608,7 +55340,7 @@ { "name": "TrackMarks", "value_type": { - "kind": "ascii_string" + "kind": "texture_file" }, "parse_fn": "parseAsciiStringLC", "doc": null @@ -54531,7 +56263,7 @@ { "name": "Texture", "value_type": { - "kind": "ascii_string" + "kind": "texture_file" }, "parse_fn": "INI::parseAsciiString", "doc": null @@ -54671,7 +56403,7 @@ { "name": "TrackMarks", "value_type": { - "kind": "ascii_string" + "kind": "texture_file" }, "parse_fn": "parseAsciiStringLC", "doc": null @@ -55604,7 +57336,7 @@ { "name": "TrackMarks", "value_type": { - "kind": "ascii_string" + "kind": "texture_file" }, "parse_fn": "parseAsciiStringLC", "doc": null @@ -56579,7 +58311,7 @@ { "name": "TrackMarks", "value_type": { - "kind": "ascii_string" + "kind": "texture_file" }, "parse_fn": "parseAsciiStringLC", "doc": null @@ -57675,7 +59407,7 @@ { "name": "TrackMarks", "value_type": { - "kind": "ascii_string" + "kind": "texture_file" }, "parse_fn": "parseAsciiStringLC", "doc": null @@ -58697,7 +60429,7 @@ { "name": "Texture", "value_type": { - "kind": "ascii_string" + "kind": "texture_file" }, "parse_fn": "INI::parseAsciiString", "doc": null @@ -58848,7 +60580,7 @@ { "name": "TrackMarks", "value_type": { - "kind": "ascii_string" + "kind": "texture_file" }, "parse_fn": "parseAsciiStringLC", "doc": null @@ -59790,7 +61522,7 @@ { "name": "TrackMarks", "value_type": { - "kind": "ascii_string" + "kind": "texture_file" }, "parse_fn": "parseAsciiStringLC", "doc": null @@ -60731,7 +62463,7 @@ { "name": "TrackMarks", "value_type": { - "kind": "ascii_string" + "kind": "texture_file" }, "parse_fn": "parseAsciiStringLC", "doc": null @@ -61706,7 +63438,7 @@ { "name": "TrackMarks", "value_type": { - "kind": "ascii_string" + "kind": "texture_file" }, "parse_fn": "parseAsciiStringLC", "doc": null @@ -62731,7 +64463,7 @@ { "name": "TextureName", "value_type": { - "kind": "ascii_string" + "kind": "texture_file" }, "parse_fn": "INI::parseAsciiString", "doc": null @@ -62953,7 +64685,7 @@ { "name": "TrackMarks", "value_type": { - "kind": "ascii_string" + "kind": "texture_file" }, "parse_fn": "parseAsciiStringLC", "doc": null diff --git a/crates/schema/src/lib.rs b/crates/schema/src/lib.rs index d143d94..73a78c7 100644 --- a/crates/schema/src/lib.rs +++ b/crates/schema/src/lib.rs @@ -169,6 +169,14 @@ pub enum ModelSource { ObjectReferenceField { field: String }, } +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum AudioExtension { + Any, + Wav, + Mp3, +} + /// The type of a field's value, derived from its engine parse function. /// /// The variant determines how the value tokens are validated and which @@ -209,6 +217,16 @@ pub enum ValueType { W3dModelList, /// A bone, subobject, mesh, or other member of a W3D model asset. W3dModelMember, + /// An indexed audio filename, optionally restricted by extension. + AudioFile { extension: AudioExtension }, + /// A variadic list of extensionless indexed WAV names. + AudioStemList, + /// An indexed texture filename. DDS transparently aliases the same TGA stem. + TextureFile, + /// An extensionless indexed TGA/DDS texture name. + TextureStem, + /// A texture stem that may be backed by a numbered `0000` first frame. + TextureSequenceStem, /// `R:r G:g B:b [A:a]` color. Color, /// `X:x Y:y` coordinate. @@ -1528,7 +1546,7 @@ mod tests { } let decal_fields = [ - ("Texture", ValueType::AsciiString), + ("Texture", ValueType::TextureStem), ("Style", bit_flags("shadow_type")), ("OpacityMin", ValueType::Percent), ("OpacityMax", ValueType::Percent), @@ -1554,6 +1572,14 @@ mod tests { assert!(radius.fields.is_empty()); assert!(radius.sub_blocks.is_empty()); + let cursor_blocks = &schema.index().block("InGameUI").unwrap().sub_blocks; + assert_eq!(cursor_blocks.len(), 29); + assert!(cursor_blocks.iter().all(|cursor| cursor + .fields + .iter() + .map(|field| (field.name.as_str(), field.value_type.clone())) + .eq(decal_fields.iter().cloned()))); + let ai_data = schema.index().block("AIData").unwrap(); let build_list = ai_data .sub_blocks @@ -1571,6 +1597,44 @@ mod tests { schema.index().block("EvaEvent").unwrap().defines, Some(RefKind::EvaEvent) ); + let eva_side_sounds = schema + .index() + .block("EvaEvent") + .unwrap() + .sub_blocks + .iter() + .find(|sub_block| sub_block.keyword == "SideSounds") + .unwrap(); + assert_eq!( + eva_side_sounds + .fields + .iter() + .map(|field| (field.name.as_str(), field.value_type.clone())) + .collect::>(), + [ + ("Side", ValueType::AsciiString), + ("Sounds", ValueType::AudioStemList), + ] + ); + for module in [ + "W3DDependencyModelDraw", + "W3DModelDraw", + "W3DOverlordAircraftDraw", + "W3DOverlordTankDraw", + "W3DOverlordTruckDraw", + "W3DPoliceCarDraw", + "W3DScienceModelDraw", + "W3DSupplyDraw", + "W3DTankDraw", + "W3DTankTruckDraw", + "W3DTruckDraw", + ] { + assert_eq!( + module_field(&schema, module, "TrackMarks").value_type, + ValueType::TextureFile, + "{module}.TrackMarks" + ); + } assert_eq!( schema.index().block("CrateData").unwrap().defines, Some(RefKind::CrateData) diff --git a/crates/server/src/backend.rs b/crates/server/src/backend.rs index 604993c..0ff071e 100644 --- a/crates/server/src/backend.rs +++ b/crates/server/src/backend.rs @@ -75,7 +75,7 @@ pub struct Backend { index: Arc>, /// Workspace roots, captured at `initialize` and scanned in `initialized`. roots: Mutex>, - /// User-configured game/mod INI roots. Entries may be directories or `.big` + /// User-configured game/mod INI and asset roots. Entries may be directories or `.big` /// archives; both seed definitions that map.ini/solo.ini can rely on. base_roots: Mutex>, /// Number of base INI files indexed from configured base roots. @@ -388,7 +388,7 @@ impl Backend { self.client .show_message( MessageType::WARNING, - "ZeroSyntax v2: map/solo.ini diagnostics are limited until base game or mod INIs are configured. Set `zerosyntax.baseIniRoots` to your game/mod `.big` files or INI folder.", + "ZeroSyntax v2: map/solo.ini diagnostics are limited until base game or mod data is configured. Set `zerosyntax.baseIniRoots` to your game/mod `.big` files or data folders.", ) .await; } @@ -439,7 +439,7 @@ impl Backend { let (scanned, base_scanned) = handle.await.unwrap_or_default(); let base_ini_count = base_scanned .iter() - .filter(|(_, _, _, _, _, _, models, _)| models.is_empty()) + .filter(|(_, _, _, _, _, _, models, assets, _)| models.is_empty() && assets.is_empty()) .count(); self.base_indexed_count .store(base_ini_count, Ordering::Relaxed); @@ -447,13 +447,23 @@ impl Backend { let ini_total = base_ini_count + scanned .iter() - .filter(|(_, _, _, _, _, _, models, _)| models.is_empty()) + .filter(|(_, _, _, _, _, _, models, assets, _)| { + models.is_empty() && assets.is_empty() + }) .count(); let model_total: usize = base_scanned .iter() .chain(scanned.iter()) - .map(|(_, _, _, _, _, _, models, _)| models.len()) + .map(|(_, _, _, _, _, _, models, _, _)| models.len()) .sum(); + let (audio_total, texture_total) = base_scanned + .iter() + .chain(scanned.iter()) + .flat_map(|(_, _, _, _, _, _, _, assets, _)| assets) + .fold((0, 0), |(audio, texture), asset| match asset.kind { + zerosyntax_analysis::index::AssetKind::Audio => (audio + 1, texture), + zerosyntax_analysis::index::AssetKind::Texture => (audio, texture + 1), + }); // Don't overwrite index entries for already-open documents with stale // disk content; `initialized` calls `refresh` for each open doc right // after this returns, so they will populate the index from live text. @@ -466,7 +476,7 @@ impl Backend { let Ok(mut idx) = self.index.write() else { return; }; - for (uri, defs, refs, tags, object_models, object_parents, models, text) in + for (uri, defs, refs, tags, object_models, object_parents, models, assets, text) in base_scanned.into_iter().chain(scanned) { if let Some(text) = text { @@ -479,11 +489,18 @@ impl Backend { idx.set_file_object_models(&uri, object_models); idx.set_file_object_parents(&uri, object_parents); idx.set_file_models(&uri, models); + idx.set_file_assets(&uri, assets); } } } - self.end_scan_progress(progress_token, ini_total, model_total) - .await; + self.end_scan_progress( + progress_token, + ini_total, + model_total, + audio_total, + texture_total, + ) + .await; } /// Ask the client to show an indexing spinner. Returns the token to end @@ -505,7 +522,7 @@ impl Backend { value: ProgressParamsValue::WorkDone(WorkDoneProgress::Begin( WorkDoneProgressBegin { title: "Indexing game data".into(), - message: Some("scanning workspace and base INI roots".into()), + message: Some("scanning workspace and configured game-data roots".into()), cancellable: Some(false), // Signals that reports will carry a percentage. percentage: Some(0), @@ -543,6 +560,8 @@ impl Backend { token: Option, ini_total: usize, model_total: usize, + audio_total: usize, + texture_total: usize, ) { let Some(token) = token else { return }; self.client @@ -550,7 +569,7 @@ impl Backend { token, value: ProgressParamsValue::WorkDone(WorkDoneProgress::End(WorkDoneProgressEnd { message: Some(format!( - "{ini_total} INI files, {model_total} W3D models indexed" + "{ini_total} INI files, {model_total} W3D models, {audio_total} audio files, {texture_total} textures indexed" )), })), }) @@ -801,19 +820,38 @@ impl LanguageServer for Backend { for uri in open { self.refresh(&uri, None).await; } - let (ini, models) = { + let (ini, models, audio, textures) = { let idx = self.index.read().ok(); let models = idx .as_ref() .map(|i| i.model_names().count()) .unwrap_or_default(); - (self.base_indexed_count.load(Ordering::Relaxed), models) + let audio = idx + .as_ref() + .map(|i| { + i.asset_names(zerosyntax_analysis::index::AssetKind::Audio) + .count() + }) + .unwrap_or_default(); + let textures = idx + .as_ref() + .map(|i| { + i.asset_names(zerosyntax_analysis::index::AssetKind::Texture) + .count() + }) + .unwrap_or_default(); + ( + self.base_indexed_count.load(Ordering::Relaxed), + models, + audio, + textures, + ) }; self.client .log_message( MessageType::INFO, format!( - "zerosyntax language server ready ({ini} base INI files, {models} W3D models indexed)" + "zerosyntax language server ready ({ini} base INI files, {models} W3D models, {audio} audio files, {textures} textures indexed)" ), ) .await; @@ -1614,36 +1652,73 @@ mod tests { } #[test] - fn scans_ini_from_big_archive() { + fn scans_ini_w3d_audio_and_texture_from_big_archive() { let dir = std::env::temp_dir(); let path = dir.join(format!("zerosyntax-test-{}.big", std::process::id())); - let entry_name = b"Data\\INI\\Test.ini\0"; - let ini = b"Object BigArchiveObject\nEnd\n"; - let data_offset = 0x10 + 8 + entry_name.len(); - let archive_size = data_offset + ini.len(); + let entries: Vec<(&str, &[u8])> = vec![ + ("Data\\INI\\Test.ini", b"Object BigArchiveObject\nEnd\n"), + ("Art\\Good.w3d", b""), + ("Audio\\Click.WAV", b"not read"), + ("Textures\\Particle.DDS", b"not read"), + ]; + let data_offset = 0x10 + + entries + .iter() + .map(|(name, _)| 8 + name.len() + 1) + .sum::(); + let archive_size = data_offset + entries.iter().map(|(_, data)| data.len()).sum::(); let mut bytes = Vec::new(); bytes.extend_from_slice(b"BIGF"); bytes.extend_from_slice(&(archive_size as u32).to_be_bytes()); - bytes.extend_from_slice(&1u32.to_be_bytes()); + bytes.extend_from_slice(&(entries.len() as u32).to_be_bytes()); bytes.extend_from_slice(&0u32.to_be_bytes()); - bytes.extend_from_slice(&(data_offset as u32).to_be_bytes()); - bytes.extend_from_slice(&(ini.len() as u32).to_be_bytes()); - bytes.extend_from_slice(entry_name); - bytes.extend_from_slice(ini); + let mut offset = data_offset; + for (name, data) in &entries { + bytes.extend_from_slice(&(offset as u32).to_be_bytes()); + bytes.extend_from_slice(&(data.len() as u32).to_be_bytes()); + bytes.extend_from_slice(name.as_bytes()); + bytes.push(0); + offset += data.len(); + } + for (_, data) in &entries { + bytes.extend_from_slice(data); + } std::fs::write(&path, bytes).unwrap(); let analyzer = Analyzer::embedded(); let scanned = scan_big(&analyzer, &path).unwrap(); let _ = std::fs::remove_file(&path); - assert_eq!(scanned.len(), 1); - assert!(Url::parse(&scanned[0].0).is_ok()); - assert!(scanned[0].1.iter().any(|d| d.name == "BigArchiveObject")); - assert_eq!( - scanned[0].7.as_deref(), - Some("Object BigArchiveObject\nEnd\n") - ); + assert_eq!(scanned.len(), 3, "INI, W3D, and one aggregated asset entry"); + let ini = scanned.iter().find(|entry| !entry.1.is_empty()).unwrap(); + assert!(Url::parse(&ini.0).is_ok()); + assert!(ini.1.iter().any(|d| d.name == "BigArchiveObject")); + assert_eq!(ini.8.as_deref(), Some("Object BigArchiveObject\nEnd\n")); + assert!(scanned + .iter() + .any(|entry| entry.6.iter().any(|model| model.name == "Good"))); + let assets = &scanned.iter().find(|entry| !entry.7.is_empty()).unwrap().7; + assert_eq!(assets.len(), 2); + assert!(assets.iter().any(|asset| asset.name == "Click.WAV")); + assert!(assets.iter().any(|asset| asset.name == "Particle.DDS")); + } + + #[test] + fn loose_directory_scan_indexes_audio_and_texture_assets() { + let dir = std::env::temp_dir().join(format!("zerosyntax-assets-{}", std::process::id())); + std::fs::create_dir_all(&dir).unwrap(); + std::fs::write(dir.join("Click.wav"), b"").unwrap(); + std::fs::write(dir.join("Particle.tga"), b"").unwrap(); + let scanned = scan_roots(&Analyzer::embedded(), std::slice::from_ref(&dir)); + std::fs::remove_dir_all(&dir).unwrap(); + let assets = scanned + .into_iter() + .flat_map(|entry| entry.7) + .collect::>(); + assert_eq!(assets.len(), 2); + assert!(assets.iter().any(|asset| asset.name == "Click.wav")); + assert!(assets.iter().any(|asset| asset.name == "Particle.tga")); } #[test] @@ -1667,13 +1742,14 @@ mod tests { let _ = std::fs::remove_dir_all(&dir); let mut idx = WorkspaceIndex::new(); - for (uri, defs, refs, tags, object_models, object_parents, models, _) in scanned { + for (uri, defs, refs, tags, object_models, object_parents, models, assets, _) in scanned { idx.set_file(&uri, defs); idx.set_file_refs(&uri, refs); idx.set_file_tags(&uri, tags); idx.set_file_object_models(&uri, object_models); idx.set_file_object_parents(&uri, object_parents); idx.set_file_models(&uri, models); + idx.set_file_assets(&uri, assets); } assert!(idx.is_model_asset("Good"), "model name from file stem"); diff --git a/crates/server/src/cli.rs b/crates/server/src/cli.rs index a9336e5..f54fe98 100644 --- a/crates/server/src/cli.rs +++ b/crates/server/src/cli.rs @@ -56,7 +56,9 @@ fn command() -> Command { .value_name("PATH") .value_parser(value_parser!(PathBuf)) .action(ArgAction::Append) - .help("INI/W3D directory or .big archive loaded before targets"), + .help( + "INI and game assets directory or .big archive loaded before targets", + ), ) .arg( Arg::new("stdin-filename") @@ -270,13 +272,16 @@ fn has_extension(path: &Path, extension: &str) -> bool { } fn apply_entries(index: &mut WorkspaceIndex, entries: Vec) { - for (file, definitions, references, tags, object_models, object_parents, models, _) in entries { + for (file, definitions, references, tags, object_models, object_parents, models, assets, _) in + entries + { index.set_file(&file, definitions); index.set_file_refs(&file, references); index.set_file_tags(&file, tags); index.set_file_object_models(&file, object_models); index.set_file_object_parents(&file, object_parents); index.set_file_models(&file, models); + index.set_file_assets(&file, assets); } } diff --git a/crates/server/src/scan.rs b/crates/server/src/scan.rs index 26a6557..0b2e557 100644 --- a/crates/server/src/scan.rs +++ b/crates/server/src/scan.rs @@ -7,8 +7,8 @@ use std::sync::Arc; use anyhow::{Context, Result}; use tower_lsp::lsp_types::Url; use zerosyntax_analysis::index::{ - definitions_in, module_tags_in, object_models_in, object_parents_in, references_in, Definition, - ModelAsset, ReferenceSite, + definitions_in, module_tags_in, object_models_in, object_parents_in, references_in, AssetKind, + Definition, FileAsset, ModelAsset, ReferenceSite, }; use zerosyntax_analysis::Analyzer; @@ -20,6 +20,7 @@ pub(crate) type ScanEntry = ( Vec<(String, Vec)>, Vec<(String, String)>, Vec, + Vec, Option>, ); @@ -134,6 +135,22 @@ fn file_stem_str(path: &str) -> String { .to_string() } +fn raw_asset(path: &str) -> Option { + let name = path.rsplit(['/', '\\']).next()?; + let (_, extension) = name.rsplit_once('.')?; + let kind = if extension.eq_ignore_ascii_case("wav") || extension.eq_ignore_ascii_case("mp3") { + AssetKind::Audio + } else if extension.eq_ignore_ascii_case("tga") || extension.eq_ignore_ascii_case("dds") { + AssetKind::Texture + } else { + return None; + }; + Some(FileAsset { + kind, + name: name.to_string(), + }) +} + pub(crate) fn parse_w3d_models(bytes: &[u8], fallback_name: &str) -> Vec { let mut names = Vec::new(); let mut members = Vec::new(); @@ -250,9 +267,14 @@ fn dedup_case_insensitive(values: &mut Vec) { pub(crate) fn scan_big(analyzer: &Analyzer, path: &Path) -> Result> { let mut out = Vec::new(); + let mut assets = Vec::new(); for entry in big_entries(path)? { let file = big_uri(path, &entry.name); - if entry.name.ends_with(".ini") || entry.name.ends_with(".INI") { + let extension = Path::new(&entry.name) + .extension() + .and_then(|value| value.to_str()) + .unwrap_or_default(); + if extension.eq_ignore_ascii_case("ini") { let bytes = read_big_entry_bytes(path, &entry).with_context(|| { format!("failed to read {} from {}", entry.name, path.display()) })?; @@ -266,9 +288,10 @@ pub(crate) fn scan_big(analyzer: &Analyzer, path: &Path) -> Result Result Result> { if ext.eq_ignore_ascii_case("big") || ext.eq_ignore_ascii_case("ini") || ext.eq_ignore_ascii_case("w3d") + || ext.eq_ignore_ascii_case("wav") + || ext.eq_ignore_ascii_case("mp3") + || ext.eq_ignore_ascii_case("tga") + || ext.eq_ignore_ascii_case("dds") { out.push(path.to_path_buf()); } @@ -377,6 +420,7 @@ fn scan_path(analyzer: &Analyzer, path: &Path) -> Result> { object_models_in(analyzer, &parse), object_parents_in(&parse), Vec::new(), + Vec::new(), None, )]) } else if ext.eq_ignore_ascii_case("w3d") { @@ -396,10 +440,23 @@ fn scan_path(analyzer: &Analyzer, path: &Path) -> Result> { Vec::new(), Vec::new(), models, + Vec::new(), None, )) .into_iter() .collect()) + } else if let Some(asset) = raw_asset(&path.to_string_lossy()) { + Ok(vec![( + uri.to_string(), + Vec::new(), + Vec::new(), + Vec::new(), + Vec::new(), + Vec::new(), + Vec::new(), + vec![asset], + None, + )]) } else { Ok(Vec::new()) } diff --git a/docs/diagnostics.md b/docs/diagnostics.md index 90f55fb..eb262fc 100644 --- a/docs/diagnostics.md +++ b/docs/diagnostics.md @@ -44,9 +44,11 @@ file. Fix error-level syntax and schema problems rather than suppressing them. | `bad-enum` | A value is not a member of the expected enum. | | `bad-flag` | A bitflag is not a member of the expected flag set. | | `bad-prefixed` | A tagged value does not use its required `Prefix:value` form. | -| `unresolved-reference` | A referenced definition is not found in the workspace or configured base INI roots. | +| `unresolved-reference` | A referenced definition is not found in the workspace or configured game-data roots. | | `unknown-model` | A model name is not found in the indexed W3D assets. | | `unknown-model-member` | A bone or subobject is not found in the models active in that scope. | +| `unknown-audio-file` | An audio filename or WAV stem is not found in indexed WAV/MP3 assets. Enabled only after audio assets are indexed. | +| `unknown-texture` | A texture filename or stem is not found in indexed TGA/DDS assets. Enabled only after texture assets are indexed. | | `unknown-suppression` | A `zerosyntax-disable` comment names an unknown code. | | `module-wrong-slot` | A module type is used under the wrong slot. | | `duplicate-module-tag` | Two modules in one object use the same module tag. | diff --git a/docs/language-server.md b/docs/language-server.md index f3ce47b..a8c0225 100644 --- a/docs/language-server.md +++ b/docs/language-server.md @@ -33,8 +33,8 @@ indexed together before diagnostics run, so references between them resolve. Overlapping targets are checked once. `--base-root` is repeatable and accepts directories or `.big` archives -containing base/mod INIs and W3D assets. Base roots participate in reference, -model, and bone checks but do not emit diagnostics themselves. For stdin, +containing base/mod INIs and game assets. Base roots participate in reference, +model, bone, audio, and texture checks but do not emit diagnostics themselves. For stdin, `--stdin-filename` supplies the displayed/indexed name and enables `map.ini` or `solo.ini` override semantics; it defaults to ``. @@ -112,8 +112,12 @@ symbols. completions remain immediate. It defaults to `250`; valid values are `0`–`5000`, where `0` refreshes as soon as possible. - `baseIniRoots` accepts directories and `.big` archives containing base game - or mod INI files and W3D assets. Those INI definitions are treated as loaded - before `map.ini` and `solo.ini`. + or mod INI files and game assets. WAV/MP3 filenames and TGA/DDS textures power + asset completion and warnings; DDS-only textures complete as the canonical + INI spelling `stem.tga`. Audio and texture warnings activate independently + only after that asset kind is indexed. Supply every loaded game/mod root to + avoid warnings caused by a partial asset index. INI definitions are treated + as loaded before `map.ini` and `solo.ini`. Restart the language server after changing initialization options. diff --git a/editors/vscode/README.md b/editors/vscode/README.md index 23f3af5..c6edf20 100644 --- a/editors/vscode/README.md +++ b/editors/vscode/README.md @@ -28,14 +28,16 @@ across its INI files. When editing `map.ini` or `solo.ini`, configure **ZeroSyntax v2: Base Ini Roots** with any game or mod folders and `.big` archives that load before the map. This -prevents false unresolved-reference warnings and enables W3D model and bone -checks. +prevents false unresolved-reference warnings and enables W3D model, bone, +WAV/MP3 audio, and TGA/DDS texture checks. Configure every loaded game/mod root; +asset warnings activate per kind once any matching asset is indexed. DDS-only +textures are offered using the engine-compatible `stem.tga` spelling. ## Settings | Setting | Default | Purpose | | --- | --- | --- | -| `zerosyntax.baseIniRoots` | `[]` | Base game/mod directories and `.big` archives used for map and model checks. | +| `zerosyntax.baseIniRoots` | `[]` | Base game/mod directories and `.big` archives used for INI and game-asset checks. | | `zerosyntax.schema.path` | empty | Custom schema JSON; invalid files fall back to the built-in schema. | | `zerosyntax.analysis.modelMemberStrictness` | `compatible` | Disables member warnings, accepts any applicable model, or requires every model. | | `zerosyntax.analysis.mapOrderingDiagnostics` | `true` | Warns about source-proven forward-order problems in `map.ini` and `solo.ini`. | diff --git a/editors/vscode/package.json b/editors/vscode/package.json index 7c4104b..1925003 100644 --- a/editors/vscode/package.json +++ b/editors/vscode/package.json @@ -68,7 +68,7 @@ "items": { "type": "string" }, - "markdownDescription": "Directories or `.big` archives containing base game/mod INI files and W3D assets. INI definitions are treated as already loaded before `map.ini`/`solo.ini`; W3D assets power model and bone completions/diagnostics. Changing this restarts the language server." + "markdownDescription": "Directories or `.big` archives containing base game/mod INI files and game assets. INI definitions are treated as already loaded before `map.ini`/`solo.ini`; W3D, WAV/MP3, and TGA/DDS assets power completions and diagnostics. Configure all loaded roots to avoid partial-index warnings. Changing this restarts the language server." }, "zerosyntax.schema.path": { "type": "string",