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(); } } }