modular-avatar/Editor/Inspector/MAAssetBundleEditor.cs
kaikoga 3667dc319a
chore: Wrap VRC dependencies and VRCAvatarDescriptors (#504)
* add version defines

* refactor: prefer BuildContext.ctor() overload taking GameObject

* wrap unit tests entirely with MA_VRCSDK3_AVATARS

* wrap unit tests smart with MA_VRCSDK3_AVATARS

* wrap runtime entirely with MA_VRCSDK3_AVATARS

* wrap VRC.SDKBase.IEditorOnly with MA_VRCSDK3_AVATARS

* wrap editor entirely with MA_VRCSDK3_AVATARS

* fix AvatarObjectReference.Get(Component)

* wrap editor smart with MA_VRCSDK3_AVATARS

* wrap BuildContext smart with MA_VRCSDK3_AVATARS

* wrap PluginDefinition smart with MA_VRCSDK3_AVATARS

* style: move conditional compiles one step outside
2023-11-10 15:37:56 +09:00

285 lines
8.9 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using UnityEditor;
using UnityEngine;
#if MA_VRCSDK3_AVATARS
using VRC.SDK3.Avatars.ScriptableObjects;
#endif
using Object = UnityEngine.Object;
namespace nadena.dev.modular_avatar.core.editor
{
[CustomEditor(typeof(MAAssetBundle))]
class MAAssetBundleEditor : MAEditorBase
{
protected override void OnInnerInspectorGUI()
{
if (GUILayout.Button("Unpack"))
{
foreach (var target in targets)
{
MAAssetBundle bundle = (MAAssetBundle) target;
MAAssetBundleExtractor.Unpack(bundle);
}
}
}
}
public class MAAssetBundleExtractor
{
private static readonly ISet<Type> RootAssets = new HashSet<Type>()
{
typeof(Mesh),
typeof(AnimationClip),
typeof(RuntimeAnimatorController),
#if MA_VRCSDK3_AVATARS
typeof(VRCExpressionParameters),
typeof(VRCExpressionsMenu),
#endif
};
private Dictionary<UnityEngine.Object, AssetInfo> _assets;
private MAAssetBundle Bundle;
private HashSet<Object> _unassigned;
private MAAssetBundleExtractor(MAAssetBundle bundle)
{
_assets = GetContainedAssets(bundle);
this.Bundle = bundle;
}
class AssetInfo
{
public readonly UnityEngine.Object Asset;
public readonly HashSet<AssetInfo> IncomingReferences = new HashSet<AssetInfo>();
public readonly HashSet<AssetInfo> OutgoingReferences = new HashSet<AssetInfo>();
public AssetInfo Root;
public AssetInfo(UnityEngine.Object obj)
{
this.Asset = obj;
}
public void PopulateReferences(Dictionary<UnityEngine.Object, AssetInfo> assets)
{
switch (Asset)
{
case Mesh _:
case AnimationClip _:
#if MA_VRCSDK3_AVATARS
case VRCExpressionsMenu _:
case VRCExpressionParameters _:
#endif
return; // No child objects
}
var so = new SerializedObject(Asset);
var prop = so.GetIterator();
// TODO extract to common code
bool enterChildren = true;
while (prop.Next(enterChildren))
{
enterChildren = true;
if (prop.propertyType == SerializedPropertyType.ObjectReference)
{
var value = prop.objectReferenceValue;
if (value != null && assets.TryGetValue(value, out var target))
{
OutgoingReferences.Add(target);
target.IncomingReferences.Add(this);
}
}
else if (prop.propertyType == SerializedPropertyType.String)
{
enterChildren = false;
}
}
}
public void ForceAssignRoot()
{
// First, see if we're reachable only from one root.
HashSet<AssetInfo> visited = new HashSet<AssetInfo>();
HashSet<AssetInfo> roots = new HashSet<AssetInfo>();
Queue<AssetInfo> queue = new Queue<AssetInfo>();
visited.Add(this);
queue.Enqueue(this);
while (queue.Count > 0 && roots.Count < 2)
{
var next = queue.Dequeue();
if (next.Root != null)
{
roots.Add(next.Root);
}
foreach (var outgoingReference in next.IncomingReferences)
{
if (visited.Add(outgoingReference))
{
queue.Enqueue(outgoingReference);
}
}
}
if (roots.Count == 1)
{
this.Root = roots.First();
}
else
{
this.Root = this;
}
}
}
public static void Unpack(MAAssetBundle bundle)
{
try
{
AssetDatabase.StartAssetEditing();
new MAAssetBundleExtractor(bundle).Extract();
}
finally
{
AssetDatabase.StopAssetEditing();
}
}
private bool TryAssignRoot(AssetInfo info)
{
if (info.Root != null)
{
return true;
}
if (RootAssets.Any(t => t.IsInstanceOfType(info.Asset)) || info.IncomingReferences.Count == 0)
{
info.Root = info;
return true;
}
var firstRoot = info.IncomingReferences.First().Root;
if (firstRoot != null && !_unassigned.Contains(firstRoot.Asset)
&& info.IncomingReferences.All(t => t.Root == firstRoot))
{
info.Root = firstRoot;
return true;
}
return false;
}
private void Extract()
{
string path = AssetDatabase.GetAssetPath(Bundle);
var directory = System.IO.Path.GetDirectoryName(path);
_unassigned = new HashSet<UnityEngine.Object>(_assets.Keys);
foreach (var info in _assets.Values)
{
info.PopulateReferences(_assets);
}
var queue = new Queue<UnityEngine.Object>();
while (_unassigned.Count > 0)
{
// Bootstrap
if (queue.Count == 0)
{
_unassigned.Where(o => TryAssignRoot(_assets[o])).ToList().ForEach(o => { queue.Enqueue(o); });
if (queue.Count == 0)
{
_assets[_unassigned.First()].ForceAssignRoot();
queue.Enqueue(_unassigned.First());
}
}
while (queue.Count > 0)
{
var next = queue.Dequeue();
ProcessSingleAsset(directory, next);
_unassigned.Remove(next);
foreach (var outgoingReference in _assets[next].OutgoingReferences)
{
if (_unassigned.Contains(outgoingReference.Asset) && TryAssignRoot(outgoingReference))
{
queue.Enqueue(outgoingReference.Asset);
}
}
}
}
AssetDatabase.DeleteAsset(path);
}
private string AssignAssetFilename(string directory, Object next)
{
string assetName = next.name;
if (string.IsNullOrEmpty(assetName))
{
next.name = next.GetType().Name + " " + GUID.Generate().ToString();
assetName = next.name;
}
string assetFile;
for (int extension = 0;; extension++)
{
assetFile = assetName + (extension == 0 ? "" : $" ({extension})") + ".asset";
assetFile = System.IO.Path.Combine(directory, assetFile);
if (!System.IO.File.Exists(assetFile))
{
break;
}
}
return assetFile;
}
private void ProcessSingleAsset(string directory, Object next)
{
AssetDatabase.RemoveObjectFromAsset(next);
var info = _assets[next];
if (info.Root != info)
{
if (!AssetDatabase.IsMainAsset(info.Root.Asset))
{
throw new Exception(
$"Desired root {info.Root.Asset.name} for asset {next.name} is not a root asset");
}
AssetDatabase.AddObjectToAsset(next, info.Root.Asset);
}
else
{
AssetDatabase.CreateAsset(next, AssignAssetFilename(directory, next));
}
}
private static Dictionary<Object, AssetInfo> GetContainedAssets(MAAssetBundle bundle)
{
string path = AssetDatabase.GetAssetPath(bundle);
var rawAssets = AssetDatabase.LoadAllAssetsAtPath(path);
Dictionary<Object, AssetInfo> infos = new Dictionary<Object, AssetInfo>(rawAssets.Length);
foreach (var asset in rawAssets)
{
if (!(asset is MAAssetBundle))
{
infos.Add(asset, new AssetInfo(asset));
}
}
return infos;
}
}
}