You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
Mipmap_Fixer/MipmapDetector.cs

583 lines
18 KiB
C#

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