236 lines
18 KiB
C#
236 lines
18 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Globalization;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Security.Cryptography;
|
|
using System.Text;
|
|
using System.Text.RegularExpressions;
|
|
using BlockOutReplica.Presentation;
|
|
using UnityEditor;
|
|
using UnityEngine;
|
|
|
|
namespace BlockOutReplica.Editor
|
|
{
|
|
public static partial class OriginalBoosterRecovery
|
|
{
|
|
private const string NativeFolder = Destination + "/Native";
|
|
private static Dictionary<string, string> nativeIndex;
|
|
private static HashSet<string> nativeCopied;
|
|
|
|
private static void RecoverNativeVisuals(string source, Document[] docs, Document root, string rootPath,
|
|
Func<string, string> absolutePath, HashSet<string> includedObjects, OriginalBoosterAnimationData data, string clipYaml)
|
|
{
|
|
nativeIndex = new Dictionary<string, string>();
|
|
nativeCopied = new HashSet<string>();
|
|
foreach (string folder in new[] {"Sprite", "Texture2D", "Material", "Mesh", "Shader"})
|
|
foreach (string meta in Directory.GetFiles(Path.Combine(source, folder), "*.meta"))
|
|
{
|
|
string guid = Regex.Match(File.ReadAllText(meta), @"(?m)^guid: (\w+)").Groups[1].Value;
|
|
if (guid.Length > 0) nativeIndex[guid] = meta.Substring(0, meta.Length - 5);
|
|
}
|
|
string rootObject = Reference(root.Text, "m_GameObject");
|
|
var visualObjects = new HashSet<string>(includedObjects) {rootObject};
|
|
// Native particle modules, renderers and sprite geometry are retained verbatim.
|
|
// Animator and stripped game MonoBehaviours are replaced by our explicit sampling adapter.
|
|
var allowed = new HashSet<int> {1, 4, 198, 199, 210, 212};
|
|
var kept = docs.Where(d => allowed.Contains(d.Type) && visualObjects.Contains(d.Type == 1 ? d.Id : Reference(d.Text, "m_GameObject")))
|
|
.ToDictionary(d => d.Id);
|
|
var output = new StringBuilder("%YAML 1.1\n%TAG !u! tag:unity3d.com,2011:\n");
|
|
foreach (Document doc in docs.Where(d => kept.ContainsKey(d.Id)))
|
|
{
|
|
string text = doc.Text;
|
|
if (doc.Type == 1)
|
|
text = Regex.Replace(text, @"(?m)^ - component: \{fileID: (\d+)\}\r?\n", m => kept.ContainsKey(m.Groups[1].Value) ? m.Value : "");
|
|
if (doc.Type == 4)
|
|
{
|
|
text = Regex.Replace(text, @"(?m)^ - \{fileID: (\d+)\}\r?\n", m => kept.ContainsKey(m.Groups[1].Value) ? m.Value : "");
|
|
if (doc.Id == root.Id)
|
|
{
|
|
text = Regex.Replace(text, @"(?m)^ m_Father:.*", " m_Father: {fileID: 0}");
|
|
text = Regex.Replace(text, @"(?m)^ m_LocalPosition:.*", " m_LocalPosition: {x: 0, y: 0, z: 0}");
|
|
text = Regex.Replace(text, @"(?m)^ m_LocalRotation:.*", " m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}");
|
|
text = Regex.Replace(text, @"(?m)^ m_LocalScale:.*", " m_LocalScale: {x: 1, y: 1, z: 1}");
|
|
}
|
|
}
|
|
if (doc.Type == 198) text = Regex.Replace(text, @"(?m)^ playOnAwake: \d+", " playOnAwake: 0");
|
|
// Scene-only local references outside the copied visual subtree cannot survive extraction.
|
|
text = Regex.Replace(text, @"\{fileID: (\d+)\}", m => m.Groups[1].Value == "0" || kept.ContainsKey(m.Groups[1].Value) ? m.Value : "{fileID: 0}");
|
|
foreach (Match guid in Regex.Matches(text, @"guid: (\w+)")) CopyNativeAsset(guid.Groups[1].Value, source);
|
|
output.Append("--- !u!").Append(doc.Type).Append(" &").Append(doc.Id).Append('\n').Append(RemapNative(text));
|
|
}
|
|
Directory.CreateDirectory(NativeFolder + "/Prefabs");
|
|
string prefabPath = NativeFolder + "/Prefabs/" + data.name + ".prefab";
|
|
File.WriteAllText(prefabPath, output.ToString());
|
|
if (!File.Exists(prefabPath + ".meta"))
|
|
File.WriteAllText(prefabPath + ".meta", "fileFormatVersion: 2\nguid: " + NamespaceGuid("prefab/" + data.name) + "\nPrefabImporter:\n externalObjects: {}\n");
|
|
AssetDatabase.Refresh(ImportAssetOptions.ForceSynchronousImport);
|
|
data.VisualPrefab = AssetDatabase.LoadAssetAtPath<GameObject>(prefabPath);
|
|
if (data.VisualPrefab == null) throw new InvalidOperationException("Native booster prefab failed to import: " + prefabPath);
|
|
data.RecoveredParticleSystems = data.VisualPrefab.GetComponentsInChildren<ParticleSystem>(true).Length;
|
|
data.UnrecoveredParticleSystems -= data.RecoveredParticleSystems;
|
|
data.ParticleStarts = docs.Where(d => d.Type == 198 && includedObjects.Contains(Reference(d.Text, "m_GameObject")))
|
|
.Select(d => {
|
|
Document transform = docs.First(t => t.Type == 4 && Reference(t.Text, "m_GameObject") == Reference(d.Text, "m_GameObject"));
|
|
return new OriginalBoosterAnimationData.ParticleStart {
|
|
Path = absolutePath(transform.Id).Substring(rootPath.Length + 1),
|
|
PlayOnAwake = Text(d.Text, "playOnAwake") == "1"
|
|
};
|
|
}).ToArray();
|
|
data.SourceEvents = Regex.Matches(clipYaml, @"(?m)^ - time: (\S+)\r?\n functionName: ([^\r\n]+)")
|
|
.Cast<Match>().Select(m => new OriginalBoosterAnimationData.SourceEvent {Time = Float(m.Groups[1].Value), Name = m.Groups[2].Value.Trim()}).ToArray();
|
|
if (data.name == "Ufo")
|
|
{
|
|
foreach (var sourceEvent in data.SourceEvents)
|
|
{
|
|
if (sourceEvent.Name == "PlayParticles")
|
|
sourceEvent.ReconstructedTargets = data.Nodes.Where(n => !n.Active && n.Path.StartsWith("Ufo_Main/Particle System", StringComparison.Ordinal))
|
|
.Select(n => n.Path).ToArray();
|
|
else if (sourceEvent.Name == "PlayParticles2") sourceEvent.ReconstructedTargets = new[] {"Ufo_Main/Vortex"};
|
|
}
|
|
}
|
|
if (data.name == "FreezeTimerStart")
|
|
foreach (var sourceEvent in data.SourceEvents)
|
|
sourceEvent.ReconstructedTargets = sourceEvent.Name == "PlayParticles2" ? new[] {"TimerFx", "clock_main/cold_smoke_particle", "clock_main/cold_smoke_particle (1)"} :
|
|
new[] {"FreeAnchor", "Top/Fx", "Bottom/Fx", "Left/Fx", "Right/Fx"};
|
|
// The event timestamps/names are original. The stripped methods' particle lists are not available.
|
|
// Expose this limitation rather than silently presenting an invented target list as recovered code.
|
|
data.EventBindingNote = data.SourceEvents.Length > 0 ? "Original event times/names recovered; event-to-particle binding reconstructed from active visual branches." : "No source animation events.";
|
|
data.Skins = RecoverSkins(docs, includedObjects, rootPath, absolutePath, data);
|
|
data.UnrecoveredSpriteSkins -= data.Skins.Length;
|
|
Debug.Log($"BLOCKOUT_BOOSTER_NATIVE_RECOVERED: name={data.name}, particles={data.RecoveredParticleSystems}, skins={data.Skins.Length}, events={data.SourceEvents.Length}, dependencies={nativeCopied.Count}, shaderCompatibilityAdapter=True");
|
|
}
|
|
|
|
private static string NamespaceGuid(string guid)
|
|
{
|
|
using (var hash = MD5.Create()) return string.Concat(hash.ComputeHash(Encoding.UTF8.GetBytes("blockout-original-booster-native/" + guid)).Select(b => b.ToString("x2", CultureInfo.InvariantCulture)));
|
|
}
|
|
|
|
private static string RemapNative(string text) => Regex.Replace(text, @"guid: (\w+)", m => "guid: " + (nativeIndex.ContainsKey(m.Groups[1].Value) ? NamespaceGuid(m.Groups[1].Value) : m.Groups[1].Value));
|
|
|
|
private static void CopyNativeAsset(string guid, string source)
|
|
{
|
|
if (!nativeIndex.TryGetValue(guid, out string original) || original.EndsWith(".shader", StringComparison.OrdinalIgnoreCase) || !nativeCopied.Add(guid)) return;
|
|
string relative = original.Substring(source.Length + 1).Replace('\\', '/');
|
|
string target = NativeFolder + "/" + relative;
|
|
Directory.CreateDirectory(Path.GetDirectoryName(target));
|
|
string extension = Path.GetExtension(original).ToLowerInvariant();
|
|
if (extension == ".png")
|
|
{
|
|
File.Copy(original, target, true);
|
|
string meta = RemapNative(File.ReadAllText(original + ".meta"));
|
|
// Keep imported atlas pixels exact; no NPOT or compression changes to source UV layout.
|
|
meta = Regex.Replace(meta, @"(?m)^(\s*)maxTextureSize: \d+", "$1maxTextureSize: 8192");
|
|
meta = Regex.Replace(meta, @"(?m)^(\s*)textureCompression: \d+", "$1textureCompression: 0");
|
|
meta = Regex.Replace(meta, @"(?m)^(\s*)nPOTScale: \d+", "$1nPOTScale: 0");
|
|
File.WriteAllText(target + ".meta", meta);
|
|
return;
|
|
}
|
|
string text = File.ReadAllText(original);
|
|
foreach (Match dependency in Regex.Matches(text, @"guid: (\w+)")) CopyNativeAsset(dependency.Groups[1].Value, source);
|
|
if (extension == ".mat")
|
|
{
|
|
bool additive = Path.GetFileName(original).IndexOf("Additive", StringComparison.OrdinalIgnoreCase) >= 0;
|
|
string shaderGuid = Regex.Match(text, @"m_Shader: \{fileID: \d+, guid: (\w+)").Groups[1].Value;
|
|
if (nativeIndex.TryGetValue(shaderGuid, out string shaderSource))
|
|
{
|
|
string shaderName = File.ReadLines(shaderSource).FirstOrDefault() ?? "";
|
|
additive |= shaderName.IndexOf("Additive", StringComparison.OrdinalIgnoreCase) >= 0;
|
|
}
|
|
string compatibleShader = AssetDatabase.AssetPathToGUID("Assets/Resources/RecoveredFx" + (additive ? "Additive" : "Alpha") + ".shader");
|
|
text = Regex.Replace(text, @"m_Shader: \{[^\r\n]+\}", "m_Shader: {fileID: 4800000, guid: " + compatibleShader + ", type: 3}");
|
|
}
|
|
File.WriteAllText(target, RemapNative(text));
|
|
File.WriteAllText(target + ".meta", RemapNative(File.ReadAllText(original + ".meta")));
|
|
}
|
|
|
|
private static OriginalBoosterAnimationData.Skin[] RecoverSkins(Document[] docs, HashSet<string> includedObjects,
|
|
string rootPath, Func<string, string> absolutePath, OriginalBoosterAnimationData data)
|
|
{
|
|
var result = new List<OriginalBoosterAnimationData.Skin>();
|
|
foreach (Document renderer in docs.Where(d => d.Type == 212 && includedObjects.Contains(Reference(d.Text, "m_GameObject"))))
|
|
{
|
|
string spriteGuid = Regex.Match(renderer.Text, @"m_Sprite: \{fileID: \d+, guid: (\w+)").Groups[1].Value;
|
|
if (!nativeIndex.TryGetValue(spriteGuid, out string spritePath)) continue;
|
|
string yaml = File.ReadAllText(spritePath);
|
|
string bonesText = Regex.Match(yaml, @"(?ms)^ m_Bones:\s*\n(.*?)(?=^ m_\w+:|\z)").Groups[1].Value;
|
|
string[] boneNames = Regex.Matches(bonesText, @"(?m)^ - name: ([^\r\n]+)").Cast<Match>().Select(m => m.Groups[1].Value.Trim()).ToArray();
|
|
if (boneNames.Length == 0) continue;
|
|
Document transform = docs.First(t => t.Type == 4 && Reference(t.Text, "m_GameObject") == Reference(renderer.Text, "m_GameObject"));
|
|
string path = absolutePath(transform.Id).Substring(rootPath.Length + 1);
|
|
string[] bonePaths = boneNames.Select(n => path + "/" + n).ToArray();
|
|
if (!bonePaths.All(p => data.Nodes.Any(n => n.Path == p)))
|
|
{ Debug.LogWarning("Source skin bone mapping unavailable: " + path); continue; }
|
|
Mesh mesh = RecoverSkinMesh(yaml, Path.GetFileNameWithoutExtension(spritePath));
|
|
string meshPath = NativeFolder + "/" + data.name + "-" + mesh.name + "-Skin.asset";
|
|
var old = AssetDatabase.LoadAssetAtPath<Mesh>(meshPath);
|
|
if (old == null) AssetDatabase.CreateAsset(mesh, meshPath);
|
|
else { EditorUtility.CopySerialized(mesh, old); UnityEngine.Object.DestroyImmediate(mesh); mesh = old; }
|
|
string recoveredSpritePath = NativeFolder + "/Sprite/" + Path.GetFileName(spritePath);
|
|
var sprite = AssetDatabase.LoadAssetAtPath<Sprite>(recoveredSpritePath);
|
|
var material = new Material(Shader.Find("BlockOut/RecoveredFxAlpha")) {name = mesh.name + " Original Skin Material", mainTexture = sprite.texture};
|
|
string materialPath = NativeFolder + "/" + data.name + "-" + mesh.name + "-Skin.mat";
|
|
var oldMaterial = AssetDatabase.LoadAssetAtPath<Material>(materialPath);
|
|
if (oldMaterial == null) AssetDatabase.CreateAsset(material, materialPath);
|
|
else { EditorUtility.CopySerialized(material, oldMaterial); UnityEngine.Object.DestroyImmediate(material); material = oldMaterial; }
|
|
result.Add(new OriginalBoosterAnimationData.Skin {Path = path, Mesh = mesh, Material = material, BonePaths = bonePaths});
|
|
}
|
|
return result.ToArray();
|
|
}
|
|
|
|
private static Mesh RecoverSkinMesh(string yaml, string name)
|
|
{
|
|
string vertexSection = Regex.Match(yaml, @"(?ms)^ m_VertexData:\s*\n(.*?)(?=^ m_Bindpose:)").Groups[1].Value;
|
|
int count = int.Parse(Text(vertexSection, "m_VertexCount"), Culture);
|
|
var channels = Regex.Matches(vertexSection, @"- stream: (\d+)\s+offset: (\d+)\s+format: (\d+)\s+dimension: (\d+)")
|
|
.Cast<Match>().Select(m => new[] {int.Parse(m.Groups[1].Value), int.Parse(m.Groups[2].Value), int.Parse(m.Groups[3].Value), int.Parse(m.Groups[4].Value)}).ToArray();
|
|
byte[] bytes = Hex(Text(vertexSection, "_typelessdata"));
|
|
var strides = new Dictionary<int, int>();
|
|
foreach (int[] channel in channels.Where(c => c[3] != 0))
|
|
{
|
|
if (channel[2] != 0 && channel[2] != 10) throw new InvalidOperationException("Unsupported source skin vertex format " + channel[2]);
|
|
strides[channel[0]] = Mathf.Max(strides.TryGetValue(channel[0], out int stride) ? stride : 0, channel[1] + channel[3] * 4);
|
|
}
|
|
var offsets = new Dictionary<int, int>(); int offset = 0;
|
|
foreach (var stream in strides.OrderBy(p => p.Key)) {offsets[stream.Key] = offset; offset = (offset + stream.Value * count + 15) & ~15;}
|
|
Func<int, int, int, int> address = (channel, vertex, component) => offsets[channels[channel][0]] + vertex * strides[channels[channel][0]] + channels[channel][1] + component * 4;
|
|
var vertices = new Vector3[count]; var uv = new Vector2[count]; var weights = new BoneWeight[count];
|
|
Match rect = Regex.Match(yaml, @"m_Rect:\s+serializedVersion: \d+\s+x: (\S+)\s+y: (\S+)\s+width: (\S+)\s+height: (\S+)");
|
|
float x = Float(rect.Groups[1].Value), y = Float(rect.Groups[2].Value), width = Float(rect.Groups[3].Value), height = Float(rect.Groups[4].Value);
|
|
string textureGuid = Regex.Match(yaml, @"texture: \{fileID: \d+, guid: (\w+)").Groups[1].Value;
|
|
string texturePath = NativeFolder + "/Texture2D/" + Path.GetFileName(nativeIndex[textureGuid]);
|
|
var texture = AssetDatabase.LoadAssetAtPath<Texture2D>(texturePath);
|
|
float ppu = Float(Text(yaml, "m_PixelsToUnits")); Vector3 pivot = Vector(yaml, "m_Pivot", Vector3.one * .5f);
|
|
for (int i = 0; i < count; i++)
|
|
{
|
|
vertices[i] = new Vector3(BitConverter.ToSingle(bytes, address(0, i, 0)), BitConverter.ToSingle(bytes, address(0, i, 1)), BitConverter.ToSingle(bytes, address(0, i, 2)));
|
|
uv[i] = new Vector2((x + vertices[i].x * ppu + pivot.x * width) / texture.width, (y + vertices[i].y * ppu + pivot.y * height) / texture.height);
|
|
weights[i] = new BoneWeight {
|
|
weight0 = BitConverter.ToSingle(bytes, address(12, i, 0)), weight1 = BitConverter.ToSingle(bytes, address(12, i, 1)),
|
|
weight2 = BitConverter.ToSingle(bytes, address(12, i, 2)), weight3 = BitConverter.ToSingle(bytes, address(12, i, 3)),
|
|
boneIndex0 = BitConverter.ToInt32(bytes, address(13, i, 0)), boneIndex1 = BitConverter.ToInt32(bytes, address(13, i, 1)),
|
|
boneIndex2 = BitConverter.ToInt32(bytes, address(13, i, 2)), boneIndex3 = BitConverter.ToInt32(bytes, address(13, i, 3))};
|
|
}
|
|
string bindSection = Regex.Match(yaml, @"(?ms)^ m_Bindpose:\s*\n(.*?)(?=^ textureRect:)").Groups[1].Value;
|
|
Matrix4x4[] bindposes = Regex.Matches(bindSection, @"(?ms)^ - e00:.*?(?=^ - e00:|\z)").Cast<Match>().Select(m => {
|
|
var matrix = new Matrix4x4();
|
|
for (int row = 0; row < 4; row++) for (int column = 0; column < 4; column++)
|
|
matrix[row, column] = Float(Regex.Match(m.Value, @"\be" + row + column + @": (\S+)").Groups[1].Value);
|
|
return matrix;
|
|
}).ToArray();
|
|
byte[] indices = Hex(Text(yaml, "m_IndexBuffer"));
|
|
var triangles = new int[indices.Length / 2];
|
|
for (int i = 0; i < triangles.Length; i++) triangles[i] = BitConverter.ToUInt16(indices, i * 2);
|
|
var mesh = new Mesh {name = name, vertices = vertices, uv = uv, triangles = triangles, boneWeights = weights, bindposes = bindposes};
|
|
mesh.RecalculateBounds(); return mesh;
|
|
}
|
|
|
|
private static byte[] Hex(string text)
|
|
{
|
|
var bytes = new byte[text.Length / 2];
|
|
for (int i = 0; i < bytes.Length; i++) bytes[i] = byte.Parse(text.Substring(i * 2, 2), NumberStyles.HexNumber, Culture);
|
|
return bytes;
|
|
}
|
|
}
|
|
}
|