diff --git a/MipmapDetector.cs b/MipmapDetector.cs
new file mode 100644
index 0000000..acf715e
--- /dev/null
+++ b/MipmapDetector.cs
@@ -0,0 +1,583 @@
+using Godot;
+using System;
+using System.Collections.Generic;
+///
+/// A tool for finding and fixing images missing mipmaps.
+/// Made with help from Claud (Sonnet)
+///
+[Tool]
+public partial class MipmapDetector : EditorPlugin
+{
+ private Button _toolButton;
+ private Control _mipmapPanel;
+ private VBoxContainer _resultContainer;
+ private ScrollContainer _scrollContainer;
+ private TextureProgressBar _progressBar;
+ private Label _statusLabel;
+
+
+ public override void _EnterTree()
+ {
+ _toolButton = new Button
+ {
+ Text = "Check Mipmaps",
+ TooltipText = "Scan project for textures with missing mipmaps"
+ };
+ _toolButton.Pressed += OnCheckMipmapsPressed;
+
+ AddControlToContainer(CustomControlContainer.Toolbar, _toolButton);
+
+ CreatePanel();
+ }
+
+ public override void _ExitTree()
+ {
+ if (_toolButton != null)
+ {
+ RemoveControlFromContainer(CustomControlContainer.Toolbar, _toolButton);
+ _toolButton.QueueFree();
+ }
+ if (_mipmapPanel != null)
+ {
+ RemoveControlFromDocks(_mipmapPanel);
+ _mipmapPanel.QueueFree();
+ }
+ // if (_missingPanel != null)
+ // {
+ // RemoveControlFromDocks(_missingPanel);
+ // _missingPanel.QueueFree();
+ // }
+ }
+ private void CheckImageValidity(string path)
+ {
+ try
+ {
+ var image = Image.LoadFromFile(path);
+ if (image == null)
+ {
+ // Add to missing/invalid files panel
+ GD.PrintErr(path, "Failed to load image file");
+ return;
+ }
+
+ // Additional image checks could go here
+ GD.Print($"Valid image found: {path}");
+ GD.Print($"Size: {image.GetWidth()}x{image.GetHeight()}, Format: {image.GetFormat()}");
+ }
+ catch (Exception e)
+ {
+ GD.PrintErr(path, $"Error loading image: {e.Message}");
+ }
+ }
+
+ // private void AddToMissingPanel(string path, string reason)
+ // {
+ // var hbox = new HBoxContainer();
+ //
+ // var pathLabel = new Label
+ // {
+ // Text = $"{path}\nReason: {reason}",
+ // SizeFlagsHorizontal = Control.SizeFlags.ExpandFill
+ // };
+ // hbox.AddChild(pathLabel);
+ //
+ // _missingContainer.AddChild(hbox);
+ // }
+ private void CreatePanel()
+ {
+ _mipmapPanel = new Control
+ {
+ Name = "Mipmap Inspector"
+ };
+
+ var vbox = new VBoxContainer
+ {
+ AnchorRight = 1,
+ AnchorBottom = 1
+ };
+
+ // Add status section at the top
+ var statusBox = new HBoxContainer();
+ _statusLabel = new Label
+ {
+ Text = "Ready",
+ SizeFlagsHorizontal = Control.SizeFlags.ExpandFill
+ };
+ statusBox.AddChild(_statusLabel);
+
+ _progressBar = new TextureProgressBar
+ {
+ Visible = false,
+ CustomMinimumSize = new Vector2(200, 8)
+ };
+ statusBox.AddChild(_progressBar);
+
+ vbox.AddChild(statusBox);
+
+ _scrollContainer = new ScrollContainer
+ {
+ SizeFlagsHorizontal = Control.SizeFlags.ExpandFill,
+ SizeFlagsVertical = Control.SizeFlags.ExpandFill
+ };
+
+ _resultContainer = new VBoxContainer
+ {
+ SizeFlagsHorizontal = Control.SizeFlags.ExpandFill
+ };
+
+ _scrollContainer.AddChild(_resultContainer);
+ vbox.AddChild(_scrollContainer);
+ _mipmapPanel.AddChild(vbox);
+
+ AddControlToDock(DockSlot.RightBl, _mipmapPanel);
+ }
+
+ private void ShowLoading(string status)
+ {
+ _statusLabel.Text = status;
+ _progressBar.Visible = true;
+ // Start progress animation
+ _progressBar.Value = 0;
+ GetTree().CreateTimer(0.1).Timeout += AnimateProgress;
+ }
+ private void HideLoading()
+ {
+ _progressBar.Visible = false;
+ _statusLabel.Text = "Ready";
+ }
+ private void AnimateProgress()
+ {
+ if (_progressBar.Visible)
+ {
+ _progressBar.Value = (_progressBar.Value + 1) % 100;
+ GetTree().CreateTimer(0.05).Timeout += AnimateProgress;
+ }
+ }
+
+
+ private void OnCheckMipmapsPressed()
+ {
+ ClearResults();
+ ShowLoading("Scanning for textures...");
+
+ // Defer the actual scan to let the UI update
+ GetTree().CreateTimer(0.1).Timeout += () =>
+ {
+ var texturePaths = FindAllTextures("res://");
+ _statusLabel.Text = "Checking mipmaps...";
+ var missingMipmaps = CheckMipmaps(texturePaths);
+ DisplayResults(missingMipmaps);
+ HideLoading();
+ };
+ }
+
+
+ private List FindAllTextures(string path)
+ {
+ var textures = new List();
+ var dir = DirAccess.Open(path);
+
+ if (dir == null)
+ return textures;
+
+ // Skip addons folder
+ if (path.Contains("addons"))
+ return textures;
+
+ // Check for .gdignore
+ if (FileAccess.FileExists(path.PathJoin(".gdignore")))
+ return textures;
+
+ dir.ListDirBegin();
+ string fileName = dir.GetNext();
+
+ while (!string.IsNullOrEmpty(fileName))
+ {
+ if (fileName != "." && fileName != "..")
+ {
+ string fullPath = path.PathJoin(fileName);
+
+ if (dir.CurrentIsDir())
+ {
+ if (path != "res://")
+ {
+ textures.AddRange(FindAllTextures(fullPath));
+ }
+ else if (!fileName.StartsWith("."))
+ {
+ textures.AddRange(FindAllTextures(fullPath));
+ }
+ }
+ else if (path != "res://" &&
+ (fileName.EndsWith(".png") || fileName.EndsWith(".jpg") ||
+ fileName.EndsWith(".jpeg") || fileName.EndsWith(".webp")))
+ {
+ if (FileAccess.FileExists(fullPath))
+ {
+ textures.Add(fullPath);
+ }
+ }
+ }
+ fileName = dir.GetNext();
+ }
+
+ dir.ListDirEnd();
+ return textures;
+ }
+
+ private List CheckMipmaps(List texturePaths)
+ {
+ var missingMipmaps = new List();
+
+ foreach (string path in texturePaths)
+ {
+ if (!ResourceLoader.Exists(path))
+ continue;
+
+ var texture = ResourceLoader.Load(path);
+ if (texture != null)
+ {
+ var image = texture.GetImage();
+ if (image != null && !image.HasMipmaps())
+ {
+ missingMipmaps.Add(path);
+ }
+ }
+ }
+
+ return missingMipmaps;
+ }
+
+ private void DisplayResults(List missingMipmaps)
+ {
+ if (missingMipmaps.Count == 0)
+ {
+ var label = new Label { Text = "No textures with missing mipmaps found!" };
+ _resultContainer.AddChild(label);
+ }
+ else
+ {
+ var headerBox = new HBoxContainer();
+
+ var headerLabel = new Label
+ {
+ Text = $"Found {missingMipmaps.Count} textures with missing mipmaps:",
+ SizeFlagsHorizontal = Control.SizeFlags.ExpandFill
+ };
+ headerBox.AddChild(headerLabel);
+
+ var fixAllButton = new Button { Text = "Fix All" };
+ fixAllButton.Pressed += () => FixAllMipmaps(missingMipmaps);
+ headerBox.AddChild(fixAllButton);
+
+ _resultContainer.AddChild(headerBox);
+
+ foreach (string path in missingMipmaps)
+ { var hbox = new HBoxContainer();
+
+ var pathLabel = new Label
+ {
+ Text = path,
+ SizeFlagsHorizontal = Control.SizeFlags.ExpandFill
+ };
+
+ var pathButton = new Button
+ {
+ Flat = true,
+ SizeFlagsHorizontal = Control.SizeFlags.ExpandFill,
+ CustomMinimumSize = new Vector2(0, 0)
+ };
+ pathButton.AddChild(pathLabel);
+
+ // Add hover effect
+ pathButton.MouseEntered += () => pathButton.Modulate = new Color(1, 1, 1, 0.7f);
+ pathButton.MouseExited += () => pathButton.Modulate = Colors.White;
+
+ // Handle click to select in FileSystem dock
+ pathButton.Pressed += () => SelectInFileSystem(path);
+
+ hbox.AddChild(pathButton);
+
+ var fixButton = new Button { Text = "Fix" };
+ fixButton.Pressed += () => FixMipmaps(path);
+ hbox.AddChild(fixButton);
+
+ _resultContainer.AddChild(hbox);
+ }
+ }
+ }
+ private void FixAllMipmaps(List paths)
+ {
+ ShowLoading($"Fixing mipmaps (0/{paths.Count})...");
+ // Immediately clear the list to show we're processing
+ ClearResults();
+ ProcessNextBatch(new Queue(paths));
+ }
+
+ private void ProcessNextBatch(Queue remainingPaths, int batchSize = 5)
+ {
+ if (remainingPaths.Count == 0)
+ {
+ HideLoading();
+ // Force a filesystem scan
+ EditorInterface.Singleton.GetResourceFilesystem().Scan();
+
+ // Refresh the display
+ GetTree().CreateTimer(0.5).Timeout += () =>
+ {
+ var texturePaths = FindAllTextures("res://");
+ var missingMipmaps = CheckMipmaps(texturePaths);
+ DisplayResults(missingMipmaps);
+ };
+ return;
+ }
+
+ int totalCount = remainingPaths.Count;
+ int processedCount = 0;
+
+ // Process a small batch
+ int count = Math.Min(batchSize, remainingPaths.Count);
+ var currentBatch = new List();
+
+ for (int i = 0; i < count; i++)
+ {
+ if (remainingPaths.TryDequeue(out string path))
+ {
+ currentBatch.Add(path);
+ processedCount++;
+ }
+ }
+
+ // Update status
+ _statusLabel.Text = $"Fixing mipmaps ({processedCount}/{totalCount})...";
+
+ // Process the batch
+ foreach (var path in currentBatch)
+ {
+ try
+ {
+ string importPath = path + ".import";
+ if (!FileAccess.FileExists(importPath))
+ continue;
+
+ var file = FileAccess.Open(importPath, FileAccess.ModeFlags.ReadWrite);
+ if (file != null)
+ {
+ string content = file.GetAsText();
+ file.Close();
+
+ content = content.Replace("mipmaps/generate=false", "mipmaps/generate=true");
+
+ file = FileAccess.Open(importPath, FileAccess.ModeFlags.Write);
+ if (file != null)
+ {
+ file.StoreString(content);
+ file.Close();
+ }
+ }
+ }
+ catch (Exception e)
+ {
+ GD.PrintErr($"Error processing {path}: {e.Message}");
+ }
+ }
+
+ // Do a single reimport for the batch
+ if (currentBatch.Count > 0)
+ {
+ EditorInterface.Singleton.GetResourceFilesystem().ReimportFiles(currentBatch.ToArray());
+ }
+
+ // Schedule next batch with a delay
+ if (remainingPaths.Count > 0)
+ {
+ GetTree().CreateTimer(0.2).Timeout += () => ProcessNextBatch(remainingPaths);
+ }
+ }
+
+ private bool UpdateImportSettings(string path)
+ {
+ try
+ {
+ // First verify the texture actually exists
+ if (!FileAccess.FileExists(path))
+ {
+ GD.PushError($"Texture file doesn't exist: {path}");
+ return false;
+ }
+
+ string importPath = path + ".import";
+ if (!FileAccess.FileExists(importPath))
+ {
+ GD.PushError($"No import file found for: {path}");
+ return false;
+ }
+
+ // Read and modify the import file
+ var file = FileAccess.Open(importPath, FileAccess.ModeFlags.ReadWrite);
+ if (file == null)
+ {
+ GD.PushError($"Failed to open import file: {importPath}");
+ return false;
+ }
+
+ string content = file.GetAsText();
+ file.Close();
+
+ // Parse and modify the import content
+ string[] lines = content.Split('\n');
+ var newLines = new List();
+ bool inMetadataSection = false;
+ bool addedMipmapSettings = false;
+
+ foreach (string line in lines)
+ {
+ string trimmedLine = line.TrimStart();
+
+ if (trimmedLine.StartsWith("["))
+ {
+ if (trimmedLine == "[metadata]")
+ {
+ inMetadataSection = true;
+ newLines.Add(line);
+ newLines.Add("mipmaps/generate=true");
+ newLines.Add("mipmaps/limit=-1");
+ addedMipmapSettings = true;
+ continue;
+ }
+ else
+ {
+ inMetadataSection = false;
+ }
+ }
+
+ if (inMetadataSection && (trimmedLine.StartsWith("mipmaps/generate") || trimmedLine.StartsWith("mipmaps/limit")))
+ {
+ continue;
+ }
+
+ newLines.Add(line);
+ }
+
+ if (!addedMipmapSettings)
+ {
+ newLines.Add("[metadata]");
+ newLines.Add("mipmaps/generate=true");
+ newLines.Add("mipmaps/limit=-1");
+ }
+
+ // Write back the modified content
+ file = FileAccess.Open(importPath, FileAccess.ModeFlags.Write);
+ if (file == null)
+ {
+ GD.PushError($"Failed to write to import file: {importPath}");
+ return false;
+ }
+
+ file.StoreString(string.Join("\n", newLines));
+ file.Close();
+
+ // Delete the existing imported file to force a clean reimport
+ string importedPath = "res://.godot/imported/" + path.GetFile() + "-" + importPath.GetFile().Hash();
+ if (FileAccess.FileExists(importedPath))
+ {
+ DirAccess.RemoveAbsolute(importedPath);
+ }
+
+ GD.Print($"Updated import settings for: {path}");
+ return true;
+ }
+ catch (Exception e)
+ {
+ GD.PushError($"Exception while processing {path}: {e.Message}");
+ return false;
+ }
+ }
+ private void SelectInFileSystem(string path)
+ {
+ // Get the EditorInterface singleton
+ var editorInterface = EditorInterface.Singleton;
+
+ // Select the file in the FileSystem dock
+ editorInterface.GetFileSystemDock().NavigateToPath(path);
+
+ // Optional: Also select it in the editor
+ editorInterface.EditResource(GD.Load(path));
+ }
+
+ private void FixMipmaps(string path)
+ {
+ try
+ {
+ string importPath = path + ".import";
+ if (!FileAccess.FileExists(importPath))
+ {
+ GD.PushError($"No import file found for: {path}");
+ return;
+ }
+
+ // Read current import settings
+ var file = FileAccess.Open(importPath, FileAccess.ModeFlags.ReadWrite);
+ if (file == null)
+ {
+ GD.PushError($"Failed to open import file: {path}");
+ return;
+ }
+
+ string content = file.GetAsText();
+ file.Close();
+
+ // Directly replace the mipmap setting
+ content = content.Replace("mipmaps/generate=false", "mipmaps/generate=true");
+
+ // Write the modified content
+ file = FileAccess.Open(importPath, FileAccess.ModeFlags.Write);
+ if (file == null)
+ {
+ GD.PushError($"Failed to write to import file: {path}");
+ return;
+ }
+
+ file.StoreString(content);
+ file.Close();
+
+ // Delete the imported resource to force a clean reimport
+ string importedDir = "res://.godot/imported/";
+ var importedFile = path.GetFile() + "-" + importPath.GetFile().Hash();
+ string importedPath = importedDir.PathJoin(importedFile);
+
+ if (FileAccess.FileExists(importedPath))
+ {
+ DirAccess.RemoveAbsolute(importedPath);
+ }
+
+ // Trigger reimport
+ EditorInterface.Singleton.GetResourceFilesystem().ReimportFiles(new[] { path });
+ EditorInterface.Singleton.GetResourceFilesystem().Scan();
+
+ // Refresh the display after a short delay to allow for reimport
+ GetTree().CreateTimer(0.5).Timeout += () =>
+ {
+ ClearResults();
+ var texturePaths = FindAllTextures("res://");
+ var missingMipmaps = CheckMipmaps(texturePaths);
+ DisplayResults(missingMipmaps);
+ };
+
+ GD.Print($"Successfully enabled mipmaps for: {path}");
+ }
+ catch (Exception e)
+ {
+ GD.PushError($"Failed to process {path}: {e.Message}");
+ }
+ }
+
+
+ private void ClearResults()
+ {
+ foreach (Node child in _resultContainer.GetChildren())
+ {
+ child.QueueFree();
+ }
+ }
+}
\ No newline at end of file
diff --git a/plugin.cfg b/plugin.cfg
new file mode 100644
index 0000000..42d0830
--- /dev/null
+++ b/plugin.cfg
@@ -0,0 +1,7 @@
+[plugin]
+
+name="MipMap Fixer"
+description="A plugin to find and list images with missing mipmaps "
+author="David of Everlasting.Media"
+version="0.1"
+script="MipmapDetector.cs"