Settings.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.");
        }


        // Wad extraction:
        static Image<Rgba32> DecalTextureToImage(Texture texture, int mipmap = 0)
        {
            var width = texture.Width >> mipmap;
            var height = texture.Height >> mipmap;
            var imageData = texture.GetImageData(mipmap);
            var decalColor = texture.Palette[255];

            var image = new Image<Rgba32>(width, height);
            for (int y = 0; y < image.Height; y++)
            {
                var rowSpan = image.GetPixelRowSpan(y);
                for (int x = 0; x < image.Width; x++)
                {
                    var paletteIndex = imageData[y * width + x];
                    rowSpan[x] = new Rgba32(decalColor.R, decalColor.G, decalColor.B, paletteIndex);
                }
            }

            return image;
        }

        static Image<Rgba32> TextureToImage(Texture texture, int mipmap = 0)
        {
            var width = texture.Width >> mipmap;
            var height = texture.Height >> mipmap;
            var imageData = texture.GetImageData(mipmap);
            var hasColorKey = texture.Name.StartsWith("{");

            var image = new Image<Rgba32>(width, height);
            for (int y = 0; y < image.Height; y++)
            {
                var rowSpan = image.GetPixelRowSpan(y);
                for (int x = 0; x < image.Width; x++)
                {
                    var paletteIndex = imageData[y * width + x];
                    if (paletteIndex == 255 && hasColorKey)
                    {
                        rowSpan[x] = new Rgba32(0, 0, 0