diff --git a/.github/unity-project/Packages/vpm-manifest.2022.3.6f1.json b/.github/unity-project/Packages/vpm-manifest.2022.3.6f1.json index 3c4cf5a..6a29124 100644 --- a/.github/unity-project/Packages/vpm-manifest.2022.3.6f1.json +++ b/.github/unity-project/Packages/vpm-manifest.2022.3.6f1.json @@ -1,21 +1,21 @@ { "dependencies": { "com.vrchat.avatars": { - "version": "3.5.0" + "version": "3.5.1" }, "com.vrchat.core.vpm-resolver": { - "version": "0.1.27" + "version": "0.1.28" } }, "locked": { "com.vrchat.avatars": { - "version": "3.5.0", + "version": "3.5.1", "dependencies": { - "com.vrchat.base": "3.5.0" + "com.vrchat.base": "3.5.1" } }, "com.vrchat.base": { - "version": "3.5.0", + "version": "3.5.1", "dependencies": {} } } diff --git a/.github/workflows/unity-tests.yml b/.github/workflows/unity-tests.yml index 72f69a0..fbac3ed 100644 --- a/.github/workflows/unity-tests.yml +++ b/.github/workflows/unity-tests.yml @@ -2,6 +2,12 @@ name: Unity Tests on: push: + branches: master + paths: + - "**.cs" + - ".github/unity-project/**/*" + - ".github/workflows/unity-tests.yml" + pull_request: paths: - "**.cs" - ".github/unity-project/**/*" @@ -9,14 +15,14 @@ on: jobs: build: - name: Build and Test (${{ matrix.unity-version }}${{ matrix.import-vrcsdk && ', VRC' || '' }}${{ matrix.import-dynbone && ', DynBone' || '' }}${{ matrix.import-univrm && ', VRM' || '' }}) + name: Build and Test (${{ matrix.unity-version }}${{ matrix.import-vrcsdk && ', VRC' || '' }}${{ matrix.import-dynbone && ', DynBone' || '' }}${{ matrix.import-univrm && ', VRM' || '' }}${{ matrix.import-ma && ', MA' || '' }}) runs-on: ubuntu-latest strategy: fail-fast: true - max-parallel: 6 matrix: unity-version: ['2019.4.31f1', '2022.3.6f1', '2023.2.2f1'] import-vrcsdk: [false, true] + import-ma: [false, true] # import-dynbone: [false, true] # import-univrm: [false, true] import-dynbone: [false] @@ -24,6 +30,8 @@ jobs: exclude: # - unity-version: "2019.4.31f1" # import-univrm: true + - import-vrcsdk: false + import-ma: true - unity-version: "2023.2.2f1" import-vrcsdk: true steps: @@ -63,8 +71,15 @@ jobs: run: | mv Packages/vpm-manifest.${{ matrix.unity-version }}.json Packages/vpm-manifest.json dotnet tool install --global vrchat.vpm.cli + vpm add repo https://vpm.chocopoi.com/index.json vpm resolve project + - name: Import ModularAvatar + if: matrix.import-ma + run: | + vpm add repo https://vpm.nadena.dev/vpm.json + vpm add package nadena.dev.modular-avatar@1.9.7 + # DynamicsBones stub - name: Import DynamicBones stub if: matrix.import-dynbone diff --git a/Editor/Context.cs b/Editor/Context.cs index e922e2f..8230c86 100644 --- a/Editor/Context.cs +++ b/Editor/Context.cs @@ -13,6 +13,7 @@ using System; using System.Collections.Generic; using Chocopoi.DressingFramework.Animations; +using Chocopoi.DressingFramework.Extensibility.Sequencing; using Chocopoi.DressingFramework.Logging; using UnityEngine; using Object = UnityEngine.Object; @@ -26,6 +27,7 @@ public abstract class Context : IContext { public GameObject AvatarGameObject { get; private set; } + public abstract BuildRuntime CurrentRuntime { get; } public abstract object RuntimeContext { get; } internal abstract Report Report { get; } public abstract Object AssetContainer { get; } diff --git a/Editor/Detail/DK/AvatarBuilder.cs b/Editor/Detail/DK/AvatarBuilder.cs index a572104..146547b 100644 --- a/Editor/Detail/DK/AvatarBuilder.cs +++ b/Editor/Detail/DK/AvatarBuilder.cs @@ -73,7 +73,7 @@ private void DispatchAnimationStore() private bool RunPassesAtStage(BuildStage stage) { // Debug.Log($"[DK] =========== {stage} Start ==========="); - var passes = _plugMgr.GetSortedBuildPassesAtStage(stage); + var passes = _plugMgr.GetSortedBuildPassesAtStage(BuildRuntime.DK, stage); if (passes == null) { diff --git a/Editor/Detail/DK/DKMAMenuStore.cs b/Editor/Detail/DK/DKMAMenuStore.cs index f898e73..c2e5104 100644 --- a/Editor/Detail/DK/DKMAMenuStore.cs +++ b/Editor/Detail/DK/DKMAMenuStore.cs @@ -1,13 +1,13 @@ /* * Copyright (c) 2024 chocopoi * - * This file is part of DressingTools. + * This file is part of DressingFramework. * - * 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. + * DressingFramework 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. + * DressingFramework 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 . + * You should have received a copy of the GNU General Public License along with DressingFramework. If not, see . */ #if DK_MA && DK_VRCSDK3A @@ -30,13 +30,13 @@ namespace Chocopoi.DressingFramework.Detail.DK internal class DKMAMenuStore : MenuStore { private readonly Context _ctx; - private readonly Dictionary> _buffer; + private readonly Dictionary _buffer; private readonly HashSet _clonedVrcMenus; public DKMAMenuStore(Context ctx) { _ctx = ctx; - _buffer = new Dictionary>(); + _buffer = new Dictionary(); _clonedVrcMenus = new HashSet(); } @@ -50,17 +50,17 @@ public override void Append(MenuItem menuItem, string path = null) if (!_buffer.TryGetValue(path, out var menuItems)) { - menuItems = _buffer[path] = new List(); + menuItems = _buffer[path] = new MenuGroup(); } menuItems.Add(menuItem); } - private static VRCExpressionsMenu.Control MakeSubMenuControl(string name, VRCExpressionsMenu subMenu) + private static VRCExpressionsMenu.Control MakeSubMenuControl(string name, Texture2D icon, VRCExpressionsMenu subMenu) { return new VRCExpressionsMenu.Control() { name = name, - icon = null, + icon = icon, type = VRCExpressionsMenu.Control.ControlType.SubMenu, parameter = new VRCExpressionsMenu.Control.Parameter() { name = "" }, style = VRCExpressionsMenu.Control.Style.Style1, @@ -76,7 +76,7 @@ private VRCExpressionsMenu MakeDownwardsMenuGroups(string[] paths, int index) _ctx.CreateUniqueAsset(menu, string.Join("_", paths, 0, index)); if (index < paths.Length) { - var newMenuItem = MakeSubMenuControl(paths[index], MakeDownwardsMenuGroups(paths, index + 1)); + var newMenuItem = MakeSubMenuControl(paths[index], null, MakeDownwardsMenuGroups(paths, index + 1)); menu.controls.Add(newMenuItem); } return menu; @@ -120,13 +120,48 @@ private VRCExpressionsMenu FindInstallTarget(VRCExpressionsMenu parent, string[] } // if not found, we create empty menu groups recursively downwards - var newMenuItem = MakeSubMenuControl(paths[index], MakeDownwardsMenuGroups(paths, index + 1)); + var newMenuItem = MakeSubMenuControl(paths[index], null, MakeDownwardsMenuGroups(paths, index + 1)); parent.controls.Add(newMenuItem); // find again return FindInstallTarget(parent, paths, index); } + private static void DKToMAMenuItem(GameObject parent, MenuItem menuItem) + { + var maItemObj = new GameObject(menuItem.Name); + maItemObj.transform.SetParent(parent.transform); + + var maItem = maItemObj.AddComponent(); + + if (menuItem is SubMenuItem subMenuItem) + { + maItem.Control = MakeSubMenuControl(menuItem.Name, menuItem.Icon, null); + maItem.MenuSource = SubmenuSource.Children; + if (subMenuItem.SubMenu != null) + { + DKGroupToMAItems(maItemObj, subMenuItem.SubMenu); + } + } + else if (menuItem is VRCSubMenuItem vrcSubMenuItem) + { + maItem.Control = MakeSubMenuControl(menuItem.Name, menuItem.Icon, vrcSubMenuItem.SubMenu); + maItem.MenuSource = SubmenuSource.MenuAsset; + } + else + { + maItem.Control = VRCMenuUtils.MenuItemToControl(menuItem); + } + } + + private static void DKGroupToMAItems(GameObject parent, MenuGroup menuGroup) + { + foreach (var item in menuGroup) + { + DKToMAMenuItem(parent, item); + } + } + public override void Flush() { if (!_ctx.AvatarGameObject.TryGetComponent(out var avatarDesc)) @@ -135,7 +170,7 @@ public override void Flush() return; } - var dkMaRootObj = new GameObject("DKMA"); + var dkMaRootObj = new GameObject("DKMAMenu"); dkMaRootObj.transform.SetParent(_ctx.AvatarGameObject.transform); foreach (var kvp in _buffer) @@ -170,14 +205,7 @@ public override void Flush() maGroup.targetObject = menuObj; // add menu items - foreach (var item in items) - { - var maItemObj = new GameObject(item.Name); - maItemObj.transform.SetParent(maGroup.transform); - - var maItem = maItemObj.AddComponent(); - // TODO - } + DKGroupToMAItems(menuObj, items); } } diff --git a/Editor/Detail/DK/DKMenuStore.cs b/Editor/Detail/DK/DKMenuStore.cs index 863fb20..b733935 100644 --- a/Editor/Detail/DK/DKMenuStore.cs +++ b/Editor/Detail/DK/DKMenuStore.cs @@ -10,131 +10,23 @@ * You should have received a copy of the GNU General Public License along with DressingFramework. If not, see . */ -using System.Collections.Generic; using Chocopoi.DressingFramework.Menu; -using UnityEngine; #if DK_VRCSDK3A using Chocopoi.DressingFramework.Menu.VRChat; -using VRC.SDK3.Avatars.ScriptableObjects; #endif namespace Chocopoi.DressingFramework.Detail.DK { - internal class DKMenuStore : MenuStore + internal class DKMenuStore : MenuRepositoryStore { - private Context _ctx; - private readonly Dictionary _buffer; -#if DK_VRCSDK3A - private readonly HashSet _clonedVrcMenus; -#endif + private readonly Context _ctx; - public DKMenuStore(Context ctx) + public DKMenuStore(Context ctx) : base(ctx) { _ctx = ctx; - _buffer = new Dictionary(); -#if DK_VRCSDK3A - _clonedVrcMenus = new HashSet(); -#endif - } - - public override void Append(MenuItem menuItem, string path = null) - { - if (path == null) - { - path = ""; - } - path = path.Trim(); - - _buffer[menuItem] = path; - } - - private IMenuRepository FindInstallTarget(IMenuRepository parent, string[] paths, int index) - { - if (index >= paths.Length) - { - return parent; - } - - foreach (var item in parent) - { - if (item.Name != paths[index]) - { - continue; - } - - if (item is SubMenuItem subMenuItem) - { - if (subMenuItem.SubMenu == null) - { - subMenuItem.SubMenu = new MenuGroup(); - } - return FindInstallTarget(subMenuItem.SubMenu, paths, index + 1); - } -#if DK_VRCSDK3A - else if (item is VRCSubMenuItem vrcSubMenuItem) - { - if (vrcSubMenuItem.SubMenu == null) - { - var newVrcMenu = Object.Instantiate(VRCMenuUtils.GetDefaultExpressionsMenu()); - _ctx.CreateUniqueAsset(newVrcMenu, string.Join("_", paths, 0, index + 1)); - vrcSubMenuItem.SubMenu = newVrcMenu; - - _clonedVrcMenus.Add(vrcSubMenuItem.SubMenu); - } - else if (!_clonedVrcMenus.Contains(vrcSubMenuItem.SubMenu)) - { - var menuCopy = Object.Instantiate(vrcSubMenuItem.SubMenu); - _ctx.CreateUniqueAsset(menuCopy, string.Join("_", paths, 0, index + 1)); - vrcSubMenuItem.SubMenu = menuCopy; - _clonedVrcMenus.Add(vrcSubMenuItem.SubMenu); - } - - return FindInstallTarget(new VRCMenuWrapper(vrcSubMenuItem.SubMenu, _ctx), paths, index + 1); - } -#endif - } - - // if not found, we create empty menu groups recursively downwards - var newMenuItem = new SubMenuItem() - { - Name = paths[index], - Icon = null, - SubMenu = MakeDownwardsMenuGroups(paths, index + 1) - }; - parent.Add(newMenuItem); - - // find again, the menu group pointers above cannot be used after the CRUD operation - return FindInstallTarget(parent, paths, index); - } - - private MenuGroup MakeDownwardsMenuGroups(string[] paths, int index) - { - var mg = new MenuGroup(); - if (index < paths.Length) - { - var newMenuItem = new SubMenuItem() - { - Name = paths[index], - Icon = null, - SubMenu = MakeDownwardsMenuGroups(paths, index + 1) - }; - mg.Add(newMenuItem); - } - return mg; - } - - private void InstallMenuItem(IMenuRepository rootMenu, MenuItem item, string path) - { - var installTarget = rootMenu; - if (!string.IsNullOrEmpty(path)) - { - var paths = path.Trim().Split('/'); - installTarget = FindInstallTarget(rootMenu, paths, 0); - } - installTarget.Add(item); } - private IMenuRepository GetRootMenu() + public override IMenuRepository GetRootMenu() { IMenuRepository rootMenu; #if DK_VRCSDK3A @@ -159,16 +51,6 @@ private IMenuRepository GetRootMenu() return rootMenu; } - public override void Flush() - { - var rootMenu = GetRootMenu(); - foreach (var kvp in _buffer) - { - InstallMenuItem(rootMenu, kvp.Key, kvp.Value); - } - _buffer.Clear(); - } - internal override void OnDisable() { } internal override void OnEnable() { } diff --git a/Editor/Detail/DK/DKNativeContext.cs b/Editor/Detail/DK/DKNativeContext.cs index bae893c..8f8a4d1 100644 --- a/Editor/Detail/DK/DKNativeContext.cs +++ b/Editor/Detail/DK/DKNativeContext.cs @@ -12,6 +12,7 @@ using Chocopoi.DressingFramework.Animations; using Chocopoi.DressingFramework.Detail.DK.Logging; +using Chocopoi.DressingFramework.Extensibility.Sequencing; using Chocopoi.DressingFramework.Logging; using UnityEditor; using UnityEngine; @@ -32,6 +33,7 @@ internal class DKNativeContext : Context /// public const string GeneratedAssetsPath = "Assets/" + GeneratedAssetsFolderName; + public override BuildRuntime CurrentRuntime { get => BuildRuntime.DK; } public override object RuntimeContext => null; internal override Report Report => _report; public override Object AssetContainer => _assetContainer; @@ -50,7 +52,16 @@ public DKNativeContext(GameObject avatarGameObject) : base(avatarGameObject) AssetDatabase.CreateAsset(_assetContainer, $"{GeneratedAssetsPath}/{AvatarGameObject.name}_{DKEditorUtils.RandomString(8)}.asset"); AddContextFeature(new AnimationStore(this)); + AddMenuStoreFeature(); + } + + private void AddMenuStoreFeature() + { +#if DK_MA && DK_VRCSDK3A + AddContextFeature(new DKMAMenuStore(this)); +#else AddContextFeature(new DKMenuStore(this)); +#endif } public override void CreateAsset(Object obj, string name) diff --git a/Editor/Detail/DK/Triggers/VRChat/BuildAvatarCallback.cs b/Editor/Detail/DK/Triggers/VRChat/BuildAvatarCallback.cs index 40344b6..83a0a0a 100644 --- a/Editor/Detail/DK/Triggers/VRChat/BuildAvatarCallback.cs +++ b/Editor/Detail/DK/Triggers/VRChat/BuildAvatarCallback.cs @@ -12,6 +12,7 @@ #if DK_VRCSDK3A using System.Collections.Generic; +using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reflection; @@ -21,6 +22,7 @@ using UnityEngine; using VRC.SDKBase; using VRC.SDKBase.Editor.BuildPipeline; +using Debug = UnityEngine.Debug; using LogType = Chocopoi.DressingFramework.Logging.LogType; namespace Chocopoi.DressingFramework.Detail.DK.Triggers.VRChat @@ -30,14 +32,40 @@ internal class BuildAvatarCallback : IVRCSDKPreprocessAvatarCallback, IVRCSDKPos { private static readonly I18nTranslator t = I18nManager.Instance.FrameworkTranslator; private const string LogLabel = "BuildAvatarCallback"; + private const string VRCAvatarBuilderTypeName = "VRC.SDK3.Builder.VRCAvatarBuilder"; public int callbackOrder => -12000; // for mocking internal UI ui = new UnityEditorUI(); + private static bool CheckCalledFromVRCAvatarBuilder() + { + if (DKEditorUtils.FindType(VRCAvatarBuilderTypeName) == null) + { + Debug.LogWarning($"[DressingFramework] Could not find {VRCAvatarBuilderTypeName} in assemblies. Maybe VRCSDK API changed? For compatibility reasons, we will act like it is called from VRCSDK instead."); + return true; + } + + var stackTrace = new StackTrace(); + var frames = stackTrace.GetFrames(); + foreach (var frame in frames) + { + if (frame.GetMethod().DeclaringType.FullName == VRCAvatarBuilderTypeName) + { + return true; + } + } + + // TODO: align with this issue later on + Debug.Log("[DressingFramework] BuildAvatarCallback was not triggered by VRCSDK! Ignoring this build to prevent issues."); + return false; + } + public bool OnPreprocessAvatar(GameObject avatarGameObject) { + if (!CheckCalledFromVRCAvatarBuilder()) return true; + ReportWindow.Reset(); var ab = new AvatarBuilder(avatarGameObject); diff --git a/Editor/Detail/DressingFrameworkPlugin.cs b/Editor/Detail/DressingFrameworkPlugin.cs index 3cb57e7..f82d490 100644 --- a/Editor/Detail/DressingFrameworkPlugin.cs +++ b/Editor/Detail/DressingFrameworkPlugin.cs @@ -39,6 +39,10 @@ private void RegisterInternalPasses() RegisterBuildPass(new DK.Passes.VRChat.ScanVRCAnimationsPass()); RegisterBuildPass(new DK.Passes.VRChat.ApplyVRCExParamsPass()); #endif + +#if DK_NDMF + RegisterBuildPass(new NDMF.Passes.CheckDKNDMFCallOrderPass()); +#endif } public override void OnDisable() diff --git a/Editor/Detail/NDMF/DKExtensionContext.cs b/Editor/Detail/NDMF/DKExtensionContext.cs index ff20789..0d10f18 100644 --- a/Editor/Detail/NDMF/DKExtensionContext.cs +++ b/Editor/Detail/NDMF/DKExtensionContext.cs @@ -11,7 +11,6 @@ */ #if DK_NDMF -using UnityEngine; using nadena.dev.ndmf; namespace Chocopoi.DressingFramework.Detail.NDMF @@ -22,16 +21,13 @@ public class DKExtensionContext : IExtensionContext public DKExtensionContext() { - Debug.Log("init ctx"); Context = null; } public void OnActivate(BuildContext ndmfCtx) { - Debug.Log("act"); if (Context == null) { - Debug.Log("null new contex"); Context = new NDMFContext(ndmfCtx); Context.OnEnable(); } @@ -39,8 +35,7 @@ public void OnActivate(BuildContext ndmfCtx) public void OnDeactivate(BuildContext ndmfCtx) { - Debug.Log("deact"); - // ndmf activates and deactivates on every phase, we do not want this behaviour + // TODO: when to disable? } } } diff --git a/Editor/Detail/NDMF/NDMFContext.cs b/Editor/Detail/NDMF/NDMFContext.cs index 747f47e..415e1a6 100644 --- a/Editor/Detail/NDMF/NDMFContext.cs +++ b/Editor/Detail/NDMF/NDMFContext.cs @@ -12,6 +12,7 @@ #if DK_NDMF using Chocopoi.DressingFramework.Detail.DK.Logging; +using Chocopoi.DressingFramework.Extensibility.Sequencing; using Chocopoi.DressingFramework.Logging; using nadena.dev.ndmf; using UnityEditor; @@ -24,6 +25,7 @@ namespace Chocopoi.DressingFramework.Detail.NDMF /// internal class NDMFContext : Context { + public override BuildRuntime CurrentRuntime { get => BuildRuntime.NDMF; } public override object RuntimeContext => _ndmfCtx; internal override Report Report => _report; diff --git a/Editor/Detail/NDMF/Passes.meta b/Editor/Detail/NDMF/Passes.meta new file mode 100644 index 0000000..a908b95 --- /dev/null +++ b/Editor/Detail/NDMF/Passes.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: a75eab3ab80e5ea4d85c4b7cd2967a91 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/Detail/NDMF/Passes/CheckDKNDMFCallOrderPass.cs b/Editor/Detail/NDMF/Passes/CheckDKNDMFCallOrderPass.cs new file mode 100644 index 0000000..9c96112 --- /dev/null +++ b/Editor/Detail/NDMF/Passes/CheckDKNDMFCallOrderPass.cs @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2024 chocopoi + * + * This file is part of DressingFramework. + * + * DressingFramework 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. + * + * DressingFramework 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 DressingFramework. If not, see . + */ + +#if DK_NDMF +using Chocopoi.DressingFramework.Extensibility.Sequencing; +using UnityEngine; + +namespace Chocopoi.DressingFramework.Detail.NDMF.Passes +{ + internal class CheckDKNDMFCallOrderPass : BuildPass + { + private static BuildRuntime? s_lastRuntime = null; + + public override string FriendlyName => "Check DK and NDMF Call Order"; + + public override BuildConstraint Constraint => + InvokeAtStage(BuildStage.Pre) + .WithRuntimes(BuildRuntime.DK, BuildRuntime.NDMF) + .Build(); + + internal static void Reset() + { + s_lastRuntime = null; + } + + internal UI ui = new UnityUI(); + + public override bool Invoke(Context ctx) + { + if (s_lastRuntime == ctx.CurrentRuntime) + { + return true; + } + + if (ctx.CurrentRuntime == BuildRuntime.NDMF) + { + if (s_lastRuntime == BuildRuntime.DK) + { + ui.Log($"[DressingFramework] DK->NDMF Run order correct."); + } + s_lastRuntime = null; + return true; + } + + s_lastRuntime = ctx.CurrentRuntime; + return true; + } + + internal interface UI + { + void Log(string str); + } + + private class UnityUI : UI + { + public void Log(string str) + { + Debug.Log(str); + } + } + } +} +#endif diff --git a/Editor/Detail/NDMF/Passes/CheckDKNDMFCallOrderPass.cs.meta b/Editor/Detail/NDMF/Passes/CheckDKNDMFCallOrderPass.cs.meta new file mode 100644 index 0000000..2086e16 --- /dev/null +++ b/Editor/Detail/NDMF/Passes/CheckDKNDMFCallOrderPass.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 8f4d69c9caab90443af60b8d1b8611bf +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/Detail/NDMF/PluginDefinition.cs b/Editor/Detail/NDMF/PluginDefinition.cs index 26b4ded..b64a62a 100644 --- a/Editor/Detail/NDMF/PluginDefinition.cs +++ b/Editor/Detail/NDMF/PluginDefinition.cs @@ -74,42 +74,38 @@ private static Tuple NDMFPhaseToDKStage(BuildPhase phase private void ConfigureStages(PluginManager pluginMgr, BuildPhase ndmfPhase, BuildStage startStage, BuildStage endStage) { - string lastIdentifier = null; - for (var stage = startStage; stage <= endStage; stage++) + InPhase(ndmfPhase).WithRequiredExtension(typeof(DKExtensionContext), seq => { - // TODO - var hooks = new BuildPass[0]; - - foreach (var hook in hooks) + string lastIdentifier = null; + for (var stage = startStage; stage <= endStage; stage++) { - var seq = InPhase(ndmfPhase); + var dkPasses = pluginMgr.GetSortedBuildPassesAtStage(BuildRuntime.NDMF, stage); - if (lastIdentifier != null) + foreach (var dkPass in dkPasses) { - // add the last hook to ensure NDMF runs the same order from DK - seq.AfterPass(lastIdentifier); - } + if (lastIdentifier != null) + { + // add the last hook to ensure NDMF runs the same order from DK + seq.AfterPass(lastIdentifier); + } - // NDMF-level dependencies - foreach (var runtimeHook in hook.Constraint.afterRuntimePasses) - { - // optionality is currently unsupported - seq.AfterPass(runtimeHook.identifier); - } + // NDMF-level dependencies (no optionality) + foreach (var dep in dkPass.Constraint.afterRuntimePasses) + { + seq.AfterPass(dep.identifier); + } - seq.WithRequiredExtension(typeof(DKExtensionContext), extSeq => - { - var pass = extSeq.Run(new DKToNDMFPassWrapper(hook)); + var ndmfDp = seq.Run(new DKToNDMFPassWrapper(dkPass)); - foreach (var runtimeHook in hook.Constraint.beforeRuntimePasses) + foreach (var dep in dkPass.Constraint.beforeRuntimePasses) { - pass.BeforePass(runtimeHook.identifier); + ndmfDp.BeforePass(dep.identifier); } - }); - lastIdentifier = hook.Identifier; + lastIdentifier = dkPass.Identifier; + } } - } + }); } protected override void Configure() diff --git a/Editor/Detail/NDMF/UnsupportedVersionChecker.cs b/Editor/Detail/NDMF/UnsupportedVersionChecker.cs new file mode 100644 index 0000000..c0d6f92 --- /dev/null +++ b/Editor/Detail/NDMF/UnsupportedVersionChecker.cs @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2024 chocopoi + * + * This file is part of DressingFramework. + * + * DressingFramework 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. + * + * DressingFramework 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 DressingFramework. If not, see . + */ +#if UNITY_EDITOR +using Chocopoi.DressingFramework.Localization; +using UnityEditor; + +namespace Chocopoi.DressingFramework.Detail.NDMF +{ + [InitializeOnLoad] + public static class UnsupportedVersionChecker + { + private static readonly I18nTranslator t = I18nManager.Instance.FrameworkTranslator; + + private static bool IsMAUnsupported() + { +#if DK_MA_UNSUPPORTED + return true; +#else + return false; +#endif + } + + private static bool IsNDMFUnsupported() + { +#if DK_NDMF_UNSUPPORTED + return true; +#else + return false; +#endif + } + + static UnsupportedVersionChecker() + { + EditorApplication.delayCall += () => + { + if (IsMAUnsupported()) + { + EditorUtility.DisplayDialog(t._("framework.name"), t._("detail.ndmf.unsupportedVersionChecker.unsupportedMaVersionDetected"), t._("common.dialog.btn.ok")); + } + + if (IsNDMFUnsupported()) + { + EditorUtility.DisplayDialog(t._("framework.name"), t._("detail.ndmf.unsupportedVersionChecker.unsupportedNdmfVersionDetected"), t._("common.dialog.btn.ok")); + } + }; + } + } +} +#endif diff --git a/Editor/Detail/NDMF/UnsupportedVersionChecker.cs.meta b/Editor/Detail/NDMF/UnsupportedVersionChecker.cs.meta new file mode 100644 index 0000000..5e68ea2 --- /dev/null +++ b/Editor/Detail/NDMF/UnsupportedVersionChecker.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 0677a70fe5a743a4e9339f94feb32027 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/Extensibility/Plugin.cs b/Editor/Extensibility/Plugin.cs index 845a87d..75e9b9a 100644 --- a/Editor/Extensibility/Plugin.cs +++ b/Editor/Extensibility/Plugin.cs @@ -47,9 +47,14 @@ protected void RegisterBuildPass(BuildPass hook) _buildPasses[hook.Identifier] = hook; } - internal List GetAllBuildPasses() + internal List GetBuildPassesAtStage(BuildRuntime runtime, BuildStage stage) { - return _buildPasses.Values.ToList(); + return _buildPasses.Values + .Where(p => + ((runtime == BuildRuntime.DK && p.Constraint.buildRuntimes.Count == 0) || + p.Constraint.buildRuntimes.Contains(runtime)) && + p.Constraint.stage == stage) + .ToList(); } } } diff --git a/Editor/Extensibility/PluginManager.cs b/Editor/Extensibility/PluginManager.cs index 7ad6988..e5169bf 100644 --- a/Editor/Extensibility/PluginManager.cs +++ b/Editor/Extensibility/PluginManager.cs @@ -68,28 +68,21 @@ private void InitPlugins() } } - public List GetBuildPassesAtStage(BuildStage stage) + public List GetBuildPassesAtStage(BuildRuntime buildRuntime, BuildStage stage) { var stageHooks = new List(); foreach (var plugin in _plugins.Values) { - var pluginHooks = plugin.GetAllBuildPasses(); - foreach (var hook in pluginHooks) - { - if (hook.Constraint.stage == stage) - { - stageHooks.Add(hook); - } - } + stageHooks.AddRange(plugin.GetBuildPassesAtStage(buildRuntime, stage)); } return stageHooks; } - public List GetSortedBuildPassesAtStage(BuildStage stage) + public List GetSortedBuildPassesAtStage(BuildRuntime buildRuntime, BuildStage stage) { - return SortBuildPassesByDependencies(GetBuildPassesAtStage(stage)); + return SortBuildPassesByDependencies(GetBuildPassesAtStage(buildRuntime, stage)); } public static List SortBuildPassesByDependencies(List hooks) diff --git a/Editor/Extensibility/Sequencing/BuildConstraint.cs b/Editor/Extensibility/Sequencing/BuildConstraint.cs index 1b64dff..c3c343e 100644 --- a/Editor/Extensibility/Sequencing/BuildConstraint.cs +++ b/Editor/Extensibility/Sequencing/BuildConstraint.cs @@ -53,11 +53,32 @@ public enum BuildStage Post = 5 } + /// + /// Build runtime enum for passes to run on + /// + public enum BuildRuntime + { + /// + /// DressingFramework, default if not explicity specified + /// + DK = 0, + + /// + /// NDMF + /// + NDMF = 1, + } + /// /// Build constraint /// public class BuildConstraint : ExecutionConstraint { + /// + /// Build runtimes to invoke on + /// + public HashSet buildRuntimes; + /// /// Stage to execute /// @@ -75,6 +96,7 @@ public class BuildConstraint : ExecutionConstraint public BuildConstraint() { + buildRuntimes = new HashSet(); beforeRuntimePasses = new List>(); afterRuntimePasses = new List>(); } @@ -88,6 +110,7 @@ public class BuildConstraintBuilder : ExecutionConstraintBuilder protected List> beforeRuntimePasses; protected List> afterRuntimePasses; + private readonly HashSet _buildRuntimes; private readonly BuildStage _stage; /// @@ -96,11 +119,26 @@ public class BuildConstraintBuilder : ExecutionConstraintBuilder /// Stage to execute public BuildConstraintBuilder(BuildStage stage) { + _buildRuntimes = new HashSet(); beforeRuntimePasses = new List>(); afterRuntimePasses = new List>(); _stage = stage; } + /// + /// Run this pass on specific runtimes. Multiple runtimes can be specified and it will run on each once. + /// + /// Build runtime + /// This builder + public BuildConstraintBuilder WithRuntimes(params BuildRuntime[] buildRuntimes) + { + foreach (var rt in buildRuntimes) + { + _buildRuntimes.Add(rt); + } + return this; + } + public BuildConstraintBuilder BeforePass(bool optional = false) { return BeforePass(typeof(T), optional); @@ -207,6 +245,20 @@ public BuildConstraintBuilder AfterRuntimePass(string identifier, bool optional return this; } +#if DK_NDMF + public BuildConstraintBuilder BeforeNDMFPass(T pass) where T : nadena.dev.ndmf.Pass, new() + { + InnerBeforeRuntimePass(pass.QualifiedName, false); + return this; + } + + public BuildConstraintBuilder AfterNDMFPass(T pass) where T : nadena.dev.ndmf.Pass, new() + { + InnerAfterRuntimePass(pass.QualifiedName, false); + return this; + } +#endif + protected BuildConstraintBuilder InnerBeforeRuntimePass(string identifier, bool optional = false) { beforeRuntimePasses.Add(new Dependency() @@ -235,6 +287,7 @@ protected BuildConstraintBuilder InnerAfterRuntimePass(string identifier, bool o { return new BuildConstraint() { + buildRuntimes = _buildRuntimes, stage = _stage, beforeDependencies = beforeDependencies, afterDependencies = afterDependencies, diff --git a/Editor/Menu/MenuRepositoryStore.cs b/Editor/Menu/MenuRepositoryStore.cs new file mode 100644 index 0000000..f304753 --- /dev/null +++ b/Editor/Menu/MenuRepositoryStore.cs @@ -0,0 +1,148 @@ +/* + * Copyright (c) 2023 chocopoi + * + * This file is part of DressingFramework. + * + * DressingFramework 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. + * + * DressingFramework 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 DressingFramework. If not, see . + */ + +using System.Collections.Generic; +using UnityEngine; +#if DK_VRCSDK3A +using Chocopoi.DressingFramework.Menu.VRChat; +using VRC.SDK3.Avatars.ScriptableObjects; +#endif + +namespace Chocopoi.DressingFramework.Menu +{ + internal abstract class MenuRepositoryStore : MenuStore + { + private readonly Context _ctx; + private readonly Dictionary _buffer; +#if DK_VRCSDK3A + private readonly HashSet _clonedVrcMenus; +#endif + + public MenuRepositoryStore(Context ctx) + { + _ctx = ctx; + _buffer = new Dictionary(); +#if DK_VRCSDK3A + _clonedVrcMenus = new HashSet(); +#endif + } + + public override void Append(MenuItem menuItem, string path = null) + { + if (path == null) + { + path = ""; + } + path = path.Trim(); + + _buffer[menuItem] = path; + } + + private IMenuRepository FindInstallTarget(IMenuRepository parent, string[] paths, int index) + { + if (index >= paths.Length) + { + return parent; + } + + foreach (var item in parent) + { + if (item.Name != paths[index]) + { + continue; + } + + if (item is SubMenuItem subMenuItem) + { + if (subMenuItem.SubMenu == null) + { + subMenuItem.SubMenu = new MenuGroup(); + } + return FindInstallTarget(subMenuItem.SubMenu, paths, index + 1); + } +#if DK_VRCSDK3A + else if (item is VRCSubMenuItem vrcSubMenuItem) + { + if (vrcSubMenuItem.SubMenu == null) + { + var newVrcMenu = Object.Instantiate(VRCMenuUtils.GetDefaultExpressionsMenu()); + _ctx.CreateUniqueAsset(newVrcMenu, string.Join("_", paths, 0, index + 1)); + vrcSubMenuItem.SubMenu = newVrcMenu; + + _clonedVrcMenus.Add(vrcSubMenuItem.SubMenu); + } + else if (!_clonedVrcMenus.Contains(vrcSubMenuItem.SubMenu)) + { + var menuCopy = Object.Instantiate(vrcSubMenuItem.SubMenu); + _ctx.CreateUniqueAsset(menuCopy, string.Join("_", paths, 0, index + 1)); + vrcSubMenuItem.SubMenu = menuCopy; + _clonedVrcMenus.Add(vrcSubMenuItem.SubMenu); + } + + return FindInstallTarget(new VRCMenuWrapper(vrcSubMenuItem.SubMenu, _ctx), paths, index + 1); + } +#endif + } + + // if not found, we create empty menu groups recursively downwards + var newMenuItem = new SubMenuItem() + { + Name = paths[index], + Icon = null, + SubMenu = MakeDownwardsMenuGroups(paths, index + 1) + }; + parent.Add(newMenuItem); + + // find again, the menu group pointers above cannot be used after the CRUD operation + return FindInstallTarget(parent, paths, index); + } + + private MenuGroup MakeDownwardsMenuGroups(string[] paths, int index) + { + var mg = new MenuGroup(); + if (index < paths.Length) + { + var newMenuItem = new SubMenuItem() + { + Name = paths[index], + Icon = null, + SubMenu = MakeDownwardsMenuGroups(paths, index + 1) + }; + mg.Add(newMenuItem); + } + return mg; + } + + private void InstallMenuItem(IMenuRepository rootMenu, MenuItem item, string path) + { + var installTarget = rootMenu; + if (!string.IsNullOrEmpty(path)) + { + var paths = path.Trim().Split('/'); + installTarget = FindInstallTarget(rootMenu, paths, 0); + } + installTarget.Add(item); + } + + public abstract IMenuRepository GetRootMenu(); + + public override void Flush() + { + var rootMenu = GetRootMenu(); + foreach (var kvp in _buffer) + { + InstallMenuItem(rootMenu, kvp.Key, kvp.Value); + } + _buffer.Clear(); + } + } +} diff --git a/Editor/Menu/MenuRepositoryStore.cs.meta b/Editor/Menu/MenuRepositoryStore.cs.meta new file mode 100644 index 0000000..20445bd --- /dev/null +++ b/Editor/Menu/MenuRepositoryStore.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: dda8552894b340e44acdabc772d63df1 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/com.chocopoi.vrc.dressingframework.Editor.asmdef b/Editor/com.chocopoi.vrc.dressingframework.Editor.asmdef index 473502b..6582e4d 100644 --- a/Editor/com.chocopoi.vrc.dressingframework.Editor.asmdef +++ b/Editor/com.chocopoi.vrc.dressingframework.Editor.asmdef @@ -28,13 +28,33 @@ }, { "name": "nadena.dev.modular-avatar", - "expression": "[1.9.0,2.0.0)", + "expression": "[1.8.0,2.0.0)", "define": "DK_MA" }, { "name": "nadena.dev.ndmf", "expression": "[1.3.0,2.0.0)", "define": "DK_NDMF" + }, + { + "name": "nadena.dev.modular-avatar", + "expression": "(,1.8.0)", + "define": "DK_MA_UNSUPPORTED" + }, + { + "name": "nadena.dev.modular-avatar", + "expression": "2.0.0", + "define": "DK_MA_UNSUPPORTED" + }, + { + "name": "nadena.dev.ndmf", + "expression": "(,1.3.0)", + "define": "DK_NDMF_UNSUPPORTED" + }, + { + "name": "nadena.dev.ndmf", + "expression": "2.0.0", + "define": "DK_NDMF_UNSUPPORTED" } ], "noEngineReferences": false diff --git a/Tests~/Editor/Animations/VRChat/VRCAnimUtilsTest.cs b/Tests~/Editor/Animations/VRChat/VRCAnimUtilsTest.cs index 9b6a9cf..a400cff 100644 --- a/Tests~/Editor/Animations/VRChat/VRCAnimUtilsTest.cs +++ b/Tests~/Editor/Animations/VRChat/VRCAnimUtilsTest.cs @@ -12,6 +12,7 @@ #if DK_VRCSDK3A using Chocopoi.DressingFramework.Animations.VRChat; +using Chocopoi.DressingFramework.Extensibility.Sequencing; using Chocopoi.DressingFramework.Logging; using NUnit.Framework; using UnityEditor; @@ -155,6 +156,7 @@ public void GetAvatarLayerAnimator() private class TestContext : Context { + public override BuildRuntime CurrentRuntime => throw new System.NotImplementedException(); public override object RuntimeContext => throw new System.NotImplementedException(); internal override Report Report => throw new System.NotImplementedException(); diff --git a/Tests~/Editor/ContextTest.cs b/Tests~/Editor/ContextTest.cs index fd1dc3b..b3945d2 100644 --- a/Tests~/Editor/ContextTest.cs +++ b/Tests~/Editor/ContextTest.cs @@ -10,6 +10,7 @@ * You should have received a copy of the GNU General Public License along with DressingFramework. If not, see . */ +using Chocopoi.DressingFramework.Extensibility.Sequencing; using Chocopoi.DressingFramework.Logging; using NUnit.Framework; using UnityEngine; @@ -20,7 +21,7 @@ public class ContextTest : EditorTestBase { public class DummyContext : Context { - + public override BuildRuntime CurrentRuntime => throw new System.NotImplementedException(); public override object RuntimeContext => throw new System.NotImplementedException(); public override Object AssetContainer => throw new System.NotImplementedException(); internal override Report Report => throw new System.NotImplementedException(); diff --git a/Tests~/Editor/Details/DK/DKMAMenuStoreTest.cs b/Tests~/Editor/Details/DK/DKMAMenuStoreTest.cs new file mode 100644 index 0000000..a9d20fb --- /dev/null +++ b/Tests~/Editor/Details/DK/DKMAMenuStoreTest.cs @@ -0,0 +1,200 @@ +/* + * Copyright (c) 2024 chocopoi + * + * This file is part of DressingFramework. + * + * DressingFramework 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. + * + * DressingFramework 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 DressingFramework. If not, see . + */ + +#if DK_MA && DK_VRCSDK3A +using Chocopoi.DressingFramework.Detail.DK; +using Chocopoi.DressingFramework.Menu; +using nadena.dev.modular_avatar.core; +using NUnit.Framework; +using UnityEngine; +using VRC.SDK3.Avatars.Components; +using VRC.SDK3.Avatars.ScriptableObjects; + +namespace Chocopoi.DressingFramework.Tests.Detail.DK +{ + public class DKMAMenuStoreTest : EditorTestBase + { + [Test] + public void SimpleFlushTest() + { + var avatar = CreateGameObject("Avatar"); + var avatarDesc = avatar.AddComponent(); + var dummyMenu = ScriptableObject.CreateInstance(); + avatarDesc.expressionsMenu = dummyMenu; + + var ctx = new DKNativeContext(avatar); + var store = new DKMAMenuStore(ctx); + + var magicName = "Item"; + var magicParam = "SomeParam"; + var magicFloat = 0.75f; + var menuItem = new ToggleItem() + { + Name = magicName, + Icon = null, + Controller = new AnimatorParameterController() + { + ParameterName = magicParam, + ParameterValue = magicFloat + } + }; + store.Append(menuItem); + store.Flush(); + + var menuRoot = avatar.transform.Find("DKMAMenu/Root"); + Assert.NotNull(menuRoot); + + Assert.True(menuRoot.TryGetComponent(out var installer)); + Assert.AreEqual(dummyMenu, installer.installTargetMenu); + Assert.True(menuRoot.TryGetComponent(out _)); + Assert.AreEqual(1, menuRoot.transform.childCount); + + var child = menuRoot.transform.GetChild(0); + Assert.True(child.TryGetComponent(out var item)); + Assert.NotNull(item.Control); + Assert.AreEqual(magicName, item.Control.name); + Assert.AreEqual(magicParam, item.Control.parameter.name); + Assert.AreEqual(magicFloat, item.Control.value); + } + + private static VRCExpressionsMenu FindMenuThroughPath(VRCExpressionsMenu menu, string[] paths, int index) + { + if (index < paths.Length) + { + foreach (var ctrl in menu.controls) + { + if (ctrl.type == VRCExpressionsMenu.Control.ControlType.SubMenu && ctrl.name == paths[index]) + { + Assert.NotNull(ctrl.subMenu, $"Submenu null at {paths[index]} through {string.Join("/", paths)}"); + return FindMenuThroughPath(ctrl.subMenu, paths, index + 1); + } + } + Assert.Fail($"Sub-menu {paths[index]} not found through {string.Join("/", paths)}"); + return null; + } + else + { + return menu; + } + } + + private static void AssertMAItems(GameObject avatar, VRCExpressionsMenu rootMenu, MenuItem dkItem1, MenuItem dkItem2, MenuItem dkItem3) + { + var vrcMenu1 = FindMenuThroughPath(rootMenu, new string[] { "A" }, 0); + var vrcMenu2 = FindMenuThroughPath(rootMenu, new string[] { "B", "C" }, 0); + var vrcMenu3 = FindMenuThroughPath(rootMenu, new string[] { "C", "D", "E" }, 0); + + var menu1 = avatar.transform.Find("DKMAMenu/A"); + Assert.NotNull(menu1); + var menu2 = avatar.transform.Find("DKMAMenu/B_C"); + Assert.NotNull(menu2); + var menu3 = avatar.transform.Find("DKMAMenu/C_D_E"); + Assert.NotNull(menu3); + + Assert.True(menu1.TryGetComponent(out var installer1)); + Assert.AreEqual(vrcMenu1, installer1.installTargetMenu); + Assert.True(menu2.TryGetComponent(out var installer2)); + Assert.AreEqual(vrcMenu2, installer2.installTargetMenu); + Assert.True(menu3.TryGetComponent(out var installer3)); + Assert.AreEqual(vrcMenu3, installer3.installTargetMenu); + + Assert.AreEqual(1, menu1.childCount); + Assert.True(menu1.GetChild(0).TryGetComponent(out var maItem1)); + Assert.NotNull(maItem1.Control); + Assert.AreEqual(dkItem1.Name, maItem1.Control.name); + Assert.AreEqual(VRCExpressionsMenu.Control.ControlType.Toggle, maItem1.Control.type); + + Assert.AreEqual(1, menu2.childCount); + Assert.True(menu2.GetChild(0).TryGetComponent(out var maItem2)); + Assert.NotNull(maItem2.Control); + Assert.AreEqual(dkItem2.Name, maItem2.Control.name); + Assert.AreEqual(VRCExpressionsMenu.Control.ControlType.Toggle, maItem2.Control.type); + + Assert.AreEqual(1, menu3.childCount); + Assert.True(menu3.GetChild(0).TryGetComponent(out var maItem3)); + Assert.NotNull(maItem3.Control); + Assert.AreEqual(dkItem3.Name, maItem3.Control.name); + Assert.AreEqual(VRCExpressionsMenu.Control.ControlType.Toggle, maItem3.Control.type); + } + + [Test] + public void InstallToPathNoExistingSubMenuTest() + { + var avatar = CreateGameObject("Avatar"); + var avatarDesc = avatar.AddComponent(); + var rootMenu = ScriptableObject.CreateInstance(); + avatarDesc.expressionsMenu = rootMenu; + + var ctx = new DKNativeContext(avatar); + var store = new DKMAMenuStore(ctx); + + var dkItem1 = new ToggleItem() { Name = "1" }; + store.Append(dkItem1, "A"); + + var dkItem2 = new ToggleItem() { Name = "2" }; + store.Append(dkItem2, "B/C"); + + var dkItem3 = new ToggleItem() { Name = "3" }; + store.Append(dkItem3, "C/D/E"); + + store.Flush(); + + AssertMAItems(avatar, rootMenu, dkItem1, dkItem2, dkItem3); + } + + private static void AddMenusThroughPath(VRCExpressionsMenu menu, string[] paths, int index) + { + if (index < paths.Length) + { + var newMenu = ScriptableObject.CreateInstance(); + menu.controls.Add(new VRCExpressionsMenu.Control() + { + name = paths[index], + type = VRCExpressionsMenu.Control.ControlType.SubMenu, + subMenu = newMenu + }); + AddMenusThroughPath(newMenu, paths, index + 1); + } + } + + [Test] + public void InstallToPathExistingSubMenuTest() + { + var avatar = CreateGameObject("Avatar"); + var avatarDesc = avatar.AddComponent(); + var rootMenu = ScriptableObject.CreateInstance(); + avatarDesc.expressionsMenu = rootMenu; + + AddMenusThroughPath(rootMenu, new string[] { "A" }, 0); + AddMenusThroughPath(rootMenu, new string[] { "B", "C" }, 0); + AddMenusThroughPath(rootMenu, new string[] { "C", "D", "E" }, 0); + + var ctx = new DKNativeContext(avatar); + var store = new DKMAMenuStore(ctx); + + var dkItem1 = new ToggleItem() { Name = "1" }; + store.Append(dkItem1, "A"); + + var dkItem2 = new ToggleItem() { Name = "2" }; + store.Append(dkItem2, "B/C"); + + var dkItem3 = new ToggleItem() { Name = "3" }; + store.Append(dkItem3, "C/D/E"); + + store.Flush(); + + // the store will clone a copy of it, so the original cannot be used for asserts + AssertMAItems(avatar, rootMenu, dkItem1, dkItem2, dkItem3); + } + } +} +#endif diff --git a/Tests~/Editor/Details/DK/DKMAMenuStoreTest.cs.meta b/Tests~/Editor/Details/DK/DKMAMenuStoreTest.cs.meta new file mode 100644 index 0000000..074002e --- /dev/null +++ b/Tests~/Editor/Details/DK/DKMAMenuStoreTest.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 53e65557e6793d14b9638e28a6be1f9c +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests~/Editor/Details/NDMF.meta b/Tests~/Editor/Details/NDMF.meta new file mode 100644 index 0000000..02aebfe --- /dev/null +++ b/Tests~/Editor/Details/NDMF.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 4c32206e0b698184bbce34a3b3424e74 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests~/Editor/Details/NDMF/CheckDKNDMFCallOrderPassTest.cs b/Tests~/Editor/Details/NDMF/CheckDKNDMFCallOrderPassTest.cs new file mode 100644 index 0000000..1560297 --- /dev/null +++ b/Tests~/Editor/Details/NDMF/CheckDKNDMFCallOrderPassTest.cs @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2024 chocopoi + * + * This file is part of DressingFramework. + * + * DressingFramework 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. + * + * DressingFramework 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 DressingFramework. If not, see . + */ + +#if DK_NDMF +using Chocopoi.DressingFramework.Detail.DK; +using Chocopoi.DressingFramework.Detail.NDMF; +using Chocopoi.DressingFramework.Detail.NDMF.Passes; +using Moq; +using NUnit.Framework; + +namespace Chocopoi.DressingFramework.Tests.Detail.NDMF +{ + public class CheckDKNDMFCallOrderPassTest : EditorTestBase + { + public override void SetUp() + { + base.SetUp(); + CheckDKNDMFCallOrderPass.Reset(); + } + + public override void TearDown() + { + base.TearDown(); + CheckDKNDMFCallOrderPass.Reset(); + } + + [Test] + public void CorrectOrderTest() + { + CheckDKNDMFCallOrderPass.Reset(); + + var avatar = CreateGameObject("Avatar"); + + var mock = new Mock(); + + var dkCtx = new DKNativeContext(avatar); + var dkPass = new CheckDKNDMFCallOrderPass { ui = mock.Object }; + dkPass.Invoke(dkCtx); + + var ndmfCtx = new NDMFContext(new nadena.dev.ndmf.BuildContext(avatar, DKNativeContext.GeneratedAssetsPath)); + var ndmfPass = new CheckDKNDMFCallOrderPass() { ui = mock.Object }; + ndmfPass.Invoke(ndmfCtx); + + mock.Verify(m => m.Log(It.IsAny()), Times.Once); + } + } +} +#endif diff --git a/Tests~/Editor/Details/NDMF/CheckDKNDMFCallOrderPassTest.cs.meta b/Tests~/Editor/Details/NDMF/CheckDKNDMFCallOrderPassTest.cs.meta new file mode 100644 index 0000000..e088f85 --- /dev/null +++ b/Tests~/Editor/Details/NDMF/CheckDKNDMFCallOrderPassTest.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: d106b2838cbf0884191852d963f18484 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests~/Editor/Extensibility/PluginManagerTest.cs b/Tests~/Editor/Extensibility/PluginManagerTest.cs index 45296de..bc66505 100644 --- a/Tests~/Editor/Extensibility/PluginManagerTest.cs +++ b/Tests~/Editor/Extensibility/PluginManagerTest.cs @@ -58,7 +58,7 @@ private static void AssertHasType(List passes, Type type) where T : class [Test] public void PassesAtStageTest() { - AssertHasType(new PluginManager().GetBuildPassesAtStage(BuildStage.Generation), typeof(TestPass)); + AssertHasType(new PluginManager().GetBuildPassesAtStage(BuildRuntime.DK, BuildStage.Generation), typeof(TestPass)); } } } diff --git a/Tests~/Editor/Extensibility/PluginTest.cs b/Tests~/Editor/Extensibility/PluginTest.cs index a5e01a7..a5033d4 100644 --- a/Tests~/Editor/Extensibility/PluginTest.cs +++ b/Tests~/Editor/Extensibility/PluginTest.cs @@ -44,7 +44,7 @@ public void RegisterTest() var plugin = new TestRegisterPlugin(); plugin.OnEnable(); - Assert.AreEqual(1, plugin.GetAllBuildPasses().Count); + Assert.AreEqual(1, plugin.GetBuildPassesAtStage(BuildRuntime.DK, BuildStage.Pre).Count); } } } diff --git a/Tests~/Editor/Menu/MenuStoreTest.cs b/Tests~/Editor/Menu/MenuRepositoryStoreTest.cs similarity index 87% rename from Tests~/Editor/Menu/MenuStoreTest.cs rename to Tests~/Editor/Menu/MenuRepositoryStoreTest.cs index 6910b42..a92a872 100644 --- a/Tests~/Editor/Menu/MenuStoreTest.cs +++ b/Tests~/Editor/Menu/MenuRepositoryStoreTest.cs @@ -16,13 +16,13 @@ namespace Chocopoi.DressingFramework.Tests.Menu { - public class MenuStoreTest : EditorTestBase + public class MenuRepositoryStoreTest : EditorTestBase { - private class TestMenuStore : MenuStore + private class TestMenuRepositoryStore : MenuRepositoryStore { private readonly MenuGroup _rootMenu; - public TestMenuStore(Context ctx) : base(ctx) + public TestMenuRepositoryStore(Context ctx) : base(ctx) { _rootMenu = new MenuGroup(); } @@ -42,7 +42,7 @@ internal override void OnEnable() public void SimpleFlushTest() { var ctx = new DKNativeContext(CreateGameObject("abc")); - var store = new TestMenuStore(ctx); + var store = new TestMenuRepositoryStore(ctx); var realMenu = store.GetRootMenu(); var menuItem = new ToggleItem(); @@ -61,14 +61,11 @@ private void AssertThroughPath(MenuGroup mg, string[] paths, int index, MenuItem var found = false; foreach (var mi in mg) { - if (index < paths.Length) + if (mi is SubMenuItem subMenuItem && subMenuItem.Name == paths[index]) { - if (mi is SubMenuItem subMenuItem && subMenuItem.Name == paths[index]) - { - found = true; - AssertThroughPath(subMenuItem.SubMenu, paths, index + 1, expectedItem); - break; - } + found = true; + AssertThroughPath(subMenuItem.SubMenu, paths, index + 1, expectedItem); + break; } } Assert.True(found, $"Sub-menu {paths[index]} not found through {string.Join("/", paths)}"); @@ -97,7 +94,7 @@ private void AssertHasMenuItem(MenuGroup mg, MenuItem expectedItem) public void InstallToPathNoExistingSubMenuTest() { var ctx = new DKNativeContext(CreateGameObject("abc")); - var store = new TestMenuStore(ctx); + var store = new TestMenuRepositoryStore(ctx); var realMenu = (MenuGroup)store.GetRootMenu(); var menuItem1 = new ToggleItem(); @@ -120,7 +117,7 @@ public void InstallToPathNoExistingSubMenuTest() public void InstallToPathExistingSubMenuTest() { var ctx = new DKNativeContext(CreateGameObject("abc")); - var store = new TestMenuStore(ctx); + var store = new TestMenuRepositoryStore(ctx); var realMenu = (MenuGroup)store.GetRootMenu(); realMenu.Add(new SubMenuItem() { diff --git a/Tests~/Editor/Menu/MenuStoreTest.cs.meta b/Tests~/Editor/Menu/MenuRepositoryStoreTest.cs.meta similarity index 100% rename from Tests~/Editor/Menu/MenuStoreTest.cs.meta rename to Tests~/Editor/Menu/MenuRepositoryStoreTest.cs.meta diff --git a/Tests~/Editor/com.chocopoi.vrc.dressingframework.Editor.Tests.asmdef b/Tests~/Editor/com.chocopoi.vrc.dressingframework.Editor.Tests.asmdef index 7169291..592796c 100644 --- a/Tests~/Editor/com.chocopoi.vrc.dressingframework.Editor.Tests.asmdef +++ b/Tests~/Editor/com.chocopoi.vrc.dressingframework.Editor.Tests.asmdef @@ -1,10 +1,13 @@ { "name": "com.chocopoi.vrc.dressingframework.Editor.Tests", + "rootNamespace": "", "references": [ "UnityEngine.TestRunner", "UnityEditor.TestRunner", "com.chocopoi.vrc.dressingframework.Editor", - "com.chocopoi.vrc.dressingframework.Runtime" + "com.chocopoi.vrc.dressingframework.Runtime", + "nadena.dev.modular-avatar.core", + "nadena.dev.ndmf" ], "includePlatforms": [ "Editor" @@ -31,7 +34,17 @@ "name": "com.vrchat.avatars", "expression": "", "define": "DK_VRCSDK3A" + }, + { + "name": "nadena.dev.modular-avatar", + "expression": "[1.9.0,2.0.0)", + "define": "DK_MA" + }, + { + "name": "nadena.dev.ndmf", + "expression": "[1.3.0,2.0.0)", + "define": "DK_NDMF" } ], "noEngineReferences": false -} \ No newline at end of file +} diff --git a/Translations/en.json b/Translations/en.json index 401ef2c..b577036 100644 --- a/Translations/en.json +++ b/Translations/en.json @@ -8,6 +8,8 @@ "detail.dk.passes.vrc.cloneVrcExMenuAndParams.msgCode.error.unableToObtainDefaultExMenuAsset": "Unable to obtain default VRC expressions menu asset", "detail.dk.passes.vrc.cloneVrcExMenuAndParams.msgCode.error.unableToObtainDefaultExParamsAsset": "Unable to obtain default VRC expressions parameters asset", "detail.dk.passes.vrc.applyVrcExParams.msgCode.error.maxParameterCostExceeded": "Maximum synced parameter cost exceeded: {0} > {1}", + "detail.ndmf.unsupportedVersionChecker.unsupportedMaVersionDetected": "Unsupported ModularAvatar version detected. Please install supported versions between >=1.8.0 and <2.0.0 for related integrations.", + "detail.ndmf.unsupportedVersionChecker.unsupportedNdmfVersionDetected": "Unsupported NDMF version detected. Please install supported versions between >=1.3.0 and <2.0.0 for related integrations.", "triggers.vrc.dialog.msg.errorPreprocessingReferReportWindow": "Error preprocessing avatar, please refer to the report window.", "common.dialog.btn.ok": "OK", "report.editor.helpbox.resultError": "Result: Errors occurred", diff --git a/package.json b/package.json index e9a6221..a693d89 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "com.chocopoi.vrc.dressingframework", "displayName": "DressingFramework", - "version": "2.0.0", + "version": "2.1.0-beta", "unity": "2019.4", "description": "A framework that assembles DressingTools and provides interfaces for third-party developers.", "author": {