#region ================== Copyright (c) 2007 Pascal vd Heiden /* * Copyright (c) 2007 Pascal vd Heiden, www.codeimp.com * This program is released under GNU General Public License * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * */ #endregion #region ================== Namespaces using System; using System.Collections.Generic; using System.IO; using CodeImp.DoomBuilder.Config; using CodeImp.DoomBuilder.ZDoom; #endregion namespace CodeImp.DoomBuilder.Data { internal abstract class PK3StructuredReader : DataReader { #region ================== Constants protected const string PATCHES_DIR = "patches"; protected const string TEXTURES_DIR = "textures"; protected const string FLATS_DIR = "flats"; protected const string HIRES_DIR = "hires"; protected const string SPRITES_DIR = "sprites"; protected const string COLORMAPS_DIR = "colormaps"; protected const string GRAPHICS_DIR = "graphics"; //mxd protected const string VOXELS_DIR = "voxels"; //mxd #endregion #region ================== Variables // Source protected readonly bool roottextures; protected readonly bool rootflats; // WAD files that must be loaded as well protected List wads; #endregion #region ================== Properties protected readonly string[] PatchLocations = { PATCHES_DIR, TEXTURES_DIR, FLATS_DIR, SPRITES_DIR, GRAPHICS_DIR }; //mxd. Because ZDoom looks for patches and sprites in this order internal List Wads { get { return wads; } } //mxd #endregion #region ================== Constructor / Disposer // Constructor protected PK3StructuredReader(DataLocation dl, bool asreadonly) : base(dl, asreadonly) { // Initialize this.roottextures = dl.option1; this.rootflats = dl.option2; } // Call this to initialize this class protected virtual void Initialize() { // [ZZ] we can have wad files already. dispose if any. if (wads != null) foreach (WADReader wr in wads) wr.Dispose(); // Load all WAD files in the root as WAD resources string[] wadfiles = GetWadFiles(); wads = new List(wadfiles.Length); foreach(string w in wadfiles) { string tempfile = CreateTempFile(w); DataLocation wdl = new DataLocation(DataLocation.RESOURCE_WAD, tempfile, Path.Combine(location.GetDisplayName(), Path.GetFileName(w)), false, false, true); wads.Add(new WADReader(wdl, location.type != DataLocation.RESOURCE_DIRECTORY) { ParentResource = this } ); } } // Disposer public override void Dispose() { // Not already disposed? if(!isdisposed) { // Clean up foreach(WADReader wr in wads) wr.Dispose(); // Done base.Dispose(); } } #endregion #region ================== Management // This suspends use of this resource public override void Suspend() { foreach(WADReader wr in wads) wr.Suspend(); base.Suspend(); } // This resumes use of this resource public override void Resume() { foreach(WADReader wr in wads) wr.Resume(); base.Resume(); } #endregion #region ================== Palette // This loads the PLAYPAL palette public override Playpal LoadPalette() { // Error when suspended if(issuspended) throw new Exception("Data reader is suspended"); // Palette from wad(s) Playpal palette = null; foreach(WADReader wr in wads) { Playpal wadpalette = wr.LoadPalette(); if(wadpalette != null) return wadpalette; } // Find in root directory string foundfile = FindFirstFile("PLAYPAL", false); if((foundfile != null) && FileExists(foundfile)) { MemoryStream stream = LoadFile(foundfile); if(stream.Length > 767) //mxd palette = new Playpal(stream); else General.ErrorLogger.Add(ErrorType.Warning, "Warning: invalid palette \"" + foundfile + "\""); stream.Dispose(); } // Done return palette; } #endregion #region ================== Textures // This loads the textures public override IEnumerable LoadTextures(PatchNames pnames, Dictionary cachedparsers) { // Error when suspended if(issuspended) throw new Exception("Data reader is suspended"); Dictionary images = new Dictionary(); IEnumerable collection; // Load from wad files (NOTE: backward order, because the last wad's images have priority) for(int i = wads.Count - 1; i >= 0; i--) { PatchNames wadpnames = wads[i].LoadPatchNames(); //mxd collection = wads[i].LoadTextures((wadpnames != null && wadpnames.Length > 0) ? wadpnames : pnames, cachedparsers); //mxd AddImagesToList(images, collection); } // Should we load the images in this directory as textures? if(roottextures) { collection = LoadDirectoryImages("", ImageDataFormat.DOOMPICTURE, false); AddImagesToList(images, collection); } // Load TEXTURE1 lump file List imgset = new List(); string texture1file = FindFirstFile("TEXTURE1", false); if((texture1file != null) && FileExists(texture1file)) { MemoryStream filedata = LoadFile(texture1file); WADReader.LoadTextureSet("TEXTURE1", filedata, ref imgset, pnames); filedata.Dispose(); } // Load TEXTURE2 lump file string texture2file = FindFirstFile("TEXTURE2", false); if((texture2file != null) && FileExists(texture2file)) { MemoryStream filedata = LoadFile(texture2file); WADReader.LoadTextureSet("TEXTURE2", filedata, ref imgset, pnames); filedata.Dispose(); } // Add images from TEXTURE1 and TEXTURE2 lump files AddImagesToList(images, imgset); // Load TEXTURES lump files imgset.Clear(); string[] alltexturefiles = GetAllFilesWhichTitleStartsWith("", "TEXTURES", false); //mxd foreach(string texturesfile in alltexturefiles) { //mxd. Added TexturesParser caching string fullpath = Path.Combine(this.location.location, texturesfile); if(cachedparsers.ContainsKey(fullpath)) { // Make the textures foreach(TextureStructure t in cachedparsers[fullpath].Textures) imgset.Add(t.MakeImage()); } else { MemoryStream filedata = LoadFile(texturesfile); TextResourceData data = new TextResourceData(this, filedata, texturesfile, true); //mxd cachedparsers.Add(fullpath, WADReader.LoadTEXTURESTextures(data, ref imgset)); //mxd filedata.Dispose(); } } // Add images from TEXTURES lump file AddImagesToList(images, imgset); //mxd. Add images from texture directory. Textures defined in TEXTURES override ones in "textures" folder collection = LoadDirectoryImages(TEXTURES_DIR, ImageDataFormat.DOOMPICTURE, true); AddImagesToList(images, collection); // Add images to the container-specific texture set foreach(ImageData img in images.Values) textureset.AddTexture(img); return new List(images.Values); } //mxd public override IEnumerable LoadHiResTextures() { // Go for all files string[] files = GetAllFiles(HIRES_DIR, true); List result = new List(files.Length); foreach(string f in files) { if(string.IsNullOrEmpty(Path.GetFileNameWithoutExtension(f))) { // Can't load image without name General.ErrorLogger.Add(ErrorType.Error, "Can't load an unnamed HiRes texture from \"" + Path.Combine(this.location.GetDisplayName(), HIRES_DIR) + "\". Please consider giving names to your resources."); } else { // Add image to list result.Add(new HiResImage(f)); } } return result; } // This returns the patch names from the PNAMES lump // A directory resource does not support this lump, but the wads in the directory may contain this lump public override PatchNames LoadPatchNames() { PatchNames pnames; // Error when suspended if(issuspended) throw new Exception("Data reader is suspended"); // Load from wad files // Note the backward order, because the last wad's images have priority for(int i = wads.Count - 1; i >= 0; i--) { pnames = wads[i].LoadPatchNames(); if(pnames != null) return pnames; } // If none of the wads provides patch names, let's see if we can string pnamesfile = FindFirstFile("PNAMES", false); if((pnamesfile != null) && FileExists(pnamesfile)) { MemoryStream pnamesdata = LoadFile(pnamesfile); pnames = new PatchNames(pnamesdata); pnamesdata.Dispose(); return pnames; } return null; } #endregion #region ================== Flats // This loads the textures public override IEnumerable LoadFlats(Dictionary cachedparsers) { Dictionary images = new Dictionary(); IEnumerable collection; List imgset = new List(); // Error when suspended if(issuspended) throw new Exception("Data reader is suspended"); // Load from wad files // Note the backward order, because the last wad's images have priority for(int i = wads.Count - 1; i >= 0; i--) { collection = wads[i].LoadFlats(cachedparsers); AddImagesToList(images, collection); } // Should we load the images in this directory as flats? if(rootflats) { collection = LoadDirectoryImages("", ImageDataFormat.DOOMFLAT, false); AddImagesToList(images, collection); } // Add images from flats directory collection = LoadDirectoryImages(FLATS_DIR, ImageDataFormat.DOOMFLAT, true); AddImagesToList(images, collection); // Load TEXTURES lump file string[] alltexturefiles = GetAllFilesWhichTitleStartsWith("", "TEXTURES", false); //mxd foreach(string texturesfile in alltexturefiles) { //mxd. Added TexturesParser caching string fullpath = Path.Combine(this.location.location, texturesfile); if(cachedparsers.ContainsKey(fullpath)) { // Make the textures foreach(TextureStructure t in cachedparsers[fullpath].Flats) imgset.Add(t.MakeImage()); } else { MemoryStream filedata = LoadFile(texturesfile); TextResourceData data = new TextResourceData(this, filedata, texturesfile, true); //mxd cachedparsers.Add(fullpath, WADReader.LoadTEXTURESFlats(data, ref imgset)); //mxd filedata.Dispose(); } } // Add images from TEXTURES lump file AddImagesToList(images, imgset); // Add images to the container-specific texture set foreach(ImageData img in images.Values) textureset.AddFlat(img); return new List(images.Values); } //mxd. public override Stream GetFlatData(string pname, bool longname, ref string flatlocation) { // Error when suspended if(issuspended) throw new Exception("Data reader is suspended"); // Find in any of the wad files // Note the backward order, because the last wad's images have priority if(!longname) //mxd. Flats with long names can't be in wads { for(int i = wads.Count - 1; i > -1; i--) { Stream data = wads[i].GetFlatData(pname, false, ref flatlocation); if(data != null) return data; } } // Nothing found return null; } #endregion #region ================== Sprites // This loads the sprites public override IEnumerable LoadSprites(Dictionary cachedparsers) { // Error when suspended if(issuspended) throw new Exception("Data reader is suspended"); Dictionary images = new Dictionary(); List imgset = new List(); // Load from wad files // Note the backward order, because the last wad's images have priority for(int i = wads.Count - 1; i >= 0; i--) { IEnumerable collection = wads[i].LoadSprites(cachedparsers); AddImagesToList(images, collection); } // Load TEXTURES lump file imgset.Clear(); string[] alltexturefiles = GetAllFilesWhichTitleStartsWith("", "TEXTURES", false); //mxd foreach(string texturesfile in alltexturefiles) { //mxd. Added TexturesParser caching string fullpath = Path.Combine(this.location.location, texturesfile); if(cachedparsers.ContainsKey(fullpath)) { // Make the textures foreach(TextureStructure t in cachedparsers[fullpath].Sprites) imgset.Add(t.MakeImage()); } else { MemoryStream filedata = LoadFile(texturesfile); TextResourceData data = new TextResourceData(this, filedata, texturesfile, true); //mxd cachedparsers.Add(fullpath, WADReader.LoadTEXTURESSprites(data, ref imgset)); //mxd filedata.Dispose(); } } // Add images from TEXTURES lump file AddImagesToList(images, imgset); return new List(images.Values); } //mxd. This returns all sprite names public override HashSet GetSpriteNames() { // Error when suspended if(issuspended) throw new Exception("Data reader is suspended"); HashSet result = new HashSet(); // Load from wad files // Note the backward order, because the last wad's images have priority for(int i = wads.Count - 1; i >= 0; i--) { result.UnionWith(wads[i].GetSpriteNames()); } // Load from out own files string[] files = GetAllFiles(SPRITES_DIR, true); foreach(string file in files) { // Some users tend to place all manner of graphics into the "Sprites" folder... string spritename = Path.GetFileNameWithoutExtension(file).ToUpperInvariant(); if(WADReader.IsValidSpriteName(spritename)) result.Add(spritename); } return result; } #endregion #region ================== Colormaps // This loads the textures public override ICollection LoadColormaps() { // Error when suspended if(issuspended) throw new Exception("Data reader is suspended"); Dictionary images = new Dictionary(); ICollection collection; // Load from wad files // Note the backward order, because the last wad's images have priority for(int i = wads.Count - 1; i >= 0; i--) { collection = wads[i].LoadColormaps(); AddImagesToList(images, collection); } // Add images from flats directory collection = LoadDirectoryImages(COLORMAPS_DIR, ImageDataFormat.DOOMCOLORMAP, true); AddImagesToList(images, collection); // Add images to the container-specific texture set foreach(ImageData img in images.Values) textureset.AddFlat(img); return new List(images.Values); } #endregion #region ================== DEHACKED // This finds and returns DEHACKED streams public override IEnumerable GetDehackedData(string pname) { // Error when suspended if (issuspended) throw new Exception("Data reader is suspended"); List result = new List(); string[] allfilenames; // Find in root directory string filename = Path.GetFileName(pname); string pathname = Path.GetDirectoryName(pname); if (filename.IndexOf('.') > -1) allfilenames = GetFileAtPath(filename, pathname, "DEHACKED"); else allfilenames = GetAllFilesWithTitle(pathname, filename, false); result.Add(new TextResourceData(this, LoadFile(allfilenames[allfilenames.Length - 1]), allfilenames[allfilenames.Length - 1], true)); return result; } #endregion #region ================== DECORATE // This finds and returns DECORATE streams public override IEnumerable GetDecorateData(string pname) { // Error when suspended if(issuspended) throw new Exception("Data reader is suspended"); List result = new List(); string[] allfilenames; // Find in root directory string filename = Path.GetFileName(pname); string pathname = Path.GetDirectoryName(pname); if(filename.IndexOf('.') > -1) { allfilenames = GetFileAtPath(filename, pathname, "DECORATE"); } else allfilenames = GetAllFilesWithTitle(pathname, filename, false); foreach(string foundfile in allfilenames) result.Add(new TextResourceData(this, LoadFile(foundfile), foundfile, true)); // Find in any of the wad files for(int i = wads.Count - 1; i >= 0; i--) result.AddRange(wads[i].GetDecorateData(pname)); return result; } #endregion #region ================== ZSCRIPT // This finds and returns ZSCRIPT streams public override IEnumerable GetZScriptData(string pname) { // Error when suspended if (issuspended) throw new Exception("Data reader is suspended"); List result = new List(); string[] allfilenames; // Find in root directory string filename = Path.GetFileName(pname); string pathname = Path.GetDirectoryName(pname); if (filename.IndexOf('.') > -1) { allfilenames = GetFileAtPath(filename, pathname, "ZSCRIPT"); } else allfilenames = GetAllFilesWithTitle(pathname, filename, false); foreach (string foundfile in allfilenames) result.Add(new TextResourceData(this, LoadFile(foundfile), foundfile, true)); // Find in any of the wad files for (int i = wads.Count - 1; i >= 0; i--) result.AddRange(wads[i].GetZScriptData(pname)); return result; } #endregion #region ================== MODELDEF // This finds and returns MODELDEF streams public override IEnumerable GetModeldefData(string pname) { // Error when suspended if (issuspended) throw new Exception("Data reader is suspended"); List result = new List(); string[] allfilenames; // Find in root directory string filename = Path.GetFileName(pname); string pathname = Path.GetDirectoryName(pname); if (filename.IndexOf('.') > -1) { allfilenames = GetFileAtPath(filename, pathname, "MODELDEF"); } else allfilenames = GetAllFilesWithTitle(pathname, filename, false); foreach (string foundfile in allfilenames) result.Add(new TextResourceData(this, LoadFile(foundfile), foundfile, true)); // Find in any of the wad files for (int i = wads.Count - 1; i >= 0; i--) result.AddRange(wads[i].GetModeldefData(pname)); return result; } #endregion #region ================== VOXELDEF (mxd) //mxd. This returns the list of voxels, which can be used without VOXELDEF definition public override HashSet GetVoxelNames() { // Error when suspended if(issuspended) throw new Exception("Data reader is suspended"); HashSet result = new HashSet(); // Load from wad files // Note the backward order, because the last wad's images have priority for(int i = wads.Count - 1; i >= 0; i--) { result.UnionWith(wads[i].GetVoxelNames()); } // Load from out own files string[] files = GetAllFiles("voxels", false); foreach(string t in files) { string s = Path.GetFileNameWithoutExtension(t).ToUpperInvariant(); if(WADReader.IsValidVoxelName(s)) result.Add(s); } return result; } #endregion #region ================== (Z)MAPINFO (mxd) //mxd public override IEnumerable GetMapinfoData() { // Error when suspended if(issuspended) throw new Exception("Data reader is suspended"); // Mapinfo should be in root folder List result = new List(); // Try to find (z)mapinfo string[] files = GetAllFilesWithTitle("", "ZMAPINFO", false); if(files.Length == 0) files = GetAllFilesWithTitle("", "MAPINFO", false); // Add to collection foreach(string s in files) result.Add(new TextResourceData(this, LoadFile(s), s, true)); // Find in any of the wad files foreach(WADReader wr in wads) result.AddRange(wr.GetMapinfoData()); return result; } #endregion #region ================== GLDEFS (mxd) //mxd public override IEnumerable GetGldefsData(string basegame) { // Error when suspended if(issuspended) throw new Exception("Data reader is suspended"); List result = new List(); // At least one of gldefs should be in the root folder List files = new List(); // Try to load game specific GLDEFS first if(basegame != GameType.UNKNOWN) { string lumpname = GameType.GldefsLumpsPerGame[basegame]; files.AddRange(GetAllFilesWhichTitleStartsWith("", lumpname, false)); } // Can be several entries files.AddRange(GetAllFilesWhichTitleStartsWith("", "GLDEFS", false)); // Add to collection foreach(string s in files) result.Add(new TextResourceData(this, LoadFile(s), s, true)); // Find in any of the wad files foreach(WADReader wr in wads) result.AddRange(wr.GetGldefsData(basegame)); return result; } #endregion #region ================== Generic text lumps loading (mxd) public override IEnumerable GetTextLumpData(ScriptType scripttype, bool singular, bool partialtitlematch) { // Error when suspended if(issuspended) throw new Exception("Data reader is suspended"); List result = new List(); List files = new List(); string lumpname = Enum.GetName(typeof(ScriptType), scripttype).ToUpperInvariant(); if(singular) { string file = FindFirstFile(lumpname, false); if(!string.IsNullOrEmpty(file)) files.Add(file); } else { files = new List(partialtitlematch ? GetAllFilesWhichTitleStartsWith("", lumpname, false) : GetAllFilesWithTitle("", lumpname, false)); } // Add to collection foreach(string s in files) result.Add(new TextResourceData(this, LoadFile(s), s, true)); // Find in any of the wad files foreach(WADReader wr in wads) result.AddRange(wr.GetTextLumpData(scripttype, singular, partialtitlematch)); return result; } #endregion #region ================== Methods // This loads the images in this directory private ICollection LoadDirectoryImages(string path, int imagetype, bool includesubdirs) { List images = new List(); // Go for all files string[] files = GetAllFiles(path, includesubdirs); foreach(string f in files) { if(string.IsNullOrEmpty(Path.GetFileNameWithoutExtension(f))) { // Can't load image without name General.ErrorLogger.Add(ErrorType.Error, "Can't load an unnamed texture from \"" + path + "\". Please consider giving names to your resources."); } else { // Add image to list images.Add(CreateImage(f, imagetype)); } } // Return result return images; } /// /// Gets a correctly cased file from a path/file. This is required for case sensitive file systems. /// /// File name without path /// Path to the file /// Type of file (i.e. everything before the first dot) /// Array with one element on success, array with no elements on failure protected string[] GetFileAtPath(string filename, string pathname, string type) { string fullname = Path.Combine(pathname, filename); if (FileExists(fullname)) { return new string[1] { fullname }; } else { General.ErrorLogger.Add(ErrorType.Warning, "Unable to load " + type + " file \"" + fullname + "\""); return new string[0]; } } // This copies images from a collection unless they already exist in the list private static void AddImagesToList(Dictionary targetlist, IEnumerable sourcelist) { // Go for all source images foreach(ImageData src in sourcelist) { // Check if exists in target list if(!targetlist.ContainsKey(src.LongName)) targetlist.Add(src.LongName, src); } } // This must create an image protected abstract ImageData CreateImage(string filename, int imagetype); // This must return all files in a given directory protected abstract string[] GetAllFiles(string path, bool subfolders); // This must return all files in a given directory that have the given file title protected abstract string[] GetAllFilesWithTitle(string path, string title, bool subfolders); //mxd. This must return all files in a given directory which title starts with given title protected abstract string[] GetAllFilesWhichTitleStartsWith(string path, string title, bool subfolders); // This must return all files in a given directory that match the given extension internal abstract string[] GetFilesWithExt(string path, string extension, bool subfolders); //mxd. This must return wad files in the root directory protected abstract string[] GetWadFiles(); // This must find the first file that has the specific name, regardless of file extension internal abstract string FindFirstFile(string beginswith, bool subfolders); // This must find the first file that has the specific name, regardless of file extension protected abstract string FindFirstFile(string path, string beginswith, bool subfolders); // This must find the first file that has the specific name protected abstract string FindFirstFileWithExt(string path, string beginswith, bool subfolders); // This must create a temp file for the speciied file and return the absolute path to the temp file // NOTE: Callers are responsible for removing the temp file when done! protected abstract string CreateTempFile(string filename); // This makes the path relative to the directory, if needed protected virtual string MakeRelativePath(string anypath) { if(Path.IsPathRooted(anypath)) { // Make relative string lowpath = anypath.ToLowerInvariant(); string lowlocation = location.location.ToLowerInvariant(); if((lowpath.Length > (lowlocation.Length + 1)) && lowpath.StartsWith(lowlocation)) return anypath.Substring(lowlocation.Length + 1); else return anypath; } else { // Path is already relative return anypath; } } /// /// Returns the correctly cased file from a path/file. This is required for case sensitive file systems. For PK3s the input will already have the correct case. /// /// File name get the the correctly cased name from /// protected virtual string GetCorrectCaseForFile(string filepathname) { return filepathname; } //mxd. Archives and Folders don't have lump indices internal override MemoryStream LoadFile(string name, int unused) { return LoadFile(name); } #endregion } }