#region ================== Namespaces using System; using System.Collections.Generic; using System.Drawing; using System.Globalization; using System.IO; using SlimDX; using CodeImp.DoomBuilder.ZDoom; using CodeImp.DoomBuilder.GZBuilder.Data; #endregion namespace CodeImp.DoomBuilder.GZBuilder.GZDoom { internal sealed class MapinfoParser : ZDTextParser { #region ================== Delegates public delegate void IncludeDelegate(MapinfoParser parser, string includefile, bool clearerror); public IncludeDelegate OnInclude; #endregion #region ================== Variables private MapInfo mapinfo; private string mapname; private readonly Dictionary spawnnums; private readonly Dictionary doomednums; // > private readonly HashSet parsedlumps; #endregion #region ================== Properties public MapInfo MapInfo { get { return mapinfo; } } public Dictionary SpawnNums { get { return spawnnums; } } public Dictionary DoomEdNums { get { return doomednums; } } #endregion #region ================== Constructor public MapinfoParser() { // Syntax whitespace = "\n \t\r\u00A0"; specialtokens = ",{}=\n"; mapinfo = new MapInfo(); spawnnums = new Dictionary(); doomednums = new Dictionary(); parsedlumps = new HashSet(StringComparer.OrdinalIgnoreCase); } #endregion #region ================== Parsing override public bool Parse(Stream stream, string sourcefilename, bool clearerrors) { if(string.IsNullOrEmpty(mapname)) throw new NotSupportedException("MapName is required!"); return Parse(stream, sourcefilename, mapname, clearerrors); } public bool Parse(Stream stream, string sourcefilename, string mapname, bool clearerrors) { this.mapname = mapname.ToLowerInvariant(); if(!base.Parse(stream, sourcefilename, clearerrors)) return false; while(SkipWhitespace(true)) { string token = ReadToken(); if(!string.IsNullOrEmpty(token)) { token = token.ToLowerInvariant(); if(ParseBlock(token, clearerrors)) break; } } //check values if(mapinfo.FadeColor.Red > 0 || mapinfo.FadeColor.Green > 0 || mapinfo.FadeColor.Blue > 0) mapinfo.HasFadeColor = true; if(mapinfo.OutsideFogColor.Red > 0 || mapinfo.OutsideFogColor.Green > 0 || mapinfo.OutsideFogColor.Blue > 0) mapinfo.HasOutsideFogColor = true; //Cannot fail here return true; } //returns true if parsing is finished private bool ParseBlock(string token, bool clearerrors) { // Keep local data Stream localstream = datastream; string localsourcename = sourcename; BinaryReader localreader = datareader; if(token == "map" || token == "defaultmap" || token == "adddefaultmap") { switch(token) { case "map": //get map name SkipWhitespace(true); token = ReadToken().ToLowerInvariant(); if(token != mapname) return false; //not finished, search for next "map", "defaultmap" or "adddefaultmap" block break; case "defaultmap": //reset MapInfo mapinfo = new MapInfo(); break; } // Track brace level int bracelevel = 0; //search for required keys while(SkipWhitespace(true)) { token = ReadToken().ToLowerInvariant(); //sky1 or sky2 if(token == "sky1" || token == "sky2") { string skyType = token; SkipWhitespace(true); token = StripTokenQuotes(ReadToken()).ToLowerInvariant(); //new format if(token == "=") { SkipWhitespace(true); //should be sky texture name token = StripTokenQuotes(ReadToken()); bool gotComma = (token.IndexOf(",", StringComparison.Ordinal) != -1); if(gotComma) token = token.Replace(",", ""); string skyTexture = StripTokenQuotes(token).ToLowerInvariant(); if(!string.IsNullOrEmpty(skyTexture)) { if(skyType == "sky1") mapinfo.Sky1 = skyTexture; else mapinfo.Sky2 = skyTexture; //check if we have scrollspeed SkipWhitespace(true); token = StripTokenQuotes(ReadToken()); if(!gotComma && token == ",") { gotComma = true; SkipWhitespace(true); token = ReadToken(); } if(gotComma) { float scrollSpeed = 0; if(!ReadSignedFloat(token, ref scrollSpeed)) { // Not numeric! ReportError("Expected " + skyType + " scroll speed value, but got '" + token + "'"); return false; } if(skyType == "sky1") mapinfo.Sky1ScrollSpeed = scrollSpeed; else mapinfo.Sky2ScrollSpeed = scrollSpeed; } else { datastream.Seek(-token.Length - 1, SeekOrigin.Current); //step back and try parsing this token again } } else { ReportError("Expected " + skyType + " texture name"); return false; } } //old format else { //token should be sky1/2 name if(!string.IsNullOrEmpty(token)) { if(skyType == "sky1") mapinfo.Sky1 = token; else mapinfo.Sky2 = token; //try to read scroll speed SkipWhitespace(true); token = StripTokenQuotes(ReadToken()); float scrollSpeed = 0; if(!ReadSignedFloat(token, ref scrollSpeed)) { // Not numeric! datastream.Seek(-token.Length - 1, SeekOrigin.Current); //step back and try parsing this token again continue; } if(skyType == "sky1") mapinfo.Sky1ScrollSpeed = scrollSpeed; else mapinfo.Sky2ScrollSpeed = scrollSpeed; } else { ReportError("Expected " + skyType + " texture name"); return false; } } } //fade or outsidefog else if(token == "fade" || token == "outsidefog") { string fadeType = token; SkipWhitespace(true); token = StripTokenQuotes(ReadToken()).ToLowerInvariant(); //new format? if(token == "=") { SkipWhitespace(true); token = ReadToken(); } //get the color value string colorVal = StripTokenQuotes(token).ToLowerInvariant().Replace(" ", ""); if(!string.IsNullOrEmpty(colorVal)) { Color4 color = new Color4(); //try to get the color... if(GetColor(colorVal, ref color)) { if(fadeType == "fade") mapinfo.FadeColor = color; else mapinfo.OutsideFogColor = color; } else //...or not { ReportError("Failed to parse " + fadeType + " value from string '" + colorVal + "'"); return false; } } else { ReportError("Expected " + fadeType + " color value"); return false; } } //vertwallshade or horizwallshade else if(token == "vertwallshade" || token == "horizwallshade") { string shadeType = token; SkipWhitespace(true); token = StripTokenQuotes(ReadToken()); //new format if(token == "=") { SkipWhitespace(true); token = StripTokenQuotes(ReadToken()); } int val = 0; if(!ReadSignedInt(token, ref val)) { // Not numeric! ReportError("Expected " + shadeType + " value, but got '" + token + "'"); return false; } if(shadeType == "vertwallshade") mapinfo.VertWallShade = General.Clamp(val, -255, 255); else mapinfo.HorizWallShade = General.Clamp(val, -255, 255); } //fogdensity or outsidefogdensity else if(token == "fogdensity" || token == "outsidefogdensity") { string densityType = token; SkipWhitespace(true); token = StripTokenQuotes(ReadToken()); //new format if(token == "=") { SkipWhitespace(true); token = StripTokenQuotes(ReadToken()); } int val; if(!int.TryParse(token, NumberStyles.Integer, CultureInfo.InvariantCulture, out val)) { // Not numeric! ReportError("Expected " + densityType + " value, but got '" + token + "'"); return false; } if(densityType == "fogdensity") mapinfo.FogDensity = (int)(1024 * (256.0f / val)); else mapinfo.OutsideFogDensity = (int)(1024 * (256.0f / val)); } //doublesky else if(token == "doublesky") { mapinfo.DoubleSky = true; } //evenlighting else if(token == "evenlighting") { mapinfo.EvenLighting = true; } //smoothlighting else if(token == "smoothlighting") { mapinfo.SmoothLighting = true; } //block end else if(token == "}") { return ParseBlock(token, clearerrors); } //child block else if(token == "{") { // Skip inner properties bracelevel++; if(bracelevel > 1) { do { SkipWhitespace(true); token = ReadToken(); if(token == "{") bracelevel++; else if(token == "}") bracelevel--; } while(!string.IsNullOrEmpty(token) && bracelevel > 1); } } } } else if(token == "include") // It's "include", not "#include". Cause fuck consistency. { SkipWhitespace(true); string includelump = StripTokenQuotes(ReadToken(false)); // Don't skip newline //INFO: ZDoom MAPINFO include paths can't be relative ("../mapstuff.txt") //or absolute ("d:/project/mapstuff.txt") //or have backward slashes ("info\mapstuff.txt") //include paths are relative to the first parsed entry, not the current one //also include paths may or may not be quoted if(!string.IsNullOrEmpty(includelump)) { // Absolute paths are not supported... if(Path.IsPathRooted(includelump)) { ReportError("Absolute include paths are not supported by ZDoom"); return false; } // Relative paths are not supported if(includelump.StartsWith(RELATIVE_PATH_MARKER) || includelump.StartsWith(CURRENT_FOLDER_PATH_MARKER) || includelump.StartsWith(ALT_RELATIVE_PATH_MARKER) || includelump.StartsWith(ALT_CURRENT_FOLDER_PATH_MARKER)) { ReportError("Relative include paths are not supported by ZDoom"); return false; } // Backward slashes are not supported if(includelump.Contains(Path.DirectorySeparatorChar.ToString(CultureInfo.InvariantCulture))) { ReportError("Only forward slashes are supported by ZDoom"); return false; } // Already parsed? if(parsedlumps.Contains(includelump)) { ReportError("Already parsed '" + includelump + "'. Check your include directives"); return false; } // Add to collection parsedlumps.Add(includelump); // Callback to parse this file if(OnInclude != null) OnInclude(this, includelump, clearerrors); // Bail out on error if(this.HasError) return false; // Set our buffers back to continue parsing datastream = localstream; datareader = localreader; sourcename = localsourcename; } else { ReportError("Expected filename to include"); return false; } } else if(token == "gameinfo") { if(!NextTokenIs("{")) return false; // Finished with this file while(SkipWhitespace(true)) { token = ReadToken(); if(string.IsNullOrEmpty(token)) { ReportError("Failed to find the end of GameInfo block"); return false; // Finished with this file } if(token == "}") break; if(token == "skyflatname") { if(!NextTokenIs("=")) return false; // Finished with this file SkipWhitespace(true); string skyflatname = StripTokenQuotes(ReadToken()); if(string.IsNullOrEmpty(skyflatname)) { ReportError("Unable to get SkyFlatName value"); return false; // Finished with this file } General.Map.Config.SkyFlatName = skyflatname.ToUpperInvariant(); } } } else if(token == "doomednums") { if(!NextTokenIs("{")) return false; // Finished with this file while(SkipWhitespace(true)) { token = ReadToken(); if(string.IsNullOrEmpty(token)) { ReportError("Failed to find the end of DoomEdNums block"); return false; // Finished with this file } if(token == "}") break; // First must be a number int id; if(!int.TryParse(token, NumberStyles.Integer, CultureInfo.InvariantCulture, out id)) { // Not numeric! ReportError("Expected DoomEdNums entry number, but got '" + token + "'"); return false; // Finished with this file } // Then "=" if(!NextTokenIs("=")) return false; // Finished with this file // Then actor class SkipWhitespace(false); string classname = StripTokenQuotes(ReadToken()); if(string.IsNullOrEmpty(classname)) { ReportError("Unable to get DoomEdNums entry class definition"); return false; // Finished with this file } // Possible special and args. We'll skip them for(int i = 0; i < 6; i++) { if(!NextTokenIs(",", false)) break; // Read special name or arg value if(!SkipWhitespace(true) || string.IsNullOrEmpty(ReadToken())) return false; } // Add to collection? if(id != 0) doomednums[id] = classname.ToLowerInvariant(); } } else if(token == "spawnnums") { if(!NextTokenIs("{")) return false; // Finished with this file while(SkipWhitespace(true)) { token = ReadToken(); if(string.IsNullOrEmpty(token)) { ReportError("Failed to find the end of SpawnNums block"); return false; // Finished with this file } if(token == "}") break; // First must be a number int id; if(!int.TryParse(token, NumberStyles.Integer, CultureInfo.InvariantCulture, out id)) { // Not numeric! ReportError("Expected SpawnNums number, but got '" + token + "'"); return false; // Finished with this file } // Then "=" if(!NextTokenIs("=")) return false; // Finished with this file // Then actor class SkipWhitespace(false); token = StripTokenQuotes(ReadToken()); if(string.IsNullOrEmpty(token)) { ReportError("Unable to get SpawnNums entry class definition"); return false; } // Add to collection spawnnums[id] = token.ToLowerInvariant(); } } else if(token == "$gzdb_skip") { return true; // Finished with this file } return false; // Not done yet } #endregion #region ================== Methods private static bool GetColor(string name, ref Color4 color) { if(name == "black") return true; //probably it's a hex color (like FFCC11)? int ci; if(int.TryParse(name, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out ci)) { color = new Color4(ci) {Alpha = 1.0f}; return true; } //probably it's a color name? Color c = Color.FromName(name); //should be similar to C++ color name detection, I suppose if(c.IsKnownColor) { color = new Color4(c); return true; } return false; } protected override string GetLanguageType() { return "MAPINFO"; } #endregion } }