diff --git a/Packages/jp.suzuryg.face-emo/Editor/AppMain/FaceEmoInstaller.cs b/Packages/jp.suzuryg.face-emo/Editor/AppMain/FaceEmoInstaller.cs index d9a7f37c..96e41588 100644 --- a/Packages/jp.suzuryg.face-emo/Editor/AppMain/FaceEmoInstaller.cs +++ b/Packages/jp.suzuryg.face-emo/Editor/AppMain/FaceEmoInstaller.cs @@ -32,10 +32,8 @@ public FaceEmoInstaller(GameObject launcherObject) RootObjectName = launcherObject.name; // Bind Monobehaviour instances - var menuRepositoryComponent = launcherObject.GetComponent(); - if (menuRepositoryComponent == null) { menuRepositoryComponent = launcherObject.AddComponent(); } - menuRepositoryComponent.hideFlags = HideFlags.HideInInspector; - Container.Bind().FromInstance(menuRepositoryComponent).AsSingle(); + BindComponent(launcherObject); + BindComponent(launcherObject); var launcher = launcherObject.GetComponent(); if (launcher == null) { launcher = launcherObject.AddComponent(); } @@ -158,6 +156,7 @@ public FaceEmoInstaller(GameObject launcherObject) Container.Bind().AsTransient(); Container.Bind().To().AsTransient(); + Container.Bind().To().AsTransient(); Container.Bind().AsTransient(); Container.Bind().AsTransient(); @@ -210,5 +209,13 @@ public static FaceEmoInstaller GetInstaller(string rootObjectPath) return new FaceEmoInstaller(launcherObject); } + + private void BindComponent(GameObject gameObject) where T : MonoBehaviour + { + var component = gameObject.GetComponent(); + if (component == null) { component = gameObject.AddComponent(); } + component.hideFlags = HideFlags.HideInInspector; + Container.Bind().FromInstance(component).AsSingle(); + } } } diff --git a/Packages/jp.suzuryg.face-emo/Editor/AppMain/FaceEmoLauncher.cs b/Packages/jp.suzuryg.face-emo/Editor/AppMain/FaceEmoLauncher.cs index fdb96667..749931de 100644 --- a/Packages/jp.suzuryg.face-emo/Editor/AppMain/FaceEmoLauncher.cs +++ b/Packages/jp.suzuryg.face-emo/Editor/AppMain/FaceEmoLauncher.cs @@ -129,9 +129,9 @@ private void ChangeLocale(Locale locale) } [MenuItem("FaceEmo/New Menu", false, 0)] - public static GameObject Create(MenuCommand menuCommand) + public static GameObject CreateCommand() { - var gameObject = GetLauncherObject(menuCommand); + var gameObject = GetLauncherObject(); // Create GameObject which has unique name in acitive scene. var baseName = DomainConstants.SystemName; @@ -148,7 +148,7 @@ public static GameObject Create(MenuCommand menuCommand) } [MenuItem("FaceEmo/Restore Menu", false, 1)] - public static void Restore(MenuCommand menuCommand) + public static void RestoreCommand() { var selectedPath = EditorUtility.OpenFilePanelWithFilters(title: null, directory: FaceEmoBackupper.BackupDir, filters: new[] { "FaceEmoProject" , "asset" }); if (string.IsNullOrEmpty(selectedPath)) { return; } @@ -156,30 +156,69 @@ public static void Restore(MenuCommand menuCommand) // OpenFilePanel path is in OS format, so convert it to Unity format var unityPath = PathConverter.ToUnityPath(selectedPath); - var gameObject = GetLauncherObject(menuCommand); - var name = System.IO.Path.GetFileName(selectedPath).Replace(".asset", string.Empty); - gameObject.name = name; - - var installer = new FaceEmoInstaller(gameObject); - var backupper = installer.Container.Resolve(); try { - backupper.Import(unityPath); + Restore(unityPath); } catch (Exception ex) { var loc = LocalizationSetting.GetTable(LocalizationSetting.GetLocale()); EditorUtility.DisplayDialog(DomainConstants.SystemName, loc.Common_Message_FailedToOpenProject + "\n" + loc.Common_Message_SeeConsole, "OK"); Debug.LogError(loc.Common_Message_FailedToOpenProject + "\n" + ex?.ToString()); + } + } + + public static bool CanRestore(RestorationCheckpoint restorationCheckpoint) + { + return + restorationCheckpoint != null && + restorationCheckpoint.LatestBackup != null && + !string.IsNullOrEmpty(AssetDatabase.GetAssetPath(restorationCheckpoint.LatestBackup)); + } + + public static GameObject Restore(RestorationCheckpoint restorationCheckpoint) + { + var launcherObject = Restore(AssetDatabase.GetAssetPath(restorationCheckpoint.LatestBackup)); + + restorationCheckpoint.gameObject.SetActive(false); + Selection.activeGameObject = launcherObject; + + // Close MainWindow to avoid users editing the old menu. + var mainWindow = EditorWindow.GetWindow(); + if (mainWindow != null) { mainWindow.Close(); } + + var newCheckpoint = launcherObject.GetComponent(); + if (newCheckpoint == null) { launcherObject.AddComponent(); } + EditorUtility.CopySerialized(restorationCheckpoint, newCheckpoint); + + return launcherObject; + } + + private static GameObject Restore(string path) + { + var gameObject = GetLauncherObject(); + var name = System.IO.Path.GetFileName(path).Replace(".asset", string.Empty); + gameObject.name = name; + + var installer = new FaceEmoInstaller(gameObject); + var backupper = installer.Container.Resolve(); + try + { + backupper.Import(path); + return gameObject; + } + catch (Exception) + { if (gameObject != null) { DestroyImmediate(gameObject); } + throw; } } - private static GameObject GetLauncherObject(MenuCommand menuCommand) + private static GameObject GetLauncherObject(MenuCommand menuCommand = null) { var gameObject = new GameObject(); gameObject.AddComponent(); - GameObjectUtility.SetParentAndAlign(gameObject, menuCommand.context as GameObject); + if (menuCommand != null) { GameObjectUtility.SetParentAndAlign(gameObject, menuCommand.context as GameObject); } Undo.RegisterCreatedObjectUndo(gameObject, $"Create {DomainConstants.SystemName} Object"); Selection.activeObject = gameObject; UnityEditorInternal.InternalEditorUtility.SetIsInspectorExpanded(gameObject.GetComponent(), true); @@ -224,46 +263,77 @@ private static void HierarchyWindowItemOnGUI(int instanceID, Rect selectionRect) { if (!CanLaunch()) { return; } - var exists = false; - foreach (var launcher in FindObjectsOfType()?.OrderBy(x => x.name)) - { - if (ReferenceEquals(launcher?.AV3Setting?.TargetAvatar, avatarDescriptor)) - { - Launch(launcher); - exists = true; - break; - } - } + if (TryLaunchFromExisting(avatarDescriptor)) { return; } + + if (TryLaunchFromCheckpoint(avatarDescriptor, loc)) { return; } + + LaunchFromNew(avatarDescriptor, loc); + } + GUI.DrawTexture(selectionRect, icon, ScaleMode.ScaleToFit, alphaBlend: true); + } - if (!exists) + private static bool TryLaunchFromExisting(VRCAvatarDescriptor avatarDescriptor) + { + foreach (var launcher in FindObjectsOfType()?.OrderBy(x => x.name)) + { + if (ReferenceEquals(launcher?.AV3Setting?.TargetAvatar, avatarDescriptor)) { - var launcherObject = Create(new MenuCommand(null, 0)); - var installer = new FaceEmoInstaller(launcherObject); + Launch(launcher); + return true; + } + } + return false; + } - var launcher = launcherObject.GetComponent(); - launcher.AV3Setting.TargetAvatar = avatarDescriptor; + private static bool TryLaunchFromCheckpoint(VRCAvatarDescriptor avatarDescriptor, LocalizationTable loc) + { + foreach (var checkpoint in FindObjectsOfType()?.OrderBy(x => x.name)) + { + if (!ReferenceEquals(checkpoint.TargetAvatar, avatarDescriptor) || !CanRestore(checkpoint)) { continue; } + if (OptoutableDialog.Show(DomainConstants.SystemName, loc.Launcher_Message_Restore, "OK", isRiskyAction: false)) + { try { - ImportPatternsAndOptions(launcherObject, installer, avatarDescriptor); + var launcherObject = Restore(checkpoint); + var launcher = launcherObject.GetComponent(); + Launch(launcher); + return true; } catch (Exception ex) { - EditorUtility.DisplayDialog(DomainConstants.SystemName, loc.Launcher_Message_ImportError + "\n\n" + ex?.Message, "OK"); - Debug.LogError(loc.Launcher_Message_ImportError + ex?.ToString()); + EditorUtility.DisplayDialog(DomainConstants.SystemName, loc.Launcher_Message_RestoreError + "\n\n" + ex?.Message, "OK"); + Debug.LogError(loc.Launcher_Message_RestoreError + ex?.ToString()); + return false; + } + } + } + return false; + } - UnityEngine.Object.DestroyImmediate(launcher); + private static void LaunchFromNew(VRCAvatarDescriptor avatarDescriptor, LocalizationTable loc) + { + var launcherObject = CreateCommand(); + var installer = new FaceEmoInstaller(launcherObject); - installer = new FaceEmoInstaller(launcherObject); + var launcher = launcherObject.GetComponent(); + launcher.AV3Setting.TargetAvatar = avatarDescriptor; - launcher = launcherObject.GetComponent(); - launcher.AV3Setting.TargetAvatar = avatarDescriptor; - } + try + { + ImportPatternsAndOptions(launcherObject, installer, avatarDescriptor); + } + catch (Exception ex) + { + EditorUtility.DisplayDialog(DomainConstants.SystemName, loc.Launcher_Message_ImportError + "\n\n" + ex?.Message, "OK"); + Debug.LogError(loc.Launcher_Message_ImportError + ex?.ToString()); - Launch(launcher); - } + DestroyImmediate(launcher); + new FaceEmoInstaller(launcherObject); + launcher = launcherObject.GetComponent(); + launcher.AV3Setting.TargetAvatar = avatarDescriptor; } - GUI.DrawTexture(selectionRect, icon, ScaleMode.ScaleToFit, alphaBlend: true); + Launch(launcher); } private static void ImportPatternsAndOptions(GameObject rootObject, FaceEmoInstaller installer, VRCAvatarDescriptor avatarDescriptor) diff --git a/Packages/jp.suzuryg.face-emo/Editor/AppMain/FaceEmoRestorer.cs b/Packages/jp.suzuryg.face-emo/Editor/AppMain/FaceEmoRestorer.cs new file mode 100644 index 00000000..5987339b --- /dev/null +++ b/Packages/jp.suzuryg.face-emo/Editor/AppMain/FaceEmoRestorer.cs @@ -0,0 +1,26 @@ +using Suzuryg.FaceEmo.AppMain; +using Suzuryg.FaceEmo.Components; + +namespace Suzuryg.FaceEmo.Detail +{ + public class FaceEmoRestorer : IRestorer + { + private RestorationCheckpoint _restorationCheckpoint; + + public FaceEmoRestorer(RestorationCheckpoint restorationCheckpoint) + { + _restorationCheckpoint = restorationCheckpoint; + } + + public bool CanRestore() => FaceEmoLauncher.CanRestore(_restorationCheckpoint); + public void Restore() => FaceEmoLauncher.Restore(_restorationCheckpoint); + + public (string current, string backup) GetNames() + { + var current = _restorationCheckpoint.gameObject != null ? _restorationCheckpoint.gameObject.name : string.Empty; + var backup = _restorationCheckpoint.LatestBackup != null ? _restorationCheckpoint.LatestBackup.name : string.Empty; + return (current, backup); + } + } +} + diff --git a/Packages/jp.suzuryg.face-emo/Editor/AppMain/FaceEmoRestorer.cs.meta b/Packages/jp.suzuryg.face-emo/Editor/AppMain/FaceEmoRestorer.cs.meta new file mode 100644 index 00000000..bd30a7d4 --- /dev/null +++ b/Packages/jp.suzuryg.face-emo/Editor/AppMain/FaceEmoRestorer.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: ae1cb9946394cd946b58149bbe3499e8 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/jp.suzuryg.face-emo/Editor/Detail/AV3/FxGenerator.cs b/Packages/jp.suzuryg.face-emo/Editor/Detail/AV3/FxGenerator.cs index edcb4d63..25d7eed7 100644 --- a/Packages/jp.suzuryg.face-emo/Editor/Detail/AV3/FxGenerator.cs +++ b/Packages/jp.suzuryg.face-emo/Editor/Detail/AV3/FxGenerator.cs @@ -1538,6 +1538,7 @@ private AacFlClip GetMouthMorphCancelerAnimation(AV3Setting aV3Setting, AacFlBas #pragma warning disable CS0612 var obsolete = aV3Setting.MouthMorphBlendShapes; + #pragma warning restore CS0612 if (obsolete.Any()) { var faceMesh = AV3Utility.GetFaceMesh(avatarDescriptor); @@ -1553,7 +1554,6 @@ private AacFlClip GetMouthMorphCancelerAnimation(AV3Setting aV3Setting, AacFlBas EditorUtility.SetDirty(aV3Setting); } } - #pragma warning restore CS0612 var excludeBlink = false; var excludeLipSync = true; diff --git a/Packages/jp.suzuryg.face-emo/Editor/Detail/FaceEmoBackupper.cs b/Packages/jp.suzuryg.face-emo/Editor/Detail/FaceEmoBackupper.cs index b6048bbf..4615bc3a 100644 --- a/Packages/jp.suzuryg.face-emo/Editor/Detail/FaceEmoBackupper.cs +++ b/Packages/jp.suzuryg.face-emo/Editor/Detail/FaceEmoBackupper.cs @@ -4,7 +4,7 @@ using Suzuryg.FaceEmo.Components.Settings; using Suzuryg.FaceEmo.Detail.AV3; using Suzuryg.FaceEmo.Detail.Data; -using Suzuryg.FaceEmo.Detail.Drawing; +using Suzuryg.FaceEmo.Detail.View; using UnityEditor; using UnityEngine; using Suzuryg.FaceEmo.Domain; @@ -25,24 +25,33 @@ public class FaceEmoBackupper : IBackupper, IDisposable private string _backupName; private IMenuBackupper _menuBackupper; + private IMenuRepository _menuRepository; + private SelectionSynchronizer _selectionSynchronizer; private AV3Setting _aV3Setting; private ExpressionEditorSetting _expressionEditorSetting; private ThumbnailSetting _thumbnailSetting; + private SerializedObject _restorationCheckpoint; private LocalizationTable _localizationTable; private CompositeDisposable _disposables = new CompositeDisposable(); public FaceEmoBackupper( IMenuBackupper menuBackupper, + IMenuRepository menuRepository, + SelectionSynchronizer selectionSynchronizer, AV3Setting aV3Setting, ExpressionEditorSetting expressionEditorSetting, ThumbnailSetting thumbnailSetting, + RestorationCheckpoint restorationCheckpoint, IReadOnlyLocalizationSetting localizationSetting) { _menuBackupper = menuBackupper; + _menuRepository = menuRepository; + _selectionSynchronizer = selectionSynchronizer; _aV3Setting = aV3Setting; _expressionEditorSetting = expressionEditorSetting; _thumbnailSetting = thumbnailSetting; + _restorationCheckpoint = new SerializedObject(restorationCheckpoint); // Localization table changed event handler localizationSetting.OnTableChanged.Synchronize().Subscribe(SetText).AddTo(_disposables); @@ -73,10 +82,17 @@ public void AutoBackup() } var path = $"{dir}/{_backupName}_{DateTime.Now.ToString("yyyyMMdd_HHmmss")}.asset"; - Export(path); + var project = SaveProject(path); + + _restorationCheckpoint.Update(); + _restorationCheckpoint.FindProperty(nameof(RestorationCheckpoint.TargetAvatar)).objectReferenceValue = _aV3Setting.TargetAvatar; + _restorationCheckpoint.FindProperty(nameof(RestorationCheckpoint.LatestBackup)).objectReferenceValue = project; + _restorationCheckpoint.ApplyModifiedProperties(); } - - public void Export(string path) + + public void Export(string path) => SaveProject(path); + + public FaceEmoProject SaveProject(string path) { BackupPath(); @@ -106,6 +122,8 @@ public void Export(string path) project.ThumbnailSetting.name = nameof(ThumbnailSetting); AssetDatabase.SaveAssets(); + + return project; } public void Import(string path) @@ -122,6 +140,12 @@ public void Import(string path) EditorUtility.CopySerialized(imported.ThumbnailSetting, _thumbnailSetting); RestorePath(); + + var menu = _menuRepository.Load(string.Empty); + if (menu.Registered.Order.Any()) + { + _selectionSynchronizer.ChangeMenuItemListViewSelection(menu.Registered.Order.First()); + } } private void SetText(LocalizationTable localizationTable) diff --git a/Packages/jp.suzuryg.face-emo/Editor/Detail/IRestorer.cs b/Packages/jp.suzuryg.face-emo/Editor/Detail/IRestorer.cs new file mode 100644 index 00000000..595d06d5 --- /dev/null +++ b/Packages/jp.suzuryg.face-emo/Editor/Detail/IRestorer.cs @@ -0,0 +1,10 @@ +namespace Suzuryg.FaceEmo.Detail +{ + public interface IRestorer + { + bool CanRestore(); + void Restore(); + (string current, string backup) GetNames(); + } +} + diff --git a/Packages/jp.suzuryg.face-emo/Editor/Detail/IRestorer.cs.meta b/Packages/jp.suzuryg.face-emo/Editor/Detail/IRestorer.cs.meta new file mode 100644 index 00000000..d4e95d91 --- /dev/null +++ b/Packages/jp.suzuryg.face-emo/Editor/Detail/IRestorer.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 9558b3c41385548449b1d8f6d8c24de7 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/jp.suzuryg.face-emo/Editor/Detail/Localization/LocalizationTable.cs b/Packages/jp.suzuryg.face-emo/Editor/Detail/Localization/LocalizationTable.cs index 1439b524..9a69e50b 100644 --- a/Packages/jp.suzuryg.face-emo/Editor/Detail/Localization/LocalizationTable.cs +++ b/Packages/jp.suzuryg.face-emo/Editor/Detail/Localization/LocalizationTable.cs @@ -14,6 +14,7 @@ public class LocalizationTable : ScriptableObject public string Common_Apply = "Apply"; public string Common_Import = "Import"; public string Common_Proceed = "Proceed"; + public string Common_Restore = "Restore"; public string Common_Colon = ": "; public string Common_Neutral = "Neutral"; @@ -61,6 +62,8 @@ public class LocalizationTable : ScriptableObject public string Launcher_Message_CheckVrcSdk3Avatars = "VRChat SDK Avatars 3.1.13 or later must be installed to use FaceEmo.\n\n" + "If VRChat SDK Avatars is not installed, or if an older version is installed, please install the latest version."; public string Launcher_Message_ImportError = "An error occurred while importing expression patterns and optional settings. The import will be canceled and FaceEmo will be newly started."; + public string Launcher_Message_Restore = "The facial expression setting for FaceEmo have been corrupted so it will be restored from the latest backup."; + public string Launcher_Message_RestoreError = "An error occurred while restoring from the backup. A new facial expression setting will be created."; public string HierarchyView_Title = "Tree View"; public string HierarchyView_RegisteredMenuItemList = "Expression Menu"; @@ -200,6 +203,11 @@ public class LocalizationTable : ScriptableObject public string InspectorView_ImportOptionalSettings = "Import Optional Settings From Avatar"; public string InspectorView_Message_ImportOptionalSettings = "Import optional settings from the avatar?"; + public string InspectorView_Restore = "Restore from AutoBackup"; + public string InspectorView_Message_Restore = "Restore the facial expression setting for FaceEmo from the latest backup?"; + public string InspectorView_Message_RestoreError = "An error occurred while restoring from the backup."; + public string InspectorView_Help_Restore = "A new \"<0>\" object is created and the current \"<1>\" object becomes inactive."; + public string InspectorView_ApplyingToMultipleAvatars = "Applying Facial Expression Menu to Multiple Avatars"; public string InspectorView_MenuPrefab = "Facial Expression Menu Prefab"; public string InspectorView_EmptyAvatars = "Press the + button to add an target avatar."; diff --git a/Packages/jp.suzuryg.face-emo/Editor/Detail/Localization/ja_JP.asset b/Packages/jp.suzuryg.face-emo/Editor/Detail/Localization/ja_JP.asset index 5ae105a4..f5f01c39 100644 --- a/Packages/jp.suzuryg.face-emo/Editor/Detail/Localization/ja_JP.asset +++ b/Packages/jp.suzuryg.face-emo/Editor/Detail/Localization/ja_JP.asset @@ -21,6 +21,7 @@ MonoBehaviour: Common_Apply: "\u9069\u7528" Common_Import: "\u8AAD\u8FBC" Common_Proceed: "\u7D9A\u884C" + Common_Restore: "\u5FA9\u5143" Common_Colon: "\uFF1A" Common_Neutral: Neutral Common_Fist: Fist @@ -67,6 +68,8 @@ MonoBehaviour: SDK Avatars 3.1.13 \u4EE5\u964D\u3092\u30A4\u30F3\u30B9\u30C8\u30FC\u30EB\u3059\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002

VRChat SDK Avatars \u304C\u30A4\u30F3\u30B9\u30C8\u30FC\u30EB\u3055\u308C\u3066\u3044\u306A\u3044\u5834\u5408\u3001\u3082\u3057\u304F\u306F\u53E4\u3044\u30D0\u30FC\u30B8\u30E7\u30F3\u304C\u30A4\u30F3\u30B9\u30C8\u30FC\u30EB\u3055\u308C\u3066\u3044\u308B\u5834\u5408\u306F\u6700\u65B0\u306E\u30D0\u30FC\u30B8\u30E7\u30F3\u3092\u30A4\u30F3\u30B9\u30C8\u30FC\u30EB\u3057\u3066\u304F\u3060\u3055\u3044\u3002" Launcher_Message_ImportError: "\u8868\u60C5\u30D1\u30BF\u30FC\u30F3\u3068\u30AA\u30D7\u30B7\u30E7\u30F3\u8A2D\u5B9A\u306E\u8AAD\u307F\u8FBC\u307F\u4E2D\u306B\u30A8\u30E9\u30FC\u304C\u767A\u751F\u3057\u307E\u3057\u305F\u3002\u8AAD\u307F\u8FBC\u307F\u3092\u30AD\u30E3\u30F3\u30BB\u30EB\u3057\u3066FaceEmo\u3092\u65B0\u3057\u304F\u958B\u59CB\u3057\u307E\u3059\u3002" + Launcher_Message_Restore: "FaceEmo\u306E\u8868\u60C5\u8A2D\u5B9A\u304C\u7834\u640D\u3057\u3066\u3044\u308B\u305F\u3081\u3001\u6700\u65B0\u306E\u30D0\u30C3\u30AF\u30A2\u30C3\u30D7\u304B\u3089\u5FA9\u5143\u3057\u307E\u3059\u3002" + Launcher_Message_RestoreError: "\u30D0\u30C3\u30AF\u30A2\u30C3\u30D7\u304B\u3089\u306E\u5FA9\u5143\u4E2D\u306B\u30A8\u30E9\u30FC\u304C\u767A\u751F\u3057\u307E\u3057\u305F\u3002\u8868\u60C5\u8A2D\u5B9A\u3092\u65B0\u3057\u304F\u4F5C\u6210\u3057\u307E\u3059\u3002" HierarchyView_Title: "\u30C4\u30EA\u30FC\u30D3\u30E5\u30FC" HierarchyView_RegisteredMenuItemList: "\u8868\u60C5\u30E1\u30CB\u30E5\u30FC" HierarchyView_UnregisteredMenuItemList: "\u30A2\u30FC\u30AB\u30A4\u30D6" @@ -190,6 +193,10 @@ MonoBehaviour: InspectorView_Info_ImportExpressionPatterns: "\u6A19\u6E96\u7684\u306A\u69CB\u6210\u306E\u30A2\u30D0\u30BF\u30FC\u3067\u306A\u3044\u5834\u5408\u3001\u8868\u60C5\u30D1\u30BF\u30FC\u30F3\u3092\u6B63\u3057\u304F\u8AAD\u307F\u8FBC\u3081\u306A\u3044\u3053\u3068\u304C\u3042\u308A\u307E\u3059\u3002" InspectorView_ImportOptionalSettings: "\u30A2\u30D0\u30BF\u30FC\u304B\u3089\u30AA\u30D7\u30B7\u30E7\u30F3\u8A2D\u5B9A\u3092\u8AAD\u307F\u8FBC\u3080" InspectorView_Message_ImportOptionalSettings: "\u30A2\u30D0\u30BF\u30FC\u304B\u3089\u30AA\u30D7\u30B7\u30E7\u30F3\u8A2D\u5B9A\u3092\u8AAD\u307F\u8FBC\u307F\u307E\u3059\u304B\uFF1F" + InspectorView_Restore: "\u81EA\u52D5\u30D0\u30C3\u30AF\u30A2\u30C3\u30D7\u304B\u3089\u5FA9\u5143" + InspectorView_Message_Restore: "\u6700\u65B0\u306E\u30D0\u30C3\u30AF\u30A2\u30C3\u30D7\u3092\u4F7F\u7528\u3057\u3066\u3001FaceEmo\u306E\u8868\u60C5\u8A2D\u5B9A\u3092\u5FA9\u5143\u3057\u307E\u3059\u304B\uFF1F " + InspectorView_Message_RestoreError: "\u30D0\u30C3\u30AF\u30A2\u30C3\u30D7\u304B\u3089\u306E\u5FA9\u5143\u4E2D\u306B\u30A8\u30E9\u30FC\u304C\u767A\u751F\u3057\u307E\u3057\u305F\u3002 " + InspectorView_Help_Restore: "\u300C<0>\u300D\u30AA\u30D6\u30B8\u30A7\u30AF\u30C8\u304C\u65B0\u3057\u304F\u4F5C\u6210\u3055\u308C\u3001\u73FE\u5728\u306E\u300C<1>\u300D\u30AA\u30D6\u30B8\u30A7\u30AF\u30C8\u306F\u975E\u30A2\u30AF\u30C6\u30A3\u30D6\u306B\u306A\u308A\u307E\u3059\u3002 " InspectorView_ApplyingToMultipleAvatars: "\u8907\u6570\u30A2\u30D0\u30BF\u30FC\u3078\u306E\u8868\u60C5\u30E1\u30CB\u30E5\u30FC\u9069\u7528" InspectorView_MenuPrefab: "\u8868\u60C5\u30E1\u30CB\u30E5\u30FC\u306EPrefab" InspectorView_EmptyAvatars: "\uFF0B\u30DC\u30BF\u30F3\u3092\u62BC\u3057\u3066\u5BFE\u8C61\u30A2\u30D0\u30BF\u30FC\u3092\u8FFD\u52A0\u3067\u304D\u307E\u3059\u3002 " diff --git a/Packages/jp.suzuryg.face-emo/Editor/Detail/View/InspectorView.cs b/Packages/jp.suzuryg.face-emo/Editor/Detail/View/InspectorView.cs index 9d218592..6dbd93e1 100644 --- a/Packages/jp.suzuryg.face-emo/Editor/Detail/View/InspectorView.cs +++ b/Packages/jp.suzuryg.face-emo/Editor/Detail/View/InspectorView.cs @@ -36,6 +36,7 @@ public class InspectorView : IDisposable private Subject<(IMenu menu, bool isModified)> _onMenuUpdated = new Subject<(IMenu menu, bool isModified)>(); private IMenuRepository _menuRepository; + private IRestorer _restorer; private ILocalizationSetting _localizationSetting; private InspectorThumbnailDrawer _thumbnailDrawer; private SerializedObject _inspectorViewState; @@ -63,6 +64,7 @@ public class InspectorView : IDisposable public InspectorView( IMenuRepository menuRepository, + IRestorer restorer, ILocalizationSetting localizationSetting, InspectorThumbnailDrawer inspectorThumbnailDrawer, InspectorViewState inspectorViewState, @@ -71,6 +73,7 @@ public InspectorView( { // Dependencies _menuRepository = menuRepository; + _restorer = restorer; _localizationSetting = localizationSetting; _thumbnailDrawer = inspectorThumbnailDrawer; _inspectorViewState = new SerializedObject(inspectorViewState); @@ -248,6 +251,10 @@ public void OnGUI() EditorGUILayout.Space(10); + Field_Restore(); + + EditorGUILayout.Space(10); + // Target avatar Field_TargetAvatar(); @@ -562,6 +569,38 @@ private void Field_ImportButtons() } } + private void Field_Restore() + { + using (new EditorGUI.DisabledScope(!_restorer.CanRestore())) + { + if (GUILayout.Button(_localizationTable.InspectorView_Restore)) + { + var names = _restorer.GetNames(); + var messages = new List<(MessageType type, string message)>() + { + (MessageType.None, _localizationTable.InspectorView_Message_Restore), + (MessageType.None, string.Empty), + (MessageType.Info, _localizationTable.InspectorView_Help_Restore.Replace("<0>", names.backup).Replace("<1>", names.current)), + }; + + if (OptoutableDialog.Show(DomainConstants.SystemName, string.Empty, + _localizationTable.Common_Restore, _localizationTable.Common_Cancel, isRiskyAction: false, + additionalMessages: messages, windowHeight: OptoutableDialog.GetHeightWithoutMessage())) + { + try + { + _restorer.Restore(); + } + catch (Exception ex) + { + EditorUtility.DisplayDialog(DomainConstants.SystemName, _localizationTable.InspectorView_Message_RestoreError + "\n\n" + ex?.Message, "OK"); + Debug.LogError(_localizationTable.InspectorView_Message_RestoreError + ex?.ToString()); + } + } + } + } + } + private void Field_TargetAvatar() { var property = _av3Setting.FindProperty(nameof(AV3Setting.TargetAvatar)); @@ -725,6 +764,7 @@ private void Field_MouthMorphBlendShape() #pragma warning disable CS0612 var obsoleteProperty = _av3Setting.FindProperty(nameof(AV3Setting.MouthMorphBlendShapes)); + #pragma warning restore CS0612 if (obsoleteProperty.arraySize > 0) { var obsolete = GetValue>(obsoleteProperty); @@ -743,7 +783,6 @@ private void Field_MouthMorphBlendShape() SetValue(mouthMorphsProperty, mouthMorphs); } } - #pragma warning restore CS0612 using (new EditorGUI.DisabledScope(useMouthMorphCancelClip.boolValue)) { diff --git a/Packages/jp.suzuryg.face-emo/Runtime/Components/RestorationCheckpoint.cs b/Packages/jp.suzuryg.face-emo/Runtime/Components/RestorationCheckpoint.cs new file mode 100644 index 00000000..35660d07 --- /dev/null +++ b/Packages/jp.suzuryg.face-emo/Runtime/Components/RestorationCheckpoint.cs @@ -0,0 +1,11 @@ +using UnityEngine; + +namespace Suzuryg.FaceEmo.Components +{ + [DisallowMultipleComponent] + public class RestorationCheckpoint : MonoBehaviour + { + public MonoBehaviour TargetAvatar; + public ScriptableObject LatestBackup; + } +} diff --git a/Packages/jp.suzuryg.face-emo/Runtime/Components/RestorationCheckpoint.cs.meta b/Packages/jp.suzuryg.face-emo/Runtime/Components/RestorationCheckpoint.cs.meta new file mode 100644 index 00000000..e057da3e --- /dev/null +++ b/Packages/jp.suzuryg.face-emo/Runtime/Components/RestorationCheckpoint.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 9a44dd69b12f8e64294350e5f728904c +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/jp.suzuryg.face-emo/Tests/Editor/Detail/FaceEmoBackupperTests.cs b/Packages/jp.suzuryg.face-emo/Tests/Editor/Detail/FaceEmoBackupperTests.cs index fb7791df..ea8f9df1 100644 --- a/Packages/jp.suzuryg.face-emo/Tests/Editor/Detail/FaceEmoBackupperTests.cs +++ b/Packages/jp.suzuryg.face-emo/Tests/Editor/Detail/FaceEmoBackupperTests.cs @@ -1,9 +1,13 @@ using NUnit.Framework; +using Suzuryg.FaceEmo.Components; using Suzuryg.FaceEmo.Components.Data; using Suzuryg.FaceEmo.Components.Settings; +using Suzuryg.FaceEmo.Components.States; using Suzuryg.FaceEmo.Detail.AV3; using Suzuryg.FaceEmo.Detail.Data; using Suzuryg.FaceEmo.Detail.Localization; +using Suzuryg.FaceEmo.Detail.View; +using Suzuryg.FaceEmo.UseCase; using UnityEditor; using UnityEngine; using VRC.Dynamics; @@ -32,12 +36,18 @@ public void Setup() var menuRepository = new MenuRepository(menuRepositoryComponent); menuRepository.Save(string.Empty, new Domain.Menu(), string.Empty); + var updateMenuSubject = new UpdateMenuSubject(); + var viewSelection = ScriptableObject.CreateInstance(); + var selectionSynchronizer = new SelectionSynchronizer(menuRepository, updateMenuSubject, viewSelection); + + var restorationCheckpoint = launcher.AddComponent(); + _av3Setting = ScriptableObject.CreateInstance(); _expressionEditorSetting = ScriptableObject.CreateInstance(); _thumbnailSetting = ScriptableObject.CreateInstance(); var localizationSetting = new LocalizationSetting(); - _backupper = new FaceEmoBackupper(menuRepository, _av3Setting, _expressionEditorSetting, _thumbnailSetting, localizationSetting); + _backupper = new FaceEmoBackupper(menuRepository, menuRepository, selectionSynchronizer, _av3Setting, _expressionEditorSetting, _thumbnailSetting, restorationCheckpoint, localizationSetting); if (!AssetDatabase.IsValidFolder(TempDirPath)) {