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.
583 lines
18 KiB
C#
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();
|
|
}
|
|
}
|
|
} |