// TODO: Also create a wadmaker.config file, if the wad contained fonts or simple images (mipmap textures are the default behavior, so those don't need a config, // unless the user wants to create a wad file and wants different settings for those images such as different dithering, etc.) static void ExtractTextures(string inputFilePath, string outputDirectory, bool extractMipmaps, bool overwriteExistingFiles) { var stopwatch = Stopwatch.StartNew(); var imageFilesCreated = 0; var textures = new List <Texture>(); if (inputFilePath.EndsWith(".bsp")) { textures = Bsp.GetEmbeddedTextures(inputFilePath); } else { textures = Wad.Load(inputFilePath).Textures; } CreateDirectory(outputDirectory); var isDecalsWad = Path.GetFileName(inputFilePath).ToLowerInvariant() == "decals.wad"; foreach (var texture in textures) { var maxMipmap = extractMipmaps ? 4 : 1; for (int mipmap = 0; mipmap < maxMipmap; mipmap++) { try { var filePath = Path.Combine(outputDirectory, texture.Name + $"{(mipmap > 0 ? ".mipmap" + mipmap : "")}.png"); if (!overwriteExistingFiles && File.Exists(filePath)) { Log($"WARNING: {filePath} already exist. Skipping texture."); continue; } using (var image = isDecalsWad ? DecalTextureToImage(texture, mipmap) : TextureToImage(texture, mipmap)) { image.SaveAsPng(filePath); imageFilesCreated += 1; } } catch (Exception ex) { Log($"ERROR: failed to extract '{texture.Name}'{(mipmap > 0 ? $" (mipmap {mipmap})" : "")}: {ex.GetType().Name}: '{ex.Message}'."); } } } Log($"Extracted {imageFilesCreated} images from {textures.Count} textures from {inputFilePath} to {outputDirectory}, in {stopwatch.Elapsed.TotalSeconds:0.000} seconds."); }
static void MakeWad(string inputDirectory, string outputWadFilePath, bool fullRebuild, bool includeSubDirectories) { var stopwatch = Stopwatch.StartNew(); var texturesAdded = 0; var texturesUpdated = 0; var texturesRemoved = 0; var wadMakingSettings = WadMakingSettings.Load(inputDirectory); var updateExistingWad = !fullRebuild && File.Exists(outputWadFilePath); var wad = updateExistingWad ? Wad.Load(outputWadFilePath) : new Wad(); var lastWadUpdateTime = updateExistingWad ? new FileInfo(outputWadFilePath).LastWriteTimeUtc : (DateTime?)null; var wadTextureNames = wad.Textures.Select(texture => texture.Name.ToLowerInvariant()).ToHashSet(); var conversionOutputDirectory = ExternalConversion.GetConversionOutputDirectory(inputDirectory); var isDecalsWad = Path.GetFileNameWithoutExtension(outputWadFilePath).ToLowerInvariant() == "decals"; // Multiple files can map to the same texture, due to different extensions and upper/lower-case differences. // We'll group files by texture name, to make these collisions easy to detect: var allInputDirectoryFiles = Directory.EnumerateFiles(inputDirectory, "*", includeSubDirectories ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly) .Where(path => !ExternalConversion.IsConversionOutputDirectory(path)) .ToHashSet(); var textureImagePaths = allInputDirectoryFiles .Where(path => ImageReading.IsSupported(path) || wadMakingSettings.GetTextureSettings(Path.GetFileName(path)).settings.Converter != null) .Where(path => !path.Contains(".mipmap")) .Where(path => !WadMakingSettings.IsConfigurationFile(path)) .GroupBy(path => GetTextureName(path)); // Check for new and updated images: try { foreach (var imagePathsGroup in textureImagePaths) { var textureName = imagePathsGroup.Key; if (!IsValidTextureName(textureName)) { Log($"WARNING: '{textureName}' is not a valid texture name ({string.Join(", ", imagePathsGroup)}). Skipping file(s)."); continue; } else if (textureName.Length > 15) { Log($"WARNING: The name '{textureName}' is too long ({string.Join(", ", imagePathsGroup)}). Skipping file(s)."); continue; } else if (imagePathsGroup.Count() > 1) { Log($"WARNING: multiple input files detected for '{textureName}' ({string.Join(", ", imagePathsGroup)}). Skipping files."); continue; } // NOTE: Texture dimensions (which must be multiples of 16) are checked later, in CreateTextureFromImage. var filePath = imagePathsGroup.Single(); var isExistingImage = wadTextureNames.Contains(textureName.ToLowerInvariant()); var isSupportedFileType = ImageReading.IsSupported(filePath); // For files that are not directly supported, we'll include their extension when looking up conversion settings: (var textureSettings, var lastSettingsChangeTime) = wadMakingSettings.GetTextureSettings(isSupportedFileType ? textureName : Path.GetFileName(filePath)); if (isExistingImage && updateExistingWad) { // NOTE: A texture will not be rebuilt if one of its mipmap files has been removed. In order to detect such cases, // WadMaker would need to store additional bookkeeping data, but right now that doesn't seem worth the trouble. // NOTE: Mipmaps must have the same extension as the main image file. var isImageUpdated = GetMipmapFilePaths(filePath) .Prepend(filePath) .Where(allInputDirectoryFiles.Contains) .Select(path => new FileInfo(path).LastWriteTimeUtc) .Any(dateTime => dateTime > lastWadUpdateTime); if (!isImageUpdated && lastSettingsChangeTime < lastWadUpdateTime) { //Log($"No modifications detected for '{textureName}' ({filePath}). Skipping file."); continue; } } try { var imageFilePath = filePath; if (textureSettings.Converter != null) { if (textureSettings.ConverterArguments == null) { throw new InvalidDataException($"Unable to convert '{filePath}': missing converter arguments."); } imageFilePath = Path.Combine(conversionOutputDirectory, textureName); CreateDirectory(conversionOutputDirectory); var outputFilePaths = ExternalConversion.ExecuteConversionCommand(textureSettings.Converter, textureSettings.ConverterArguments, filePath, imageFilePath, Log); if (imageFilePath.Length < 1) { throw new IOException("Unable to find converter output file. An output file must have the same name as the input file (different extensions are ok)."); } var supportedOutputFilePaths = outputFilePaths.Where(ImageReading.IsSupported).ToArray(); if (supportedOutputFilePaths.Length < 1) { throw new IOException("The converter did not produce a supported file type."); } else if (supportedOutputFilePaths.Length > 1) { throw new IOException("The converted produced multiple supported file types. Only one output file should be created."); } imageFilePath = supportedOutputFilePaths[0]; } // Create texture from image: var texture = CreateTextureFromImage(imageFilePath, textureName, textureSettings, isDecalsWad); if (isExistingImage) { // Update (replace) existing texture: for (int i = 0; i < wad.Textures.Count; i++) { if (wad.Textures[i].Name == texture.Name) { wad.Textures[i] = texture; break; } } texturesUpdated += 1; Log($"Updated texture '{textureName}' (from '{filePath}')."); } else { // Add new texture: wad.Textures.Add(texture); wadTextureNames.Add(textureName); texturesAdded += 1; Log($"Added texture '{textureName}' (from '{filePath}')."); } } catch (Exception ex) { Log($"ERROR: failed to build '{filePath}': {ex.GetType().Name}: '{ex.Message}'."); } } if (updateExistingWad) { // Check for removed images: var directoryTextureNames = textureImagePaths .Select(group => group.Key) .ToHashSet(); foreach (var textureName in wadTextureNames) { if (!directoryTextureNames.Contains(textureName)) { // Delete texture: wad.Textures.Remove(wad.Textures.First(texture => texture.Name.ToLowerInvariant() == textureName)); texturesRemoved += 1; Log($"Removed texture '{textureName}'."); } } } // Finally, save the wad file: CreateDirectory(Path.GetDirectoryName(outputWadFilePath)); wad.Save(outputWadFilePath); } finally { try { if (Directory.Exists(conversionOutputDirectory)) { Directory.Delete(conversionOutputDirectory, true); } } catch (Exception ex) { Log($"WARNING: Failed to delete temporary conversion output directory: {ex.GetType().Name}: '{ex.Message}'."); } } if (updateExistingWad) { Log($"Updated {outputWadFilePath} from {inputDirectory}: added {texturesAdded}, updated {texturesUpdated} and removed {texturesRemoved} textures, in {stopwatch.Elapsed.TotalSeconds:0.000} seconds."); } else { Log($"Created {outputWadFilePath}, with {texturesAdded} textures from {inputDirectory}, in {stopwatch.Elapsed.TotalSeconds:0.000} seconds."); } }