parent
b0cc5b2cbe
commit
48d82b7b8a
@ -0,0 +1,583 @@
|
||||
using Godot;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
/// <summary>
|
||||
/// A tool for finding and fixing images missing mipmaps.
|
||||
/// Made with help from Claud (Sonnet)
|
||||
/// </summary>
|
||||
[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<string> FindAllTextures(string path)
|
||||
{
|
||||
var textures = new List<string>();
|
||||
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<string> CheckMipmaps(List<string> texturePaths)
|
||||
{
|
||||
var missingMipmaps = new List<string>();
|
||||
|
||||
foreach (string path in texturePaths)
|
||||
{
|
||||
if (!ResourceLoader.Exists(path))
|
||||
continue;
|
||||
|
||||
var texture = ResourceLoader.Load<Texture2D>(path);
|
||||
if (texture != null)
|
||||
{
|
||||
var image = texture.GetImage();
|
||||
if (image != null && !image.HasMipmaps())
|
||||
{
|
||||
missingMipmaps.Add(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return missingMipmaps;
|
||||
}
|
||||
|
||||
private void DisplayResults(List<string> 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<string> paths)
|
||||
{
|
||||
ShowLoading($"Fixing mipmaps (0/{paths.Count})...");
|
||||
// Immediately clear the list to show we're processing
|
||||
ClearResults();
|
||||
ProcessNextBatch(new Queue<string>(paths));
|
||||
}
|
||||
|
||||
private void ProcessNextBatch(Queue<string> 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<string>();
|
||||
|
||||
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<string>();
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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"
|
Loading…
Reference in New Issue