// Copyright © Gamesmiths Guild. #if TOOLS using System; using System.Collections.Generic; using System.Linq; using Gamesmiths.Forge.Godot.Core; using Gamesmiths.Forge.Godot.Resources; using Gamesmiths.Forge.Tags; using Godot; using Godot.Collections; namespace Gamesmiths.Forge.Godot.Editor; [Tool] public partial class AssetRepairTool : EditorPlugin { public static void RepairAllAssetsTags() { ForgeData pluginData = ResourceLoader.Load("uid://8j4xg16o3qnl"); var tagsManager = new TagsManager([.. pluginData.RegisteredTags]); List scenes = GetScenePaths("res://"); GD.Print($"Found {scenes.Count} scene(s) to process."); var openedScenes = EditorInterface.Singleton.GetOpenScenes(); foreach (var originalScenePath in scenes) { // For some weird reason scenes from the GetScenePath are coming with 3 slashes instead of just two. var scenePath = originalScenePath.Replace("res:///", "res://"); GD.Print($"Processing scene: {scenePath}."); PackedScene? packedScene = ResourceLoader.Load(scenePath); if (packedScene is null) { GD.PrintErr($"Failed to load scene: {scenePath}."); continue; } Node sceneInstance = packedScene.Instantiate(); var modified = ProcessNode(sceneInstance, tagsManager); if (!modified) { GD.Print($"No changes needed for {scenePath}."); continue; } // 'sceneInstance' is the modified scene instance in memory, need to save to disk and reload if needed. var newScene = new PackedScene(); Error error = newScene.Pack(sceneInstance); if (error != Error.Ok) { GD.PrintErr($"Failed to pack scene: {error}."); continue; } error = ResourceSaver.Save(newScene, scenePath); if (error != Error.Ok) { GD.PrintErr($"Failed to save scene: {error}."); continue; } if (openedScenes.Contains(scenePath)) { GD.Print($"Scene was opened, reloading background scene: {scenePath}."); EditorInterface.Singleton.ReloadSceneFromPath(scenePath); } } } /// /// Recursively get scene files from a folder. /// /// Current path iteration. /// List of scenes found. private static List GetScenePaths(string basePath) { var scenePaths = new List(); var dir = DirAccess.Open(basePath); if (dir is null) { GD.PrintErr($"Failed to open directory: {basePath}"); return scenePaths; } // Start listing directory entries; skip navigational and hidden files. dir.ListDirBegin(); while (true) { var fileName = dir.GetNext(); if (string.IsNullOrEmpty(fileName)) { break; } var filePath = $"{basePath}/{fileName}"; if (dir.CurrentIsDir()) { // Recursively scan subdirectories. scenePaths.AddRange(GetScenePaths(filePath)); } else if (fileName.EndsWith(".tscn", StringComparison.InvariantCultureIgnoreCase) || fileName.EndsWith(".scn", StringComparison.InvariantCultureIgnoreCase)) { scenePaths.Add(filePath); } } dir.ListDirEnd(); return scenePaths; } /// /// Recursively process nodes; returns true if any ForgeEntity was modified. /// /// Current node iteration. /// The tags manager used to validate tags. /// if any ForgeEntity was modified. private static bool ProcessNode(Node node, TagsManager tagsManager) { var modified = ValidateNode(node, tagsManager); foreach (Node child in node.GetChildren()) { modified |= ProcessNode(child, tagsManager); } return modified; } private static bool ValidateNode(Node node, TagsManager tagsManager) { var modified = false; foreach (Dictionary propertyInfo in node.GetPropertyList()) { if (!propertyInfo.TryGetValue("class_name", out Variant className)) { continue; } if (className.AsString() != "TagContainer") { continue; } if (!propertyInfo.TryGetValue("name", out Variant nameObj)) { continue; } var propertyName = nameObj.AsString(); Variant value = node.Get(propertyName); if (value.VariantType != Variant.Type.Object) { continue; } if (value.As() is ForgeTagContainer tagContainer) { modified |= ValidateTagContainerProperty(tagContainer, node.Name, tagsManager); } } return modified; } private static bool ValidateTagContainerProperty( ForgeTagContainer container, string nodeName, TagsManager tagsManager) { if (container.ContainerTags is null) { return false; } Array originalTags = container.ContainerTags; var newTags = new Array(); var modified = false; foreach (var tag in originalTags) { try { Tag.RequestTag(tagsManager, tag); newTags.Add(tag); } catch (TagNotRegisteredException) { GD.PrintRich( $"[color=LIGHT_STEEL_BLUE][RepairTool] Removing invalid tag [{tag}] from node {nodeName}."); modified = true; } } if (modified) { container.ContainerTags = newTags; } return modified; } } #endif