Files
2026-10-20 13:21:04 +08:00

412 lines
30 KiB
C#

using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using BlockOutReplica.Presentation;
using UnityEditor;
using UnityEngine;
namespace BlockOutReplica.Editor
{
public static partial class OriginalBoosterRecovery
{
private const string Destination = "Assets/Resources/OriginalBoosters";
private static readonly CultureInfo Culture = CultureInfo.InvariantCulture;
private static readonly Regex DocumentPattern = new Regex(@"(?ms)^--- !u!(\d+) &(\d+)\s*\n(.*?)(?=^--- !u!|\z)");
private sealed class Document { public int Type; public string Id, Text; }
[MenuItem("Block Out/Recover Original Booster Animations")]
public static void Recover()
{
string source = Path.GetFullPath(Path.Combine(Application.dataPath,
"../../reverse/assetripper_export/unity_project/ExportedProject/Assets"));
Directory.CreateDirectory(Destination);
var sprites = IndexGuids(Path.Combine(source, "Sprite"), "*.asset.meta");
var textures = IndexGuids(Path.Combine(source, "Texture2D"), "*.png.meta");
var cache = new Dictionary<string, Sprite>();
var documents = DocumentPattern.Matches(File.ReadAllText(Path.Combine(source, "Scenes/GamePlayScene.unity")))
.Cast<Match>().Select(m => new Document {Type = int.Parse(m.Groups[1].Value), Id = m.Groups[2].Value, Text = m.Groups[3].Value}).ToArray();
RecoverClip(source, documents, sprites, textures, cache, "Ufo", "UfoAnimation", "AnimatorRoot", "UfoBoosterAnimationController");
RecoverClip(source, documents, sprites, textures, cache, "Rocket", "RocketBoosterAnimation", "Animator", "RocketBoosterAnimationController");
RecoverClip(source, documents, sprites, textures, cache, "Freeze", "FreezeGun", "FreezeGunAnimator", "FreezeTimerBoosterAnimationController");
RecoverClip(source, documents, sprites, textures, cache, "FreezeTimerStart", "FreezeTimerStartAnimation", "AnimatorRoot", "FreezeTimerBoosterAnimationController");
RecoverClip(source, documents, sprites, textures, cache, "FreezeTimerEnd", "FreezeTimerEndAnimation", "AnimatorRoot", "FreezeTimerBoosterAnimationController");
RecoverClip(source, documents, sprites, textures, cache, "FreezeScreen", "FreezeTimerEndAnimation", "TimeFreezeBG", "UIRoot", true);
AssetDatabase.SaveAssets();
Debug.Log($"BLOCKOUT_BOOSTER_ANIMATIONS_RECOVERED: clips=5, staticScreenLayers=1, sprites={cache.Count}, nativeParticlesRecovered=True, shadersAdapted=True");
}
[MenuItem("Block Out/Validate Original Booster Animation Sources")]
public static void Validate()
{
int checks = 0;
foreach (string name in new[] {"Ufo", "Rocket", "Freeze", "FreezeTimerStart", "FreezeTimerEnd"})
{
var data = Resources.Load<OriginalBoosterAnimationData>("OriginalBoosters/" + name);
Require(data != null && data.Duration > 0, name + ": recovered clip missing", ref checks);
Require(data.Nodes.Length > 0 && data.Nodes.Any(n => n.Sprite != null), name + ": original sprite layers missing", ref checks);
foreach (var track in data.SpriteTracks)
{
Require(track.Times.Length == track.Sprites.Length && track.Times.Length > 0,
name + ": invalid frame timing", ref checks);
Require(track.Sprites.All(s => s != null), name + ": unresolved source sprite", ref checks);
}
var instance = data.CreateInstance(null);
try
{
Require(data.VisualPrefab != null && data.UnrecoveredParticleSystems == 0 &&
data.RecoveredParticleSystems == instance.Particles.Count && instance.Particles.Count > 0,
name + ": native particle subtree differs from source", ref checks);
Require(data.UnresolvedSourcePaths.Length == data.TransformTracks.Select(t => t.Path).Concat(data.ScalarTracks.Select(t => t.Path))
.Where(p => p.StartsWith("path_0x", StringComparison.Ordinal)).Distinct().Count(),
name + ": unresolved source animation hashes not reported", ref checks);
int peak = 0;
for (int sample = 0; sample <= 20; sample++)
{
data.Sample(instance, data.Duration * sample / 20f);
peak = Mathf.Max(peak, instance.LiveParticleCount);
Require(instance.Transforms.Values.All(t => !float.IsNaN(t.localPosition.x) && !float.IsInfinity(t.localPosition.x) &&
!float.IsNaN(t.localScale.x) && !float.IsInfinity(t.localScale.x)), name + ": non-finite sampled pose", ref checks);
Require(instance.Renderers.Values.All(r => r.sprite != null && r.sharedMaterial != null &&
r.sharedMaterial.shader != null && r.sharedMaterial.shader.isSupported), name + ": invalid sprite/material", ref checks);
Require(instance.Particles.Values.All(p => p.GetComponent<ParticleSystemRenderer>().sharedMaterial != null &&
p.GetComponent<ParticleSystemRenderer>().sharedMaterial.shader.isSupported), name + ": unsupported particle material", ref checks);
Require(instance.Particles.Values.All(p => p.GetComponent<ParticleSystemRenderer>().sharedMaterial.shader.name
.StartsWith("BlockOut/RecoveredFx", StringComparison.Ordinal)), name + ": unadapted builtin particle shader", ref checks);
Require(instance.Particles.Values.All(p => {
var sheet = p.textureSheetAnimation;
return sheet.mode != ParticleSystemAnimationMode.Sprites || sheet.spriteCount == 0 || sheet.GetSprite(0) == null ||
p.GetComponent<ParticleSystemRenderer>().sharedMaterial.mainTexture == sheet.GetSprite(0).texture;
}), name + ": source sprite sheet texture not bound to particle material", ref checks);
}
if (name == "FreezeTimerEnd")
{
// The end clip is meant to fade the already-running start instance.
// Its own source emitters have playOnAwake=false and no new source events.
Require(peak == 0 && data.SourceEvents.Length == 0,
"FreezeTimerEnd: end-only source unexpectedly started new particles", ref checks);
ValidateFreezeEndHandoff(data, ref checks);
}
else Require(peak > 0, name + ": recovered particle modules never emitted", ref checks);
data.Sample(instance, data.Duration * .5f);
int pausedCount = instance.LiveParticleCount;
data.Sample(instance, data.Duration * .5f);
Require(instance.LiveParticleCount == pausedCount, name + ": repeated-time sampling advanced particles", ref checks);
if (name == "Ufo")
{
data.Sample(instance, 1.55f);
Transform main = instance.Transforms["Ufo_Main"];
SpriteRenderer image = instance.Renderers["Ufo_Main/UfoImage"];
Require((main.localPosition - new Vector3(0, 10.09f, 0)).magnitude < .01f,
"Ufo: stable absorption position differs from source", ref checks);
Require(main.localScale.x > 1 && main.localScale.x < 1.3f &&
image.transform.localScale == Vector3.one && image.sprite.bounds.size.x > 2 && image.sprite.bounds.size.x < 4,
"Ufo: stable image/source scale differs", ref checks);
Debug.Log($"BLOCKOUT_UFO_ABSORB_POSE: explicitTime=1.55, position={main.localPosition}, scale={main.localScale}, imageScale={image.transform.localScale}, sprite={image.sprite.name}, spriteBounds={image.sprite.bounds.size}, drawMode={image.drawMode}");
}
foreach (var skin in data.Skins)
{
Require(skin.Mesh.vertexCount == 102 && skin.Mesh.triangles.Length == 441 && skin.Mesh.bindposes.Length == 3 &&
skin.Mesh.boneWeights.All(w => Mathf.Abs(w.weight0 + w.weight1 + w.weight2 + w.weight3 - 1) < .001f),
"Freeze: source skin mesh/weights changed", ref checks);
var baked = new Mesh();
try
{
data.Sample(instance, 0); instance.SkinRenderers[skin.Path].BakeMesh(baked); Vector3[] before = baked.vertices;
data.Sample(instance, .8f); instance.SkinRenderers[skin.Path].BakeMesh(baked);
Require(baked.vertices.Where((v, i) => (v - before[i]).sqrMagnitude > .000001f).Any(),
"Freeze: recovered bone curves did not deform source skin", ref checks);
}
finally {UnityEngine.Object.DestroyImmediate(baked);}
}
Debug.Log($"BLOCKOUT_BOOSTER_PARTICLE_REVIEW: name={name}, systems={instance.Particles.Count}, peak={peak}, skins={data.Skins.Length}, unresolvedPaths={data.UnresolvedSourcePaths.Length}");
}
finally { UnityEngine.Object.DestroyImmediate(instance.Root); }
if (name == "Ufo" || name == "Rocket") Require(data.SpriteTracks.Length > 0 && data.SpriteTracks[0].Times.Length > 30,
name + ": expected native sprite animation", ref checks);
else if (name == "Freeze") Require(data.TransformTracks.Length > 10, "Freeze: expected native multipart curves", ref checks);
}
var screenData = Resources.Load<OriginalBoosterAnimationData>("OriginalBoosters/FreezeScreen");
Require(screenData != null && screenData.VisualPrefab != null, "FreezeScreen: source overlay missing", ref checks);
var screen = screenData.CreateInstance(null);
try
{
Require(screen.Renderers.TryGetValue("freezefront", out SpriteRenderer front) && front.sprite != null &&
front.drawMode == SpriteDrawMode.Sliced && Mathf.Abs(front.size.y - 20.48f) < .01f,
"FreezeScreen: native source screen sprite/size changed", ref checks);
screenData.SetOverlayOpacity(screen, 0);
Require(screen.Renderers.Values.All(r => r.color.a == 0), "FreezeScreen: hide adapter", ref checks);
screenData.SetOverlayOpacity(screen, .6f);
Require(screen.Renderers.Values.All(r => Mathf.Abs(r.color.a - .6f) < .001f), "FreezeScreen: show adapter", ref checks);
}
finally {UnityEngine.Object.DestroyImmediate(screen.Root);}
Debug.Log($"BLOCKOUT_BOOSTER_SOURCE_VALIDATION: passed=True, checks={checks}");
}
private static void Require(bool condition, string message, ref int checks)
{ if (!condition) throw new InvalidOperationException(message); checks++; }
private static void ValidateFreezeEndHandoff(OriginalBoosterAnimationData endData, ref int checks)
{
var startData = Resources.Load<OriginalBoosterAnimationData>("OriginalBoosters/FreezeTimerStart");
Require(startData != null, "FreezeTimerEnd: source start data missing", ref checks);
var running = startData.CreateInstance(null);
try
{
// Seek forward through the real start event until there is an actual live emitter.
// Do not manufacture a new end burst or assume a specific source startDelay.
for (float time = .05f; time <= startData.Duration && running.LiveParticleCount == 0; time += .05f)
startData.Sample(running, time);
Require(running.LiveParticleCount > 0, "FreezeTimerEnd: actual start instance has no live particles", ref checks);
var before = running.Particles.ToDictionary(p => p.Key, p => p.Value.time);
int live = running.LiveParticleCount;
for (int sample = 0; sample <= 10; sample++) endData.SamplePoseOnly(running, endData.Duration * sample / 10f);
Require(running.LiveParticleCount == live && running.Particles.All(p => Mathf.Abs(p.Value.time - before[p.Key]) < .00001f),
"FreezeTimerEnd: pose-only handoff reset/advanced existing particles", ref checks);
Require(running.Particles.Values.All(p => !p.isPlaying), "FreezeTimerEnd: handoff resumed Unity automatic simulation", ref checks);
// The end source's fourteen bindings are unresolved in the scene export. Sampling them
// must not invent targets or reset the existing start instance; report this gap explicitly.
Require(endData.UnresolvedSourcePaths.Length > 0 && endData.ScalarTracks.All(t =>
endData.UnresolvedSourcePaths.Contains(t.Path)),
"FreezeTimerEnd: source binding limitation was silently hidden", ref checks);
startData.AdvanceParticles(running, .05f);
Require(running.Particles.Any(p => Mathf.Abs(p.Value.time - before[p.Key]) > .00001f),
"FreezeTimerEnd: explicit tail step did not advance existing native systems", ref checks);
Debug.Log($"BLOCKOUT_FREEZE_END_HANDOFF: sourceStartParticles={live}, poseOnlyDoesNotReemit=True, nativeTailAdvances=True, unresolvedEndBindings={endData.UnresolvedSourcePaths.Length}");
}
finally {UnityEngine.Object.DestroyImmediate(running.Root);}
}
private static void RecoverClip(string source, Document[] docs, Dictionary<string, string> sprites,
Dictionary<string, string> textures, Dictionary<string, Sprite> cache,
string name, string clipName, string rootName, string controllerName, bool staticOnly = false)
{
string yaml = File.ReadAllText(Path.Combine(source, "AnimationClip", clipName + ".anim"));
var data = ScriptableObject.CreateInstance<OriginalBoosterAnimationData>();
data.name = name;
data.SourceClip = "AnimationClip/" + clipName + ".anim";
data.Duration = Float(Regex.Match(yaml, @"m_StopTime: (\S+)").Groups[1].Value);
data.SerializedStopTime = data.Duration;
data.SampleRate = Float(Regex.Match(yaml, @"m_SampleRate: (\S+)").Groups[1].Value);
var gameObjects = docs.Where(d => d.Type == 1).ToDictionary(d => d.Id);
var transforms = docs.Where(d => d.Type == 4 || d.Type == 224).ToDictionary(d => d.Id);
Func<string, string> absolutePath = null;
absolutePath = id => {
if (!transforms.TryGetValue(id, out Document transform)) return "";
string parent = Reference(transform.Text, "m_Father");
string ownName = Text(gameObjects[Reference(transform.Text, "m_GameObject")].Text, "m_Name");
return transforms.ContainsKey(parent) ? absolutePath(parent) + "/" + ownName : ownName;
};
Document root = transforms.Values.Single(d => absolutePath(d.Id).EndsWith(controllerName + "/" + rootName, StringComparison.Ordinal));
data.SourceRootPosition = Vector(root.Text, "m_LocalPosition", Vector3.zero);
data.SourceRootScale = Vector(root.Text, "m_LocalScale", Vector3.one);
string rootPath = absolutePath(root.Id);
var nodes = new List<OriginalBoosterAnimationData.Node>();
var renderers = docs.Where(d => d.Type == 212).ToDictionary(d => Reference(d.Text, "m_GameObject"));
var includedObjects = new HashSet<string>();
foreach (Document transform in transforms.Values.OrderBy(d => absolutePath(d.Id).Length))
{
string fullPath = absolutePath(transform.Id);
if (!fullPath.StartsWith(rootPath + "/", StringComparison.Ordinal)) continue;
string objectId = Reference(transform.Text, "m_GameObject");
includedObjects.Add(objectId);
var node = new OriginalBoosterAnimationData.Node {
Path = fullPath.Substring(rootPath.Length + 1),
Position = Vector(transform.Text, "m_LocalPosition", Vector3.zero),
Scale = Vector(transform.Text, "m_LocalScale", Vector3.one),
Rotation = Rotation(transform.Text),
Active = Text(gameObjects[objectId].Text, "m_IsActive") != "0"
};
if (renderers.TryGetValue(objectId, out Document renderer))
{
string guid = Regex.Match(renderer.Text, @"m_Sprite: \{fileID: \d+, guid: (\w+)").Groups[1].Value;
if (guid.Length > 0) node.Sprite = RecoverSprite(guid, sprites, textures, cache);
node.RendererEnabled = Text(renderer.Text, "m_Enabled") != "0";
node.Color = ColorValue(renderer.Text);
node.SortingOrder = int.Parse(Text(renderer.Text, "m_SortingOrder"), Culture);
node.DrawMode = (SpriteDrawMode)int.Parse(Text(renderer.Text, "m_DrawMode"), Culture);
Vector3 size = Vector(renderer.Text, "m_Size", Vector3.one);
node.Size = new Vector2(size.x, size.y);
node.FlipX = Text(renderer.Text, "m_FlipX") == "1";
node.FlipY = Text(renderer.Text, "m_FlipY") == "1";
}
nodes.Add(node);
}
data.Nodes = nodes.ToArray();
data.UnrecoveredParticleSystems = docs.Count(d => d.Type == 198 && includedObjects.Contains(Reference(d.Text, "m_GameObject")));
// SpriteSkin is exported as MonoBehaviour; flag only actual source class GUIDs with SpriteSkin metadata.
var skinGuids = new HashSet<string>(Directory.GetFiles(Path.Combine(source, "Scripts"), "SpriteSkin.cs.meta", SearchOption.AllDirectories)
.Select(p => Regex.Match(File.ReadAllText(p), @"(?m)^guid: (\w+)").Groups[1].Value));
data.UnrecoveredSpriteSkins = docs.Count(d => d.Type == 114 && includedObjects.Contains(Reference(d.Text, "m_GameObject")) &&
skinGuids.Any(g => d.Text.Contains("guid: " + g)));
data.SpriteTracks = ParseSprites(yaml, sprites, textures, cache);
data.TransformTracks = ParseTransforms(yaml);
data.ScalarTracks = ParseScalars(yaml);
if (staticOnly)
{
data.SourceClip = "Scenes/GamePlayScene.unity: " + controllerName + "/" + rootName + " (static scene layer; opacity script unavailable)";
data.SpriteTracks = Array.Empty<OriginalBoosterAnimationData.SpriteTrack>();
data.TransformTracks = Array.Empty<OriginalBoosterAnimationData.TransformTrack>();
data.ScalarTracks = Array.Empty<OriginalBoosterAnimationData.ScalarTrack>();
yaml = "";
}
if (Text(yaml, "m_Legacy") == "1")
{
foreach (var track in data.TransformTracks)
foreach (var curve in new[] {track.X, track.Y, track.Z, track.W})
if (curve.length > 0) data.Duration = Mathf.Max(data.Duration, curve.keys[curve.length - 1].time);
foreach (var track in data.ScalarTracks)
if (track.Curve.length > 0) data.Duration = Mathf.Max(data.Duration, track.Curve.keys[track.Curve.length - 1].time);
}
ResolveHashedPaths(data);
RecoverNativeVisuals(source, docs, root, rootPath, absolutePath, includedObjects, data, yaml);
string path = Destination + "/" + name + ".asset";
var existing = AssetDatabase.LoadAssetAtPath<OriginalBoosterAnimationData>(path);
if (existing == null) AssetDatabase.CreateAsset(data, path);
else { EditorUtility.CopySerialized(data, existing); UnityEngine.Object.DestroyImmediate(data); data = existing; }
Debug.Log($"BLOCKOUT_BOOSTER_SOURCE: name={name}, duration={data.Duration}, nodes={data.Nodes.Length}, spriteKeys={data.SpriteTracks.Sum(t => t.Times.Length)}, transformTracks={data.TransformTracks.Length}, scalarTracks={data.ScalarTracks.Length}, particlesMissing={data.UnrecoveredParticleSystems}, skinsMissing={data.UnrecoveredSpriteSkins}");
}
private static void ResolveHashedPaths(OriginalBoosterAnimationData data)
{
// Repeated sibling names can produce the same source path (the rocket has two "glow"
// children). That is not a CRC collision. Never let it abort recovery, and never choose
// arbitrarily if two genuinely different paths share one CRC.
var paths = data.Nodes.Select(n => n.Path).Distinct(StringComparer.Ordinal)
.GroupBy(PathHash).ToDictionary(group => group.Key, group => group.ToArray());
Func<string, string> resolve = path => {
Match match = Regex.Match(path, @"^path_0x([0-9A-Fa-f]+)_");
if (!match.Success) return path;
uint hash = uint.Parse(match.Groups[1].Value, NumberStyles.HexNumber, Culture);
if (paths.TryGetValue(hash, out string[] candidates))
{
if (candidates.Length == 1) return candidates[0];
Debug.LogWarning("Ambiguous source animation hash: " + data.name + "/" + path + " => " + string.Join(" | ", candidates));
return path;
}
Debug.LogWarning("Unresolved source animation hash: " + data.name + "/" + path);
return path;
};
foreach (var track in data.TransformTracks) track.Path = resolve(track.Path);
foreach (var track in data.SpriteTracks) track.Path = resolve(track.Path);
foreach (var track in data.ScalarTracks) track.Path = resolve(track.Path);
data.UnresolvedSourcePaths = data.TransformTracks.Select(t => t.Path).Concat(data.SpriteTracks.Select(t => t.Path)).Concat(data.ScalarTracks.Select(t => t.Path))
.Where(p => p.StartsWith("path_0x", StringComparison.Ordinal)).Distinct().ToArray();
}
private static uint PathHash(string path)
{
uint crc = 0xffffffff;
foreach (byte value in System.Text.Encoding.UTF8.GetBytes(path))
{crc ^= value; for (int bit = 0; bit < 8; bit++) crc = (crc & 1) != 0 ? (crc >> 1) ^ 0xedb88320 : crc >> 1;}
return ~crc;
}
private static OriginalBoosterAnimationData.SpriteTrack[] ParseSprites(string yaml, Dictionary<string, string> sprites,
Dictionary<string, string> textures, Dictionary<string, Sprite> cache)
{
var tracks = new List<OriginalBoosterAnimationData.SpriteTrack>();
foreach (string block in CurveBlocks(Section(yaml, "m_PPtrCurves")))
{
if (Text(block, "attribute") != "m_Sprite") continue;
var keys = Regex.Matches(block, @"time: (\S+)\s+value: \{fileID: \d+, guid: (\w+), type: \d+\}").Cast<Match>().ToArray();
tracks.Add(new OriginalBoosterAnimationData.SpriteTrack {Path = Text(block, "path"),
Times = keys.Select(k => Float(k.Groups[1].Value)).ToArray(),
Sprites = keys.Select(k => RecoverSprite(k.Groups[2].Value, sprites, textures, cache)).ToArray()});
}
return tracks.ToArray();
}
private static OriginalBoosterAnimationData.TransformTrack[] ParseTransforms(string yaml)
{
var result = new List<OriginalBoosterAnimationData.TransformTrack>();
foreach (var section in new[] {new[] {"m_PositionCurves", "Position"}, new[] {"m_ScaleCurves", "Scale"}, new[] {"m_EulerCurves", "Euler"}, new[] {"m_RotationCurves", "Rotation"}})
foreach (string block in CurveBlocks(Section(yaml, section[0])))
result.Add(new OriginalBoosterAnimationData.TransformTrack {Path = Text(block, "path"), Property = section[1],
X = ParseCurve(block, "x"), Y = ParseCurve(block, "y"), Z = ParseCurve(block, "z"), W = ParseCurve(block, "w")});
return result.ToArray();
}
private static OriginalBoosterAnimationData.ScalarTrack[] ParseScalars(string yaml) =>
CurveBlocks(Section(yaml, "m_FloatCurves")).Select(block => new OriginalBoosterAnimationData.ScalarTrack {
Path = Text(block, "path"), Property = Text(block, "attribute"), Curve = ParseCurve(block, null)
}).ToArray();
private static AnimationCurve ParseCurve(string block, string component)
{
var keys = new List<Keyframe>();
foreach (Match match in Regex.Matches(block, @"(?ms)^\s*- serializedVersion: 3\s*\n(.*?)(?=^\s*- serializedVersion: 3|^\s*m_PreInfinity:|\z)"))
{
string text = match.Groups[1].Value;
string time = Text(text, "time");
if (time.Length == 0) continue;
keys.Add(new Keyframe(Float(time), Component(text, "value", component), Component(text, "inSlope", component), Component(text, "outSlope", component)));
}
return new AnimationCurve(keys.ToArray());
}
private static float Component(string text, string field, string component)
{
string value = Text(text, field);
if (component == null) return Float(value);
Match match = Regex.Match(value, @"\b" + component + @": ([^,}\s]+)");
return match.Success ? Float(match.Groups[1].Value) : 0;
}
private static string Section(string yaml, string name) => Regex.Match(yaml,
@"(?ms)^ " + name + @":(?: \[\])?\s*\n(.*?)(?=^ m_\w+:|\z)").Groups[1].Value;
private static IEnumerable<string> CurveBlocks(string section) => Regex.Matches(section,
@"(?ms)^ - curve:\s*\n(.*?)(?=^ - curve:|\z)").Cast<Match>().Select(m => m.Groups[1].Value);
private static Sprite RecoverSprite(string guid, Dictionary<string, string> sprites,
Dictionary<string, string> textures, Dictionary<string, Sprite> cache)
{
if (cache.TryGetValue(guid, out Sprite found)) return found;
if (!sprites.TryGetValue(guid, out string sourceSprite)) throw new InvalidOperationException("Missing booster sprite GUID: " + guid);
string yaml = File.ReadAllText(sourceSprite);
string textureGuid = Regex.Match(yaml, @"texture: \{fileID: \d+, guid: (\w+)").Groups[1].Value;
string texturePath = Destination + "/" + Path.GetFileName(textures[textureGuid]);
if (!File.Exists(texturePath)) File.Copy(textures[textureGuid], texturePath);
if (AssetImporter.GetAtPath(texturePath) == null) 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();
}
Match rect = Regex.Match(yaml, @"m_Rect:\s+serializedVersion: \d+\s+x: (\S+)\s+y: (\S+)\s+width: (\S+)\s+height: (\S+)");
Vector3 pivot = Vector(yaml, "m_Pivot", Vector3.one * .5f);
Sprite recovered = Sprite.Create(AssetDatabase.LoadAssetAtPath<Texture2D>(texturePath),
new Rect(Float(rect.Groups[1].Value), Float(rect.Groups[2].Value), Float(rect.Groups[3].Value), Float(rect.Groups[4].Value)),
new Vector2(pivot.x, pivot.y), Float(Text(yaml, "m_PixelsToUnits")), 0, SpriteMeshType.FullRect);
recovered.name = Path.GetFileNameWithoutExtension(sourceSprite);
string assetPath = Destination + "/" + recovered.name + ".asset";
Sprite existing = AssetDatabase.LoadAssetAtPath<Sprite>(assetPath);
if (existing == null) { AssetDatabase.CreateAsset(recovered, assetPath); existing = recovered; }
else { EditorUtility.CopySerialized(recovered, existing); UnityEngine.Object.DestroyImmediate(recovered); }
cache[guid] = existing;
return existing;
}
private static Dictionary<string, string> IndexGuids(string folder, string pattern) => Directory.GetFiles(folder, pattern)
.ToDictionary(p => Regex.Match(File.ReadAllText(p), @"(?m)^guid: (\w+)").Groups[1].Value, p => p.Substring(0, p.Length - 5));
private static string Reference(string text, string field) => Regex.Match(text, @"(?m)^\s*" + field + @": \{fileID: (\d+)").Groups[1].Value;
private static string Text(string text, string field) => Regex.Match(text, @"(?m)^\s*" + Regex.Escape(field) + @":([^\r\n]*)").Groups[1].Value.Trim();
private static Vector3 Vector(string text, string field, Vector3 fallback)
{
string value = Text(text, field);
if (value.Length == 0) return fallback;
return new Vector3(Component(text, field, "x"), Component(text, field, "y"), Component(text, field, "z"));
}
private static Quaternion Rotation(string text) => new Quaternion(Component(text, "m_LocalRotation", "x"), Component(text, "m_LocalRotation", "y"), Component(text, "m_LocalRotation", "z"), Component(text, "m_LocalRotation", "w"));
private static Color ColorValue(string text) => new Color(Component(text, "m_Color", "r"), Component(text, "m_Color", "g"), Component(text, "m_Color", "b"), Component(text, "m_Color", "a"));
private static float Float(string text) => text == ".inf" ? float.PositiveInfinity : text == "-.inf" ? float.NegativeInfinity : float.Parse(text, Culture);
}
}