Skip to content

Commit

Permalink
add impl
Browse files Browse the repository at this point in the history
  • Loading branch information
poi-vrc committed Sep 5, 2023
1 parent 7dfb44c commit 00ceab0
Show file tree
Hide file tree
Showing 10 changed files with 524 additions and 196 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
*/

using System.Collections.Generic;
using System.Collections.ObjectModel;
using Chocopoi.DressingTools.Lib;
using Chocopoi.DressingTools.Lib.Cabinet;
using Chocopoi.DressingTools.Lib.Extensibility.Providers;
Expand Down Expand Up @@ -156,38 +157,43 @@ private bool ApplyWearable(ApplyWearableContext wearCtx)
wearableObj = Object.Instantiate(wearCtx.wearableGameObject, _cabCtx.avatarGameObject.transform);
}

// sort modules according to their apply order
var modules = new List<WearableModule>(wearCtx.wearableConfig.modules);
modules.Sort((m1, m2) =>
var moduleByProvider = new Dictionary<WearableModuleProviderBase, List<WearableModule>>();

// prepare module by provider
foreach (var module in wearCtx.wearableConfig.modules)
{
var m1Provider = WearableModuleProviderLocator.Instance.GetProvider(m1.moduleName);
var m2Provider = WearableModuleProviderLocator.Instance.GetProvider(m2.moduleName);
// locate the module provider
var provider = WearableModuleProviderLocator.Instance.GetProvider(module.moduleName);

if (m1Provider == null)
if (provider == null)
{
return -1;
DTReportUtils.LogErrorLocalized(_cabCtx.report, LogLabel, MessageCode.ModuleHasNoProviderAvailable, module.moduleName);
return false;
}
else if (m2Provider == null)

if (moduleByProvider.TryGetValue(provider, out var groupedList))
{
return 1;
groupedList = new List<WearableModule>();
moduleByProvider[provider] = groupedList;
}
groupedList.Add(module);
}

return m1Provider.CallOrder.CompareTo(m2Provider.CallOrder);
});
// sort providers according to their call order
var allProviders = WearableModuleProviderLocator.Instance.GetAllProviders();
var sortedProviderList = new List<WearableModuleProviderBase>(allProviders);
sortedProviderList.Sort((m1, m2) => m1.CallOrder.CompareTo(m2.CallOrder));

// do module apply
foreach (var module in modules)
// call provider callbacks
foreach (var provider in allProviders)
{
// locate the module provider
var provider = WearableModuleProviderLocator.Instance.GetProvider(module.moduleName);

if (provider == null)
if (moduleByProvider.TryGetValue(provider, out var modules))
{
DTReportUtils.LogErrorLocalized(_cabCtx.report, LogLabel, MessageCode.ModuleHasNoProviderAvailable, module.moduleName);
return false;
// create a empty list
modules = new List<WearableModule>();
}

if (!provider.OnApplyWearable(_cabCtx, wearCtx, module))
if (!provider.OnApplyWearable(_cabCtx, wearCtx, new ReadOnlyCollection<WearableModule>(modules)))
{
DTReportUtils.LogErrorLocalized(_cabCtx.report, LogLabel, MessageCode.ApplyingModuleHasErrors);
return false;
Expand Down
85 changes: 63 additions & 22 deletions Packages/com.chocopoi.vrc.dressingtools/Editor/DTEditorUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
*/

using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using Chocopoi.AvatarLib.Animations;
Expand All @@ -27,6 +28,7 @@
using Chocopoi.DressingTools.Lib.Proxy;
using Chocopoi.DressingTools.Lib.Wearable;
using Chocopoi.DressingTools.Lib.Wearable.Modules;
using Chocopoi.DressingTools.Logging;
using Chocopoi.DressingTools.Proxy;
using Newtonsoft.Json;
using UnityEditor;
Expand Down Expand Up @@ -198,25 +200,64 @@ public static bool AddCabinetWearable(CabinetConfig cabinetConfig, GameObject av
cabinetWearable.wearableGameObject = wearableGameObject;
cabinetWearable.configJson = wearableConfig.Serialize();

// do provider hooks
var providers = WearableModuleProviderLocator.Instance.GetAllProviders();
foreach (var provider in providers)
DoWearableModuleProviderCallbacks(wearableConfig.modules, (WearableModuleProviderBase provider, List<WearableModule> modules) =>
{
var module = FindWearableModule(wearableConfig, provider.ModuleIdentifier);
if (module == null)
if (!provider.OnAddWearableToCabinet(cabinetConfig, avatarGameObject, wearableConfig, wearableGameObject, new ReadOnlyCollection<WearableModule>(modules)))
{
// config does not have such module
continue;
Debug.LogWarning("[DressingTools] [AddCabinetWearable] Error processing provider OnAddWearableToCabinet hook: " + provider.ModuleIdentifier);
return false;
}
return true;
});

if (!provider.OnAddWearableToCabinet(cabinetConfig, avatarGameObject, wearableConfig, wearableGameObject, module))
return true;
}

private static bool DoWearableModuleProviderCallbacks(List<WearableModule> modules, System.Func<WearableModuleProviderBase, List<WearableModule>, bool> callback)
{
var moduleByProvider = new Dictionary<WearableModuleProviderBase, List<WearableModule>>();

// prepare module by provider
foreach (var module in modules)
{
// locate the module provider
var provider = WearableModuleProviderLocator.Instance.GetProvider(module.moduleName);

if (provider == null)
{
Debug.LogWarning("[DressingTools] [AddCabinetWearable] Error processing provider OnAddWearableToCabinet hook: " + provider.ModuleIdentifier);
Debug.Log("[DressingTools] Missing wearable module provider detected: " + module.moduleName);
return false;
}

if (moduleByProvider.TryGetValue(provider, out var groupedList))
{
groupedList = new List<WearableModule>();
moduleByProvider[provider] = groupedList;
}
groupedList.Add(module);
}

return true;
// sort providers according to their call order
var allProviders = WearableModuleProviderLocator.Instance.GetAllProviders();
var sortedProviderList = new List<WearableModuleProviderBase>(allProviders);
sortedProviderList.Sort((m1, m2) => m1.CallOrder.CompareTo(m2.CallOrder));

// call provider callbacks
foreach (var provider in allProviders)
{
if (moduleByProvider.TryGetValue(provider, out var providerModules))
{
// create a empty list
modules = new List<WearableModule>();
}

if (!callback.Invoke(provider, providerModules))
{
return false;
}
}

return false;
}

public static void RemoveCabinetWearable(DTCabinet cabinet, DTCabinetWearable wearable)
Expand Down Expand Up @@ -703,23 +744,23 @@ public static void UpdatePreviewAvatar(GameObject targetAvatar, WearableConfig n
wearableContexts = new Dictionary<DTCabinetWearable, ApplyWearableContext>()
};

var providers = WearableModuleProviderLocator.Instance.GetAllProviders();
foreach (var provider in providers)
var wearCtx = new ApplyWearableContext()
{
wearableConfig = newWearableConfig,
wearableGameObject = previewWearable,
wearableDynamics = ScanDynamics(previewWearable)
};

DoWearableModuleProviderCallbacks(newWearableConfig.modules, (WearableModuleProviderBase provider, List<WearableModule> modules) =>
{
var wearCtx = new ApplyWearableContext()
{
wearableConfig = newWearableConfig,
wearableGameObject = previewWearable,
wearableDynamics = ScanDynamics(previewWearable)
};
var module = FindWearableModule(newWearableConfig, provider.ModuleIdentifier);
if (!provider.OnPreviewWearable(cabCtx, wearCtx, module))
if (!provider.OnPreviewWearable(cabCtx, wearCtx, new ReadOnlyCollection<WearableModule>(modules)))
{
Debug.LogError("[DressingTools] Error applying wearable in preview!");
return;
return false;
}
}
return true;
});
}

public static void PreviewAvatar(GameObject targetAvatar, GameObject targetWearable, out GameObject previewAvatar, out GameObject previewWearable)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
/*
* File: VRCEditorUtils.cs
* Project: DressingTools
* Created Date: Tuesday, September 5th 2023, 9:41:36 am
* Author: chocopoi ([email protected])
* -----
* Copyright (c) 2023 chocopoi
*
* This file is part of DressingTools.
*
* DressingTools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
*
* DressingTools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with DressingTools. If not, see <https://www.gnu.org/licenses/>.
*/

#if VRC_SDK_VRCSDK3
using Chocopoi.DressingTools.Cabinet;
using UnityEditor;
using UnityEditor.Animations;
using VRC.SDK3.Avatars.Components;
using VRC.SDK3.Avatars.ScriptableObjects;

namespace Chocopoi.DressingTools.Integrations.VRChat
{
internal static class VRCEditorUtils
{
private const string ExpressionParametersAssetName = "cpDT_VRC_ExParams";
private const string ExpressionMenuAssetName = "cpDT_VRC_ExMenu";
private const string AnimLayerAssetNamePrefix = "cpDT_VRC_AnimLayer_";

public static VRCExpressionParameters CopyAndReplaceExpressionParameters(VRCAvatarDescriptor avatarDescriptor)
{
var expressionParameters = avatarDescriptor.expressionParameters;

if (expressionParameters == null)
{
expressionParameters = AssetDatabase.LoadAssetAtPath<VRCExpressionParameters>("Packages/com.vrchat.avatars/Samples/AV3 Demo Assets/Expressions Menu/DefaultExpressionParameters.asset");
if (expressionParameters == null)
{
expressionParameters = AssetDatabase.LoadAssetAtPath<VRCExpressionParameters>("Assets/VRCSDK/Examples3/Animation/Controllers/Expressions Menu/DefaultExpressionParameters.asset");
}

if (expressionParameters == null)
{
// we can't obtain the default asset
return null;
}
}

// do not copy again if we have copied before
if (expressionParameters.name == ExpressionParametersAssetName)
{
return expressionParameters;
}

var copiedPath = string.Format("{0}/{1}.asset", CabinetApplier.GeneratedAssetsPath, ExpressionParametersAssetName);
AssetDatabase.CopyAsset(AssetDatabase.GetAssetPath(expressionParameters), copiedPath);
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
AssetDatabase.ImportAsset(copiedPath);
var copiedParams = AssetDatabase.LoadAssetAtPath<VRCExpressionParameters>(copiedPath);
avatarDescriptor.expressionParameters = copiedParams;
return copiedParams;
}

public static VRCExpressionsMenu CopyAndReplaceExpressionMenu(VRCAvatarDescriptor avatarDescriptor)
{
var expressionsMenu = avatarDescriptor.expressionsMenu;

if (expressionsMenu == null)
{
expressionsMenu = AssetDatabase.LoadAssetAtPath<VRCExpressionsMenu>("Packages/com.vrchat.avatars/Samples/AV3 Demo Assets/Expressions Menu/DefaultExpressionsMenu.asset");
if (expressionsMenu == null)
{
expressionsMenu = AssetDatabase.LoadAssetAtPath<VRCExpressionsMenu>("Assets/VRCSDK/Examples3/Animation/Controllers/Expressions Menu/DefaultExpressionsMenu.asset");
}

if (expressionsMenu == null)
{
// we can't obtain the default asset
return null;
}
}

// do not copy again if we have copied before
if (expressionsMenu.name == ExpressionMenuAssetName)
{
return expressionsMenu;
}

var copiedPath = string.Format("{0}/{1}.asset", CabinetApplier.GeneratedAssetsPath, ExpressionMenuAssetName);
AssetDatabase.CopyAsset(AssetDatabase.GetAssetPath(expressionsMenu), copiedPath);
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
AssetDatabase.ImportAsset(copiedPath);
var copiedMenu = AssetDatabase.LoadAssetAtPath<VRCExpressionsMenu>(copiedPath);
avatarDescriptor.expressionsMenu = copiedMenu;
return copiedMenu;
}

public static AnimatorController CopyAndReplaceLayerAnimator(VRCAvatarDescriptor avatarDescriptor, VRCAvatarDescriptor.AnimLayerType animLayerType)
{
var customAnimLayerIndex = -1;
for (var i = 0; i < avatarDescriptor.baseAnimationLayers.Length; i++)
{
if (avatarDescriptor.baseAnimationLayers[i].type == animLayerType)
{
customAnimLayerIndex = i;
break;
}
}

if (customAnimLayerIndex == -1)
{
return null;
}

var animLayer = avatarDescriptor.baseAnimationLayers[customAnimLayerIndex];
var animator = GetAnimLayerAnimator(animLayer);

// do not copy again if we have copied before
if (animator.name.StartsWith(AnimLayerAssetNamePrefix))
{
return animator;
}

// copy to our asset path
var copiedPath = string.Format("{0}/{1}_{2}.controller", CabinetApplier.GeneratedAssetsPath, AnimLayerAssetNamePrefix, animLayerType.ToString());
AssetDatabase.CopyAsset(AssetDatabase.GetAssetPath(animator), copiedPath);
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
AssetDatabase.ImportAsset(copiedPath);

// get back here
var copiedAnimator = AssetDatabase.LoadAssetAtPath<AnimatorController>(copiedPath);
animLayer.animatorController = copiedAnimator;
avatarDescriptor.baseAnimationLayers[customAnimLayerIndex] = animLayer;

return copiedAnimator;
}

public static AnimatorController GetAnimLayerAnimator(VRCAvatarDescriptor.CustomAnimLayer animLayer)
{
return !animLayer.isDefault && animLayer.animatorController != null && animLayer.animatorController is AnimatorController controller ?
controller :
GetDefaultLayerAnimator(animLayer.type);
}

public static AnimatorController GetDefaultLayerAnimator(VRCAvatarDescriptor.AnimLayerType animLayerType)
{
string defaultControllerName = null;
switch (animLayerType)
{
case VRCAvatarDescriptor.AnimLayerType.Base:
defaultControllerName = "Locomotion";
break;
case VRCAvatarDescriptor.AnimLayerType.Additive:
defaultControllerName = "Idle";
break;
case VRCAvatarDescriptor.AnimLayerType.Action:
defaultControllerName = "Action";
break;
case VRCAvatarDescriptor.AnimLayerType.Gesture:
defaultControllerName = "Hands";
break;
case VRCAvatarDescriptor.AnimLayerType.FX:
defaultControllerName = "Face";
break;
case VRCAvatarDescriptor.AnimLayerType.Sitting:
defaultControllerName = "Sitting";
break;
case VRCAvatarDescriptor.AnimLayerType.IKPose:
defaultControllerName = "UtilityIKPose";
break;
case VRCAvatarDescriptor.AnimLayerType.TPose:
defaultControllerName = "UtilityTPose";
break;
}

if (defaultControllerName == null)
{
return null;
}

var controller = AssetDatabase.LoadAssetAtPath<AnimatorController>("Packages/com.vrchat.avatars/Samples/AV3 Demo Assets/Animation/Controllers/vrc_AvatarV3" + defaultControllerName + "Layer.controller");
if (controller == null)
{
controller = AssetDatabase.LoadAssetAtPath<AnimatorController>("Assets/VRCSDK/Examples3/Animation/Controllers/vrc_AvatarV3" + defaultControllerName + "Layer.controller");
}
return controller;
}
}
}
#endif
Loading

0 comments on commit 00ceab0

Please sign in to comment.