#region ================== Namespaces using System; using System.Collections.Generic; using System.IO; using CodeImp.DoomBuilder.Config; using CodeImp.DoomBuilder.Data; #endregion namespace CodeImp.DoomBuilder.ZDoom { internal sealed class SndInfoParser : ZDTextParser { #region ================== Variables private Dictionary ambientsounds; private Dictionary sounds; private SoundInfo globalprops; // There are two formats supported, with the old format being // logicalname lumpname // and the new format being // logicalname = lumpname // Only one format may be used private enum SndInfoFormat { None, Old, New } private SndInfoFormat format = SndInfoFormat.None; #endregion #region ================== Properties internal override ScriptType ScriptType { get { return ScriptType.SNDINFO; } } internal Dictionary AmbientSounds { get { return ambientsounds; } } internal Dictionary Sounds { get { return sounds; } } #endregion #region ================== Constructor public SndInfoParser() { specialtokens = "{}"; ambientsounds = new Dictionary(); sounds = new Dictionary(StringComparer.OrdinalIgnoreCase); skipeditorcomments = true; // otherwise //$AMBIENT will be treated like one... globalprops = new SoundInfo(); } #endregion #region ================== Parsing public override bool Parse(TextResourceData data, bool clearerrors) { //mxd. Already parsed? if(!base.AddTextResource(data)) { if(clearerrors) ClearError(); return true; } // Cannot process? if(!base.Parse(data, clearerrors)) return false; // Continue until at the end of the stream string currentgametype = GameType.UNKNOWN; while(SkipWhitespace(true)) { //INFO: For many commands, using * as the sound name will mean that //INFO: the command will apply to all sounds that do not specify otherwise. string token = StripTokenQuotes(ReadToken()).ToLowerInvariant(); if(string.IsNullOrEmpty(token)) continue; // Skipping block for different game? if(currentgametype != GameType.UNKNOWN && currentgametype != General.Map.Config.BaseGame) { // Should we stop skipping? if(token == "$endif") currentgametype = GameType.UNKNOWN; continue; } switch(token) { // Must parse all commands to reliably get sound assignments... case "$alias": if(!ParseAlias()) return false; break; case "$ambient": if(!ParseAmbient()) return false; break; case "$archivepath": if(!SkipTokens(1)) return false; break; case "$attenuation": if(!ParseAttenuation()) return false; break; case "$edfoverride": break; case "$limit": if(!ParseLimit()) return false; break; case "$map": if(!SkipTokens(2)) return false; break; case "$mididevice": if(!ParseMidiDevice()) return false; break; case "$musicalias": if(!SkipTokens(2)) return false; break; case "$musicvolume": if(!SkipTokens(2)) return false; break; case "$pitchshift": if(!SkipTokens(2)) return false; break; case "$pitchshiftrange": if(!SkipTokens(1)) return false; break; case "$playeralias": if(!SkipTokens(4)) return false; break; case "$playercompat": if(!SkipTokens(4)) return false; break; case "$playersound": if(!SkipTokens(4)) return false; break; case "$playersounddup": if(!SkipTokens(4)) return false; break; case "$random": if(!ParseRandom()) return false; break; case "$registered": break; case "$rolloff": if(!ParseRolloff()) return false; break; case "$singular": if(!SkipTokens(1)) return false; break; case "$volume": if(!ParseVolume()) return false; break; // Game type blocks... case "$ifdoom": currentgametype = GameType.DOOM; break; case "$ifheretic": currentgametype = GameType.HERETIC; break; case "$ifhexen": currentgametype = GameType.HEXEN; break; case "$ifstrife": currentgametype = GameType.STRIFE; break; // Should be logicalname lumpname pair... default: if(!ParseSoundAssignment(token)) return false; break; } } return true; } // $ambient [type] private bool ParseAmbient() { if(!SkipWhitespace(true)) return false; AmbientSoundInfo asi = new AmbientSoundInfo(); if(!asi.Setup(this)) return false; // Skip strange cases... if(asi.SoundName.StartsWith("*")) return true; // Check for duplicates if(ambientsounds.ContainsKey(asi.Index)) LogWarning("Ambient sound " + asi.Index + " is double-defined as \"" + ambientsounds[asi.Index].SoundName + "\" and \"" + asi.SoundName + "\""); // Add to collection ambientsounds[asi.Index] = asi; return true; } // $alias aliasname soundname private bool ParseAlias() { // Read aliasname if(!SkipWhitespace(true)) return false; string aliasname = StripTokenQuotes(ReadToken()); if(string.IsNullOrEmpty(aliasname)) return false; // Read soundname if(!SkipWhitespace(true)) return false; string soundname = StripTokenQuotes(ReadToken()); if(string.IsNullOrEmpty(soundname)) return false; SoundInfo info = GetSoundInfo(soundname); // Check for duplicates if(sounds.ContainsKey(aliasname)) LogWarning("$alias name \"" + aliasname + "\" is double-defined"); // Add to collection sounds[aliasname] = info; return true; } // $attenuation aliasname value private bool ParseAttenuation() { // Read aliasname if(!SkipWhitespace(true)) return false; string aliasname = StripTokenQuotes(ReadToken()); if(string.IsNullOrEmpty(aliasname)) return false; SoundInfo info = GetSoundInfo(aliasname); // Read value if(!SkipWhitespace(true)) return false; if(!ReadSignedFloat(ref info.Attenuation) || info.Attenuation < 0f) { ReportError("Expected $attenuation value"); return false; } return true; } // Needed because of optional parameter... // $limit soundname [limitdistance] private bool ParseLimit() { // Read soundname if(!SkipWhitespace(true)) return false; string soundname = StripTokenQuotes(ReadToken()); if(string.IsNullOrEmpty(soundname)) return false; // Must be if(!SkipWhitespace(false)) return false; int amount = 2; if(!ReadSignedInt(ref amount)) { ReportError("Expected $limit value"); return false; } // Can be [limitdistance] if(!SkipWhitespace(false)) return false; int limitdistance = 256; string next = ReadToken(false); if(!ReadSignedInt(next, ref limitdistance) || limitdistance < 0f) { // Rewind so this structure can be read again DataStream.Seek(-next.Length - 1, SeekOrigin.Current); } return true; } // Needed because of optional parameter... // $mididevice musicname device [parameter] private bool ParseMidiDevice() { // Read musicname if(!SkipWhitespace(true)) return false; string musicname = StripTokenQuotes(ReadToken()); if(string.IsNullOrEmpty(musicname)) return false; // Read device if(!SkipWhitespace(true)) return false; string device = StripTokenQuotes(ReadToken()); if(string.IsNullOrEmpty(device)) return false; // Try to read parameter if(!SkipWhitespace(true)) return false; string parameter = StripTokenQuotes(ReadToken()).ToLowerInvariant(); if(string.IsNullOrEmpty(parameter)) return false; HashSet validparams = new HashSet { "opl", "fluidsynth", "timidity", "wildmidy" }; if(!validparams.Contains(parameter)) { // Rewind so this structure can be read again DataStream.Seek(-parameter.Length - 1, SeekOrigin.Current); } return true; } // $rolloff soundname // $rolloff soundname private bool ParseRolloff() { // Read soundname if(!SkipWhitespace(true)) return false; string soundname = StripTokenQuotes(ReadToken()); SoundInfo info = GetSoundInfo(soundname); // Next token can be ... if(!SkipWhitespace(true)) return false; string token = ReadToken(false).ToLowerInvariant(); if(token == "custom" || token == "linear" || token == "log") { if(token == "linear") { // Must be pair if(!SkipWhitespace(false)) return false; if(!ReadSignedInt(ref info.MinimumDistance) || info.MinimumDistance < 0) { ReportError("Expected $rolloff linear value"); return false; } if(!SkipWhitespace(false)) return false; if(!ReadSignedInt(ref info.MaximumDistance) || info.MaximumDistance < 0) { ReportError("Expected $rolloff linear value"); return false; } } else if(token == "log") { // Must be pair if(!SkipWhitespace(false)) return false; if(!ReadSignedInt(ref info.MinimumDistance) || info.MinimumDistance < 0) { ReportError("Expected $rolloff log value"); return false; } if(!SkipWhitespace(false)) return false; if(!ReadSignedFloat(ref info.RolloffFactor) || info.RolloffFactor < 0f) { ReportError("Expected $rolloff log value"); return false; } } // Store type switch(token) { case "custom": info.Rolloff = SoundInfo.RolloffType.CUSTOM; break; case "linear": info.Rolloff = SoundInfo.RolloffType.LINEAR; break; case "log": info.Rolloff = SoundInfo.RolloffType.LOG; break; } } // Must be pair else { if(!ReadSignedInt(token, ref info.MinimumDistance) || info.MinimumDistance < 0) { ReportError("Expected $rolloff value"); return false; } if(!SkipWhitespace(false)) return false; if(!ReadSignedInt(ref info.MaximumDistance) || info.MaximumDistance < 0) { ReportError("Expected $rolloff value"); return false; } } return true; } // $volume soundname private bool ParseVolume() { // Read soundname if(!SkipWhitespace(true)) return false; string soundname = StripTokenQuotes(ReadToken()); if(string.IsNullOrEmpty(soundname)) return false; SoundInfo info = GetSoundInfo(soundname); // Read value if(!SkipWhitespace(true)) return false; if(!ReadSignedFloat(ref info.Volume) || info.Volume < 0f) { ReportError("Expected $volume value"); return false; } // Clamp it info.Volume = General.Clamp(info.Volume, 0.0f, 1.0f); return true; } // $random aliasname { logicalname1 logicalname2 logicalname3 ... } private bool ParseRandom() { // Read aliasname if(!SkipWhitespace(true)) return false; string aliasname = StripTokenQuotes(ReadToken()); if(string.IsNullOrEmpty(aliasname)) return false; SoundInfo info = GetSoundInfo(aliasname); // Must be opening brace if(!SkipWhitespace(true) || !NextTokenIs("{")) return false; // Read logicalnames List logicalnames = new List(); while(true) { if(!SkipWhitespace(true)) return false; string token = StripTokenQuotes(ReadToken()); if(string.IsNullOrEmpty(token) || token == "}") break; logicalnames.Add(token); } if(logicalnames.Count == 0) { ReportError("$random " + aliasname + " definition is empty"); return false; } if(logicalnames.Contains(aliasname)) { ReportError("$random " + aliasname + " references itself"); return false; } // Assign logicalnames info.Type = SoundInfo.SoundInfoType.GROUP_RANDOM; foreach(string name in logicalnames) { SoundInfo rinfo = GetSoundInfo(name); info.Children.Add(rinfo); } return true; } // Reads logicalname lumpname pair private bool ParseSoundAssignment(string logicalname) { // Check logicalname logicalname = StripTokenQuotes(logicalname); if(string.IsNullOrEmpty(logicalname)) return false; // Check sound assignment format and report an error if both the old and new format is used if ((format == SndInfoFormat.New && !NextTokenIs("=", false)) || (format == SndInfoFormat.Old && NextTokenIs("=", false))) { ReportError("Mixing old and new sound assignment format is not permitted"); return false; } else if(format == SndInfoFormat.None) { format = NextTokenIs("=", false) ? SndInfoFormat.New : SndInfoFormat.Old; } // Read lumpname if(!SkipWhitespace(true)) return false; string lumpname = StripTokenQuotes(ReadToken()); if(string.IsNullOrEmpty(lumpname)) return false; SoundInfo info = GetSoundInfo(logicalname); info.LumpName = lumpname; return true; } private bool SkipTokens(int count) { for(int i = 0; i < count; i++) { if(!SkipWhitespace(true)) return false; if(string.IsNullOrEmpty(ReadToken(false))) return false; } return true; } #endregion #region ================== Methods private SoundInfo GetSoundInfo(string soundname) { if(soundname == "*") return globalprops; if(!sounds.ContainsKey(soundname)) sounds[soundname] = new SoundInfo(soundname); return sounds[soundname]; } internal void FinishSetup() { // Check undefined sounds List toremove = new List(); foreach(SoundInfo sound in sounds.Values) { if(!IsValid(sound)) { if(sound.Type == SoundInfo.SoundInfoType.SOUND) General.ErrorLogger.Add(ErrorType.Warning, ScriptType + " warning: sound \"" + sound.Name + "\" is not defined."); toremove.Add(sound); } else { // Apply settings from the first child... if(sound.Type == SoundInfo.SoundInfoType.GROUP_RANDOM) { SoundInfo src = sound; do { src = src.Children[0]; }while(src.Type != SoundInfo.SoundInfoType.SOUND); if(src.Type == SoundInfo.SoundInfoType.SOUND) { sound.Volume = src.Volume; sound.Attenuation = src.Attenuation; sound.MinimumDistance = src.MinimumDistance; sound.MaximumDistance = src.MaximumDistance; sound.Rolloff = src.Rolloff; sound.RolloffFactor = src.RolloffFactor; } } // Apply global settings... SoundInfo defprops = new SoundInfo("#DEFAULT_PROPERTIES#"); if(sound.Volume == defprops.Volume) sound.Volume = globalprops.Volume; if(sound.Attenuation == defprops.Attenuation) sound.Attenuation = globalprops.Attenuation; if(sound.MinimumDistance == defprops.MinimumDistance) sound.MinimumDistance = globalprops.MinimumDistance; if(sound.MaximumDistance == defprops.MaximumDistance) sound.MaximumDistance = globalprops.MaximumDistance; if(sound.Rolloff == defprops.Rolloff) sound.Rolloff = globalprops.Rolloff; if(sound.RolloffFactor == defprops.RolloffFactor) sound.RolloffFactor = globalprops.RolloffFactor; } } // Connect SoundInfos to AmbientSoundInfos... foreach(AmbientSoundInfo info in ambientsounds.Values) { if(!sounds.ContainsKey(info.SoundName)) { General.ErrorLogger.Add(ErrorType.Warning, ScriptType + " warning: $ambient sound " + info.Index + " has undefined sound \"" + info.SoundName + "\"."); continue; } info.SetupSound(sounds[info.SoundName]); } // Remove invalid sounds foreach(SoundInfo info in toremove) sounds.Remove(info.Name); } private static bool IsValid(SoundInfo info) { switch(info.Type) { case SoundInfo.SoundInfoType.SOUND: return !string.IsNullOrEmpty(info.LumpName) || General.Map.Config.InternalSoundNames.Contains(info.Name); case SoundInfo.SoundInfoType.GROUP_RANDOM: foreach(SoundInfo child in info.Children) if(!IsValid(child)) return false; return true; default: throw new NotImplementedException("Unknown SoundInfoType"); } } #endregion } }