412 lines
30 KiB
C#
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);
|
|
}
|
|
}
|