Bläddra i källkod

Merge branch 'master' into converter-rewrite

habeebweeb 2 år sedan
förälder
incheckning
294b5e8ec3
34 ändrade filer med 815 tillägg och 607 borttagningar
  1. 70 10
      Documentation/readme.adoc
  2. 0 1
      MeidoPhotoStudio.Plugin.sln
  3. 17 6
      readme.md
  4. 6 3
      src/MeidoPhotoStudio.Plugin/Config/MeidoPhotoStudio/Translations/en/translation.ui.json
  5. 10 10
      src/MeidoPhotoStudio.Plugin/Constants.cs
  6. 9 1
      src/MeidoPhotoStudio.Plugin/DragPoint/CustomGizmo.cs
  7. 1 1
      src/MeidoPhotoStudio.Plugin/DragPoint/DragPointGeneral.cs
  8. 4 4
      src/MeidoPhotoStudio.Plugin/DragPoint/DragPointProp.cs
  9. 16 9
      src/MeidoPhotoStudio.Plugin/GUI/Panes/BackgroundWindowPanes/EffectsPanes/OtherEffectsPane.cs
  10. 4 2
      src/MeidoPhotoStudio.Plugin/GUI/Panes/BackgroundWindowPanes/LightsPane.cs
  11. 4 0
      src/MeidoPhotoStudio.Plugin/GUI/Panes/BasePane.cs
  12. 72 25
      src/MeidoPhotoStudio.Plugin/GUI/Panes/CallWindowPanes/MaidSelectorPane.cs
  13. 1 6
      src/MeidoPhotoStudio.Plugin/GUI/Panes/MainWindowPanes/BaseMainWindowPane.cs
  14. 42 20
      src/MeidoPhotoStudio.Plugin/GUI/Panes/PoseWindowPanes/MaidPoseSelectorPane.cs
  15. 52 13
      src/MeidoPhotoStudio.Plugin/GUI/Windows/BaseWindow.cs
  16. 0 23
      src/MeidoPhotoStudio.Plugin/GUI/Windows/BaseWindowPane.cs
  17. 17 3
      src/MeidoPhotoStudio.Plugin/GUI/Windows/MainWindow.cs
  18. 20 1
      src/MeidoPhotoStudio.Plugin/GUI/Windows/MessageWindow.cs
  19. 0 2
      src/MeidoPhotoStudio.Plugin/GUI/Windows/SceneWindow.cs
  20. 0 222
      src/MeidoPhotoStudio.Plugin/LexicographicStringComparer.cs
  21. 17 3
      src/MeidoPhotoStudio.Plugin/Managers/CameraManager.cs
  22. 136 79
      src/MeidoPhotoStudio.Plugin/Managers/MeidoManager.cs
  23. 94 55
      src/MeidoPhotoStudio.Plugin/Managers/MessageWindowManager.cs
  24. 2 2
      src/MeidoPhotoStudio.Plugin/Managers/SceneManager.cs
  25. 33 12
      src/MeidoPhotoStudio.Plugin/Meido/Meido.cs
  26. 71 13
      src/MeidoPhotoStudio.Plugin/Meido/MeidoDragPointManager.cs
  27. 19 35
      src/MeidoPhotoStudio.Plugin/MeidoPhotoStudio.Plugin.csproj
  28. 44 31
      src/MeidoPhotoStudio.Plugin/MeidoPhotoStudio.cs
  29. 4 4
      src/MeidoPhotoStudio.Plugin/Serialization/SceneMetadata.cs
  30. 4 0
      src/MeidoPhotoStudio.Plugin/Serialization/Serializers/ManagerSerializers/CameraManagerSerializer.cs
  31. 23 2
      src/MeidoPhotoStudio.Plugin/Serialization/Serializers/MeidoSerializer.cs
  32. 4 4
      src/MeidoPhotoStudio.Plugin/Serialization/SimpleSerializers/DragPointPropDTOSerializer.cs
  33. 5 5
      src/MeidoPhotoStudio.Plugin/Serialization/SimpleSerializers/TransformDTOSerializer.cs
  34. 14 0
      src/MeidoPhotoStudio.Plugin/WindowsLogicalComparer.cs

+ 70 - 10
Documentation/readme.adoc

@@ -108,9 +108,15 @@ NOTE: Environments, scenes, and each preset folder can have single level deep fo
 | `Shift + Scroll`
 | Zoom camera faster
 
+| `Control + Scroll`
+| Zoom camera slower
+
 | `Shift + Middle Drag`
 | Pan camera faster
 
+| `Control + Middle Drag`
+| Pan camera slower
+
 |===
 
 === Drag Handles
@@ -257,6 +263,60 @@ NOTE: Environments, scenes, and each preset folder can have single level deep fo
 
 == Changelog
 
+=== {pluginname}.1.0.0-beta.4.1
+
+==== Enhancements
+
+* IK drag handles for the arms and fingers match body node position rather than bone position so drag handles are no
+longer offset from maid body.
+
+==== Fixes
+
+* Fix message box and text disappearing after leaving edit mode
+* Fix background switcher breaking when `MyRoom` directory is missing from game root
+* Fix blur effect not turning off properly
+* Fix issue where loading a scene that uses a non-existent pose breaks the pose selector
+* Fix MPS naively restoring edit mode's OK button's original functionality
+** Other plugins may have hooked onto the OK button and MPS restoring original functionality effectively removes those
+hooks
+
+=== {pluginname}.1.0.0-beta.4
+
+==== New Features
+
+* Add a toggle to only list active maids in the scene
+
+* Add hotkey to slow down camera zoom and movement
+** Added to ease the difficulty of manipulating objects at a very small scale.
+
+==== Enhancements
+
+* Remove exist check for mod prop icon files
+** Makes loading the mod prop list a lot faster.
+
+* Prevent already active maids from being reactivated when called again
+* Add confirmation when exiting MPS in edit mode
+
+==== Changes
+
+* Reduce drag point size for fingers/toes
+* Move "Colour" (now "Hide BG") toggle next to light type radio buttons
+
+* Set lower limit for object scale to 0x
+** Not a very comfortable experience but it's there now.
+
+* Update Translations
+
+==== Fixes
+
+* Fix preset change breaking hair/skirt gravity
+* Fix "Private Mode" maid interfering with MPS
+* Fix non-existent pose soft locking pose selector
+* Fix alternate mune rotation (control + alt + shift) not being saved
+
+* Stop camera movement and rotation when saving/loading scene
+** This was present before but was missing when save system was reworked.
+
 === {pluginname}.1.0.0-beta.3.1
 
 ==== Fixes
@@ -268,16 +328,6 @@ NOTE: Environments, scenes, and each preset folder can have single level deep fo
 
 === {pluginname}.1.0.0-beta.3
 
-==== Fixes
-* Fix face tab sliders/toggles doing nothing when using face shapekeys in ShapeAnimator
-* Fix face blush toggles doing nothing
-
-==== Changes
-* Make bone mode drag handles way smaller and more transparent
-
-==== Enhancements
-* Add spine as attach points for props
-
 ==== New Features
 * Add camera Z rotation and FOV slider
 
@@ -293,3 +343,13 @@ NOTE: Environments, scenes, and each preset folder can have single level deep fo
 
 * Add user configurable face slider limits
 ** `Config\MeidoPhotoStudio\Database\face_slider_limits.json` has been added
+
+==== Enhancements
+* Add spine as attach points for props
+
+==== Changes
+* Make bone mode drag handles way smaller and more transparent
+
+==== Fixes
+* Fix face tab sliders/toggles doing nothing when using face shapekeys in ShapeAnimator
+* Fix face blush toggles doing nothing

+ 0 - 1
MeidoPhotoStudio.Plugin.sln

@@ -19,7 +19,6 @@ Global
 		{19D28B0C-3537-4FEE-B7B3-1ABF70B16D5E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
 		{19D28B0C-3537-4FEE-B7B3-1ABF70B16D5E}.Debug|Any CPU.Build.0 = Debug|Any CPU
 		{19D28B0C-3537-4FEE-B7B3-1ABF70B16D5E}.Release|Any CPU.ActiveCfg = Release|Any CPU
-		{19D28B0C-3537-4FEE-B7B3-1ABF70B16D5E}.Release|Any CPU.Build.0 = Release|Any CPU
 	EndGlobalSection
 	GlobalSection(SolutionProperties) = preSolution
 		HideSolutionNode = FALSE

+ 17 - 6
readme.md

@@ -1,6 +1,6 @@
 # MeidoPhotoStudio
 
-A MultipleMaids alternative
+A screenshot making plugin alternative to Studio Mode.
 
 ## Features
 
@@ -33,16 +33,27 @@ A MultipleMaids alternative
 
 ![MM is spaghetti code](./img/spaghetti_code.jpg)
 
-Despite being an overall good plugin, MultipleMaids (MM) is plagued with many bugs and is seldom maintained by the original developers.
+Despite being an overall good plugin (in terms of functionality), MultipleMaids (MM) is plagued with many bugs and is
+seldom maintained by the original developer.
 
-ModifiedMM was an effort to fix some of the problems with MM and even add new features. Although great progress was made, MM's code base prevents anyone, but the original developer (hopefully), from making any meaningful changes. The code base can rival that of the flying spaghetti monster.
+A previous project I worked on, ModifiedMM, was an effort to fix some of the problems with MM and even add new features.
+Although great progress was made, MM's code base prevents anyone, but the original developer (hopefully),
+from making any meaningful changes.
 
-As well as MM being hard to maintain, ModifiedMM was created against the wishes of the original developers.
+ModifiedMM was also created against the wishes of the original developer as stated in MM's `使い方.txt` (`Usage.txt`).
 
 > "転載・配布・改変したプラグインの公開は禁止します。" (使い方.txt)  
-> "Publication of reprinted/distributed/modified plug-ins is prohibited" (Google Translate).
 
-MeidoPhotoStudio is an attempt at making MultipleMaids truly great again.
+Which translates to _"Publication of reprinted/distributed/modified plug-ins is prohibited"_ (Google Translate).
+
+MeidoPhotoStudio is a completely new plugin, written from the ground up, that aims to deliver a simple and more
+streamlined screenshot making experience that is familiar to MultipleMaids users.
+
+## その他 / Note
+
+このプラグインは複数メイド撮影プラグインの改変・改造ではありません。
+
+MeidoPhotoStudio is not based on and does not use any MultipleMaids source code.
 
 ## Building
 

+ 6 - 3
src/MeidoPhotoStudio.Plugin/Config/MeidoPhotoStudio/Translations/en/translation.ui.json

@@ -9,7 +9,8 @@
     "maidCallWindow": {
         "okButton": "OK",
         "clearButton": "Clear",
-        "callButton": "Call"
+        "callButton": "Call",
+        "activeOnlyToggle": "Active Only"
     },
     "placementDropdown": {
         "normal": "Normal",
@@ -263,8 +264,8 @@
         "resetPosition": "Position",
         "resetProperties": "Properties",
         "clear": "Clear",
-        "colour": "Colour",
-        "disable": "On",
+        "colour": "Hide BG",
+        "disable": "Off",
         "resetLabel": "Reset"
     },
     "lightType": {
@@ -429,6 +430,8 @@
         "openSceneManager": "Open Scene Manager"
     },
     "settingsLabels": {
+        "settingsButton": "Settings",
+        "closeSettingsButton": "Close",
         "reloadTranslation": "Reload Translation",
         "reloadAllPresets": "Reload Presets"
     },

+ 10 - 10
src/MeidoPhotoStudio.Plugin/Constants.cs

@@ -181,7 +181,7 @@ namespace MeidoPhotoStudio.Plugin
             else category = faceGroup;
 
             CustomFaceDict[category].Add(fullPath);
-            CustomFaceDict[category].Sort(LexicographicStringComparer.Comparison);
+            CustomFaceDict[category].Sort(WindowsLogicalComparer.StrCmpLogicalW);
 
             CustomFaceChange?.Invoke(null, new PresetChangeEventArgs(fullPath, category));
         }
@@ -231,7 +231,7 @@ namespace MeidoPhotoStudio.Plugin
             else category = poseGroup;
 
             CustomPoseDict[category].Add(fullPath);
-            CustomPoseDict[category].Sort(LexicographicStringComparer.Comparison);
+            CustomPoseDict[category].Sort(WindowsLogicalComparer.StrCmpLogicalW);
 
             CustomPoseChange?.Invoke(null, new PresetChangeEventArgs(fullPath, category));
         }
@@ -290,7 +290,7 @@ namespace MeidoPhotoStudio.Plugin
             else category = handGroup;
 
             CustomHandDict[category].Add(fullPath);
-            CustomHandDict[category].Sort(LexicographicStringComparer.Comparison);
+            CustomHandDict[category].Sort(WindowsLogicalComparer.StrCmpLogicalW);
 
             CustomHandChange?.Invoke(null, new PresetChangeEventArgs(fullPath, category));
         }
@@ -322,7 +322,7 @@ namespace MeidoPhotoStudio.Plugin
             if (a == b) return 0;
             if (a == topItem) return -1;
             if (b == topItem) return 1;
-            else return LexicographicStringComparer.Comparison(a, b);
+            else return WindowsLogicalComparer.StrCmpLogicalW(a, b);
         }
 
         public static void InitializePoses()
@@ -421,7 +421,7 @@ namespace MeidoPhotoStudio.Plugin
                     string poseGroupName = new DirectoryInfo(directory).Name;
                     if (poseGroupName != customPoseDirectory) CustomPoseGroupList.Add(poseGroupName);
                     CustomPoseDict[poseGroupName] = poseList.ToList();
-                    CustomPoseDict[poseGroupName].Sort(LexicographicStringComparer.Comparison);
+                    CustomPoseDict[poseGroupName].Sort(WindowsLogicalComparer.StrCmpLogicalW);
                 }
             }
 
@@ -451,7 +451,7 @@ namespace MeidoPhotoStudio.Plugin
                     string presetCategory = new DirectoryInfo(directory).Name;
                     if (presetCategory != customHandDirectory) CustomHandGroupList.Add(presetCategory);
                     CustomHandDict[presetCategory] = presetList.ToList();
-                    CustomHandDict[presetCategory].Sort(LexicographicStringComparer.Comparison);
+                    CustomHandDict[presetCategory].Sort(WindowsLogicalComparer.StrCmpLogicalW);
                 }
             }
 
@@ -495,7 +495,7 @@ namespace MeidoPhotoStudio.Plugin
                     string faceGroupName = new DirectoryInfo(directory).Name;
                     if (faceGroupName != customFaceDirectory) CustomFaceGroupList.Add(faceGroupName);
                     CustomFaceDict[faceGroupName] = presetList.ToList();
-                    CustomFaceDict[faceGroupName].Sort(LexicographicStringComparer.Comparison);
+                    CustomFaceDict[faceGroupName].Sort(WindowsLogicalComparer.StrCmpLogicalW);
                 }
             }
 
@@ -542,13 +542,13 @@ namespace MeidoPhotoStudio.Plugin
                 }
             }
 
+            // Set index regardless of there being myRoom bgs or not
+            MyRoomCustomBGIndex = BGList.Count;
+
             Dictionary<string, string> saveDataDict = CreativeRoomManager.GetSaveDataDic();
 
             if (saveDataDict != null)
-            {
-                MyRoomCustomBGIndex = BGList.Count;
                 MyRoomCustomBGList.AddRange(saveDataDict);
-            }
         }
 
         public static void InitializeDogu()

+ 9 - 1
src/MeidoPhotoStudio.Plugin/DragPoint/CustomGizmo.cs

@@ -8,6 +8,8 @@ namespace MeidoPhotoStudio.Plugin
     {
         private static readonly Camera camera = GameMain.Instance.MainCamera.camera;
         private Transform target;
+        private bool hasAlternateTarget;
+        private Transform positionTransform;
         private readonly FieldInfo beSelectedType = Utility.GetFieldInfo<GizmoRender>("beSelectedType");
         private int SelectedType => (int)beSelectedType.GetValue(this);
         private static readonly FieldInfo is_drag_ = Utility.GetFieldInfo<GizmoRender>("is_drag_");
@@ -70,6 +72,12 @@ namespace MeidoPhotoStudio.Plugin
             return gizmo;
         }
 
+        public void SetAlternateTarget(Transform trans)
+        {
+            positionTransform = trans;
+            hasAlternateTarget = trans != null;
+        }
+
         public override void Update()
         {
             BeginUpdate();
@@ -130,7 +138,7 @@ namespace MeidoPhotoStudio.Plugin
         private void SetTransform()
         {
             Transform transform = this.transform;
-            transform.position = target.position;
+            transform.position = (hasAlternateTarget ? positionTransform : target).position;
             transform.localScale = Vector3.one;
             transform.rotation = gizmoMode switch
             {

+ 1 - 1
src/MeidoPhotoStudio.Plugin/DragPoint/DragPointGeneral.cs

@@ -197,7 +197,7 @@ namespace MeidoPhotoStudio.Plugin
             {
                 scaling = true;
                 float scale = currentScale + (mouseDelta.y / 200f * ScaleFactor);
-                if (scale < 0.1f) scale = 0.1f;
+                if (scale < 0f) scale = 0f;
                 MyObject.localScale = new Vector3(scale, scale, scale);
                 OnScale();
             }

+ 4 - 4
src/MeidoPhotoStudio.Plugin/DragPoint/DragPointProp.cs

@@ -88,10 +88,10 @@ namespace MeidoPhotoStudio.Plugin
         public enum PropType { Mod, MyRoom, Bg, Odogu }
 
         public PropType Type { get; }
-        public string IconFile { get; init; }
-        public string Filename { get; init; }
-        public string SubFilename { get; init; }
-        public int MyRoomID { get; init; }
+        public string IconFile { get; set; }
+        public string Filename { get; set; }
+        public string SubFilename { get; set; }
+        public int MyRoomID { get; set; }
 
         public PropInfo(PropType type) => Type = type;
 

+ 16 - 9
src/MeidoPhotoStudio.Plugin/GUI/Panes/BackgroundWindowPanes/EffectsPanes/OtherEffectsPane.cs

@@ -27,11 +27,17 @@ namespace MeidoPhotoStudio.Plugin
             blurSlider = new Slider(Translation.Get("otherEffectsPane", "blurSlider"), 0f, 18f);
             blurSlider.ControlEvent += (s, a) =>
             {
-                float value = blurSlider.Value;
-                if (!blurEffectManager.Active && value > 0f) blurEffectManager.SetEffectActive(true);
-                else if (blurEffectManager.Active && value == 0f) blurEffectManager.SetEffectActive(false);
+                if (updating)
+                    return;
 
-                if (blurEffectManager.Active) blurEffectManager.BlurSize = blurSlider.Value;
+                var value = blurSlider.Value;
+
+                if (!blurEffectManager.Active && value > 0f)
+                    blurEffectManager.SetEffectActive(true);
+                else if (blurEffectManager.Active && Mathf.Approximately(value, 0f))
+                    blurEffectManager.SetEffectActive(false);
+
+                blurEffectManager.BlurSize = value;
             };
         }
 
@@ -51,14 +57,15 @@ namespace MeidoPhotoStudio.Plugin
 
         public override void UpdatePane()
         {
+            updating = true;
+
             if (sepiaToneEffectManger.Ready)
-            {
-                updating = true;
                 sepiaToggle.Value = sepiaToneEffectManger.Active;
-                updating = false;
-            }
 
-            if (blurEffectManager.Ready) blurSlider.Value = blurEffectManager.BlurSize;
+            if (blurEffectManager.Ready)
+                blurSlider.Value = blurEffectManager.BlurSize;
+
+            updating = false;
         }
     }
 }

+ 4 - 2
src/MeidoPhotoStudio.Plugin/GUI/Panes/BackgroundWindowPanes/LightsPane.cs

@@ -258,6 +258,10 @@ namespace MeidoPhotoStudio.Plugin
                 GUI.enabled = true;
                 disableToggle.Draw();
             }
+
+            if (lightManager.SelectedLightIndex == 0 && currentLightType == MPSLightType.Normal) 
+                colorToggle.Draw();
+            
             GUILayout.EndHorizontal();
 
             GUI.enabled = !isDisabled;
@@ -281,8 +285,6 @@ namespace MeidoPhotoStudio.Plugin
             lightSlider[LightProp.Green].Draw();
             lightSlider[LightProp.Blue].Draw();
 
-            if (lightManager.SelectedLightIndex == 0 && currentLightType == MPSLightType.Normal) colorToggle.Draw();
-
             GUILayout.BeginHorizontal();
             GUILayout.Label(resetLabel, noExpandWidth);
             resetPropsButton.Draw(noExpandWidth);

+ 4 - 0
src/MeidoPhotoStudio.Plugin/GUI/Panes/BasePane.cs

@@ -22,5 +22,9 @@ namespace MeidoPhotoStudio.Plugin
         public virtual void UpdatePane() { }
 
         public virtual void Draw() { }
+
+        public virtual void Activate() { }
+
+        public virtual void Deactivate() { }
     }
 }

+ 72 - 25
src/MeidoPhotoStudio.Plugin/GUI/Panes/CallWindowPanes/MaidSelectorPane.cs

@@ -1,3 +1,4 @@
+using System.Collections.Generic;
 using UnityEngine;
 
 namespace MeidoPhotoStudio.Plugin
@@ -6,8 +7,11 @@ namespace MeidoPhotoStudio.Plugin
     {
         private readonly MeidoManager meidoManager;
         private Vector2 maidListScrollPos;
+        private Vector2 activeMaidListScrollPos;
         private readonly Button clearMaidsButton;
         private readonly Button callMaidsButton;
+        private readonly Toggle activeMeidoListToggle;
+
         public MaidSelectorPane(MeidoManager meidoManager)
         {
             this.meidoManager = meidoManager;
@@ -16,12 +20,28 @@ namespace MeidoPhotoStudio.Plugin
 
             callMaidsButton = new Button(Translation.Get("maidCallWindow", "callButton"));
             callMaidsButton.ControlEvent += (s, a) => this.meidoManager.CallMeidos();
+
+            activeMeidoListToggle = new(Translation.Get("maidCallWindow", "activeOnlyToggle"));
+
+            this.meidoManager.BeginCallMeidos += (_, _) =>
+            {
+                if (meidoManager.SelectedMeidoSet.Count == 0)
+                    activeMeidoListToggle.Value = false;
+            };
         }
 
         protected override void ReloadTranslation()
         {
             clearMaidsButton.Label = Translation.Get("maidCallWindow", "clearButton");
             callMaidsButton.Label = Translation.Get("maidCallWindow", "callButton");
+            activeMeidoListToggle.Label = Translation.Get("maidCallWindow", "activeOnlyToggle");
+        }
+        
+        public override void Activate()
+        {
+            base.Activate();
+            // Leaving this mode enabled pretty much softlocks meido selection so disable it on activation
+            activeMeidoListToggle.Value = false;
         }
 
         public override void Draw()
@@ -31,48 +51,75 @@ namespace MeidoPhotoStudio.Plugin
             callMaidsButton.Draw();
             GUILayout.EndHorizontal();
 
-            GUIStyle labelStyle = new GUIStyle(GUI.skin.label) { fontSize = 14 };
-            GUIStyle selectLabelStyle = new GUIStyle(labelStyle);
-            selectLabelStyle.normal.textColor = Color.black;
-            selectLabelStyle.alignment = TextAnchor.UpperRight;
-            GUIStyle labelSelectedStyle = new GUIStyle(labelStyle);
-            labelSelectedStyle.normal.textColor = Color.black;
+            MpsGui.WhiteLine();
+
+            GUI.enabled = meidoManager.HasActiveMeido;
+
+            activeMeidoListToggle.Draw();
+
+            GUI.enabled = true;
+
+            var onlyActiveMeido = activeMeidoListToggle.Value;
+
+            IList<Meido> meidoList = onlyActiveMeido
+                ? meidoManager.ActiveMeidoList
+                : meidoManager.Meidos;
 
-            Rect windowRect = parent.WindowRect;
-            float windowHeight = windowRect.height;
-            float buttonWidth = windowRect.width - 30f;
+            var labelStyle = new GUIStyle(GUI.skin.label) { fontSize = 14 };
+
+            var selectLabelStyle = new GUIStyle(labelStyle)
+            {
+                normal = { textColor = Color.black },
+                alignment = TextAnchor.UpperRight,
+            };
+
+            var labelSelectedStyle = new GUIStyle(labelStyle)
+            {
+                normal = { textColor = Color.black },
+            };
+
+            var windowRect = parent.WindowRect;
+            var windowHeight = windowRect.height;
+            var buttonWidth = windowRect.width - 30f;
             const float buttonHeight = 85f;
+            const float offsetTop = 130f;
 
-            Rect positionRect = new Rect(5f, 90f, windowRect.width - 10f, windowHeight - 125f);
-            Rect viewRect = new Rect(0f, 0f, buttonWidth, (buttonHeight * meidoManager.Meidos.Length) + 5f);
-            maidListScrollPos = GUI.BeginScrollView(positionRect, maidListScrollPos, viewRect);
+            var positionRect = new Rect(5f, offsetTop, windowRect.width - 10f, windowHeight - (offsetTop + 35));
+            var viewRect = new Rect(0f, 0f, buttonWidth, buttonHeight * meidoList.Count + 5f);
 
-            for (int i = 0; i < meidoManager.Meidos.Length; i++)
+            if (onlyActiveMeido)
+                activeMaidListScrollPos = GUI.BeginScrollView(positionRect, activeMaidListScrollPos, viewRect);
+            else
+                maidListScrollPos = GUI.BeginScrollView(positionRect, maidListScrollPos, viewRect);
+
+            for (var i = 0; i < meidoList.Count; i++)
             {
-                Meido meido = meidoManager.Meidos[i];
-                float y = i * buttonHeight;
-                bool selectedMaid = meidoManager.SelectedMeidoSet.Contains(i);
+                var meido = meidoList[i];
+                var y = i * buttonHeight;
+                var selectedMaid = meidoManager.SelectedMeidoSet.Contains(meido.StockNo);
 
-                if (GUI.Button(new Rect(0f, y, buttonWidth, buttonHeight), string.Empty)) meidoManager.SelectMeido(i);
+                if (GUI.Button(new(0f, y, buttonWidth, buttonHeight), string.Empty))
+                    meidoManager.SelectMeido(meido.StockNo);
 
                 if (selectedMaid)
                 {
-                    int selectedIndex = meidoManager.SelectMeidoList.IndexOf(i) + 1;
-                    GUI.DrawTexture(
-                        new Rect(5f, y + 5f, buttonWidth - 10f, buttonHeight - 10f), Texture2D.whiteTexture
-                    );
+                    var selectedIndex = meidoManager.SelectMeidoList.IndexOf(meido.StockNo) + 1;
+                    GUI.DrawTexture(new(5f, y + 5f, buttonWidth - 10f, buttonHeight - 10f), Texture2D.whiteTexture);
+
                     GUI.Label(
-                        new Rect(0f, y + 5f, buttonWidth - 10f, buttonHeight),
-                        selectedIndex.ToString(), selectLabelStyle
+                        new(0f, y + 5f, buttonWidth - 10f, buttonHeight), selectedIndex.ToString(), selectLabelStyle
                     );
                 }
 
-                if (meido.Portrait) GUI.DrawTexture(new Rect(5f, y, buttonHeight, buttonHeight), meido.Portrait);
+                if (meido.Portrait != null)
+                    GUI.DrawTexture(new(5f, y, buttonHeight, buttonHeight), meido.Portrait);
+
                 GUI.Label(
-                    new Rect(95f, y + 30f, buttonWidth - 80f, buttonHeight),
+                    new(95f, y + 30f, buttonWidth - 80f, buttonHeight),
                     $"{meido.LastName}\n{meido.FirstName}", selectedMaid ? labelSelectedStyle : labelStyle
                 );
             }
+
             GUI.EndScrollView();
         }
     }

+ 1 - 6
src/MeidoPhotoStudio.Plugin/GUI/Panes/MainWindowPanes/BaseMainWindowPane.cs

@@ -1,13 +1,8 @@
 namespace MeidoPhotoStudio.Plugin
 {
-    public abstract class BaseMainWindowPane : BaseWindowPane
+    public abstract class BaseMainWindowPane : BaseWindow
     {
         protected TabsPane tabsPane;
         public void SetTabsPane(TabsPane tabsPane) => this.tabsPane = tabsPane;
-        /* Main window panes have panes within them while being a pane itself of the main window */
-        public override void SetParent(BaseWindow window)
-        {
-            foreach (BasePane pane in Panes) pane.SetParent(window);
-        }
     }
 }

+ 42 - 20
src/MeidoPhotoStudio.Plugin/GUI/Panes/PoseWindowPanes/MaidPoseSelectorPane.cs

@@ -112,33 +112,51 @@ namespace MeidoPhotoStudio.Plugin
         {
             updating = true;
 
-            PoseInfo poseInfo = meidoManager.ActiveMeido.CachedPose;
+            try
+            {
+                var cachedPose = meidoManager.ActiveMeido.CachedPose;
 
-            bool oldPoseMode = customPoseMode;
+                poseModeGrid.SelectedItemIndex = cachedPose.CustomPose ? 1 : 0;
 
-            poseModeGrid.SelectedItemIndex = poseInfo.CustomPose ? 1 : 0;
+                var oldCustomPoseMode = customPoseMode;
+                customPoseMode = cachedPose.CustomPose;
 
-            int poseGroupIndex = CurrentPoseGroupList.IndexOf(poseInfo.PoseGroup);
+                if (oldCustomPoseMode != customPoseMode)
+                    poseGroupDropdown.SetDropdownItems(
+                        customPoseMode ? CurrentPoseGroupList.ToArray() : Translation.GetArray(
+                            "poseGroupDropdown", CurrentPoseGroupList
+                        )
+                    );
 
-            if (poseGroupIndex < 0) poseGroupIndex = 0;
+                var newPoseGroupIndex = CurrentPoseGroupList.IndexOf(cachedPose.PoseGroup);
 
-            int poseIndex = CurrentPoseDict[poseInfo.PoseGroup].IndexOf(poseInfo.Pose);
+                if (newPoseGroupIndex < 0)
+                    poseGroupDropdown.SelectedItemIndex = 0;
+                else if (oldCustomPoseMode != customPoseMode
+                    || poseGroupDropdown.SelectedItemIndex != newPoseGroupIndex)
+                {
+                    poseGroupDropdown.SelectedItemIndex = newPoseGroupIndex;
+                    poseDropdown.SetDropdownItems(UIPoseList());
+                }
 
-            if (poseIndex < 0) poseIndex = 0;
+                var newPoseIndex = CurrentPoseDict.TryGetValue(cachedPose.PoseGroup, out var poseList)
+                    ? poseList.IndexOf(cachedPose.Pose)
+                    : 0;
 
-            if (oldPoseMode != customPoseMode)
-            {
-                string[] list = customPoseMode
-                    ? CurrentPoseGroupList.ToArray()
-                    : Translation.GetArray("poseGroupDropdown", CurrentPoseGroupList);
+                if (newPoseIndex < 0)
+                    newPoseIndex = 0;
 
-                poseGroupDropdown.SetDropdownItems(list);
+                poseDropdown.SelectedItemIndex = newPoseIndex;
+                poseListEnabled = CurrentPoseList.Count > 0;
+            }
+            catch
+            {
+                // Do nothing
+            }
+            finally
+            {
+                updating = false;
             }
-
-            poseGroupDropdown.SelectedItemIndex = poseGroupIndex;
-            poseDropdown.SelectedItemIndex = poseIndex;
-
-            updating = false;
         }
 
         private void OnPresetChange(object sender, PresetChangeEventArgs args)
@@ -169,9 +187,10 @@ namespace MeidoPhotoStudio.Plugin
 
         private void SetPoseMode()
         {
-            customPoseMode = poseModeGrid.SelectedItemIndex == 1;
+            if (updating)
+                return;
 
-            if (updating) return;
+            customPoseMode = poseModeGrid.SelectedItemIndex == 1;
 
             string[] list = customPoseMode
                 ? CurrentPoseGroupList.ToArray()
@@ -182,6 +201,9 @@ namespace MeidoPhotoStudio.Plugin
 
         private void ChangePoseGroup()
         {
+            if (updating)
+                return;
+
             poseListEnabled = CurrentPoseList.Count > 0;
             if (previousPoseGroup == SelectedPoseGroup)
             {

+ 52 - 13
src/MeidoPhotoStudio.Plugin/GUI/Windows/BaseWindow.cs

@@ -1,13 +1,17 @@
+using System.Collections.Generic;
 using UnityEngine;
 
 namespace MeidoPhotoStudio.Plugin
 {
-    public abstract class BaseWindow : BaseWindowPane
+    public abstract class BaseWindow : BasePane
     {
         private static int id = 765;
         private static int ID => id++;
         public readonly int windowID = ID;
-        protected Rect windowRect = new Rect(0f, 0f, 480f, 270f);
+        protected readonly List<BasePane> Panes = new();
+        protected Vector2 scrollPos;
+        public bool ActiveWindow { get; set; }
+        protected Rect windowRect = new(0f, 0f, 480f, 270f);
         public virtual Rect WindowRect
         {
             get => windowRect;
@@ -16,35 +20,70 @@ namespace MeidoPhotoStudio.Plugin
                 value.x = Mathf.Clamp(
                     value.x, -value.width + Utility.GetPix(20), Screen.width - Utility.GetPix(20)
                 );
+
                 value.y = Mathf.Clamp(
                     value.y, -value.height + Utility.GetPix(20), Screen.height - Utility.GetPix(20)
                 );
+
                 windowRect = value;
             }
         }
-        protected Vector2 MiddlePosition => new Vector2(
-            (Screen.width / 2) - (windowRect.width / 2), (Screen.height / 2) - (windowRect.height / 2)
+        protected Vector2 MiddlePosition => new(
+            (float)Screen.width / 2 - windowRect.width / 2, (float)Screen.height / 2 - windowRect.height / 2
         );
 
-        public virtual void HandleZoom()
+        protected T AddPane<T>(T pane) where T : BasePane
         {
-            if (Input.mouseScrollDelta.y != 0f && Visible)
-            {
-                Vector2 mousePos = new Vector2(Input.mousePosition.x, Screen.height - Input.mousePosition.y);
-                if (WindowRect.Contains(mousePos)) Input.ResetInputAxes();
-            }
+            Panes.Add(pane);
+            pane.SetParent(this);
+            return pane;
         }
 
-        public virtual void Update() => HandleZoom();
+        public override void SetParent(BaseWindow window)
+        {
+            foreach (var pane in Panes) 
+                pane.SetParent(window);
+        }
 
-        public virtual void Activate() { }
+        private void HandleZoom()
+        {
+            if (Input.mouseScrollDelta.y == 0f || !Visible) return;
+
+            var mousePos = new Vector2(Input.mousePosition.x, Screen.height - Input.mousePosition.y);
 
-        public virtual void Deactivate() { }
+            if (WindowRect.Contains(mousePos))
+                Input.ResetInputAxes();
+        }
+
+        public virtual void Update() =>
+            HandleZoom();
 
         public virtual void GUIFunc(int id)
         {
             Draw();
             GUI.DragWindow();
         }
+
+        public virtual void UpdatePanes()
+        {
+            foreach (var pane in Panes)
+                pane.UpdatePane();
+        }
+
+        public override void Activate()
+        {
+            base.Activate();
+
+            foreach (var pane in Panes)
+                pane.Activate();
+        }
+
+        public override void Deactivate()
+        {
+            base.Deactivate();
+
+            foreach (var pane in Panes)
+                pane.Deactivate();
+        }
     }
 }

+ 0 - 23
src/MeidoPhotoStudio.Plugin/GUI/Windows/BaseWindowPane.cs

@@ -1,23 +0,0 @@
-using System.Collections.Generic;
-using UnityEngine;
-
-namespace MeidoPhotoStudio.Plugin
-{
-    public abstract class BaseWindowPane : BasePane
-    {
-        protected List<BasePane> Panes = new List<BasePane>();
-        protected Vector2 scrollPos;
-        public bool ActiveWindow { get; set; }
-
-        public T AddPane<T>(T pane) where T : BasePane
-        {
-            Panes.Add(pane);
-            return pane;
-        }
-
-        public virtual void UpdatePanes()
-        {
-            foreach (BasePane pane in Panes) pane.UpdatePane();
-        }
-    }
-}

+ 17 - 3
src/MeidoPhotoStudio.Plugin/GUI/Windows/MainWindow.cs

@@ -12,6 +12,9 @@ namespace MeidoPhotoStudio.Plugin
         private readonly TabsPane tabsPane;
         private readonly Button settingsButton;
         private BaseMainWindowPane currentWindowPane;
+        private string settingsButtonLabel;
+        private string closeButtonLabel;
+
         public override Rect WindowRect
         {
             set
@@ -50,20 +53,31 @@ namespace MeidoPhotoStudio.Plugin
             tabsPane = new TabsPane();
             tabsPane.TabChange += (s, a) => ChangeTab();
 
-            settingsButton = new Button("Settings");
+            settingsButtonLabel = Translation.Get("settingsLabels", "settingsButton");
+            closeButtonLabel = Translation.Get("settingsLabels", "closeSettingsButton");
+
+            settingsButton = new(settingsButtonLabel);
             settingsButton.ControlEvent += (s, a) =>
             {
                 if (selectedWindow == Constants.Window.Settings) ChangeTab();
                 else
                 {
-                    settingsButton.Label = "Close";
+                    settingsButton.Label = closeButtonLabel;
                     SetCurrentWindow(Constants.Window.Settings);
                 }
             };
         }
 
+        protected override void ReloadTranslation()
+        {
+            settingsButtonLabel = Translation.Get("settingsLabels", "settingsButton");
+            closeButtonLabel = Translation.Get("settingsLabels", "closeSettingsButton");
+            settingsButton.Label = selectedWindow == Constants.Window.Settings ? closeButtonLabel : settingsButtonLabel;
+        }
+
         public override void Activate()
         {
+            base.Activate();
             updating = true;
             tabsPane.SelectedTab = Constants.Window.Call;
             updating = false;
@@ -84,7 +98,7 @@ namespace MeidoPhotoStudio.Plugin
 
         private void ChangeTab()
         {
-            settingsButton.Label = "Settings";
+            settingsButton.Label = Translation.Get("settingsLabels", "settingsButton");
             SetCurrentWindow(tabsPane.SelectedTab);
         }
 

+ 20 - 1
src/MeidoPhotoStudio.Plugin/GUI/Windows/MessageWindow.cs

@@ -29,7 +29,7 @@ namespace MeidoPhotoStudio.Plugin
             this.messageWindowManager = messageWindowManager;
             nameTextField = new TextField();
 
-            fontSizeSlider = new Slider(MessageWindowManager.fontBounds);
+            fontSizeSlider = new Slider(MessageWindowManager.FontBounds);
             fontSizeSlider.ControlEvent += ChangeFontSize;
 
             messageTextArea = new TextArea();
@@ -47,6 +47,10 @@ namespace MeidoPhotoStudio.Plugin
         private void ChangeFontSize(object sender, EventArgs args)
         {
             fontSize = (int)fontSizeSlider.Value;
+
+            if (updating)
+                return;
+
             messageWindowManager.FontSize = fontSize;
         }
 
@@ -56,6 +60,17 @@ namespace MeidoPhotoStudio.Plugin
             messageWindowManager.ShowMessage(nameTextField.Value, messageTextArea.Value);
         }
 
+        private void ResetUI()
+        {
+            updating = true;
+
+            fontSizeSlider.Value = MessageWindowManager.FontBounds.Left;
+            nameTextField.Value = string.Empty;
+            messageTextArea.Value = string.Empty;
+
+            updating = false;
+        }
+
         public override void Update()
         {
             base.Update();
@@ -83,6 +98,10 @@ namespace MeidoPhotoStudio.Plugin
         {
             messageWindowManager.CloseMessagePanel();
             Visible = false;
+            ResetUI();
         }
+
+        public override void Activate() =>
+            ResetUI();
     }
 }

+ 0 - 2
src/MeidoPhotoStudio.Plugin/GUI/Windows/SceneWindow.cs

@@ -40,8 +40,6 @@ namespace MeidoPhotoStudio.Plugin
             directoryList = AddPane(new SceneManagerDirectoryPane(sceneManager, sceneModalWindow));
 
             sceneGrid = AddPane(new SceneManagerScenePane(sceneManager, sceneModalWindow));
-
-            sceneGrid.SetParent(this);
         }
 
         public override void GUIFunc(int id)

+ 0 - 222
src/MeidoPhotoStudio.Plugin/LexicographicStringComparer.cs

@@ -1,222 +0,0 @@
-/* 
-    Taken from https://gist.github.com/mstum/63a6e3e8cf54e8ae55b6aa28ca6f20c5
-
-    Modified slightly to remove the need for unsafe and changed namespace to plugin namespace
-*/
-using System;
-using System.Collections.Generic;
-
-namespace MeidoPhotoStudio.Plugin
-{
-    /// <summary>
-    /// A string comparer that behaves like StrCmpLogicalW
-    /// https://msdn.microsoft.com/en-us/library/windows/desktop/bb759947
-    /// 
-    /// This means:
-    /// * case insensitive (ZA == za)
-    /// * numbers are treated as numbers (z20 &gt; z3) and assumed positive
-    ///     (-100 comes AFTER 10 and 100, because the minus is seen
-    ///         as a char, not as part of the number)
-    /// * leading zeroes come before anything else (z001 &lt; z01 &lt; z1)
-    /// 
-    /// Note: Instead of instantiating this, you can also use
-    /// <see cref="Comparison(string, string)"/>
-    /// if you don't need an <see cref="IComparer{string}"/> but can
-    /// use a <see cref="Comparison{string}"/> delegate instead.
-    /// </summary>
-    /// <remarks>
-    /// NOTE: This behaves slightly different than StrCmpLogicalW because
-    /// it handles large numbers.
-    /// 
-    /// At some point, StrCmpLogicalW just gives up trying to parse
-    /// something as a number (see the Test cases), while we keep going.
-    /// Since we want to sort lexicographily as much as possible,
-    /// that difference makes sense.
-    /// </remarks>
-    public class LexicographicStringComparer : IComparer<string>
-    {
-        /// <summary>
-        /// A <see cref="Comparison{string}"/> delegate.
-        /// </summary>
-        public static int Comparison(string x, string y)
-        {
-            // 1 = x > y, -1 = y > x, 0 = x == y
-            // Rules: Numbers < Letters. Space < everything
-            if (x == y) return 0;
-            if (string.IsNullOrEmpty(x) && !string.IsNullOrEmpty(y)) return -1;
-            if (!string.IsNullOrEmpty(x) && string.IsNullOrEmpty(y)) return 1;
-            if (string.IsNullOrEmpty(x) && string.IsNullOrEmpty(y)) return 0; // "" and null are the same for the purposes of this
-
-            var yl = y.Length;
-            for (int i = 0; i < x.Length; i++)
-            {
-                if (yl <= i) return 1;
-                var cx = x[i];
-                var cy = y[i];
-
-                if (Char.IsWhiteSpace(cx) && !Char.IsWhiteSpace(cy)) return -1;
-                if (!Char.IsWhiteSpace(cx) && Char.IsWhiteSpace(cy)) return 1;
-
-                if (IsDigit(cx))
-                {
-                    if (!IsDigit(cy))
-                    {
-                        return -1;
-                    }
-
-                    // Both are digits, but now we need to look at them as a whole, since
-                    // 10 > 2, but 10 > 002 > 02 > 2
-                    var numCmp = CompareNumbers(x, y, i, out int numChars);
-                    if (numCmp != 0) return numCmp;
-                    i += numChars; // We might have looked at more than one char, e.g., "10" is 2 chars
-                }
-                else if (IsDigit(cy))
-                {
-                    return 1;
-                }
-                else
-                {
-                    // Do this after the digit check
-                    // Case insensitive
-                    // Normalize to Uppercase:
-                    // https://docs.microsoft.com/en-US/visualstudio/code-quality/ca1308-normalize-strings-to-uppercase
-                    var cmp = Char.ToUpper(cx).CompareTo(Char.ToUpper(cy));
-                    if (cmp != 0) return cmp;
-                }
-            }
-
-            // Strings are equal to that point, and y is at least as large as x
-            if (y.Length > x.Length) return -1;
-
-            return 0;
-        }
-
-        /// <summary>
-        /// <see cref="IComparer{T}.Compare(T, T)"/>
-        /// </summary>
-        public int Compare(string x, string y)
-            => Comparison(x, y);
-
-        private static int CompareNumbers(string x, string y, int ix, out int numChars)
-        {
-            var xParsed = ParseNumber(x, ix);
-            var yParsed = ParseNumber(y, ix);
-
-            numChars = yParsed.NumCharsRead > xParsed.NumCharsRead
-                ? xParsed.NumCharsRead
-                : yParsed.NumCharsRead;
-
-            return xParsed.CompareTo(yParsed);
-        }
-
-        private static ParsedNumber ParseNumber(string str, int offset)
-        {
-            var result = 0;
-            var numChars = 0;
-            var leadingZeroes = 0;
-            var numOverflows = 0;
-            bool countZeroes = true;
-
-            for (int j = offset; j < str.Length; j++)
-            {
-                char c = str[j];
-                if (IsDigit(c))
-                {
-                    var cInt = (c - 48); // 48/0x30 is '0'
-
-                    checked
-                    {
-                        long tmp = (result * 10L) + cInt;
-                        if (tmp > int.MaxValue)
-                        {
-                            numOverflows++;
-                            tmp = tmp % int.MaxValue;
-                        }
-                        result = (int)tmp;
-                        numChars++;
-                    }
-
-                    if (cInt == 0 && countZeroes)
-                    {
-                        leadingZeroes++;
-                    }
-                    else
-                    {
-                        countZeroes = false;
-                    }
-                }
-                else
-                {
-                    break;
-                }
-            }
-            return new ParsedNumber(result, numOverflows, leadingZeroes, numChars);
-        }
-
-        private static bool IsDigit(char c) => (c >= '0' && c <= '9');
-
-        /// <summary>
-        /// Note that the ParsedNumber is not very useful as a number,
-        /// but purely as a way to compare two numbers that are stored in a string.
-        /// </summary>
-        private struct ParsedNumber : IComparable<ParsedNumber>, IComparer<ParsedNumber>
-        {
-            /// <summary>
-            /// The remainder, that is, the part of the number that
-            /// didn't overflow int.MaxValue.
-            /// </summary>
-            public int Remainder;
-
-            /// <summary>
-            /// How often did the number overflow int.MaxValue during parsing?
-            /// </summary>
-            public int Overflows;
-
-            /// <summary>
-            /// How many leading zeroes were there in the string during parsing?
-            /// "001" has a LeadingZeroesCount of 2.
-            /// "100" has a LeadingZeroesCount of 0.
-            /// "010" has a LeadingZeroesCount of 1.
-            /// 
-            /// This is important, because 001 comes before 01 comes before 1.
-            /// </summary>
-            public int LeadingZeroesCount;
-
-            /// <summary>
-            /// How many characters were read from the input during parsing?
-            /// </summary>
-            public int NumCharsRead;
-
-            public ParsedNumber(int remainder, int overflows, int leadingZeroes, int numChars)
-            {
-                Remainder = remainder;
-                Overflows = overflows;
-                LeadingZeroesCount = leadingZeroes;
-                NumCharsRead = numChars;
-            }
-
-            public int Compare(ParsedNumber x, ParsedNumber y)
-            {
-                // Note: if numCharsX and Y aren't equal, this doesn't matter
-                // as the return value will be either -1 or 1 anyway
-
-                if (x.Overflows > y.Overflows) return 1;
-                if (x.Overflows < y.Overflows) return -1;
-
-                // 001 > 01 > 1
-                if (x.Remainder == y.Remainder)
-                {
-                    if (x.LeadingZeroesCount > y.LeadingZeroesCount) return -1;
-                    if (x.LeadingZeroesCount < y.LeadingZeroesCount) return 1;
-                }
-
-                if (x.Remainder > y.Remainder) return 1;
-                if (x.Remainder < y.Remainder) return -1;
-                return 0;
-            }
-
-            public int CompareTo(ParsedNumber other)
-                => Compare(this, other);
-        }
-    }
-}

+ 17 - 3
src/MeidoPhotoStudio.Plugin/Managers/CameraManager.cs

@@ -14,6 +14,8 @@ namespace MeidoPhotoStudio.Plugin
         private float defaultCameraZoomSpeed;
         private const float cameraFastMoveSpeed = 0.1f;
         private const float cameraFastZoomSpeed = 3f;
+        private const float cameraSlowMoveSpeed = 0.004f;
+        private const float cameraSlowZoomSpeed = 0.1f;
         private Camera subCamera;
         private CameraInfo tempCameraInfo = new CameraInfo();
         private const KeyCode AlphaOne = KeyCode.Alpha1;
@@ -108,9 +110,21 @@ namespace MeidoPhotoStudio.Plugin
 
             subCamera.fieldOfView = mainCamera.camera.fieldOfView;
 
-            var shift = Input.Shift;
-            ultimateOrbitCamera.moveSpeed = shift ? cameraFastMoveSpeed : defaultCameraMoveSpeed;
-            ultimateOrbitCamera.zoomSpeed = shift ? cameraFastZoomSpeed : defaultCameraZoomSpeed;
+            if (Input.Shift)
+            {
+                ultimateOrbitCamera.moveSpeed = cameraFastMoveSpeed;
+                ultimateOrbitCamera.zoomSpeed = cameraFastZoomSpeed;
+            }
+            else if (Input.Control)
+            {
+                ultimateOrbitCamera.moveSpeed = cameraSlowMoveSpeed;
+                ultimateOrbitCamera.zoomSpeed = cameraSlowZoomSpeed;
+            }
+            else
+            {
+                ultimateOrbitCamera.moveSpeed = defaultCameraMoveSpeed;
+                ultimateOrbitCamera.zoomSpeed = defaultCameraZoomSpeed;
+            }
         }
 
         private void SaveTempCamera()

+ 136 - 79
src/MeidoPhotoStudio.Plugin/Managers/MeidoManager.cs

@@ -1,6 +1,7 @@
 using System;
 using System.Collections.Generic;
 using System.Linq;
+using HarmonyLib;
 using UnityEngine;
 
 namespace MeidoPhotoStudio.Plugin
@@ -9,8 +10,9 @@ namespace MeidoPhotoStudio.Plugin
     {
         public const string header = "MEIDO";
         private static readonly CharacterMgr characterMgr = GameMain.Instance.CharacterMgr;
+        private static bool active;
+        private static int EditMaidIndex { get; set; }
         private int undress;
-        private int numberOfMeidos;
         private int tempEditMaidIndex = -1;
         public Meido[] Meidos { get; private set; }
         public HashSet<int> SelectedMeidoSet { get; } = new HashSet<int>();
@@ -28,7 +30,6 @@ namespace MeidoPhotoStudio.Plugin
             get => selectedMeido;
             private set => selectedMeido = Utility.Bound(value, 0, ActiveMeidoList.Count - 1);
         }
-        public int EditMaidIndex { get; private set; }
         public bool Busy => ActiveMeidoList.Any(meido => meido.Busy);
         private bool globalGravity;
         public bool GlobalGravity
@@ -62,37 +63,21 @@ namespace MeidoPhotoStudio.Plugin
 
         public void Activate()
         {
-            GameMain.Instance.CharacterMgr.ResetCharaPosAll();
-            numberOfMeidos = characterMgr.GetStockMaidCount();
-            Meidos = new Meido[numberOfMeidos];
+            characterMgr.ResetCharaPosAll();
 
-            tempEditMaidIndex = -1;
+            if (!MeidoPhotoStudio.EditMode)
+                characterMgr.DeactivateMaid(0);
 
-            for (int stockMaidIndex = 0; stockMaidIndex < numberOfMeidos; stockMaidIndex++)
-            {
-                Meidos[stockMaidIndex] = new Meido(stockMaidIndex);
-            }
+            Meidos = characterMgr.GetStockMaidList()
+                .Select((_, stockNo) => new Meido(stockNo)).ToArray();
 
-            if (MeidoPhotoStudio.EditMode)
-            {
-                Maid editMaid = GameMain.Instance.CharacterMgr.GetMaid(0);
-                EditMaidIndex = Array.FindIndex(Meidos, meido => meido.Maid.status.guid == editMaid.status.guid);
-                EditMeido.IsEditMaid = true;
-
-                var editOkCancel = UTY.GetChildObject(GameObject.Find("UI Root"), "OkCancel")
-                    .GetComponent<EditOkCancel>();
-
-                // Ensure MPS resets editor state before setting maid
-                EditOkCancel.OnClick newEditOnClick = () => SetEditMaid(Meidos[EditMaidIndex]);
-                newEditOnClick += OkCancelDelegate();
-
-                Utility.SetFieldValue(editOkCancel, "m_dgOnClickOk", newEditOnClick);
+            tempEditMaidIndex = -1;
 
-                // Only for setting custom parts placement animation just in case body was changed before activating MPS
-                SetEditMaid(Meidos[EditMaidIndex]);
-            }
+            if (MeidoPhotoStudio.EditMode && EditMaidIndex >= 0)
+                Meidos[EditMaidIndex].IsEditMaid = true;
 
             ClearSelectList();
+            active = true;
         }
 
         public void Deactivate()
@@ -113,22 +98,10 @@ namespace MeidoPhotoStudio.Plugin
                 meido.Stop = false;
                 meido.EyeToCam = true;
 
-                SetEditMaid(meido);
-
-                // Restore original OK button functionality
-                GameObject okButton = UTY.GetChildObjectNoError(GameObject.Find("UI Root"), "OkCancel");
-                if (okButton)
-                {
-                    EditOkCancel editOkCancel = okButton.GetComponent<EditOkCancel>();
-                    Utility.SetFieldValue(editOkCancel, "m_dgOnClickOk", OkCancelDelegate());
-                }
+                SetEditorMaid(meido.Maid);
             }
-        }
 
-        private EditOkCancel.OnClick OkCancelDelegate()
-        {
-            return (EditOkCancel.OnClick)Delegate
-                .CreateDelegate(typeof(EditOkCancel.OnClick), SceneEdit.Instance, "OnEditOk");
+            active = false;
         }
 
         public void Update()
@@ -139,12 +112,20 @@ namespace MeidoPhotoStudio.Plugin
         private void UnloadMeidos()
         {
             SelectedMeido = 0;
+
+            var commonMeidoIDs = new HashSet<int>(
+                ActiveMeidoList.Where(meido => SelectedMeidoSet.Contains(meido.StockNo)).Select(meido => meido.StockNo)
+            );
+
             foreach (Meido meido in ActiveMeidoList)
             {
                 meido.UpdateMeido -= OnUpdateMeido;
                 meido.GravityMove -= OnGravityMove;
-                meido.Unload();
+                
+                if (!commonMeidoIDs.Contains(meido.StockNo))
+                    meido.Unload();
             }
+
             ActiveMeidoList.Clear();
         }
 
@@ -215,46 +196,11 @@ namespace MeidoPhotoStudio.Plugin
 
             EditMeido.IsEditMaid = false;
 
-            tempEditMaidIndex = meido.Maid.status.guid == Meidos[EditMaidIndex].Maid.status.guid
-                ? -1
-                : Array.FindIndex(Meidos, maid => maid.Maid.status.guid == meido.Maid.status.guid);
+            tempEditMaidIndex = meido.StockNo == EditMaidIndex ? -1 : meido.StockNo;
 
             EditMeido.IsEditMaid = true;
 
-            Maid newEditMaid = EditMeido.Maid;
-
-            GameObject uiRoot = GameObject.Find("UI Root");
-
-            var presetCtrl = UTY.GetChildObjectNoError(uiRoot, "PresetPanel")?.GetComponent<PresetCtrl>();
-            var presetButton = UTY.GetChildObjectNoError(uiRoot, "PresetButtonPanel")?.GetComponent<PresetButtonCtrl>();
-            var profileCtrl = UTY.GetChildObjectNoError(uiRoot, "ProfilePanel")?.GetComponent<ProfileCtrl>();
-            var customPartsWindow = UTY.GetChildObjectNoError(uiRoot, "Window/CustomPartsWindow")
-                ?.GetComponent<SceneEditWindow.CustomPartsWindow>();
-
-            if (!(presetCtrl || presetButton || profileCtrl || customPartsWindow)) return;
-
-            // Preset application
-            Utility.SetFieldValue(presetCtrl, "m_maid", newEditMaid);
-
-            // Preset saving
-            Utility.SetFieldValue(presetButton, "m_maid", newEditMaid);
-
-            // Maid profile (name, description, experience etc)
-            Utility.SetFieldValue(profileCtrl, "m_maidStatus", newEditMaid.status);
-
-            // Accessory/Parts placement
-            Utility.SetFieldValue(customPartsWindow, "maid", newEditMaid);
-
-            // Stopping maid animation and head movement when customizing parts placement
-            Utility.SetFieldValue(customPartsWindow, "animation", newEditMaid.GetAnimation());
-
-            // Clothing/body in general and maybe other things
-            Utility.SetFieldValue(SceneEdit.Instance, "m_maid", newEditMaid);
-
-            // Body status, parts colours and maybe more
-            Utility.GetFieldValue<CharacterMgr, Maid[]>(
-                GameMain.Instance.CharacterMgr, "m_gcActiveMaid"
-            )[0] = newEditMaid;
+            SetEditorMaid(EditMeido.Maid);
         }
 
         public Meido GetMeido(string guid)
@@ -314,6 +260,117 @@ namespace MeidoPhotoStudio.Plugin
                 meido.ApplyGravity(args.LocalPosition, args.IsSkirt);
             }
         }
+
+        private static void SetEditorMaid(Maid maid)
+        {
+            if (maid == null)
+            {
+                Utility.LogWarning("Refusing to change editing maid because the new maid is null!");
+                return;
+            }
+
+            if (SceneEdit.Instance.maid.status.guid == maid.status.guid)
+            {
+                Utility.LogDebug("Editing maid is the same as new maid");
+                return;
+            }
+
+            var uiRoot = GameObject.Find("UI Root");
+
+            if (!TryGetUIControl<PresetCtrl>(uiRoot, "PresetPanel", out var presetCtrl))
+                return;
+
+            if (!TryGetUIControl<PresetButtonCtrl>(uiRoot, "PresetButtonPanel", out var presetButtonCtrl))
+                return;
+
+            if (!TryGetUIControl<ProfileCtrl>(uiRoot, "ProfilePanel", out var profileCtrl))
+                return;
+
+            if (!TryGetUIControl<SceneEditWindow.CustomPartsWindow>(
+                uiRoot, "Window/CustomPartsWindow", out var sceneEditWindow
+            ))
+                return;
+
+            // Preset application
+            presetCtrl.m_maid = maid;
+
+            // Preset saving
+            presetButtonCtrl.m_maid = maid;
+
+            // Maid profile (name, description, experience etc)
+            profileCtrl.m_maidStatus = maid.status;
+
+            // Accessory/Parts placement
+            sceneEditWindow.maid = maid;
+
+            // Stopping maid animation and head movement when customizing parts placement
+            sceneEditWindow.animation = maid.GetAnimation();
+
+            // Clothing/body in general and maybe other things
+            SceneEdit.Instance.m_maid = maid;
+
+            // Body status, parts colours and maybe more
+            GameMain.Instance.CharacterMgr.m_gcActiveMaid[0] = maid;
+
+            static bool TryGetUIControl<T>(GameObject root, string hierarchy, out T uiControl) where T : MonoBehaviour
+            {
+                uiControl = null;
+
+                var uiElement = UTY.GetChildObjectNoError(root, hierarchy);
+
+                if (!uiElement)
+                    return false;
+
+                uiControl = uiElement.GetComponent<T>();
+
+                return uiControl;
+            }
+        }
+
+        [HarmonyPostfix]
+        [HarmonyPatch(typeof(SceneEdit), nameof(SceneEdit.Start))]
+        private static void SceneEditStartPostfix()
+        {
+            EditMaidIndex = -1;
+
+            if (SceneEdit.Instance.maid == null)
+                return;
+
+            var originalEditingMaid = SceneEdit.Instance.maid;
+
+            EditMaidIndex = GameMain.Instance.CharacterMgr.GetStockMaidList()
+                .FindIndex(maid => maid.status.guid == originalEditingMaid.status.guid);
+
+            try
+            {
+                var editOkCancelButton = UTY.GetChildObject(GameObject.Find("UI Root"), "OkCancel")
+                    .GetComponent<EditOkCancel>();
+
+                EditOkCancel.OnClick newEditOkCancelDelegate = RestoreOriginalEditingMaid;
+
+                newEditOkCancelDelegate += editOkCancelButton.m_dgOnClickOk;
+
+                editOkCancelButton.m_dgOnClickOk = newEditOkCancelDelegate;
+
+                void RestoreOriginalEditingMaid()
+                {
+                    // Only restore original editing maid when active.
+                    if (!active)
+                        return;
+
+                    Utility.LogDebug($"Setting Editing maid back to '{originalEditingMaid.status.fullNameJpStyle}'");
+
+                    SetEditorMaid(originalEditingMaid);
+
+                    // Set SceneEdit's maid regardless of UI integration failing
+                    SceneEdit.Instance.m_maid = originalEditingMaid;
+                }
+            }
+            catch (Exception e)
+            {
+                Utility.LogWarning($"Failed to hook onto Edit Mode OK button: {e}");
+            }
+        }
     }
 
     public class MeidoUpdateEventArgs : EventArgs

+ 94 - 55
src/MeidoPhotoStudio.Plugin/Managers/MessageWindowManager.cs

@@ -1,3 +1,4 @@
+using System.Collections.Generic;
 using UnityEngine;
 
 namespace MeidoPhotoStudio.Plugin
@@ -5,21 +6,44 @@ namespace MeidoPhotoStudio.Plugin
     public class MessageWindowManager : IManager
     {
         public const string header = "TEXTBOX";
-        public static readonly SliderProp fontBounds = new SliderProp(25f, 60f);
-        private static GameObject sysRoot;
-        private readonly MessageClass msgClass;
-        private readonly MessageWindowMgr msgWnd;
-        private readonly UILabel msgLabel;
-        private readonly UILabel nameLabel;
-        private readonly GameObject msgGameObject;
-        public bool ShowingMessage { get; private set; }
-        public string MessageName { get; private set; } = string.Empty;
-        public string MessageText { get; private set; } = string.Empty;
+        public static readonly SliderProp FontBounds = new SliderProp(25f, 60f);
+
+        private readonly MessageWindowMgr messageWindowMgr;
+        private readonly GameObject subtitlesDisplayPanel;
+        private readonly GameObject hitRetSprite;
+        private readonly GameObject messageBox;
+        private readonly GameObject messageButtons;
+        private readonly UILabel messageLabel;
+        private readonly UILabel speakerLabel;
+
+        public bool ShowingMessage
+        {
+            get => messageWindowMgr.IsVisibleMessageViewer;
+            private set
+            {
+                if (value)
+                    messageWindowMgr.OpenMessageWindowPanel();
+                else
+                    messageWindowMgr.CloseMessageWindowPanel();
+            }
+        }
+
+        public string MessageName
+        {
+            get => speakerLabel.text;
+            private set => speakerLabel.text = value;
+        }
+
+        public string MessageText
+        {
+            get => messageLabel.text;
+            private set => messageLabel.text = value;
+        }
 
         public int FontSize
         {
-            get => msgLabel.fontSize;
-            set => msgLabel.fontSize = (int)Mathf.Clamp(value, fontBounds.Left, fontBounds.Right);
+            get => messageLabel.fontSize;
+            set => messageLabel.fontSize = (int)Mathf.Clamp(value, FontBounds.Left, FontBounds.Right);
         }
 
         static MessageWindowManager()
@@ -29,55 +53,52 @@ namespace MeidoPhotoStudio.Plugin
 
         public MessageWindowManager()
         {
-            sysRoot = GameObject.Find("__GameMain__/SystemUI Root");
-            msgWnd = GameMain.Instance.MsgWnd;
-            msgGameObject = sysRoot.transform.Find("MessageWindowPanel").gameObject;
-            msgClass = new MessageClass(msgGameObject, msgWnd);
-            nameLabel = UTY.GetChildObject(msgGameObject, "MessageViewer/MsgParent/SpeakerName/Name")
-                .GetComponent<UILabel>();
-            msgLabel = UTY.GetChildObject(msgGameObject, "MessageViewer/MsgParent/Message")
-                .GetComponent<UILabel>();
-            Utility.SetFieldValue(msgClass, "message_label_", msgLabel);
-            Utility.SetFieldValue(msgClass, "name_label_", nameLabel);
-            Activate();
-        }
+            messageWindowMgr = GameMain.Instance.MsgWnd;
 
-        public void Activate() => SetPhotoMessageWindowActive(true);
+            var messageWindowPanel =
+                Utility.GetFieldValue<MessageWindowMgr, GameObject>(messageWindowMgr, "m_goMessageWindowPanel");
 
-        public void Deactivate()
-        {
-            msgWnd.CloseMessageWindowPanel();
-            SetPhotoMessageWindowActive(false);
+            var msgParent = UTY.GetChildObject(messageWindowPanel, "MessageViewer/MsgParent");
+
+            messageButtons = UTY.GetChildObject(msgParent, "Buttons");
+            hitRetSprite = UTY.GetChildObject(msgParent, "Hitret");
+            subtitlesDisplayPanel = UTY.GetChildObject(msgParent, "SubtitlesDisplayPanel");
+
+            messageBox = UTY.GetChildObject(msgParent, "MessageBox");
+            speakerLabel = UTY.GetChildObject(msgParent, "SpeakerName/Name").GetComponent<UILabel>();
+            messageLabel = UTY.GetChildObject(msgParent, "Message").GetComponent<UILabel>();
         }
 
         public void Update() { }
 
-        private void SetPhotoMessageWindowActive(bool active)
+        public void Activate()
         {
-            UTY.GetChildObject(msgGameObject, "MessageViewer/MsgParent/MessageBox").SetActive(active);
-            UTY.GetChildObject(msgGameObject, "MessageViewer/MsgParent/Hitret")
-                .GetComponent<UISprite>().enabled = !active;
-            nameLabel.gameObject.SetActive(active);
-            msgLabel.gameObject.SetActive(active);
-
-            Transform transform = sysRoot.transform.Find("MessageWindowPanel/MessageViewer/MsgParent/Buttons");
-            var msgButtons = new[]
-            {
-                MessageWindowMgr.MessageWindowUnderButton.Skip,
-                MessageWindowMgr.MessageWindowUnderButton.Auto,
-                MessageWindowMgr.MessageWindowUnderButton.Voice,
-                MessageWindowMgr.MessageWindowUnderButton.BackLog,
-                MessageWindowMgr.MessageWindowUnderButton.Config
-            };
-            foreach (MessageWindowMgr.MessageWindowUnderButton msgButton in msgButtons)
+            if (Product.supportMultiLanguage)
+                subtitlesDisplayPanel.SetActive(false);
+
+            ResetMessageBoxProperties();
+
+            SetMessageBoxActive(true);
+
+            SetMessageBoxExtrasActive(false);
+
+            CloseMessagePanel();
+        }
+
+        public void Deactivate()
+        {
+            if (Product.supportMultiLanguage)
             {
-                transform.Find(msgButton.ToString()).gameObject.SetActive(!active);
+                subtitlesDisplayPanel.SetActive(true);
+
+                SetMessageBoxActive(false);
             }
 
-            if (!msgClass.subtitles_manager_) return;
+            ResetMessageBoxProperties();
 
-            msgClass.subtitles_manager_.visible = false;
-            msgClass.subtitles_manager_ = null;
+            SetMessageBoxExtrasActive(true);
+
+            CloseMessagePanel();
         }
 
         public void ShowMessage(string name, string message)
@@ -85,16 +106,34 @@ namespace MeidoPhotoStudio.Plugin
             MessageName = name;
             MessageText = message;
             ShowingMessage = true;
-            msgWnd.OpenMessageWindowPanel();
-            msgLabel.ProcessText();
-            msgClass.SetText(name, message, "", 0, AudioSourceMgr.Type.System);
-            msgClass.FinishChAnime();
         }
 
         public void CloseMessagePanel()
         {
+            if (!ShowingMessage)
+                return;
+
             ShowingMessage = false;
-            msgWnd.CloseMessageWindowPanel();
+        }
+
+        private void SetMessageBoxActive(bool active)
+        {
+            messageBox.SetActive(active);
+            messageLabel.gameObject.SetActive(active);
+            speakerLabel.gameObject.SetActive(active);
+        }
+
+        private void SetMessageBoxExtrasActive(bool active)
+        {
+            messageButtons.SetActive(active);
+            hitRetSprite.SetActive(active);
+        }
+
+        private void ResetMessageBoxProperties()
+        {
+            FontSize = 25;
+            MessageName = string.Empty;
+            MessageText = string.Empty;
         }
     }
 }

+ 2 - 2
src/MeidoPhotoStudio.Plugin/Managers/SceneManager.cs

@@ -209,7 +209,7 @@ namespace MeidoPhotoStudio.Plugin
 
         private int SortByName(MPSScene a, MPSScene b)
         {
-            return SortDirection * LexicographicStringComparer.Comparison(a.FileInfo.Name, b.FileInfo.Name);
+            return SortDirection * WindowsLogicalComparer.StrCmpLogicalW(a.FileInfo.Name, b.FileInfo.Name);
         }
 
         private int SortByDateCreated(MPSScene a, MPSScene b)
@@ -246,7 +246,7 @@ namespace MeidoPhotoStudio.Plugin
             string baseDirectoryName = KankyoMode ? Constants.kankyoDirectory : Constants.sceneDirectory;
             CurrentDirectoryList.Sort((a, b)
                 => a.Equals(baseDirectoryName, StringComparison.InvariantCultureIgnoreCase) 
-                    ? -1 : LexicographicStringComparer.Comparison(a, b));
+                    ? -1 : WindowsLogicalComparer.StrCmpLogicalW(a, b));
         }
 
         private void ClearSceneList()

+ 33 - 12
src/MeidoPhotoStudio.Plugin/Meido/Meido.cs

@@ -7,6 +7,7 @@ using System.Linq;
 using System.Xml.Linq;
 using UnityEngine;
 using static TBody;
+using Object = UnityEngine.Object;
 
 namespace MeidoPhotoStudio.Plugin
 {
@@ -66,6 +67,7 @@ namespace MeidoPhotoStudio.Plugin
         public string FirstName => Maid.status.firstName;
         public string LastName => Maid.status.lastName;
         public bool Busy => Maid.IsBusy || Loading;
+        public bool Active { get; private set; }
         public bool CurlingFront => Maid.IsItemChange("skirt", "めくれスカート")
             || Maid.IsItemChange("onepiece", "めくれスカート");
         public bool CurlingBack => Maid.IsItemChange("skirt", "めくれスカート後ろ")
@@ -163,6 +165,9 @@ namespace MeidoPhotoStudio.Plugin
 
             Slot = slot;
 
+            if (Active) 
+                return;
+
             FreeLook = false;
             Maid.Visible = true;
             Body.boHeadToCam = true;
@@ -213,9 +218,13 @@ namespace MeidoPhotoStudio.Plugin
 
             IKManager.Initialize();
 
+            SetFaceBlendSet(defaultFaceBlendSet);
+
             IK = true;
             Stop = false;
             Bone = false;
+
+            Active = true;
         }
 
         private void ReinitializeBody(object sender, ProcStartEventArgs args)
@@ -229,31 +238,37 @@ namespace MeidoPhotoStudio.Plugin
                     MPN.hairf, MPN.hairr, MPN.hairs, MPN.hairt
                 };
 
+                Action action = null;
+
                 // Change body
                 if (Maid.GetProp(MPN.body).boDut)
                 {
                     IKManager.Destroy();
-                    StartLoad(reinitializeBody);
+                    action += ReinitializeBody;
                 }
+
                 // Change face
-                else if (Maid.GetProp(MPN.head).boDut)
+                if (Maid.GetProp(MPN.head).boDut)
                 {
                     SetFaceBlendSet(defaultFaceBlendSet);
-                    StartLoad(reinitializeFace);
+                    action += ReinitializeFace;
                 }
+
                 // Gravity control clothing/hair change
-                else if (gravityControlProps.Any(prop => Maid.GetProp(prop).boDut))
+                if (gravityControlProps.Any(prop => Maid.GetProp(prop).boDut))
                 {
-                    if (HairGravityControl) GameObject.Destroy(HairGravityControl.gameObject);
-                    if (SkirtGravityControl) GameObject.Destroy(SkirtGravityControl.gameObject);
-
-                    StartLoad(reinitializeGravity);
+                    if (HairGravityControl) Object.Destroy(HairGravityControl.gameObject);
+                    if (SkirtGravityControl) Object.Destroy(SkirtGravityControl.gameObject);
+                    action += ReinitializeGravity;
                 }
+
                 // Clothing/accessory changes
                 // Includes null_mpn too but any button click results in null_mpn bodut I think
-                else StartLoad(() => OnUpdateMeido());
+                action ??= () => OnUpdateMeido();
+
+                StartLoad(action);
 
-                void reinitializeBody()
+                void ReinitializeBody()
                 {
                     IKManager.Initialize();
                     Stop = false;
@@ -267,14 +282,14 @@ namespace MeidoPhotoStudio.Plugin
                     Utility.SetFieldValue(customPartsWindow, "animation", Maid.GetAnimation());
                 }
 
-                void reinitializeFace()
+                void ReinitializeFace()
                 {
                     DefaultEyeRotL = Body.quaDefEyeL;
                     DefaultEyeRotR = Body.quaDefEyeR;
                     BackupBlendSetValues();
                 }
 
-                void reinitializeGravity()
+                void ReinitializeGravity()
                 {
                     InitializeGravityControls();
                     OnUpdateMeido();
@@ -322,6 +337,8 @@ namespace MeidoPhotoStudio.Plugin
             Maid.Visible = false;
 
             IKManager.Destroy();
+            
+            Active = false;
         }
 
         public void Deactivate()
@@ -363,11 +380,15 @@ namespace MeidoPhotoStudio.Plugin
                 {
                     Utility.LogWarning($"{poseFilename}: Could not open because {e.Message}");
                     Constants.InitializeCustomPoses();
+                    SetPose(PoseInfo.DefaultPose);
+                    OnUpdateMeido();
                     return;
                 }
                 catch (Exception e)
                 {
                     Utility.LogWarning($"{poseFilename}: Could not apply pose because {e.Message}");
+                    SetPose(PoseInfo.DefaultPose);
+                    OnUpdateMeido();
                     return;
                 }
                 SetMune(true, left: true);

+ 71 - 13
src/MeidoPhotoStudio.Plugin/Meido/MeidoDragPointManager.cs

@@ -490,28 +490,38 @@ namespace MeidoPhotoStudio.Plugin
             InitializeMuneDragPoint(left: true);
             InitializeMuneDragPoint(left: false);
 
-            DragPointLimb[] armDragPointL = MakeIKChain(BoneTransform[Bone.HandL]);
+            var armDragPointL = MakeArmChain(BoneTransform[Bone.HandL], meido);
             DragPoints[Bone.UpperArmL] = armDragPointL[0];
             DragPoints[Bone.ForearmL] = armDragPointL[1];
             DragPoints[Bone.HandL] = armDragPointL[2];
 
-            DragPointLimb[] armDragPointR = MakeIKChain(BoneTransform[Bone.HandR]);
+            var armDragPointR = MakeArmChain(BoneTransform[Bone.HandR], meido);
             DragPoints[Bone.UpperArmR] = armDragPointR[0];
             DragPoints[Bone.ForearmR] = armDragPointR[1];
             DragPoints[Bone.HandR] = armDragPointR[2];
 
-            DragPointLimb[] legDragPointL = MakeIKChain(BoneTransform[Bone.FootL]);
+            var legDragPointL = MakeLegChain(BoneTransform[Bone.FootL]);
             DragPoints[Bone.CalfL] = legDragPointL[0];
             DragPoints[Bone.FootL] = legDragPointL[1];
 
-            DragPointLimb[] legDragPointR = MakeIKChain(BoneTransform[Bone.FootR]);
+            var legDragPointR = MakeLegChain(BoneTransform[Bone.FootR]);
             DragPoints[Bone.CalfR] = legDragPointR[0];
             DragPoints[Bone.FootR] = legDragPointR[1];
 
             InitializeSpineDragPoint(SpineBones);
 
-            InitializeFingerDragPoint(Bone.Finger0L, Bone.Finger4R);
-            InitializeFingerDragPoint(Bone.Toe0L, Bone.Toe2R);
+            for (var bone = Bone.Finger4NubR; bone >= Bone.Finger0L; bone -= 4)
+            {
+                var i = 2;
+                var chain = MakeFingerChain(BoneTransform[bone], meido);
+
+                for (var joint = bone - 1; joint > bone - 4; joint--)
+                {
+                    DragPoints[joint] = chain[i];
+                    i--;
+                }
+            }
+            MakeToeChain(Bone.Toe0L, Bone.Toe2R);
         }
 
         private void InitializeMuneDragPoint(bool left)
@@ -527,13 +537,11 @@ namespace MeidoPhotoStudio.Plugin
             DragPoints[mune] = muneDragPoint;
         }
 
-        private DragPointLimb[] MakeIKChain(Transform lower)
+        private DragPointLimb[] MakeLegChain(Transform lower)
         {
             Vector3 limbDragPointSize = Vector3.one * 0.12f;
-            // Ignore Thigh transform when making a leg IK chain
-            bool isLeg = lower.name.EndsWith("Foot");
-            DragPointLimb[] dragPoints = new DragPointLimb[isLeg ? 2 : 3];
-            for (int i = dragPoints.Length - 1; i >= 0; i--)
+            DragPointLimb[] dragPoints = new DragPointLimb[2];
+            for (var i = dragPoints.Length - 1; i >= 0; i--)
             {
                 Transform joint = lower;
                 dragPoints[i] = DragPoint.Make<DragPointLimb>(PrimitiveType.Sphere, limbDragPointSize);
@@ -545,10 +553,36 @@ namespace MeidoPhotoStudio.Plugin
             return dragPoints;
         }
 
-        private void InitializeFingerDragPoint(Bone start, Bone end)
+        private static DragPointLimb[] MakeArmChain(Transform lower, Meido meido)
+        {
+            var limbDragPointSize = Vector3.one * 0.12f;
+
+            var realLower = CMT.SearchObjName(meido.Body.goSlot[0].obj_tr, lower.name, false);
+
+            var dragPoints = new DragPointLimb[3];
+
+            for (var i = dragPoints.Length - 1; i >= 0; i--)
+            {
+                var joint = lower;
+                var positionJoint = realLower;
+
+                dragPoints[i] = DragPoint.Make<DragPointLimb>(PrimitiveType.Sphere, limbDragPointSize);
+                dragPoints[i].Initialize(meido, () => positionJoint.position, () => Vector3.zero);
+                dragPoints[i].Set(joint);
+                dragPoints[i].AddGizmo();
+                dragPoints[i].Gizmo.SetAlternateTarget(positionJoint);
+
+                lower = lower.parent;
+                realLower = realLower.parent;
+            }
+
+            return dragPoints;
+        }
+
+        private void MakeToeChain(Bone start, Bone end)
         {
             Vector3 fingerDragPointSize = Vector3.one * 0.01f;
-            int joints = BoneTransform[start].name.Split(' ')[2].StartsWith("Finger") ? 4 : 3;
+            const int joints = 3;
             for (Bone bone = start; bone <= end; bone += joints)
             {
                 for (int i = 1; i < joints; i++)
@@ -562,6 +596,30 @@ namespace MeidoPhotoStudio.Plugin
             }
         }
 
+        private static DragPointFinger[] MakeFingerChain(Transform lower, Meido meido)
+        {
+            var fingerDragPointSize = Vector3.one * 0.01f;
+
+            var dragPoints = new DragPointFinger[3];
+
+            var realLower = CMT.SearchObjName(meido.Body.goSlot[0].obj_tr, lower.parent.name, false);
+
+            for (var i = dragPoints.Length - 1; i >= 0; i--)
+            {
+                var joint = lower;
+                var positionJoint = realLower;
+
+                dragPoints[i] = DragPoint.Make<DragPointFinger>(PrimitiveType.Sphere, fingerDragPointSize);
+                dragPoints[i].Initialize(meido, () => positionJoint.position, () => Vector3.zero);
+                dragPoints[i].Set(joint);
+
+                lower = lower.parent;
+                realLower = realLower.parent;
+            }
+
+            return dragPoints;
+        }
+
         private void InitializeSpineDragPoint(params Bone[] bones)
         {
             Vector3 spineDragPointSize = DragPointMeido.boneScale;

+ 19 - 35
src/MeidoPhotoStudio.Plugin/MeidoPhotoStudio.Plugin.csproj

@@ -1,47 +1,31 @@
-<Project Sdk="Microsoft.Net.Sdk">
+<Project Sdk="Microsoft.NET.Sdk">
     <PropertyGroup>
         <TargetFramework>net35</TargetFramework>
-        <AssemblyVersion>0.0</AssemblyVersion>
-        <FileVersion>0.0.0</FileVersion>
-        <GenerateAssemblyInfo>true</GenerateAssemblyInfo>
-        <FrameworkPathOverride Condition="'$(TargetFramework)' == 'net35'">$(MSBuildProgramFiles32)\Reference Assemblies\Microsoft\Framework\.NETFramework\v3.5\Profile\Client</FrameworkPathOverride>
         <LangVersion>9</LangVersion>
+        <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
+        <Configurations>Debug;Release</Configurations>
+        <Platforms>AnyCPU</Platforms>
     </PropertyGroup>
+
+    <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
+        <DefineConstants>DEBUG;TRACE</DefineConstants>
+    </PropertyGroup>
+
     <ItemGroup>
-        <Reference Include="Assembly-CSharp">
-            <HintPath>..\..\lib\Assembly-CSharp.dll</HintPath>
-        </Reference>
-        <Reference Include="Assembly-CSharp-firstpass">
-            <HintPath>..\..\lib\Assembly-CSharp-firstpass.dll</HintPath>
-        </Reference>
-        <Reference Include="Assembly-UnityScript-firstpass">
-            <HintPath>..\..\lib\Assembly-UnityScript-firstpass.dll</HintPath>
-        </Reference>
-        <Reference Include="UnityEngine">
-            <HintPath>..\..\lib\UnityEngine.dll</HintPath>
-        </Reference>
-        <Reference Include="Newtonsoft.Json">
-            <HintPath>..\..\lib\Newtonsoft.Json.dll</HintPath>
-        </Reference>
-        <Reference Include="Ionic.Zlib">
-            <HintPath>..\..\lib\Ionic.Zlib.dll</HintPath>
-        </Reference>
-        <Reference Include="BepInEx">
-            <HintPath>..\..\lib\BepInEx.dll</HintPath>
-        </Reference>
-        <Reference Include="0Harmony">
-            <HintPath>..\..\lib\0Harmony.dll</HintPath>
-        </Reference>
+        <Reference Include="..\..\lib\Assembly-CSharp.dll" />
+        <Reference Include="..\..\lib\Assembly-CSharp-firstpass.dll" />
+        <Reference Include="..\..\lib\Assembly-UnityScript-firstpass.dll" />
+        <Reference Include="..\..\lib\UnityEngine.dll" />
+        <Reference Include="..\..\lib\UnityEngine.UI.dll" />
+        <Reference Include="..\..\lib\Newtonsoft.Json.dll" />
+        <Reference Include="..\..\lib\Ionic.Zlib.dll" />
+        <Reference Include="..\..\lib\BepInEx.dll" />
+        <Reference Include="..\..\lib\0Harmony.dll" />
     </ItemGroup>
+
     <ItemGroup>
         <Content Include="Config\**">
             <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
         </Content>
     </ItemGroup>
-    <ItemGroup>
-      <PackageReference Include="IsExternalInit" Version="1.0.0">
-        <PrivateAssets>all</PrivateAssets>
-        <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
-      </PackageReference>
-    </ItemGroup>
 </Project>

+ 44 - 31
src/MeidoPhotoStudio.Plugin/MeidoPhotoStudio.cs

@@ -19,12 +19,13 @@ namespace MeidoPhotoStudio.Plugin
         private const string pluginGuid = "com.habeebweeb.com3d2.meidophotostudio";
         public const string pluginName = "MeidoPhotoStudio";
         public const string pluginVersion = "1.0.0";
-        public const string pluginSubVersion = "beta.3";
-        public const short sceneVersion = 1;
+        public const string pluginSubVersion = "beta.4.1";
+        public const short sceneVersion = 2;
         public const int kankyoMagic = -765;
         public static readonly string pluginString = $"{pluginName} {pluginVersion}";
         public static bool EditMode => currentScene == Constants.Scene.Edit;
         public static event EventHandler<ScreenshotEventArgs> NotifyRawScreenshot;
+        private HarmonyLib.Harmony harmony;
         private WindowManager windowManager;
         private SceneManager sceneManager;
         private MeidoManager meidoManager;
@@ -49,8 +50,9 @@ namespace MeidoPhotoStudio.Plugin
 
         private void Awake()
         {
-            var harmony = HarmonyLib.Harmony.CreateAndPatchAll(typeof(AllProcPropSeqStartPatcher));
+            harmony = HarmonyLib.Harmony.CreateAndPatchAll(typeof(AllProcPropSeqStartPatcher));
             harmony.PatchAll(typeof(BgMgrPatcher));
+            harmony.PatchAll(typeof(MeidoManager));
             ScreenshotEvent += OnScreenshotEvent;
             DontDestroyOnLoad(this);
             UnityEngine.SceneManagement.SceneManager.sceneLoaded += OnSceneLoaded;
@@ -140,6 +142,13 @@ namespace MeidoPhotoStudio.Plugin
 
             var metadata = SceneMetadata.ReadMetadata(headerReader);
 
+            if (metadata.Version > sceneVersion)
+            {
+                Utility.LogWarning("Cannot load scene. Scene is too new.");
+                Utility.LogWarning($"Your version: {sceneVersion}, Scene version: {metadata.Version}");
+                return;
+            }
+
             using var uncompressed = memoryStream.Decompress();
             using var dataReader = new BinaryReader(uncompressed, Encoding.UTF8);
 
@@ -181,7 +190,7 @@ namespace MeidoPhotoStudio.Plugin
             catch (Exception e)
             {
                 Utility.LogError(
-                    $"Failed to deserialize scene TEST because {e.Message}"
+                    $"Failed to deserialize scene because {e.Message}"
                     + $"\nCurrent header: '{header}'. Last header: '{previousHeader}'"
                 );
                 Utility.LogError(e.StackTrace);
@@ -360,6 +369,7 @@ namespace MeidoPhotoStudio.Plugin
             meidoManager = new MeidoManager();
             environmentManager = new EnvironmentManager();
             messageWindowManager = new MessageWindowManager();
+            messageWindowManager.Activate();
             lightManager = new LightManager();
             propManager = new PropManager(meidoManager);
             sceneManager = new SceneManager(this);
@@ -430,9 +440,32 @@ namespace MeidoPhotoStudio.Plugin
         {
             if (meidoManager.Busy || SceneManager.Busy) return;
 
-            SystemDialog sysDialog = GameMain.Instance.SysDlg;
+            var sysDialog = GameMain.Instance.SysDlg;
+
+            if (!sysDialog.IsDecided && !force) return;
+
+            uiActive = false;
+            active = false;
+
+            if (force)
+            {
+                Exit();
+                return;
+            }
+
+            sysDialog.Show(
+                string.Format(Translation.Get("systemMessage", "exitConfirm"), pluginName),
+                SystemDialog.TYPE.OK_CANCEL,
+                Exit,
+                () =>
+                {
+                    sysDialog.Close();
+                    uiActive = true;
+                    active = true;
+                }
+            );
 
-            void exit()
+            void Exit()
             {
                 sysDialog.Close();
 
@@ -448,34 +481,14 @@ namespace MeidoPhotoStudio.Plugin
 
                 Modal.Close();
 
-                if (!EditMode)
-                {
-                    GameObject dailyPanel = GameObject.Find("UI Root")?.transform.Find("DailyPanel")?.gameObject;
-                    dailyPanel?.SetActive(true);
-                }
-
                 Configuration.Config.Save();
-            }
 
-            if (sysDialog.IsDecided || EditMode || force)
-            {
-                uiActive = false;
-                active = false;
+                if (EditMode) return;
 
-                if (EditMode || force) exit();
-                else
-                {
-                    string exitMessage = string.Format(Translation.Get("systemMessage", "exitConfirm"), pluginName);
-                    sysDialog.Show(exitMessage, SystemDialog.TYPE.OK_CANCEL,
-                        f_dgOk: exit,
-                        f_dgCancel: () =>
-                        {
-                            sysDialog.Close();
-                            uiActive = true;
-                            active = true;
-                        }
-                    );
-                }
+                var dailyPanel = GameObject.Find("UI Root")?.transform.Find("DailyPanel")?.gameObject;
+
+                if (dailyPanel != null)
+                    dailyPanel.SetActive(true);
             }
         }
     }

+ 4 - 4
src/MeidoPhotoStudio.Plugin/Serialization/SceneMetadata.cs

@@ -4,10 +4,10 @@ namespace MeidoPhotoStudio.Plugin
 {
     public class SceneMetadata
     {
-        public short Version { get; init; }
-        public bool Environment { get; init; }
-        public int MaidCount { get; init; }
-        public bool MMConverted { get; init; }
+        public short Version { get; set; }
+        public bool Environment { get; set; }
+        public int MaidCount { get; set; }
+        public bool MMConverted { get; set; }
 
         public void WriteMetadata(BinaryWriter writer)
         {

+ 4 - 0
src/MeidoPhotoStudio.Plugin/Serialization/Serializers/ManagerSerializers/CameraManagerSerializer.cs

@@ -19,6 +19,8 @@ namespace MeidoPhotoStudio.Plugin
             writer.Write(manager.CurrentCameraIndex);
             writer.Write(manager.CameraCount);
             foreach (var info in cameraInfos) InfoSerializer.Serialize(info, writer);
+
+            CameraUtility.StopAll();
         }
 
         public override void Deserialize(CameraManager manager, BinaryReader reader, SceneMetadata metadata)
@@ -38,6 +40,8 @@ namespace MeidoPhotoStudio.Plugin
             if (metadata.Environment) return;
 
             cameraInfos[manager.CurrentCameraIndex].Apply(camera);
+
+            CameraUtility.StopAll();
         }
 
         private static CameraInfo[] GetCameraInfos(CameraManager manager)

+ 23 - 2
src/MeidoPhotoStudio.Plugin/Serialization/Serializers/MeidoSerializer.cs

@@ -10,7 +10,7 @@ namespace MeidoPhotoStudio.Plugin
     {
         private const short version = 1;
         private const short headVersion = 1;
-        private const short bodyVersion = 1;
+        private const short bodyVersion = 2;
         private const short clothingVersion = 1;
 
         private static SimpleSerializer<PoseInfo> PoseInfoSerializer => Serialization.GetSimple<PoseInfo>();
@@ -104,6 +104,13 @@ namespace MeidoPhotoStudio.Plugin
             writer.Write(poseBuffer);
 
             PoseInfoSerializer.Serialize(meido.CachedPose, writer);
+
+            // v2 start
+            // sub mune rotation
+            var body = meido.Body;
+            writer.WriteQuaternion(body.GetBone("Mune_L_sub").localRotation);
+            writer.WriteQuaternion(body.GetBone("Mune_R_sub").localRotation);
+            // v2 end
         }
 
         private static void SerializeClothing(Meido meido, BinaryWriter writer)
@@ -205,7 +212,7 @@ namespace MeidoPhotoStudio.Plugin
 
         private static void DeserializeBody(Meido meido, BinaryReader reader, SceneMetadata metadata)
         {
-            _ = reader.ReadVersion();
+            var version = reader.ReadVersion();
 
             var muneSetting = new KeyValuePair<bool, bool>(true, true);
             if (metadata.MMConverted) meido.IKManager.Deserialize(reader);
@@ -221,6 +228,20 @@ namespace MeidoPhotoStudio.Plugin
             
             meido.SetMune(!muneSetting.Key, true);
             meido.SetMune(!muneSetting.Value);
+
+            if (version >= 2)
+            {
+                var muneLSubRotation = reader.ReadQuaternion();
+                var muneSubRRotation = reader.ReadQuaternion();
+
+                var body = meido.Body;
+
+                if (muneSetting.Key)
+                    body.GetBone("Mune_L_sub").localRotation = muneLSubRotation;
+
+                if (muneSetting.Value)
+                    body.GetBone("Mune_R_sub").localRotation = muneSubRRotation;
+            }
         }
 
         private static void DeserializeClothing(Meido meido, BinaryReader reader, SceneMetadata metadata)

+ 4 - 4
src/MeidoPhotoStudio.Plugin/Serialization/SimpleSerializers/DragPointPropDTOSerializer.cs

@@ -41,10 +41,10 @@ namespace MeidoPhotoStudio.Plugin
 
     public class DragPointPropDTO
     {
-        public TransformDTO TransformDTO { get; init; }
-        public AttachPointInfo AttachPointInfo { get; init; }
-        public PropInfo PropInfo { get; init; }
-        public bool ShadowCasting { get; init; }
+        public TransformDTO TransformDTO { get; set; }
+        public AttachPointInfo AttachPointInfo { get; set; }
+        public PropInfo PropInfo { get; set; }
+        public bool ShadowCasting { get; set; }
 
         public DragPointPropDTO() { }
 

+ 5 - 5
src/MeidoPhotoStudio.Plugin/Serialization/SimpleSerializers/TransformDTOSerializer.cs

@@ -35,11 +35,11 @@ namespace MeidoPhotoStudio.Plugin
 
     public class TransformDTO
     {
-        public Vector3 Position { get; init; }
-        public Vector3 LocalPosition { get; init; }
-        public Quaternion Rotation { get; init; } = Quaternion.identity;
-        public Quaternion LocalRotation { get; init; } = Quaternion.identity;
-        public Vector3 LocalScale { get; init; } = Vector3.one;
+        public Vector3 Position { get; set; }
+        public Vector3 LocalPosition { get; set; }
+        public Quaternion Rotation { get; set; } = Quaternion.identity;
+        public Quaternion LocalRotation { get; set; } = Quaternion.identity;
+        public Vector3 LocalScale { get; set; } = Vector3.one;
 
         public TransformDTO() { }
 

+ 14 - 0
src/MeidoPhotoStudio.Plugin/WindowsLogicalComparer.cs

@@ -0,0 +1,14 @@
+using System.Collections.Generic;
+using System.Runtime.InteropServices;
+
+namespace MeidoPhotoStudio.Plugin
+{
+    public class WindowsLogicalComparer : IComparer<string>
+    {
+        [DllImport("shlwapi.dll", CharSet = CharSet.Unicode, ExactSpelling = true)]
+        public static extern int StrCmpLogicalW(string x, string y);
+
+        public int Compare(string x, string y) =>
+            StrCmpLogicalW(x, y);
+    }
+}