Files
blockout/BlockOutReplica/Assets/Editor/OriginalBoosterNativeRecovery.cs
2026-10-20 13:21:04 +08:00

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;
}
}
}