using System; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Text.RegularExpressions; using UnityEditor; using UnityEngine; namespace BlockOutReplica.Editor { public static class OriginalVisualRecovery { private const string Destination = "Assets/Resources/OriginalRecovered"; [MenuItem("Block Out/Capture Current Game View")] public static void CaptureCurrentGameView() { if (!EditorApplication.isPlaying) return; Directory.CreateDirectory("Docs/iterations"); ScreenCapture.CaptureScreenshot("Docs/iterations/2026-10-19-visual-collision.png"); } [MenuItem("Block Out/Validate Collision Feedback (Play Mode)")] public static void ValidateCollisionFeedback() { var game = UnityEngine.Object.FindObjectOfType(); if (!EditorApplication.isPlaying || game == null) { Debug.LogError("Enter Play Mode before validating collisions."); return; } game.ReviewCollisionFeedback(); } [MenuItem("Block Out/Recover Original Visual Layers")] public static void Recover() => RecoverLayers(true); [MenuItem("Block Out/Recover Original UI And Typography")] public static void RecoverUiOnly() => RecoverLayers(false); private static void RecoverLayers(bool includeBoosters) { string source = Path.GetFullPath(Path.Combine(Application.dataPath, "../../reverse/assetripper_export/unity_project/ExportedProject/Assets")); Directory.CreateDirectory(Destination); var textures = new Dictionary(); foreach (string meta in Directory.GetFiles(Path.Combine(source, "Texture2D"), "*.png.meta")) { Match guid = Regex.Match(File.ReadAllText(meta), @"(?m)^guid: (\w+)"); if (guid.Success) textures[guid.Groups[1].Value] = meta.Substring(0, meta.Length - 5); } var names = new HashSet(BlockOutReplica.Presentation.OriginalAssetCatalog.RequiredSpriteNames()); string[] layers = { "Ingame_Header_Button_Default", "ingame_booster_timefreeze2", "ConvexLeftTop", "ConvexRightTop", "ConvexLeftBottom", "ConvexRightBottom", "LinearX-Up", "LinearX-Down_0", "BorderLeft", "BorderRight", "BorderTop", "BorderUpLeftCorner", "BorderUpRightCorner", "BorderDownLeftCorner", "BorderDownRightCorner" }; names.UnionWith(layers); foreach (string existing in Directory.GetFiles("Assets/Resources/OriginalUI/Sprites", "*.asset")) names.Add(Path.GetFileNameWithoutExtension(existing)); names.UnionWith(new[] {"Popup_BG_Default_0", "Popup_BG_Default_Bottom", "Pause_Btn_Resume", "Pause_Btn_Quit", "Pause_Toggle_BG", "Pause_Toggle_Btn_On", "Pause_Toggle_Btn_Off", "Pause_Icon_Sounds", "Pause_Icon_Musics", "Pause_Icon_Haptics", "Settings_Exit_Btn", "Settings_Exit_Icon", "Home_Settings_Button", "Home_Settings_Icon", "Home_Button_Red", "Fail_Popup_Title_Default", "Fail_Popup_Coins", "MoreLives_Icon_Heart", "common_timefreeze2", "common_rocket", "Profile_Avatar_00", "Profile_Frame_Purple_Big", "Profile_Avatar_Glass_Effect", "Home_ProfilePic_Ref", "heart_limiless-Multiple", "icon_notime", "icon_coin_bttn", "Ingame_Booster_Counter_Purple", "Ingame_Icon_Coin", "PrelevelPopup_Lock_Icon", "Shop Small", "Shop Icon", "Shop Icon_Big", "Profile_Frame_Blue_Big", "Profile_Frame_Silver_Big 1", "Endgame_Popup_Left_Default", "Endgame_Popup_Right_Default", "Endgame_Popup_Inside_Default", "Endgame_Btn_Quit", "Endgame_Btn_Stroke_Quit", "Endgame_Coin_BG_Defaut", "Endgame_Coin_Icon", "broken_heart", "Fail_Popup_Coins_Counter_Default", "Fail_Popup_X_Icon", "Fail_Popup_Btn", "Fail_Popup_Btn_Stroke"}); foreach (string sprite in Directory.GetFiles(Path.Combine(source, "Sprite"), "*.asset")) { string name = Path.GetFileNameWithoutExtension(sprite); if (name.StartsWith("Journey_", StringComparison.Ordinal) || name.StartsWith("Nav_Bar_", StringComparison.Ordinal) || name.StartsWith("Home_", StringComparison.Ordinal) || name.StartsWith("Prelevel_", StringComparison.Ordinal) || name.StartsWith("Pause_", StringComparison.Ordinal) || name.StartsWith("Settings_Exit_", StringComparison.Ordinal) || name.StartsWith("Nav_Bar_Icon_Home", StringComparison.Ordinal) || name == "freeze_front" || Regex.IsMatch(name, @"^poster[123](?:_thumbnail)?$") || name == "Ingame_Freeze_Time_Bar" || name == "Ingame_Freeze_Time_Bar_bg") names.Add(name); } foreach (string name in names) { string yaml = File.ReadAllText(Path.Combine(source, "Sprite", name + ".asset")); string guid = Regex.Match(yaml, @"texture: \{fileID: \d+, guid: (\w+)").Groups[1].Value; string sourceTexture = textures[guid]; string texturePath = Destination + "/" + Path.GetFileName(sourceTexture); if (!File.Exists(texturePath)) File.Copy(sourceTexture, texturePath); AssetDatabase.ImportAsset(texturePath, ImportAssetOptions.ForceSynchronousImport); var importer = (TextureImporter)AssetImporter.GetAtPath(texturePath); if (importer.textureCompression != TextureImporterCompression.Uncompressed || importer.mipmapEnabled || importer.maxTextureSize < 8192 || importer.npotScale != TextureImporterNPOTScale.None) { importer.textureType = TextureImporterType.Default; importer.textureCompression = TextureImporterCompression.Uncompressed; importer.mipmapEnabled = false; importer.alphaIsTransparency = true; importer.maxTextureSize = 8192; importer.npotScale = TextureImporterNPOTScale.None; importer.SaveAndReimport(); } var texture = AssetDatabase.LoadAssetAtPath(texturePath); Match rectMatch = Regex.Match(yaml, @"m_Rect:\s+serializedVersion: \d+\s+x: ([\d.]+)\s+y: ([\d.]+)\s+width: ([\d.]+)\s+height: ([\d.]+)"); var rect = new Rect(Number(rectMatch, 1), Number(rectMatch, 2), Number(rectMatch, 3), Number(rectMatch, 4)); Match border = Regex.Match(yaml, @"m_Border: \{x: ([\d.]+), y: ([\d.]+), z: ([\d.]+), w: ([\d.]+)\}"); Vector4 sliceBorder = border.Success ? new Vector4(Number(border, 1), Number(border, 2), Number(border, 3), Number(border, 4)) : Vector4.zero; Match pivotMatch = Regex.Match(yaml, @"m_Pivot: \{x: ([\d.]+), y: ([\d.]+)\}"); Vector2 pivot = pivotMatch.Success ? new Vector2(Number(pivotMatch, 1), Number(pivotMatch, 2)) : Vector2.one * .5f; Sprite recovered = Sprite.Create(texture, rect, pivot, 100f, 0, SpriteMeshType.FullRect, sliceBorder); recovered.name = name; string assetPath = Destination + "/" + name + ".asset"; Sprite existing = AssetDatabase.LoadAssetAtPath(assetPath); if (existing == null) AssetDatabase.CreateAsset(recovered, assetPath); else { EditorUtility.CopySerialized(recovered, existing); UnityEngine.Object.DestroyImmediate(recovered); } } AssetDatabase.SaveAssets(); const string audioDestination = "Assets/Resources/OriginalInteractions/Audio"; Directory.CreateDirectory(audioDestination); foreach (string name in new[] {"gameplay-music", "home-music2", "ui-click-v1", "win_screen", "Timer_Warning", "Timer_Freeze", "Rocket_Launch", "Rocket_Land", "UFO"}) { string destination = audioDestination + "/" + name + ".ogg"; if (!File.Exists(destination)) File.Copy(Path.Combine(source, "AudioClip", name + ".ogg"), destination); } AssetDatabase.Refresh(); OriginalTypographyRecovery.Recover(); if (includeBoosters) OriginalBoosterRecovery.Recover(); Debug.Log("BLOCKOUT_VISUAL_LAYERS_RECOVERED: " + names.Count); } private static float Number(Match match, int index) => float.Parse(match.Groups[index].Value, CultureInfo.InvariantCulture); } }