Browse Source

Reformat entire project

This was a huge endeavour.

I used rules defined in both the .editorconfig and StyleCop. Here's a
summary of style changes made.

- Use file scoped namespaces.
- Prefer using is and is not operator rather than == or != for
  comparisons with constants and null.
- Prefer early return/continue/break etc to prevent deep nesting.
- Prefer no braces and expression body for single line
  statements and expressions.
- Prefer using pattern matching.
- Prefer expression body on its own line.
- Prefer UnityEngine.Object implicit bool operator for null checks.
    - Don't use implicit bool for assignment.
- Use discards for unused lambda expression parameters.
habeebweeb 1 year ago
parent
commit
4ee2dadedf
183 changed files with 16683 additions and 14179 deletions
  1. 12 0
      Directory.Build.props
  2. 31 0
      StyleCop.ruleset
  3. 4 5
      src/MeidoPhotoStudio.Converter/Converters/IConverter.cs
  4. 89 94
      src/MeidoPhotoStudio.Converter/Converters/MMConverter.cs
  5. 73 68
      src/MeidoPhotoStudio.Converter/Converters/MMPngConverter.cs
  6. 36 33
      src/MeidoPhotoStudio.Converter/MPSSceneSerializer.cs
  7. 34 27
      src/MeidoPhotoStudio.Converter/MultipleMaids/ConversionUtility.cs
  8. 29 29
      src/MeidoPhotoStudio.Converter/MultipleMaids/MMConstants.cs
  9. 10 10
      src/MeidoPhotoStudio.Converter/MultipleMaids/MMScene.cs
  10. 646 630
      src/MeidoPhotoStudio.Converter/MultipleMaids/MMSceneConverter.cs
  11. 30 29
      src/MeidoPhotoStudio.Converter/Plugin.cs
  12. 27 25
      src/MeidoPhotoStudio.Converter/PluginCore.cs
  13. 28 28
      src/MeidoPhotoStudio.Converter/UI.cs
  14. 23 24
      src/MeidoPhotoStudio.Converter/Utility/LZMA.cs
  15. 41 41
      src/MeidoPhotoStudio.Converter/Utility/PngUtility.cs
  16. 92 0
      src/MeidoPhotoStudio.Plugin/BinaryExtensions.cs
  17. 42 0
      src/MeidoPhotoStudio.Plugin/CameraUtility.cs
  18. 8 8
      src/MeidoPhotoStudio.Plugin/Configuration.cs
  19. 893 804
      src/MeidoPhotoStudio.Plugin/Constants.cs
  20. 169 126
      src/MeidoPhotoStudio.Plugin/DragPoint/CustomGizmo.cs
  21. 262 206
      src/MeidoPhotoStudio.Plugin/DragPoint/DragPoint.cs
  22. 19 0
      src/MeidoPhotoStudio.Plugin/DragPoint/DragPointBG.cs
  23. 32 0
      src/MeidoPhotoStudio.Plugin/DragPoint/DragPointBody.cs
  24. 236 181
      src/MeidoPhotoStudio.Plugin/DragPoint/DragPointGeneral.cs
  25. 86 68
      src/MeidoPhotoStudio.Plugin/DragPoint/DragPointGravity.cs
  26. 285 222
      src/MeidoPhotoStudio.Plugin/DragPoint/DragPointLight.cs
  27. 55 50
      src/MeidoPhotoStudio.Plugin/DragPoint/DragPointMeido.cs
  28. 0 47
      src/MeidoPhotoStudio.Plugin/DragPoint/DragPointOther.cs
  29. 64 83
      src/MeidoPhotoStudio.Plugin/DragPoint/DragPointProp.cs
  30. 21 0
      src/MeidoPhotoStudio.Plugin/DragPoint/LightProperty.cs
  31. 47 0
      src/MeidoPhotoStudio.Plugin/DragPoint/PropInfo.cs
  32. 10 5
      src/MeidoPhotoStudio.Plugin/GUI/Controls/BaseControl.cs
  33. 18 13
      src/MeidoPhotoStudio.Plugin/GUI/Controls/Button.cs
  34. 44 34
      src/MeidoPhotoStudio.Plugin/GUI/Controls/ComboBox.cs
  35. 137 310
      src/MeidoPhotoStudio.Plugin/GUI/Controls/DropDown.cs
  36. 230 0
      src/MeidoPhotoStudio.Plugin/GUI/Controls/DropdownHelper.cs
  37. 50 43
      src/MeidoPhotoStudio.Plugin/GUI/Controls/KeyRebindButton.cs
  38. 32 28
      src/MeidoPhotoStudio.Plugin/GUI/Controls/Modal.cs
  39. 90 74
      src/MeidoPhotoStudio.Plugin/GUI/Controls/SelectionGrid.cs
  40. 119 122
      src/MeidoPhotoStudio.Plugin/GUI/Controls/Slider.cs
  41. 20 0
      src/MeidoPhotoStudio.Plugin/GUI/Controls/SliderProp.cs
  42. 10 13
      src/MeidoPhotoStudio.Plugin/GUI/Controls/TextArea.cs
  43. 22 17
      src/MeidoPhotoStudio.Plugin/GUI/Controls/TextField.cs
  44. 34 30
      src/MeidoPhotoStudio.Plugin/GUI/Controls/Toggle.cs
  45. 178 145
      src/MeidoPhotoStudio.Plugin/GUI/Panes/BackgroundWindow2Panes/AttachPropPane.cs
  46. 165 147
      src/MeidoPhotoStudio.Plugin/GUI/Panes/BackgroundWindow2Panes/ModPropsPane.cs
  47. 78 78
      src/MeidoPhotoStudio.Plugin/GUI/Panes/BackgroundWindow2Panes/MyRoomPropsPane.cs
  48. 158 138
      src/MeidoPhotoStudio.Plugin/GUI/Panes/BackgroundWindow2Panes/PropManagerPane.cs
  49. 147 135
      src/MeidoPhotoStudio.Plugin/GUI/Panes/BackgroundWindow2Panes/PropsPane.cs
  50. 74 67
      src/MeidoPhotoStudio.Plugin/GUI/Panes/BackgroundWindowPanes/BackgroundSelectorPane.cs
  51. 83 70
      src/MeidoPhotoStudio.Plugin/GUI/Panes/BackgroundWindowPanes/CameraPane.cs
  52. 79 62
      src/MeidoPhotoStudio.Plugin/GUI/Panes/BackgroundWindowPanes/DragPointPane.cs
  53. 106 94
      src/MeidoPhotoStudio.Plugin/GUI/Panes/BackgroundWindowPanes/EffectsPanes/BloomPane.cs
  54. 92 77
      src/MeidoPhotoStudio.Plugin/GUI/Panes/BackgroundWindowPanes/EffectsPanes/DepthOfFieldPane.cs
  55. 70 58
      src/MeidoPhotoStudio.Plugin/GUI/Panes/BackgroundWindowPanes/EffectsPanes/EffectPane.cs
  56. 38 38
      src/MeidoPhotoStudio.Plugin/GUI/Panes/BackgroundWindowPanes/EffectsPanes/EffectsPane.cs
  57. 115 101
      src/MeidoPhotoStudio.Plugin/GUI/Panes/BackgroundWindowPanes/EffectsPanes/FogPane.cs
  58. 53 52
      src/MeidoPhotoStudio.Plugin/GUI/Panes/BackgroundWindowPanes/EffectsPanes/OtherEffectsPane.cs
  59. 72 60
      src/MeidoPhotoStudio.Plugin/GUI/Panes/BackgroundWindowPanes/EffectsPanes/VignettePane.cs
  60. 298 238
      src/MeidoPhotoStudio.Plugin/GUI/Panes/BackgroundWindowPanes/LightsPane.cs
  61. 32 16
      src/MeidoPhotoStudio.Plugin/GUI/Panes/BasePane.cs
  62. 103 96
      src/MeidoPhotoStudio.Plugin/GUI/Panes/CallWindowPanes/MaidSelectorPane.cs
  63. 151 129
      src/MeidoPhotoStudio.Plugin/GUI/Panes/FaceWindowPanes/MaidFaceBlendPane.cs
  64. 190 173
      src/MeidoPhotoStudio.Plugin/GUI/Panes/FaceWindowPanes/MaidFaceSliderPane.cs
  65. 61 57
      src/MeidoPhotoStudio.Plugin/GUI/Panes/FaceWindowPanes/SaveFacePane.cs
  66. 57 53
      src/MeidoPhotoStudio.Plugin/GUI/Panes/MainWindowPanes/BG2WindowPane.cs
  67. 52 52
      src/MeidoPhotoStudio.Plugin/GUI/Panes/MainWindowPanes/BGWindowPane.cs
  68. 7 6
      src/MeidoPhotoStudio.Plugin/GUI/Panes/MainWindowPanes/BaseMainWindowPane.cs
  69. 38 41
      src/MeidoPhotoStudio.Plugin/GUI/Panes/MainWindowPanes/CallWindowPane.cs
  70. 60 58
      src/MeidoPhotoStudio.Plugin/GUI/Panes/MainWindowPanes/FaceWindowPane.cs
  71. 120 110
      src/MeidoPhotoStudio.Plugin/GUI/Panes/MainWindowPanes/PoseWindowPane.cs
  72. 130 131
      src/MeidoPhotoStudio.Plugin/GUI/Panes/MainWindowPanes/SettingsWindowPane.cs
  73. 112 83
      src/MeidoPhotoStudio.Plugin/GUI/Panes/OtherPanes/MaidSwitcherPane.cs
  74. 47 38
      src/MeidoPhotoStudio.Plugin/GUI/Panes/OtherPanes/TabsPane.cs
  75. 65 56
      src/MeidoPhotoStudio.Plugin/GUI/Panes/PoseWindowPanes/CopyPosePane.cs
  76. 78 66
      src/MeidoPhotoStudio.Plugin/GUI/Panes/PoseWindowPanes/GravityControlPane.cs
  77. 119 99
      src/MeidoPhotoStudio.Plugin/GUI/Panes/PoseWindowPanes/HandPresetPane.cs
  78. 264 217
      src/MeidoPhotoStudio.Plugin/GUI/Panes/PoseWindowPanes/MaidDressingPane.cs
  79. 92 79
      src/MeidoPhotoStudio.Plugin/GUI/Panes/PoseWindowPanes/MaidFreeLookPane.cs
  80. 64 54
      src/MeidoPhotoStudio.Plugin/GUI/Panes/PoseWindowPanes/MaidIKPane.cs
  81. 205 181
      src/MeidoPhotoStudio.Plugin/GUI/Panes/PoseWindowPanes/MaidPoseSelectorPane.cs
  82. 89 79
      src/MeidoPhotoStudio.Plugin/GUI/Panes/PoseWindowPanes/MpnAttachPropPane.cs
  83. 52 49
      src/MeidoPhotoStudio.Plugin/GUI/Panes/PoseWindowPanes/SaveHandPane.cs
  84. 59 56
      src/MeidoPhotoStudio.Plugin/GUI/Panes/PoseWindowPanes/SavePosePane.cs
  85. 91 83
      src/MeidoPhotoStudio.Plugin/GUI/Panes/SceneManagerPanes/SceneManagerDirectoryPane.cs
  86. 64 56
      src/MeidoPhotoStudio.Plugin/GUI/Panes/SceneManagerPanes/SceneManagerScenePane.cs
  87. 0 108
      src/MeidoPhotoStudio.Plugin/GUI/Panes/SceneManagerPanes/SceneManagerTitleBar.cs
  88. 125 0
      src/MeidoPhotoStudio.Plugin/GUI/Panes/SceneManagerPanes/SceneManagerTitleBarPane.cs
  89. 76 68
      src/MeidoPhotoStudio.Plugin/GUI/Windows/BaseWindow.cs
  90. 153 126
      src/MeidoPhotoStudio.Plugin/GUI/Windows/MainWindow.cs
  91. 87 79
      src/MeidoPhotoStudio.Plugin/GUI/Windows/MessageWindow.cs
  92. 197 185
      src/MeidoPhotoStudio.Plugin/GUI/Windows/SceneModalWindow.cs
  93. 84 79
      src/MeidoPhotoStudio.Plugin/GUI/Windows/SceneWindow.cs
  94. 12 0
      src/MeidoPhotoStudio.Plugin/KeyValuePairExtensions.cs
  95. 55 45
      src/MeidoPhotoStudio.Plugin/MPSScene.cs
  96. 135 99
      src/MeidoPhotoStudio.Plugin/MaidPlacementUtility.cs
  97. 50 0
      src/MeidoPhotoStudio.Plugin/Managers/CameraInfo.cs
  98. 128 152
      src/MeidoPhotoStudio.Plugin/Managers/CameraManager.cs
  99. 36 28
      src/MeidoPhotoStudio.Plugin/Managers/EffectManager.cs
  100. 146 116
      src/MeidoPhotoStudio.Plugin/Managers/EffectManagers/BloomEffectManager.cs
  101. 64 54
      src/MeidoPhotoStudio.Plugin/Managers/EffectManagers/BlurEffectManager.cs
  102. 112 0
      src/MeidoPhotoStudio.Plugin/Managers/EffectManagers/DepthOfFieldEffectManager.cs
  103. 0 100
      src/MeidoPhotoStudio.Plugin/Managers/EffectManagers/DepthOfFieldManager.cs
  104. 124 99
      src/MeidoPhotoStudio.Plugin/Managers/EffectManagers/FogEffectManager.cs
  105. 10 8
      src/MeidoPhotoStudio.Plugin/Managers/EffectManagers/IEffectManager.cs
  106. 30 20
      src/MeidoPhotoStudio.Plugin/Managers/EffectManagers/SepiaToneEffectManager.cs
  107. 87 73
      src/MeidoPhotoStudio.Plugin/Managers/EffectManagers/VignetteEffectManager.cs
  108. 137 109
      src/MeidoPhotoStudio.Plugin/Managers/EnvironmentManager.cs
  109. 8 7
      src/MeidoPhotoStudio.Plugin/Managers/IManager.cs
  110. 160 114
      src/MeidoPhotoStudio.Plugin/Managers/InputManager.cs
  111. 174 132
      src/MeidoPhotoStudio.Plugin/Managers/LightManager.cs
  112. 292 273
      src/MeidoPhotoStudio.Plugin/Managers/MeidoManager.cs
  113. 24 0
      src/MeidoPhotoStudio.Plugin/Managers/MeidoUpdateEventArgs.cs
  114. 105 106
      src/MeidoPhotoStudio.Plugin/Managers/MessageWindowManager.cs
  115. 289 234
      src/MeidoPhotoStudio.Plugin/Managers/PropManager.cs
  116. 302 254
      src/MeidoPhotoStudio.Plugin/Managers/SceneManager.cs
  117. 44 36
      src/MeidoPhotoStudio.Plugin/Managers/WindowManager.cs
  118. 27 0
      src/MeidoPhotoStudio.Plugin/Meido/AttachPoint.cs
  119. 29 0
      src/MeidoPhotoStudio.Plugin/Meido/AttachPointInfo.cs
  120. 18 0
      src/MeidoPhotoStudio.Plugin/Meido/GravityEventArgs.cs
  121. 78 69
      src/MeidoPhotoStudio.Plugin/Meido/IK/DragPointFinger.cs
  122. 95 87
      src/MeidoPhotoStudio.Plugin/Meido/IK/DragPointHead.cs
  123. 47 37
      src/MeidoPhotoStudio.Plugin/Meido/IK/DragPointPelvis.cs
  124. 100 80
      src/MeidoPhotoStudio.Plugin/Meido/IK/DragPointSpine.cs
  125. 63 56
      src/MeidoPhotoStudio.Plugin/Meido/IK/DragPointTorso.cs
  126. 28 26
      src/MeidoPhotoStudio.Plugin/Meido/IK/IK Chain/DragPointChain.cs
  127. 122 84
      src/MeidoPhotoStudio.Plugin/Meido/IK/IK Chain/DragPointLimb.cs
  128. 52 40
      src/MeidoPhotoStudio.Plugin/Meido/IK/IK Chain/DragPointMune.cs
  129. 732 584
      src/MeidoPhotoStudio.Plugin/Meido/Meido.cs
  130. 755 684
      src/MeidoPhotoStudio.Plugin/Meido/MeidoDragPointManager.cs
  131. 23 0
      src/MeidoPhotoStudio.Plugin/Meido/PoseInfo.cs
  132. 453 401
      src/MeidoPhotoStudio.Plugin/MeidoPhotoStudio.cs
  133. 54 41
      src/MeidoPhotoStudio.Plugin/MenuFileCache.cs
  134. 228 166
      src/MeidoPhotoStudio.Plugin/MenuFileUtility.cs
  135. 18 0
      src/MeidoPhotoStudio.Plugin/MenuFilesEventArgs.cs
  136. 5 65
      src/MeidoPhotoStudio.Plugin/MenuItem.cs
  137. 75 0
      src/MeidoPhotoStudio.Plugin/ModItem.cs
  138. 480 423
      src/MeidoPhotoStudio.Plugin/ModelUtility.cs
  139. 31 0
      src/MeidoPhotoStudio.Plugin/MousePosition.cs
  140. 14 0
      src/MeidoPhotoStudio.Plugin/MpnAttachProp.cs
  141. 97 0
      src/MeidoPhotoStudio.Plugin/MpsGui.cs
  142. 0 85
      src/MeidoPhotoStudio.Plugin/MyGui.cs
  143. 11 0
      src/MeidoPhotoStudio.Plugin/MyRoomItem.cs
  144. 12 18
      src/MeidoPhotoStudio.Plugin/Patchers/AllProcPropSeqStartPatcher.cs
  145. 17 14
      src/MeidoPhotoStudio.Plugin/Patchers/BgMgrPatcher.cs
  146. 11 0
      src/MeidoPhotoStudio.Plugin/Patchers/ProcStartEventArgs.cs
  147. 18 0
      src/MeidoPhotoStudio.Plugin/PresetChangeEventArgs.cs
  148. 18 0
      src/MeidoPhotoStudio.Plugin/ScreenshotEventArgs.cs
  149. 7 7
      src/MeidoPhotoStudio.Plugin/Serialization/ISerializer.cs
  150. 7 7
      src/MeidoPhotoStudio.Plugin/Serialization/ISimpleSerializer.cs
  151. 32 34
      src/MeidoPhotoStudio.Plugin/Serialization/SceneMetadata.cs
  152. 34 28
      src/MeidoPhotoStudio.Plugin/Serialization/Serialization.cs
  153. 11 10
      src/MeidoPhotoStudio.Plugin/Serialization/Serializer.cs
  154. 17 18
      src/MeidoPhotoStudio.Plugin/Serialization/Serializers/AttachPointInfoSerializer.cs
  155. 20 21
      src/MeidoPhotoStudio.Plugin/Serialization/Serializers/CameraInfoSerializer.cs
  156. 32 29
      src/MeidoPhotoStudio.Plugin/Serialization/Serializers/DragPointLightSerializer.cs
  157. 29 29
      src/MeidoPhotoStudio.Plugin/Serialization/Serializers/EffectSerializers/BloomEffectSerializer.cs
  158. 18 18
      src/MeidoPhotoStudio.Plugin/Serialization/Serializers/EffectSerializers/BlurEffectSerializer.cs
  159. 31 31
      src/MeidoPhotoStudio.Plugin/Serialization/Serializers/EffectSerializers/DepthOfFieldEffectSerializer.cs
  160. 31 31
      src/MeidoPhotoStudio.Plugin/Serialization/Serializers/EffectSerializers/FogEffectSerializer.cs
  161. 15 15
      src/MeidoPhotoStudio.Plugin/Serialization/Serializers/EffectSerializers/SepiaToneEffectSerializer.cs
  162. 29 29
      src/MeidoPhotoStudio.Plugin/Serialization/Serializers/EffectSerializers/VignetteEffectSerializer.cs
  163. 24 25
      src/MeidoPhotoStudio.Plugin/Serialization/Serializers/LightPropertySerializer.cs
  164. 38 32
      src/MeidoPhotoStudio.Plugin/Serialization/Serializers/ManagerSerializers/CameraManagerSerializer.cs
  165. 30 29
      src/MeidoPhotoStudio.Plugin/Serialization/Serializers/ManagerSerializers/EffectManagerSerializer.cs
  166. 52 49
      src/MeidoPhotoStudio.Plugin/Serialization/Serializers/ManagerSerializers/EnvironmentManagerSerializer.cs
  167. 33 30
      src/MeidoPhotoStudio.Plugin/Serialization/Serializers/ManagerSerializers/LightManagerSerializer.cs
  168. 67 60
      src/MeidoPhotoStudio.Plugin/Serialization/Serializers/ManagerSerializers/MeidoManagerSerializer.cs
  169. 31 28
      src/MeidoPhotoStudio.Plugin/Serialization/Serializers/ManagerSerializers/MessageWindowManagerSerializer.cs
  170. 48 45
      src/MeidoPhotoStudio.Plugin/Serialization/Serializers/ManagerSerializers/PropManagerSerializer.cs
  171. 139 96
      src/MeidoPhotoStudio.Plugin/Serialization/Serializers/MeidoSerializer.cs
  172. 11 10
      src/MeidoPhotoStudio.Plugin/Serialization/SimpleSerializer.cs
  173. 32 0
      src/MeidoPhotoStudio.Plugin/Serialization/SimpleSerializers/DragPointPropDTO.cs
  174. 26 49
      src/MeidoPhotoStudio.Plugin/Serialization/SimpleSerializers/DragPointPropDTOSerializer.cs
  175. 16 17
      src/MeidoPhotoStudio.Plugin/Serialization/SimpleSerializers/PoseInfoSerializer.cs
  176. 23 24
      src/MeidoPhotoStudio.Plugin/Serialization/SimpleSerializers/PropInfoSerializer.cs
  177. 29 0
      src/MeidoPhotoStudio.Plugin/Serialization/SimpleSerializers/TransformDTO.cs
  178. 22 44
      src/MeidoPhotoStudio.Plugin/Serialization/SimpleSerializers/TransformDTOSerializer.cs
  179. 35 0
      src/MeidoPhotoStudio.Plugin/StreamExtensions.cs
  180. 100 110
      src/MeidoPhotoStudio.Plugin/Translation.cs
  181. 185 346
      src/MeidoPhotoStudio.Plugin/Utility.cs
  182. 8 9
      src/MeidoPhotoStudio.Plugin/WindowsLogicalComparer.cs
  183. 16 0
      stylecop.json

+ 12 - 0
Directory.Build.props

@@ -4,6 +4,7 @@
     <TargetFramework>net35</TargetFramework>
     <LangVersion>10</LangVersion>
     <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
+    <CodeAnalysisRuleSet>$(MSBuildThisFileDirectory)StyleCop.ruleset</CodeAnalysisRuleSet>
   </PropertyGroup>
 
   <ItemGroup>
@@ -14,10 +15,21 @@
   </ItemGroup>
 
   <ItemGroup>
+
     <PackageReference Include="Microsoft.Unity.Analyzers" Version="1.13.0">
       <PrivateAssets>all</PrivateAssets>
       <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
     </PackageReference>
+
+    <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.406">
+      <PrivateAssets>all</PrivateAssets>
+      <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
+    </PackageReference>
+
+  </ItemGroup>
+
+  <ItemGroup>
+    <AdditionalFiles Include="$(MSBuildThisFileDirectory)stylecop.json" Link="stylecop.json" />
   </ItemGroup>
 
 </Project>

+ 31 - 0
StyleCop.ruleset

@@ -0,0 +1,31 @@
+<?xml version="1.0"?>
+<RuleSet Name="StyleCop.Analyzers rules" Description="My style rule preferences" ToolsVersion="14.0">
+
+  <!-- <Rules AnalyzerId="StyleCop.Analyzers" RuleNamespace="StyleCop.Analyzers."> -->
+  <!--   <Rule Id="" Action="None" /> -->
+  <!-- </Rules> -->
+
+  <Rules AnalyzerId="StyleCop.Analyzers" RuleNamespace="StyleCop.Analyzers.ReadabilityRules">
+    <Rule Id="SA1101" Action="None" Description="Prefix local calls with this" />
+  </Rules>
+
+  <Rules AnalyzerId="StyleCop.Analyzers" RuleNamespace="StyleCop.Analyzers.MaintainabilityRules">
+    <!-- NOTE: SA1401 is suppressed temporarily and will be addressed in the future -->
+    <Rule Id="SA1401" Action="None" Description="Fields should be private" />
+    <Rule Id="SA1407" Action="None" Description="Arithmetic expressions should declare precedence" />
+    <Rule Id="SA1408" Action="None" Description="Conditional expressions should declare precedence" />
+  </Rules>
+
+  <Rules AnalyzerId="StyleCop.Analyzers" RuleNamespace="StyleCop.Analyzers.LayoutRules">
+    <Rule Id="SA1503" Action="None" Description="Braces should not be omitted" />
+    <Rule Id="SA1519" Action="None" Description="Braces should not be omitted from multi-line child statement" />
+  </Rules>
+
+  <Rules AnalyzerId="StyleCop.Analyzers" RuleNamespace="StyleCop.Analyzers.DocumentationRules">
+    <!-- NOTE: Documentation rules should only be temporary as I might document things in the future. -->
+    <Rule Id="SA1600" Action="None" Description="Elements should be documented" />
+    <Rule Id="SA1602" Action="None" Description="Enumeration items should be documented" />
+    <Rule Id="SA1633" Action="None" Description="File should have header" />
+  </Rules>
+
+</RuleSet>

+ 4 - 5
src/MeidoPhotoStudio.Converter/Converters/IConverter.cs

@@ -1,7 +1,6 @@
-namespace MeidoPhotoStudio.Converter.Converters
+namespace MeidoPhotoStudio.Converter.Converters;
+
+public interface IConverter
 {
-    public interface IConverter
-    {
-        void Convert(string workingDirectory);
-    }
+    void Convert(string workingDirectory);
 }

+ 89 - 94
src/MeidoPhotoStudio.Converter/Converters/MMConverter.cs

@@ -1,134 +1,129 @@
-using System;
+using System;
 using System.IO;
 using System.Linq;
+
 using ExIni;
 using MeidoPhotoStudio.Converter.MultipleMaids;
 
-namespace MeidoPhotoStudio.Converter.Converters
+namespace MeidoPhotoStudio.Converter.Converters;
+
+public class MMConverter : IConverter
 {
-    public class MMConverter : IConverter
+    public const string ConverterName = "MultipleMaids";
+
+    private const string InputDirectoryName = "Input";
+
+    public void Convert(string workingDirectory)
     {
-        private const string InputDirectoryName = "Input";
-        public const string ConverterName = "MultipleMaids";
+        var baseDirectory = Path.Combine(workingDirectory, ConverterName);
+        var baseInputDirectory = Path.Combine(baseDirectory, InputDirectoryName);
+        var baseOutputDirectory = Path.Combine(baseDirectory, MPSSceneSerializer.FormatDate(DateTime.Now));
 
-        public void Convert(string workingDirectory)
-        {
-            var baseDirectory = Path.Combine(workingDirectory, ConverterName);
-            var baseInputDirectory = Path.Combine(baseDirectory, InputDirectoryName);
-            var baseOutputDirectory = Path.Combine(baseDirectory, MPSSceneSerializer.FormatDate(DateTime.Now));
+        Convert(baseInputDirectory, baseOutputDirectory);
+    }
 
-            Convert(baseInputDirectory, baseOutputDirectory);
-        }
+    private static void Convert(string workingDirectory, string destination)
+    {
+        var directory = new DirectoryInfo(workingDirectory);
 
-        private static void Convert(string workingDirectory, string destination)
-        {
-            var directory = new DirectoryInfo(workingDirectory);
+        if (!directory.Exists)
+            return;
 
-            if (!directory.Exists)
-                return;
+        Directory.CreateDirectory(destination);
 
-            Directory.CreateDirectory(destination);
+        foreach (var iniFile in directory.GetFiles("*.ini"))
+            ConvertIniFile(iniFile, destination);
 
-            foreach (var iniFile in directory.GetFiles("*.ini"))
-                ConvertIniFile(iniFile, destination);
+        foreach (var subDirectory in directory.GetDirectories())
+        {
+            var subDestination = Path.Combine(destination, subDirectory.Name);
 
-            foreach (var subDirectory in directory.GetDirectories())
-            {
-                var subDestination = Path.Combine(destination, subDirectory.Name);
-                Convert(subDirectory.FullName, subDestination);
-            }
+            Convert(subDirectory.FullName, subDestination);
         }
+    }
 
-        private static void ConvertIniFile(FileInfo iniFile, string destination)
-        {
-            var section = GetSceneSection(iniFile.FullName);
+    private static void ConvertIniFile(FileInfo iniFile, string destination)
+    {
+        var section = GetSceneSection(iniFile.FullName);
 
-            if (section is null)
-                return;
+        if (section is null)
+            return;
 
-            var outputDirectory = Path.Combine(destination, Path.GetFileNameWithoutExtension(iniFile.Name));
+        var outputDirectory = Path.Combine(destination, Path.GetFileNameWithoutExtension(iniFile.Name));
 
-            Directory.CreateDirectory(outputDirectory);
+        Directory.CreateDirectory(outputDirectory);
 
-            foreach (var key in section.Keys.Where(
-                key => !key.Key.StartsWith("ss") && !string.IsNullOrEmpty(key.Value)
-            ))
-                ConvertScene(section, key, Path.Combine(outputDirectory, GenerateFilename(iniFile.Name, key)));
-        }
+        foreach (var key in section.Keys.Where(key => !key.Key.StartsWith("ss") && !string.IsNullOrEmpty(key.Value)))
+            ConvertScene(section, key, Path.Combine(outputDirectory, GenerateFilename(iniFile.Name, key)));
+    }
 
-        private static void ConvertScene(IniSection section, IniKey key, string filePath)
+    private static void ConvertScene(IniSection section, IniKey key, string filePath)
+    {
+        var background = int.Parse(key.Key.Substring(1)) >= 10000;
+
+        byte[] convertedData;
+        MeidoPhotoStudio.Plugin.SceneMetadata sceneMetadata;
+
+        try
+        {
+            convertedData = MMSceneConverter.Convert(key.Value, background);
+            sceneMetadata = MMSceneConverter.GetSceneMetadata(key.Value, background);
+        }
+        catch (Exception e)
         {
-            var background = int.Parse(key.Key.Substring(1)) >= 10000;
-
-            byte[] convertedData;
-            MeidoPhotoStudio.Plugin.SceneMetadata sceneMetadata;
-
-            try
-            {
-                convertedData = MMSceneConverter.Convert(key.Value, background);
-                sceneMetadata = MMSceneConverter.GetSceneMetadata(key.Value, background);
-            }
-            catch (Exception e)
-            {
-                if (Plugin.Instance == null)
-                    return;
-
-                Plugin.Instance.Logger.LogError($"Could not convert {Path.GetFileName(filePath)} scene because {e}");
+            if (!Plugin.Instance || Plugin.Instance!.Logger is null)
                 return;
-            }
-
-            var screenshotKey = $"s{key.Key}"; // ex. ss100=thumb_base64
-            string? screenshotBase64 = null;
 
-            if (section.HasKey(screenshotKey) && !string.IsNullOrEmpty(section[screenshotKey].Value))
-                screenshotBase64 = section[screenshotKey].Value;
+            Plugin.Instance.Logger.LogError($"Could not convert {Path.GetFileName(filePath)} scene because {e}");
 
-            MPSSceneSerializer.SaveToFile(filePath, sceneMetadata, convertedData, screenshotBase64);
+            return;
         }
 
-        private static string GenerateFilename(string iniFilePath, IniKey sceneKey)
-        {
-            var background = int.Parse(sceneKey.Key.Substring(1)) >= 10000;
+        var screenshotKey = $"s{key.Key}"; // ex. ss100=thumb_base64
+        string? screenshotBase64 = null;
 
-            var iniFilename = Path.GetFileNameWithoutExtension(iniFilePath);
+        if (section.HasKey(screenshotKey) && !string.IsNullOrEmpty(section[screenshotKey].Value))
+            screenshotBase64 = section[screenshotKey].Value;
 
-            var sceneName = sceneKey.Key;
+        MPSSceneSerializer.SaveToFile(filePath, sceneMetadata, convertedData, screenshotBase64);
+    }
 
-            var data = sceneKey.Value;
-            var date = DateTime.Parse(data.Substring(0, data.IndexOf(',')));
+    private static string GenerateFilename(string iniFilePath, IniKey sceneKey)
+    {
+        var background = int.Parse(sceneKey.Key.Substring(1)) >= 10000;
+        var iniFilename = Path.GetFileNameWithoutExtension(iniFilePath);
+        var sceneName = sceneKey.Key;
+        var data = sceneKey.Value;
+        var date = DateTime.Parse(data.Substring(0, data.IndexOf(',')));
+        var sceneDate = MPSSceneSerializer.FormatDate(date);
+
+        return $"mm{(background ? "kankyo" : "scene")}_{iniFilename}_{sceneName}_{sceneDate}.png";
+    }
 
-            var sceneDate = MPSSceneSerializer.FormatDate(date);
+    private static IniSection? GetSceneSection(string filePath)
+    {
+        IniFile iniFile;
 
-            return $"mm{(background ? "kankyo" : "scene")}_{iniFilename}_{sceneName}_{sceneDate}.png";
+        try
+        {
+            iniFile = IniFile.FromFile(filePath);
         }
-
-        private static IniSection? GetSceneSection(string filePath)
+        catch (Exception e)
         {
-            IniFile iniFile;
-
-            try
-            {
-                iniFile = IniFile.FromFile(filePath);
-            }
-            catch (Exception e)
-            {
-                if (Plugin.Instance != null)
-                    Plugin.Instance.Logger.LogWarning(
-                        $"Could not {(e is IOException ? "read" : "parse")} ini file {filePath}"
-                    );
-
-                return null;
-            }
-
-            if (iniFile.HasSection("scene"))
-                return iniFile.GetSection("scene");
-
-            if (Plugin.Instance != null)
+            if (Plugin.Instance && Plugin.Instance!.Logger is not null)
                 Plugin.Instance.Logger.LogWarning(
-                    $"{filePath} is not a valid MM config because '[scene]' section is missing"
-                );
+                    $"Could not {(e is IOException ? "read" : "parse")} ini file {filePath}");
 
             return null;
         }
+
+        if (iniFile.HasSection("scene"))
+            return iniFile.GetSection("scene");
+
+        if (Plugin.Instance && Plugin.Instance!.Logger is not null)
+            Plugin.Instance.Logger.LogWarning(
+                $"{filePath} is not a valid MM config because '[scene]' section is missing");
+
+        return null;
     }
 }

+ 73 - 68
src/MeidoPhotoStudio.Converter/Converters/MMPngConverter.cs

@@ -1,101 +1,106 @@
-using System;
+using System;
 using System.IO;
 using System.Text;
+
 using MeidoPhotoStudio.Converter.MultipleMaids;
 using MeidoPhotoStudio.Converter.Utility;
 
-namespace MeidoPhotoStudio.Converter.Converters
+namespace MeidoPhotoStudio.Converter.Converters;
+
+public class MMPngConverter : IConverter
 {
-    public class MMPngConverter : IConverter
-    {
-        private static readonly byte[] KankyoHeader = Encoding.ASCII.GetBytes("KANKYO");
-        private const string InputDirectoryName = "Input";
-        public const string ConverterName = "ModifiedMM PNG";
+    public const string ConverterName = "ModifiedMM PNG";
+    private const string InputDirectoryName = "Input";
 
-        public void Convert(string workingDirectory)
-        {
-            var baseDirectory = Path.Combine(workingDirectory, ConverterName);
-            var baseInputDirectory = Path.Combine(baseDirectory, InputDirectoryName);
-            var baseOutputDirectory = Path.Combine(baseDirectory, MPSSceneSerializer.FormatDate(DateTime.Now));
+    private static readonly byte[] KankyoHeader = Encoding.ASCII.GetBytes("KANKYO");
 
-            Convert(baseInputDirectory, baseOutputDirectory);
-        }
+    public void Convert(string workingDirectory)
+    {
+        var baseDirectory = Path.Combine(workingDirectory, ConverterName);
+        var baseInputDirectory = Path.Combine(baseDirectory, InputDirectoryName);
+        var baseOutputDirectory = Path.Combine(baseDirectory, MPSSceneSerializer.FormatDate(DateTime.Now));
 
-        private static void Convert(string workingDirectory, string destination)
-        {
-            var directory = new DirectoryInfo(workingDirectory);
+        Convert(baseInputDirectory, baseOutputDirectory);
+    }
 
-            if (!directory.Exists)
-                return;
+    private static void Convert(string workingDirectory, string destination)
+    {
+        var directory = new DirectoryInfo(workingDirectory);
 
-            Directory.CreateDirectory(destination);
+        if (!directory.Exists)
+            return;
 
-            foreach (var file in directory.GetFiles("*.png"))
-                ConvertScene(file.FullName, Path.Combine(destination, file.Name));
+        Directory.CreateDirectory(destination);
 
-            foreach (var subDirectory in directory.GetDirectories())
-            {
-                var subDestination = Path.Combine(destination, subDirectory.Name);
-                Convert(subDirectory.FullName, subDestination);
-            }
-        }
+        foreach (var file in directory.GetFiles("*.png"))
+            ConvertScene(file.FullName, Path.Combine(destination, file.Name));
 
-        private static void ConvertScene(string pngFile, string outputFilename)
+        foreach (var subDirectory in directory.GetDirectories())
         {
-            var fileStream = File.OpenRead(pngFile);
-
-            var thumbnailData = PngUtility.ExtractPng(fileStream) ?? MPSSceneSerializer.NoThumb;
+            var subDestination = Path.Combine(destination, subDirectory.Name);
 
-            var kankyo = new byte[KankyoHeader.Length];
-            fileStream.Read(kankyo, 0, KankyoHeader.Length);
+            Convert(subDirectory.FullName, subDestination);
+        }
+    }
 
-            var background = false;
+    private static void ConvertScene(string pngFile, string outputFilename)
+    {
+        var fileStream = File.OpenRead(pngFile);
+        var thumbnailData = PngUtility.ExtractPng(fileStream) ?? MPSSceneSerializer.NoThumb;
+        var kankyo = new byte[KankyoHeader.Length];
+        fileStream.Read(kankyo, 0, KankyoHeader.Length);
 
-            // ModifiedMM habeebweeb fork scene data uses 'KANKYO' as a header to identify saved environments.
-            // Regular scenes will lack a 'KANKYO' header so the filestream position has to be pulled back. 
-            if (MeidoPhotoStudio.Plugin.Utility.BytesEqual(kankyo, KankyoHeader))
-                background = true;
-            else
-                fileStream.Position -= KankyoHeader.Length;
+        var background = false;
 
-            string sceneData;
+        // ModifiedMM habeebweeb fork scene data uses 'KANKYO' as a header to identify saved environments.
+        // Regular scenes will lack a 'KANKYO' header so the filestream position has to be pulled back.
+        if (MeidoPhotoStudio.Plugin.Utility.BytesEqual(kankyo, KankyoHeader))
+            background = true;
+        else
+            fileStream.Position -= KankyoHeader.Length;
 
-            try
-            {
-                using var sceneStream = LZMA.Decompress(fileStream);
-                sceneData = Encoding.Unicode.GetString(sceneStream.ToArray());
-            }
-            catch (Exception e)
-            {
-                if (Plugin.Instance == null)
-                    return;
+        string sceneData;
 
-                Plugin.Instance.Logger.LogWarning($"Could not decompress scene data from {pngFile} because {e}");
+        try
+        {
+            using var sceneStream = LZMA.Decompress(fileStream);
 
+            sceneData = Encoding.Unicode.GetString(sceneStream.ToArray());
+        }
+        catch (Exception e)
+        {
+            if (!Plugin.Instance)
                 return;
-            }
 
-            if (string.IsNullOrEmpty(sceneData))
+            if (Plugin.Instance!.Logger is null)
                 return;
 
-            byte[] convertedData;
-            MeidoPhotoStudio.Plugin.SceneMetadata sceneMetadata;
+            Plugin.Instance.Logger.LogWarning($"Could not decompress scene data from {pngFile} because {e}");
+
+            return;
+        }
 
-            try
-            {
-                convertedData = MMSceneConverter.Convert(sceneData, background);
-                sceneMetadata = MMSceneConverter.GetSceneMetadata(sceneData, background);
-            }
-            catch (Exception e)
-            {
-                if (Plugin.Instance == null)
-                    return;
+        if (string.IsNullOrEmpty(sceneData))
+            return;
 
-                Plugin.Instance.Logger.LogError($"Could not convert {pngFile} because {e}");
+        byte[] convertedData;
+        MeidoPhotoStudio.Plugin.SceneMetadata sceneMetadata;
+
+        try
+        {
+            convertedData = MMSceneConverter.Convert(sceneData, background);
+            sceneMetadata = MMSceneConverter.GetSceneMetadata(sceneData, background);
+        }
+        catch (Exception e)
+        {
+            if (!Plugin.Instance || Plugin.Instance!.Logger is null)
                 return;
-            }
 
-            MPSSceneSerializer.SaveToFile(outputFilename, sceneMetadata, convertedData, thumbnailData);
+            Plugin.Instance.Logger.LogError($"Could not convert {pngFile} because {e}");
+
+            return;
         }
+
+        MPSSceneSerializer.SaveToFile(outputFilename, sceneMetadata, convertedData, thumbnailData);
     }
 }

+ 36 - 33
src/MeidoPhotoStudio.Converter/MPSSceneSerializer.cs

@@ -1,53 +1,56 @@
-using System;
+using System;
 using System.IO;
 using System.Text;
-using MeidoPhotoStudio.Plugin;
+
 using Ionic.Zlib;
+using MeidoPhotoStudio.Plugin;
 
-namespace MeidoPhotoStudio.Converter
+namespace MeidoPhotoStudio.Converter;
+
+public static class MPSSceneSerializer
 {
-    public static class MPSSceneSerializer
-    {
-        private const string NoThumbBase64 =
-            "iVBORw0KGgoAAAANSUhEUgAAADIAAAAyCAIAAACRXR/mAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7D"
-            + "AcdvqGQAAAFOSURBVFhH3dJbjoMwEETRLIRP9r+zrCGpqJABY+x+2Ua5ys9EcteJNK/3sj7ws7E+j2ln8Q9+O7eE2Vjpq4kdJTsLTZRl"
-            + "jBMLTZFdDTkLDZYVAQUWGia7Wy+z0ABZZfqWhbrK6rs1Fuoka442WChcJllss1CgTDgnYqEQmXxLykJOmWpIwUJmmXZFx0IGmWFCzUKq"
-            + "J7b7FhYSvjIfN7JQ86Hnsp2FKm+dZ10sVHzuv+lloexCyMEAFkpHoq7FsBDuBJ76a1Y6EnXtT//li8/9N12sylvnWTur+dBz2cgSvjIf"
-            + "t7BUT2z31azePwOpWQYT064oWGYTUw1JWU4Tk2+JWCEmJpxrswJNTLLYYIWbWHO0xupkYvXdW1ZXE6tMl1kDTOxuvcAaZmJFQM4abGJX"
-            + "w4k1xcQyxs6aaGJHycaabmIJ82M9xMTo2VjP+izrF8NPHwq3SYqeAAAAAElFTkSuQmCC";
+    private const string NoThumbBase64 =
+        "iVBORw0KGgoAAAANSUhEUgAAADIAAAAyCAIAAACRXR/mAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7D"
+        + "AcdvqGQAAAFOSURBVFhH3dJbjoMwEETRLIRP9r+zrCGpqJABY+x+2Ua5ys9EcteJNK/3sj7ws7E+j2ln8Q9+O7eE2Vjpq4kdJTsLTZRl"
+        + "jBMLTZFdDTkLDZYVAQUWGia7Wy+z0ABZZfqWhbrK6rs1Fuoka442WChcJllss1CgTDgnYqEQmXxLykJOmWpIwUJmmXZFx0IGmWFCzUKq"
+        + "J7b7FhYSvjIfN7JQ86Hnsp2FKm+dZ10sVHzuv+lloexCyMEAFkpHoq7FsBDuBJ76a1Y6EnXtT//li8/9N12sylvnWTur+dBz2cgSvjIf"
+        + "t7BUT2z31azePwOpWQYT064oWGYTUw1JWU4Tk2+JWCEmJpxrswJNTLLYYIWbWHO0xupkYvXdW1ZXE6tMl1kDTOxuvcAaZmJFQM4abGJX"
+        + "w4k1xcQyxs6aaGJHycaabmIJ82M9xMTo2VjP+izrF8NPHwq3SYqeAAAAAElFTkSuQmCC";
 
-        private static byte[]? noThumb;
-        public static byte[] NoThumb => noThumb ??= Convert.FromBase64String(NoThumbBase64);
+    private static byte[]? noThumb;
 
-        public static void SaveToFile(string filename, SceneMetadata metadata, byte[] rawSceneData, string? thumbnail)
-        {
-            var rawThumbnail = string.IsNullOrEmpty(thumbnail) ? NoThumb : Convert.FromBase64String(thumbnail);
+    public static byte[] NoThumb =>
+        noThumb ??= Convert.FromBase64String(NoThumbBase64);
 
-            SaveToFile(filename, metadata, rawSceneData, rawThumbnail);
-        }
+    public static void SaveToFile(string filename, SceneMetadata metadata, byte[] rawSceneData, string? thumbnail)
+    {
+        var rawThumbnail = string.IsNullOrEmpty(thumbnail) ? NoThumb : Convert.FromBase64String(thumbnail);
 
-        public static void SaveToFile(string filename, SceneMetadata metadata, byte[] rawSceneData, byte[] thumbnail)
-        {
-            if (!string.Equals(Path.GetExtension(filename), ".png", StringComparison.OrdinalIgnoreCase))
-                filename += ".png";
+        SaveToFile(filename, metadata, rawSceneData, rawThumbnail);
+    }
 
-            using var fileStream = File.Create(filename);
+    public static void SaveToFile(string filename, SceneMetadata metadata, byte[] rawSceneData, byte[] thumbnail)
+    {
+        if (!string.Equals(Path.GetExtension(filename), ".png", StringComparison.OrdinalIgnoreCase))
+            filename += ".png";
 
-            fileStream.Write(thumbnail, 0, thumbnail.Length);
+        using var fileStream = File.Create(filename);
 
-            using var headerWriter = new BinaryWriter(fileStream, Encoding.UTF8);
+        fileStream.Write(thumbnail, 0, thumbnail.Length);
 
-            headerWriter.Write(MeidoPhotoStudio.Plugin.MeidoPhotoStudio.SceneHeader);
+        using var headerWriter = new BinaryWriter(fileStream, Encoding.UTF8);
 
-            metadata.WriteMetadata(headerWriter);
+        headerWriter.Write(MeidoPhotoStudio.Plugin.MeidoPhotoStudio.SceneHeader);
 
-            using var compressionStream = new DeflateStream(fileStream, CompressionMode.Compress);
+        metadata.WriteMetadata(headerWriter);
 
-            compressionStream.Write(rawSceneData, 0, rawSceneData.Length);
+        using var compressionStream = new DeflateStream(fileStream, CompressionMode.Compress);
 
-            compressionStream.Close();
-        }
+        compressionStream.Write(rawSceneData, 0, rawSceneData.Length);
 
-        public static string FormatDate(DateTime date) => date.ToString("yyyyMMddHHmmss");
+        compressionStream.Close();
     }
+
+    public static string FormatDate(DateTime date) =>
+        date.ToString("yyyyMMddHHmmss");
 }

+ 34 - 27
src/MeidoPhotoStudio.Converter/MultipleMaids/ConversionUtility.cs

@@ -1,40 +1,47 @@
-using UnityEngine;
+using UnityEngine;
 
-namespace MeidoPhotoStudio.Converter.MultipleMaids
+namespace MeidoPhotoStudio.Converter.MultipleMaids;
+
+internal static class ConversionUtility
 {
-    internal static class ConversionUtility
+    public static Quaternion ParseEulerAngle(string euler)
     {
-        public static Quaternion ParseEulerAngle(string euler)
-        {
-            var data = euler.Split(',');
+        var data = euler.Split(',');
 
-            return Quaternion.Euler(float.Parse(data[0]), float.Parse(data[1]), float.Parse(data[2]));
-        }
+        return Quaternion.Euler(float.Parse(data[0]), float.Parse(data[1]), float.Parse(data[2]));
+    }
 
-        public static Vector3 ParseVector3(string vector3)
-        {
-            var data = vector3.Split(',');
-            return new(float.Parse(data[0]), float.Parse(data[1]), float.Parse(data[2]));
-        }
+    public static Vector3 ParseVector3(string vector3)
+    {
+        var data = vector3.Split(',');
 
-        /// <summary>
-        /// Checks if the string has 3 euler angle components delimited by commas before parsing
-        /// </summary>
-        /// <param name="euler">Euler angle string in the form "x,y,z"</param>
-        /// <param name="result">Resulting angle as a <c>Quaternion</c></param>
-        /// <returns>Whether or not the euler string can be safely parsed</returns>
-        public static bool TryParseEulerAngle(string euler, out Quaternion result)
-        {
-            result = Quaternion.identity;
+        return new(float.Parse(data[0]), float.Parse(data[1]), float.Parse(data[2]));
+    }
 
-            var data = euler.Split(',');
+    /// <summary>
+    /// Checks if the string has 3 euler angle components delimited by commas before parsing.
+    /// </summary>
+    /// <param name="euler">Euler angle string in the form "x,y,z".</param>
+    /// <param name="result">Resulting angle as a <c>Quaternion</c>.</param>
+    /// <returns>Whether or not the euler string can be safely parsed.</returns>
+    public static bool TryParseEulerAngle(string euler, out Quaternion result)
+    {
+        result = Quaternion.identity;
 
-            if (data.Length != 3) return false;
+        var data = euler.Split(',');
 
-            try { result = Quaternion.Euler(float.Parse(data[0]), float.Parse(data[1]), float.Parse(data[2])); }
-            catch { return false; }
+        if (data.Length is not 3)
+            return false;
 
-            return true;
+        try
+        {
+            result = Quaternion.Euler(float.Parse(data[0]), float.Parse(data[1]), float.Parse(data[2]));
         }
+        catch
+        {
+            return false;
+        }
+
+        return true;
     }
 }

+ 29 - 29
src/MeidoPhotoStudio.Converter/MultipleMaids/MMConstants.cs

@@ -1,39 +1,39 @@
-using System;
+using System;
 using System.Collections.Generic;
 using System.Linq;
+
 using MyRoomCustom;
 using UnityEngine;
 
-namespace MeidoPhotoStudio.Converter.MultipleMaids
+namespace MeidoPhotoStudio.Converter.MultipleMaids;
+
+public static class MMConstants
 {
-    public static class MMConstants
+    public static readonly Vector3 DefaultSoftG = new(0f, -3f / 1000f, 0f);
+
+    public static readonly string[] FaceKeys =
+    {
+        "eyeclose", "eyeclose2", "eyeclose3", "eyeclose6", "hitomih", "hitomis", "mayuha", "mayuup", "mayuv",
+        "mayuvhalf", "moutha", "mouths", "mouthdw", "mouthup", "tangout", "tangup", "eyebig", "eyeclose5", "mayuw",
+        "mouthhe", "mouthc", "mouthi", "mouthuphalf", "tangopen", "namida", "tear1", "tear2", "tear3", "shock",
+        "yodare", "hoho", "hoho2", "hohos", "hohol", "toothoff", "nosefook",
+    };
+
+    public static readonly string[] MpnAttachProps =
     {
-        public static readonly string[] FaceKeys =
-        {
-            "eyeclose", "eyeclose2", "eyeclose3", "eyeclose6", "hitomih", "hitomis", "mayuha",
-            "mayuup", "mayuv", "mayuvhalf", "moutha", "mouths", "mouthdw", "mouthup", "tangout",
-            "tangup", "eyebig", "eyeclose5", "mayuw", "mouthhe", "mouthc", "mouthi", "mouthuphalf",
-            "tangopen",
-            "namida", "tear1", "tear2", "tear3", "shock", "yodare", "hoho", "hoho2", "hohos", "hohol",
-            "toothoff", "nosefook",
-        };
+        // NOTE: MPS only allows a subset of attached MPN props because MPS has a better method of attaching props.
+        // "", "", "", "", "", "", "", "", "",
+        "kousokuu_tekaseone_i_.menu", "kousokuu_tekasetwo_i_.menu", "kousokul_ashikaseup_i_.menu",
+        "kousokuu_tekasetwo_i_.menu", "kousokul_ashikasedown_i_.menu", "kousokuu_tekasetwodown_i_.menu",
+        "kousokuu_ushirode_i_.menu", "kousokuu_smroom_haritsuke_i_.menu",
+    };
 
-        public static readonly string[] MpnAttachProps =
-        {
-            /* "", "", "", "", "", "", "", "", "", */
-            "kousokuu_tekaseone_i_.menu", "kousokuu_tekasetwo_i_.menu", "kousokul_ashikaseup_i_.menu",
-            "kousokuu_tekasetwo_i_.menu", "kousokul_ashikasedown_i_.menu", "kousokuu_tekasetwodown_i_.menu",
-            "kousokuu_ushirode_i_.menu", "kousokuu_smroom_haritsuke_i_.menu",
-        };
+    private static Dictionary<string, PlacementData.Data>? myrAssetNameToData;
 
-        private static Dictionary<string, PlacementData.Data>? myrAssetNameToData;
-        public static Dictionary<string, PlacementData.Data> MyrAssetNameToData =>
-            myrAssetNameToData ??= PlacementData.GetAllDatas(false)
-                .ToDictionary(
-                    data => string.IsNullOrEmpty(data.assetName) ? data.resourceName : data.assetName,
-                    data => data,
-                    StringComparer.InvariantCultureIgnoreCase
-                );
-        public static readonly Vector3 DefaultSoftG = new(0f, -3f / 1000f, 0f);
-    }
+    public static Dictionary<string, PlacementData.Data> MyrAssetNameToData =>
+        myrAssetNameToData ??= PlacementData.GetAllDatas(false)
+            .ToDictionary(
+                data => string.IsNullOrEmpty(data.assetName) ? data.resourceName : data.assetName,
+                data => data,
+                StringComparer.InvariantCultureIgnoreCase);
 }

+ 10 - 10
src/MeidoPhotoStudio.Converter/MultipleMaids/MMScene.cs

@@ -1,14 +1,14 @@
-namespace MeidoPhotoStudio.Converter.MultipleMaids
+namespace MeidoPhotoStudio.Converter.MultipleMaids;
+
+// TODO: This class is unused so delete it.
+public class MMScene
 {
-    public class MMScene
-    {
-        public readonly string Data;
-        public readonly string? ScreenshotBase64;
+    public readonly string Data;
+    public readonly string? ScreenshotBase64;
 
-        public MMScene(string data, string? screenshotBase64)
-        {
-            Data = data;
-            ScreenshotBase64 = screenshotBase64;
-        }
+    public MMScene(string data, string? screenshotBase64)
+    {
+        Data = data;
+        ScreenshotBase64 = screenshotBase64;
     }
 }

File diff suppressed because it is too large
+ 646 - 630
src/MeidoPhotoStudio.Converter/MultipleMaids/MMSceneConverter.cs


+ 30 - 29
src/MeidoPhotoStudio.Converter/Plugin.cs

@@ -1,46 +1,47 @@
-using System.IO;
+using System.IO;
+
 using BepInEx;
 using BepInEx.Logging;
 using MeidoPhotoStudio.Converter.Converters;
-using UnityEngine;
 using UnityEngine.SceneManagement;
 
-namespace MeidoPhotoStudio.Converter
+namespace MeidoPhotoStudio.Converter;
+
+[BepInPlugin(PluginGuid, PluginName, PluginVersion)]
+[BepInDependency("com.habeebweeb.com3d2.meidophotostudio")]
+public class Plugin : BaseUnityPlugin
 {
-    [BepInPlugin(PluginGuid, PluginName, PluginVersion)]
-    [BepInDependency("com.habeebweeb.com3d2.meidophotostudio")]
-    public class Plugin : BaseUnityPlugin
-    {
-        private const string PluginGuid = "com.habeebweeb.com3d2.meidophotostudio.converter";
-        public const string PluginName = "MeidoPhotoStudio Converter";
-        public const string PluginVersion = "0.0.1";
+    public const string PluginName = "MeidoPhotoStudio Converter";
+    public const string PluginVersion = "0.0.1";
 
-        private PluginCore pluginCore;
-        private UI ui;
+    private const string PluginGuid = "com.habeebweeb.com3d2.meidophotostudio.converter";
 
-        public static Plugin? Instance { get; private set; }
-        public new ManualLogSource Logger { get; private set; }
+    private PluginCore? pluginCore;
+    private UI? ui;
 
-        private void Awake()
-        {
-            DontDestroyOnLoad(this);
+    public static Plugin? Instance { get; private set; }
 
-            Instance = this;
-            Logger = base.Logger;
+    public new ManualLogSource? Logger { get; private set; }
 
-            var workingDirectory = Path.Combine(Paths.ConfigPath, PluginName);
+    private void Awake()
+    {
+        DontDestroyOnLoad(this);
+
+        Instance = this;
+        Logger = base.Logger;
 
-            if (!Directory.Exists(workingDirectory))
-                Directory.CreateDirectory(workingDirectory);
+        var workingDirectory = Path.Combine(Paths.ConfigPath, PluginName);
 
-            pluginCore = new(workingDirectory, new MMConverter(), new MMPngConverter());
-            ui = new(pluginCore);
+        if (!Directory.Exists(workingDirectory))
+            Directory.CreateDirectory(workingDirectory);
 
-            SceneManager.sceneLoaded += (scene, _) =>
-                ui.Visible = scene.buildIndex is 3 or 9;
-        }
+        pluginCore = new(workingDirectory, new MMConverter(), new MMPngConverter());
+        ui = new(pluginCore);
 
-        private void OnGUI() =>
-            ui.Draw();
+        SceneManager.sceneLoaded += (scene, _) =>
+            ui.Visible = scene.buildIndex is 3 or 9;
     }
+
+    private void OnGUI() =>
+        ui!.Draw();
 }

+ 27 - 25
src/MeidoPhotoStudio.Converter/PluginCore.cs

@@ -1,37 +1,39 @@
-using System;
+using System;
 using System.IO;
+
 using MeidoPhotoStudio.Converter.Converters;
 
-namespace MeidoPhotoStudio.Converter
+namespace MeidoPhotoStudio.Converter;
+
+public class PluginCore
 {
-    public class PluginCore
+    private readonly IConverter[] converters;
+
+    public PluginCore(string workingDirectory, params IConverter[] converters)
     {
-        private readonly IConverter[] converters;
-        public string WorkingDirectory { get; set; }
+        WorkingDirectory = workingDirectory;
 
-        public PluginCore(string workingDirectory, params IConverter[] converters)
-        {
-            WorkingDirectory = workingDirectory;
-            this.converters = converters;
-        }
+        this.converters = converters;
+    }
 
-        public void Convert()
-        {
-            Directory.CreateDirectory(WorkingDirectory);
+    public string WorkingDirectory { get; set; }
 
-            foreach (var converter in converters)
+    public void Convert()
+    {
+        Directory.CreateDirectory(WorkingDirectory);
+
+        foreach (var converter in converters)
+        {
+            try
             {
-                try
-                {
-                    converter.Convert(WorkingDirectory);
-                }
-                catch (Exception e)
-                {
-                    if (Plugin.Instance == null)
-                        continue;
-
-                    Plugin.Instance.Logger.LogError($"Could not convert data because {e}");
-                }
+                converter.Convert(WorkingDirectory);
+            }
+            catch (Exception e)
+            {
+                if (!Plugin.Instance)
+                    continue;
+
+                Plugin.Instance!.Logger!.LogError($"Could not convert data because {e}");
             }
         }
     }

+ 28 - 28
src/MeidoPhotoStudio.Converter/UI.cs

@@ -1,40 +1,40 @@
-using UnityEngine;
+using UnityEngine;
 
-namespace MeidoPhotoStudio.Converter
+namespace MeidoPhotoStudio.Converter;
+
+public class UI
 {
-    public class UI
-    {
-        private const int WindowID = 0xEA4040;
-        private const string WindowTitle = Plugin.PluginName + " " + Plugin.PluginVersion;
-        private Rect windowRect;
+    public bool Visible;
 
-        private PluginCore core;
+    private const int WindowID = 0xEA4040;
+    private const string WindowTitle = Plugin.PluginName + " " + Plugin.PluginVersion;
 
-        public bool Visible;
+    private readonly PluginCore core;
 
-        public UI(PluginCore pluginCore) =>
-            core = pluginCore;
+    private Rect windowRect;
 
-        public void Draw()
-        {
-            if (!Visible)
-                return;
+    public UI(PluginCore pluginCore) =>
+        core = pluginCore;
 
-            windowRect.width = 230f;
-            windowRect.height = 100f;
-            windowRect.x = Mathf.Clamp(windowRect.x, 0, Screen.width - windowRect.width);
-            windowRect.y = Mathf.Clamp(windowRect.y, 0, Screen.height - windowRect.height);
-            windowRect = GUI.Window(WindowID, windowRect, GUIFunc, WindowTitle);
-        }
+    public void Draw()
+    {
+        if (!Visible)
+            return;
+
+        windowRect.width = 230f;
+        windowRect.height = 100f;
+        windowRect.x = Mathf.Clamp(windowRect.x, 0, Screen.width - windowRect.width);
+        windowRect.y = Mathf.Clamp(windowRect.y, 0, Screen.height - windowRect.height);
+        windowRect = GUI.Window(WindowID, windowRect, GUIFunc, WindowTitle);
+    }
 
-        private void GUIFunc(int windowId)
-        {
-            GUILayout.FlexibleSpace();
+    private void GUIFunc(int windowId)
+    {
+        GUILayout.FlexibleSpace();
 
-            if (GUILayout.Button("Convert"))
-                core.Convert();
+        if (GUILayout.Button("Convert"))
+            core.Convert();
 
-            GUI.DragWindow();
-        }
+        GUI.DragWindow();
     }
 }

+ 23 - 24
src/MeidoPhotoStudio.Converter/Utility/LZMA.cs

@@ -1,40 +1,39 @@
-using System.IO;
+using System.IO;
+
 using SevenZip.Compression.LZMA;
 
-namespace MeidoPhotoStudio.Converter.Utility
+namespace MeidoPhotoStudio.Converter.Utility;
+
+internal static class LZMA
 {
-    internal static class LZMA
+    public static MemoryStream Decompress(Stream inStream)
     {
-        public static MemoryStream Decompress(Stream inStream)
-        {
-            var outStream = new MemoryStream();
-
-            var properties = new byte[5];
+        var outStream = new MemoryStream();
+        var properties = new byte[5];
 
-            if (inStream.Read(properties, 0, 5) != 5)
-                throw new("input .lzma is too short");
+        if (inStream.Read(properties, 0, 5) is not 5)
+            throw new("input .lzma is too short");
 
-            var decoder = new Decoder();
+        var decoder = new Decoder();
 
-            decoder.SetDecoderProperties(properties);
+        decoder.SetDecoderProperties(properties);
 
-            var outSize = 0L;
+        var outSize = 0L;
 
-            for (var i = 0; i < 8; i++)
-            {
-                var v = inStream.ReadByte();
+        for (var i = 0; i < 8; i++)
+        {
+            var v = inStream.ReadByte();
 
-                if (v < 0)
-                    throw new("Can't Read 1");
+            if (v < 0)
+                throw new("Can't Read 1");
 
-                outSize |= ((long)(byte)v) << (8 * i);
-            }
+            outSize |= ((long)(byte)v) << (8 * i);
+        }
 
-            var compressedSize = inStream.Length - inStream.Position;
+        var compressedSize = inStream.Length - inStream.Position;
 
-            decoder.Code(inStream, outStream, compressedSize, outSize, null);
+        decoder.Code(inStream, outStream, compressedSize, outSize, null);
 
-            return outStream;
-        }
+        return outStream;
     }
 }

+ 41 - 41
src/MeidoPhotoStudio.Converter/Utility/PngUtility.cs

@@ -1,60 +1,60 @@
-using System;
+using System;
 using System.IO;
 
-namespace MeidoPhotoStudio.Converter.Utility
-{
-    internal static class PngUtility
-    {
-        private static readonly byte[] PngHeader = { 137, 80, 78, 71, 13, 10, 26, 10 };
-        private static readonly byte[] PngEnd = System.Text.Encoding.ASCII.GetBytes("IEND");
+namespace MeidoPhotoStudio.Converter.Utility;
 
-        public static byte[]? ExtractPng(Stream stream)
-        {
-            var memoryStream = new MemoryStream();
+internal static class PngUtility
+{
+    private static readonly byte[] PngHeader = { 137, 80, 78, 71, 13, 10, 26, 10 };
+    private static readonly byte[] PngEnd = System.Text.Encoding.ASCII.GetBytes("IEND");
 
-            var headerBuffer = new byte[PngHeader.Length];
+    public static byte[]? ExtractPng(Stream stream)
+    {
+        var memoryStream = new MemoryStream();
+        var headerBuffer = new byte[PngHeader.Length];
 
-            stream.Read(headerBuffer, 0, headerBuffer.Length);
+        stream.Read(headerBuffer, 0, headerBuffer.Length);
 
-            if (!MeidoPhotoStudio.Plugin.Utility.BytesEqual(headerBuffer, PngHeader))
-                return null;
+        if (!MeidoPhotoStudio.Plugin.Utility.BytesEqual(headerBuffer, PngHeader))
+            return null;
 
-            memoryStream.Write(headerBuffer, 0, headerBuffer.Length);
+        memoryStream.Write(headerBuffer, 0, headerBuffer.Length);
 
-            var fourByteBuffer = new byte[4];
-            var chunkBuffer = new byte[1024];
+        var fourByteBuffer = new byte[4];
+        var chunkBuffer = new byte[1024];
 
-            try
+        try
+        {
+            do
             {
-                do
-                {
-                    // chunk length
-                    var read = stream.Read(fourByteBuffer, 0, 4);
-                    memoryStream.Write(fourByteBuffer, 0, read);
+                // chunk length
+                var read = stream.Read(fourByteBuffer, 0, 4);
 
-                    if (BitConverter.IsLittleEndian)
-                        Array.Reverse(fourByteBuffer);
+                memoryStream.Write(fourByteBuffer, 0, read);
 
-                    var length = BitConverter.ToUInt32(fourByteBuffer, 0);
+                if (BitConverter.IsLittleEndian)
+                    Array.Reverse(fourByteBuffer);
 
-                    // chunk type
-                    read = stream.Read(fourByteBuffer, 0, 4);
-                    memoryStream.Write(fourByteBuffer, 0, read);
+                var length = BitConverter.ToUInt32(fourByteBuffer, 0);
 
-                    if (chunkBuffer.Length < length + 4L)
-                        chunkBuffer = new byte[length + 4L];
+                // chunk type
+                read = stream.Read(fourByteBuffer, 0, 4);
+                memoryStream.Write(fourByteBuffer, 0, read);
 
-                    // chunk data + CRC
-                    read = stream.Read(chunkBuffer, 0, (int)(length + 4L));
-                    memoryStream.Write(chunkBuffer, 0, read);
-                } while (!MeidoPhotoStudio.Plugin.Utility.BytesEqual(fourByteBuffer, PngEnd));
-            }
-            catch
-            {
-                return null;
-            }
+                if (chunkBuffer.Length < length + 4L)
+                    chunkBuffer = new byte[length + 4L];
 
-            return memoryStream.ToArray();
+                // chunk data + CRC
+                read = stream.Read(chunkBuffer, 0, (int)(length + 4L));
+                memoryStream.Write(chunkBuffer, 0, read);
+            }
+            while (!MeidoPhotoStudio.Plugin.Utility.BytesEqual(fourByteBuffer, PngEnd));
         }
+        catch
+        {
+            return null;
+        }
+
+        return memoryStream.ToArray();
     }
 }

+ 92 - 0
src/MeidoPhotoStudio.Plugin/BinaryExtensions.cs

@@ -0,0 +1,92 @@
+using System.IO;
+
+using UnityEngine;
+
+namespace MeidoPhotoStudio.Plugin;
+
+public static class BinaryExtensions
+{
+    public static string ReadNullableString(this BinaryReader binaryReader) =>
+        binaryReader.ReadBoolean()
+            ? binaryReader.ReadString()
+            : null;
+
+    public static void WriteNullableString(this BinaryWriter binaryWriter, string str)
+    {
+        binaryWriter.Write(str is not null);
+
+        if (str is not null)
+            binaryWriter.Write(str);
+    }
+
+    public static void Write(this BinaryWriter binaryWriter, Vector3 vector3)
+    {
+        binaryWriter.Write(vector3.x);
+        binaryWriter.Write(vector3.y);
+        binaryWriter.Write(vector3.z);
+    }
+
+    public static void WriteVector3(this BinaryWriter binaryWriter, Vector3 vector3)
+    {
+        binaryWriter.Write(vector3.x);
+        binaryWriter.Write(vector3.y);
+        binaryWriter.Write(vector3.z);
+    }
+
+    public static Vector2 ReadVector2(this BinaryReader binaryReader) =>
+        new(binaryReader.ReadSingle(), binaryReader.ReadSingle());
+
+    public static Vector3 ReadVector3(this BinaryReader binaryReader) =>
+        new(binaryReader.ReadSingle(), binaryReader.ReadSingle(), binaryReader.ReadSingle());
+
+    public static Vector4 ReadVector4(this BinaryReader binaryReader) =>
+        new(binaryReader.ReadSingle(), binaryReader.ReadSingle(), binaryReader.ReadSingle(), binaryReader.ReadSingle());
+
+    public static void Write(this BinaryWriter binaryWriter, Quaternion quaternion)
+    {
+        binaryWriter.Write(quaternion.x);
+        binaryWriter.Write(quaternion.y);
+        binaryWriter.Write(quaternion.z);
+        binaryWriter.Write(quaternion.w);
+    }
+
+    public static void WriteQuaternion(this BinaryWriter binaryWriter, Quaternion quaternion)
+    {
+        binaryWriter.Write(quaternion.x);
+        binaryWriter.Write(quaternion.y);
+        binaryWriter.Write(quaternion.z);
+        binaryWriter.Write(quaternion.w);
+    }
+
+    public static Quaternion ReadQuaternion(this BinaryReader binaryReader) =>
+        new(binaryReader.ReadSingle(), binaryReader.ReadSingle(), binaryReader.ReadSingle(), binaryReader.ReadSingle());
+
+    public static void Write(this BinaryWriter binaryWriter, Color colour)
+    {
+        binaryWriter.Write(colour.r);
+        binaryWriter.Write(colour.g);
+        binaryWriter.Write(colour.b);
+        binaryWriter.Write(colour.a);
+    }
+
+    public static void WriteColour(this BinaryWriter binaryWriter, Color colour)
+    {
+        binaryWriter.Write(colour.r);
+        binaryWriter.Write(colour.g);
+        binaryWriter.Write(colour.b);
+        binaryWriter.Write(colour.a);
+    }
+
+    public static Color ReadColour(this BinaryReader binaryReader) =>
+        new(binaryReader.ReadSingle(), binaryReader.ReadSingle(), binaryReader.ReadSingle(), binaryReader.ReadSingle());
+
+    public static Matrix4x4 ReadMatrix4x4(this BinaryReader binaryReader)
+    {
+        Matrix4x4 matrix = default;
+
+        for (var i = 0; i < 16; i++)
+            matrix[i] = binaryReader.ReadSingle();
+
+        return matrix;
+    }
+}

+ 42 - 0
src/MeidoPhotoStudio.Plugin/CameraUtility.cs

@@ -0,0 +1,42 @@
+namespace MeidoPhotoStudio.Plugin;
+
+public static class CameraUtility
+{
+    public static CameraMain MainCamera =>
+        GameMain.Instance.MainCamera;
+
+    public static UltimateOrbitCamera UOCamera { get; } =
+        GameMain.Instance.MainCamera.GetComponent<UltimateOrbitCamera>();
+
+    public static void StopSpin()
+    {
+        Utility.SetFieldValue(UOCamera, "xVelocity", 0f);
+        Utility.SetFieldValue(UOCamera, "yVelocity", 0f);
+    }
+
+    public static void StopMovement() =>
+        MainCamera.SetTargetPos(MainCamera.GetTargetPos());
+
+    public static void StopAll()
+    {
+        StopSpin();
+        StopMovement();
+    }
+
+    public static void ForceCalcNearClip(this CameraMain camera)
+    {
+        camera.StopAllCoroutines();
+        camera.m_bCalcNearClip = false;
+        camera.camera.nearClipPlane = 0.01f;
+    }
+
+    public static void ResetCalcNearClip(this CameraMain camera)
+    {
+        if (camera.m_bCalcNearClip)
+            return;
+
+        camera.StopAllCoroutines();
+        camera.m_bCalcNearClip = true;
+        camera.Start();
+    }
+}

+ 8 - 8
src/MeidoPhotoStudio.Plugin/Configuration.cs

@@ -1,15 +1,15 @@
 using BepInEx.Configuration;
 
-namespace MeidoPhotoStudio.Plugin
+namespace MeidoPhotoStudio.Plugin;
+
+public static class Configuration
 {
-    public static class Configuration
+    static Configuration()
     {
-        public static ConfigFile Config { get; }
+        var configPath = System.IO.Path.Combine(Constants.ConfigPath, $"{MeidoPhotoStudio.PluginName}.cfg");
 
-        static Configuration()
-        {
-            string configPath = System.IO.Path.Combine(Constants.configPath, $"{MeidoPhotoStudio.pluginName}.cfg");
-            Config = new ConfigFile(configPath, false);
-        }
+        Config = new(configPath, false);
     }
+
+    public static ConfigFile Config { get; }
 }

File diff suppressed because it is too large
+ 893 - 804
src/MeidoPhotoStudio.Plugin/Constants.cs


+ 169 - 126
src/MeidoPhotoStudio.Plugin/DragPoint/CustomGizmo.cs

@@ -1,154 +1,197 @@
 using System;
 using System.Reflection;
+
 using UnityEngine;
 
-namespace MeidoPhotoStudio.Plugin
+namespace MeidoPhotoStudio.Plugin;
+
+public class CustomGizmo : GizmoRender
 {
-    public class CustomGizmo : GizmoRender
+    public GizmoMode Mode;
+
+    private static readonly Camera Camera = GameMain.Instance.MainCamera.camera;
+
+#pragma warning disable SA1310, SA1311
+
+    // TODO: Refactor reflection to using private members directly.
+    private static new readonly FieldInfo is_drag_ = Utility.GetFieldInfo<GizmoRender>("is_drag_");
+#pragma warning restore SA1310, SA1311
+
+    private new readonly FieldInfo beSelectedType = Utility.GetFieldInfo<GizmoRender>("beSelectedType");
+
+    private GizmoType gizmoType;
+    private Transform target;
+    private bool hasAlternateTarget;
+    private Transform positionTransform;
+    private Vector3 positionOld = Vector3.zero;
+    private Vector3 deltaPosition = Vector3.zero;
+    private Vector3 deltaLocalPosition = Vector3.zero;
+    private Quaternion rotationOld = Quaternion.identity;
+    private Quaternion deltaRotation = Quaternion.identity;
+    private Quaternion deltaLocalRotation = Quaternion.identity;
+    private Vector3 deltaScale = Vector3.zero;
+    private Vector3 scaleOld = Vector3.one;
+    private GizmoType gizmoTypeOld;
+
+    public event EventHandler GizmoDrag;
+
+    public enum GizmoType
     {
-        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_");
-        public static bool IsDrag
-        {
-            get => (bool)is_drag_.GetValue(null);
-            private set => is_drag_.SetValue(null, value);
-        }
-        private Vector3 positionOld = Vector3.zero;
-        private Vector3 deltaPosition = Vector3.zero;
-        private Vector3 deltaLocalPosition = Vector3.zero;
-        private Quaternion rotationOld = Quaternion.identity;
-        private Quaternion deltaRotation = Quaternion.identity;
-        private Quaternion deltaLocalRotation = Quaternion.identity;
-        private Vector3 deltaScale = Vector3.zero;
-        private Vector3 scaleOld = Vector3.one;
-        private GizmoType gizmoTypeOld;
-        private GizmoType gizmoType;
-        public GizmoType CurrentGizmoType
-        {
-            get => gizmoType;
-            set
-            {
-                gizmoType = value;
-                if (gizmoTypeOld == gizmoType) return;
-
-                gizmoTypeOld = gizmoType;
-                eAxis = gizmoType == GizmoType.Move;
-                eScal = gizmoType == GizmoType.Scale;
-                eRotate = gizmoType == GizmoType.Rotate;
-            }
-        }
-        public bool IsGizmoDrag => GizmoVisible && IsDrag && SelectedType != 0;
-        public bool GizmoVisible
-        {
-            get => Visible;
-            set
-            {
-                if (value && IsDrag) IsDrag = false;
-                Visible = value;
-            }
-        }
-        public GizmoMode gizmoMode;
-        public event EventHandler GizmoDrag;
-        public enum GizmoType { Rotate, Move, Scale }
-        public enum GizmoMode { Local, World, Global }
+        Rotate,
+        Move,
+        Scale,
+    }
 
-        public static CustomGizmo Make(Transform target, float scale = 0.25f, GizmoMode mode = GizmoMode.Local)
+    public enum GizmoMode
+    {
+        Local,
+        World,
+        Global,
+    }
+
+    public static bool IsDrag
+    {
+        get => (bool)is_drag_.GetValue(null);
+        private set => is_drag_.SetValue(null, value);
+    }
+
+    public GizmoType CurrentGizmoType
+    {
+        get => gizmoType;
+        set
         {
-            var gizmoGo = new GameObject($"[MPS Gizmo {target.gameObject.name}]");
-            gizmoGo.transform.SetParent(target);
+            gizmoType = value;
 
-            var gizmo = gizmoGo.AddComponent<CustomGizmo>();
-            gizmo.target = target;
-            gizmo.lineRSelectedThick = 0.25f;
-            gizmo.offsetScale = scale;
-            gizmo.gizmoMode = mode;
-            gizmo.CurrentGizmoType = GizmoType.Rotate;
+            if (gizmoTypeOld == gizmoType)
+                return;
 
-            return gizmo;
+            gizmoTypeOld = gizmoType;
+            eAxis = gizmoType is GizmoType.Move;
+            eScal = gizmoType is GizmoType.Scale;
+            eRotate = gizmoType is GizmoType.Rotate;
         }
+    }
 
-        public void SetAlternateTarget(Transform trans)
+    public bool IsGizmoDrag =>
+        GizmoVisible && IsDrag && SelectedType is not 0;
+
+    public bool GizmoVisible
+    {
+        get => Visible;
+        set
         {
-            positionTransform = trans;
-            hasAlternateTarget = trans != null;
+            if (value && IsDrag)
+                IsDrag = false;
+
+            Visible = value;
         }
+    }
 
-        public override void Update()
-        {
-            BeginUpdate();
+    private int SelectedType =>
+        (int)beSelectedType.GetValue(this);
 
-            base.Update();
+    public static CustomGizmo Make(Transform target, float scale = 0.25f, GizmoMode mode = GizmoMode.Local)
+    {
+        var gizmoGo = new GameObject($"[MPS Gizmo {target.gameObject.name}]");
 
-            if (IsGizmoDrag) SetTargetTransform();
+        gizmoGo.transform.SetParent(target);
 
-            SetTransform();
+        var gizmo = gizmoGo.AddComponent<CustomGizmo>();
 
-            EndUpdate();
-        }
+        gizmo.target = target;
+        gizmo.lineRSelectedThick = 0.25f;
+        gizmo.offsetScale = scale;
+        gizmo.Mode = mode;
+        gizmo.CurrentGizmoType = GizmoType.Rotate;
 
-        private void BeginUpdate()
-        {
-            Quaternion rotation = transform.rotation;
-            deltaPosition = transform.position - positionOld;
-            deltaRotation = rotation * Quaternion.Inverse(rotationOld);
-            deltaLocalPosition = transform.InverseTransformVector(deltaPosition);
-            deltaLocalRotation = Quaternion.Inverse(rotationOld) * rotation;
-            deltaScale = transform.localScale - scaleOld;
-        }
+        return gizmo;
+    }
 
-        private void EndUpdate()
-        {
-            Transform transform = this.transform;
-            positionOld = transform.position;
-            rotationOld = transform.rotation;
-            scaleOld = transform.localScale;
-        }
+    public override void Update()
+    {
+        BeginUpdate();
 
-        private void SetTargetTransform()
-        {
-            bool dragged;
-
-            switch (gizmoMode)
-            {
-                case GizmoMode.Local:
-                    target.position += target.transform.TransformVector(deltaLocalPosition).normalized
-                        * deltaLocalPosition.magnitude;
-                    target.rotation *= deltaLocalRotation;
-                    target.localScale += deltaScale;
-                    dragged = deltaLocalRotation != Quaternion.identity || deltaLocalPosition != Vector3.zero
-                        || deltaScale != Vector3.zero;
-                    break;
-                case GizmoMode.World:
-                case GizmoMode.Global:
-                    target.position += deltaPosition;
-                    target.rotation = deltaRotation * target.rotation;
-                    dragged = deltaRotation != Quaternion.identity || deltaPosition != Vector3.zero;
-                    break;
-                default: throw new ArgumentOutOfRangeException();
-            }
-
-            if (dragged) OnGizmoDrag();
-        }
+        base.Update();
 
-        private void SetTransform()
+        if (IsGizmoDrag)
+            SetTargetTransform();
+
+        SetTransform();
+
+        EndUpdate();
+    }
+
+    public void SetAlternateTarget(Transform trans)
+    {
+        positionTransform = trans;
+        hasAlternateTarget = trans != null;
+    }
+
+    private void BeginUpdate()
+    {
+        var rotation = transform.rotation;
+
+        deltaPosition = transform.position - positionOld;
+        deltaRotation = rotation * Quaternion.Inverse(rotationOld);
+        deltaLocalPosition = transform.InverseTransformVector(deltaPosition);
+        deltaLocalRotation = Quaternion.Inverse(rotationOld) * rotation;
+        deltaScale = transform.localScale - scaleOld;
+    }
+
+    private void EndUpdate()
+    {
+        var transform = this.transform;
+
+        positionOld = transform.position;
+        rotationOld = transform.rotation;
+        scaleOld = transform.localScale;
+    }
+
+    private void SetTargetTransform()
+    {
+        bool dragged;
+
+        switch (Mode)
         {
-            Transform transform = this.transform;
-            transform.position = (hasAlternateTarget ? positionTransform : target).position;
-            transform.localScale = Vector3.one;
-            transform.rotation = gizmoMode switch
-            {
-                GizmoMode.Local => target.rotation,
-                GizmoMode.World => Quaternion.identity,
-                GizmoMode.Global => Quaternion.LookRotation(transform.position - camera.transform.position),
-                _ => target.rotation
-            };
+            case GizmoMode.Local:
+                target.position += target.transform.TransformVector(deltaLocalPosition).normalized
+                    * deltaLocalPosition.magnitude;
+                target.rotation *= deltaLocalRotation;
+                target.localScale += deltaScale;
+                dragged = deltaLocalRotation != Quaternion.identity || deltaLocalPosition != Vector3.zero
+                    || deltaScale != Vector3.zero;
+
+                break;
+            case GizmoMode.World:
+            case GizmoMode.Global:
+                target.position += deltaPosition;
+                target.rotation = deltaRotation * target.rotation;
+                dragged = deltaRotation != Quaternion.identity || deltaPosition != Vector3.zero;
+
+                break;
+            default:
+                throw new ArgumentOutOfRangeException();
         }
 
-        private void OnGizmoDrag() => GizmoDrag?.Invoke(this, EventArgs.Empty);
+        if (dragged)
+            OnGizmoDrag();
     }
+
+    private void SetTransform()
+    {
+        var transform = this.transform;
+
+        transform.position = (hasAlternateTarget ? positionTransform : target).position;
+        transform.localScale = Vector3.one;
+        transform.rotation = Mode switch
+        {
+            GizmoMode.Local => target.rotation,
+            GizmoMode.World => Quaternion.identity,
+            GizmoMode.Global => Quaternion.LookRotation(transform.position - Camera.transform.position),
+            _ => target.rotation,
+        };
+    }
+
+    private void OnGizmoDrag() =>
+        GizmoDrag?.Invoke(this, EventArgs.Empty);
 }

+ 262 - 206
src/MeidoPhotoStudio.Plugin/DragPoint/DragPoint.cs

@@ -1,258 +1,314 @@
 using System;
+
 using UnityEngine;
 
-namespace MeidoPhotoStudio.Plugin
+using static MeidoPhotoStudio.Plugin.CustomGizmo;
+
+namespace MeidoPhotoStudio.Plugin;
+
+public abstract class DragPoint : MonoBehaviour
 {
-    using static CustomGizmo;
-    public abstract class DragPoint : MonoBehaviour
+    public const float DefaultAlpha = 0.75f;
+
+    public static readonly Color DefaultColour = new(0f, 0f, 0f, 0.4f);
+
+    public static Material DragPointMaterial = new(Shader.Find("CM3D2/Trans_AbsoluteFront"));
+
+    protected static Camera camera = GameMain.Instance.MainCamera.camera;
+
+    private const float DoubleClickSensitivity = 0.3f;
+
+    // TODO: Use this value or just throw it away.
+    private static readonly int DragPointLayer = (int)Mathf.Log(LayerMask.GetMask("AbsolutFront"), 2);
+    private static GameObject dragPointParent;
+
+    private Func<Vector3> position;
+    private Func<Vector3> rotation;
+    private Collider collider;
+    private Renderer renderer;
+    private bool reinitializeDrag;
+    private Vector3 startMousePosition;
+    private float startDoubleClick;
+    private Vector3 screenPoint;
+    private Vector3 startOffset;
+    private Vector3 newOffset;
+    private Vector3 baseScale;
+    private DragType oldDragType;
+    private DragType currentDragType;
+    private bool dragPointEnabled = true;
+    private float dragPointScale = 1f;
+    private bool gizmoEnabled = true;
+
+    static DragPoint()
     {
-        private static readonly int layer = (int) Mathf.Log(LayerMask.GetMask("AbsolutFront"), 2);
-        public const float defaultAlpha = 0.75f;
-        private static GameObject dragPointParent;
-        private const float doubleClickSensitivity = 0.3f;
-        private Func<Vector3> position;
-        private Func<Vector3> rotation;
-        private Collider collider;
-        private Renderer renderer;
-        private bool reinitializeDrag;
-        protected bool Transforming => CurrentDragType >= DragType.MoveXZ;
-        protected bool Special => CurrentDragType == DragType.Select || CurrentDragType == DragType.Delete;
-        protected bool Moving => CurrentDragType == DragType.MoveXZ || CurrentDragType == DragType.MoveY;
-        protected bool Rotating => CurrentDragType >= DragType.RotLocalXZ && CurrentDragType <= DragType.RotLocalY;
-        protected bool Scaling => CurrentDragType == DragType.Scale;
-        protected bool Selecting => CurrentDragType == DragType.Select;
-        protected bool Deleting => CurrentDragType == DragType.Delete;
-        private Vector3 startMousePosition;
-        protected static Camera camera = GameMain.Instance.MainCamera.camera;
-        public enum DragType
-        {
-            None, Ignore, Select, Delete,
-            MoveXZ, MoveY,
-            RotLocalXZ, RotY, RotLocalY,
-            Scale
-        }
-        public Transform MyObject { get; protected set; }
-        public GameObject MyGameObject => MyObject.gameObject;
-        private float startDoubleClick;
-        private Vector3 screenPoint;
-        private Vector3 startOffset;
-        private Vector3 newOffset;
-        public static Material dragPointMaterial = new Material(Shader.Find("CM3D2/Trans_AbsoluteFront"));
-        public static readonly Color defaultColour = new Color(0f, 0f, 0f, 0.4f);
-        public Vector3 OriginalScale { get; private set; }
-        private Vector3 baseScale;
-        public Vector3 BaseScale
-        {
-            get => baseScale;
-            protected set
-            {
-                baseScale = value;
-                transform.localScale = BaseScale * DragPointScale;
-            }
-        }
-        private float dragPointScale = 1f;
-        public float DragPointScale
-        {
-            get => dragPointScale;
-            set
-            {
-                dragPointScale = value;
-                transform.localScale = BaseScale * dragPointScale;
-            }
-        }
-        public GameObject GizmoGo { get; protected set; }
-        public CustomGizmo Gizmo { get; protected set; }
-        private DragType oldDragType;
-        private DragType currentDragType;
-        protected DragType CurrentDragType
-        {
-            get => currentDragType;
-            set
-            {
-                if (value != oldDragType)
-                {
-                    currentDragType = value;
-                    reinitializeDrag = true;
-                    oldDragType = currentDragType;
-                    ApplyDragType();
-                }
-            }
-        }
-        private bool dragPointEnabled = true;
-        public bool DragPointEnabled
+        InputManager.Register(MpsKey.DragSelect, KeyCode.A, "Select handle mode");
+        InputManager.Register(MpsKey.DragDelete, KeyCode.D, "Delete handle mode");
+        InputManager.Register(MpsKey.DragMove, KeyCode.Z, "Move handle mode");
+        InputManager.Register(MpsKey.DragRotate, KeyCode.X, "Rotate handle mode");
+        InputManager.Register(MpsKey.DragScale, KeyCode.C, "Scale handle mode");
+        InputManager.Register(MpsKey.DragFinger, KeyCode.Space, "Show finger handles");
+    }
+
+    public enum DragType
+    {
+        None,
+        Ignore,
+        Select,
+        Delete,
+        MoveXZ,
+        MoveY,
+        RotLocalXZ,
+        RotY,
+        RotLocalY,
+        Scale,
+    }
+
+    public Vector3 OriginalScale { get; private set; }
+
+    public Transform MyObject { get; protected set; }
+
+    public GameObject GizmoGo { get; protected set; }
+
+    public CustomGizmo Gizmo { get; protected set; }
+
+    public GameObject MyGameObject =>
+        MyObject.gameObject;
+
+    public Vector3 BaseScale
+    {
+        get => baseScale;
+        protected set
         {
-            get => dragPointEnabled;
-            set
-            {
-                if (dragPointEnabled == value) return;
-                dragPointEnabled = value;
-                ApplyDragType();
-            }
+            baseScale = value;
+            transform.localScale = BaseScale * DragPointScale;
         }
-        private bool gizmoEnabled = true;
-        public bool GizmoEnabled
+    }
+
+    public float DragPointScale
+    {
+        get => dragPointScale;
+        set
         {
-            get => GizmoGo != null && gizmoEnabled;
-            set
-            {
-                if (GizmoGo == null || (gizmoEnabled == value)) return;
-                gizmoEnabled = value;
-                ApplyDragType();
-            }
+            dragPointScale = value;
+            transform.localScale = BaseScale * dragPointScale;
         }
+    }
 
-        static DragPoint()
+    public bool DragPointEnabled
+    {
+        get => dragPointEnabled;
+        set
         {
-            InputManager.Register(MpsKey.DragSelect, KeyCode.A, "Select handle mode");
-            InputManager.Register(MpsKey.DragDelete, KeyCode.D, "Delete handle mode");
-            InputManager.Register(MpsKey.DragMove, KeyCode.Z, "Move handle mode");
-            InputManager.Register(MpsKey.DragRotate, KeyCode.X, "Rotate handle mode");
-            InputManager.Register(MpsKey.DragScale, KeyCode.C, "Scale handle mode");
-            InputManager.Register(MpsKey.DragFinger, KeyCode.Space, "Show finger handles");
+            if (dragPointEnabled == value)
+                return;
+
+            dragPointEnabled = value;
+            ApplyDragType();
         }
+    }
 
-        private void Awake()
+    public bool GizmoEnabled
+    {
+        get => GizmoGo && gizmoEnabled;
+        set
         {
-            BaseScale = OriginalScale = transform.localScale;
-            collider = GetComponent<Collider>();
-            renderer = GetComponent<Renderer>();
+            if (!GizmoGo || gizmoEnabled == value)
+                return;
+
+            gizmoEnabled = value;
             ApplyDragType();
         }
+    }
 
-        private static GameObject DragPointParent()
+    protected DragType CurrentDragType
+    {
+        get => currentDragType;
+        set
         {
-            return dragPointParent ? dragPointParent : (dragPointParent = new GameObject("[MPS DragPoint Parent]"));
+            if (value == oldDragType)
+                return;
+
+            currentDragType = value;
+            reinitializeDrag = true;
+            oldDragType = currentDragType;
+            ApplyDragType();
         }
+    }
 
-        public static T Make<T>(PrimitiveType primitiveType, Vector3 scale) where T : DragPoint
-        {
-            GameObject dragPointGo = GameObject.CreatePrimitive(primitiveType);
-            dragPointGo.transform.SetParent(DragPointParent().transform, false);
-            dragPointGo.transform.localScale = scale;
-            dragPointGo.layer = 8;
+    protected bool Transforming =>
+        CurrentDragType >= DragType.MoveXZ;
 
-            T dragPoint = dragPointGo.AddComponent<T>();
-            dragPoint.renderer.material = dragPointMaterial;
-            dragPoint.renderer.material.color = defaultColour;
+    protected bool Special =>
+        CurrentDragType is DragType.Select or DragType.Delete;
 
-            return dragPoint;
-        }
+    protected bool Moving =>
+        CurrentDragType is DragType.MoveXZ or DragType.MoveY;
 
-        public virtual void Initialize(Func<Vector3> position, Func<Vector3> rotation)
-        {
-            this.position = position;
-            this.rotation = rotation;
-        }
+    protected bool Rotating =>
+        CurrentDragType is >= DragType.RotLocalXZ and <= DragType.RotLocalY;
 
-        public virtual void Set(Transform myObject)
-        {
-            MyObject = myObject;
-            gameObject.name = $"[MPS DragPoint: {MyObject.name}]";
-        }
+    protected bool Scaling =>
+        CurrentDragType is DragType.Scale;
 
-        public virtual void AddGizmo(float scale = 0.25f, GizmoMode mode = GizmoMode.Local)
-        {
-            Gizmo = CustomGizmo.Make(MyObject, scale, mode);
-            GizmoGo = Gizmo.gameObject;
-            Gizmo.GizmoVisible = false;
-            ApplyDragType();
-        }
+    protected bool Selecting =>
+        CurrentDragType is DragType.Select;
 
-        protected virtual void ApplyDragType() { }
+    protected bool Deleting =>
+        CurrentDragType is DragType.Delete;
 
-        public void ApplyProperties(bool active = false, bool visible = false, bool gizmo = false)
-        {
-            collider.enabled = active;
-            renderer.enabled = visible;
-            if (Gizmo) Gizmo.GizmoVisible = gizmo;
-        }
+    public static T Make<T>(PrimitiveType primitiveType, Vector3 scale)
+        where T : DragPoint
+    {
+        var dragPointGo = GameObject.CreatePrimitive(primitiveType);
 
-        protected void ApplyColour(Color colour) => renderer.material.color = colour;
+        dragPointGo.transform.SetParent(DragPointParent().transform, false);
+        dragPointGo.transform.localScale = scale;
+        dragPointGo.layer = 8;
 
-        protected void ApplyColour(float r, float g, float b, float a = defaultAlpha)
+        var dragPoint = dragPointGo.AddComponent<T>();
+
+        dragPoint.renderer.material = DragPointMaterial;
+        dragPoint.renderer.material.color = DefaultColour;
+
+        return dragPoint;
+    }
+
+    public virtual void Initialize(Func<Vector3> position, Func<Vector3> rotation)
+    {
+        this.position = position;
+        this.rotation = rotation;
+    }
+
+    public virtual void Set(Transform myObject)
+    {
+        MyObject = myObject;
+        gameObject.name = $"[MPS DragPoint: {MyObject.name}]";
+    }
+
+    public virtual void AddGizmo(float scale = 0.25f, GizmoMode mode = GizmoMode.Local)
+    {
+        Gizmo = CustomGizmo.Make(MyObject, scale, mode);
+        GizmoGo = Gizmo.gameObject;
+        Gizmo.GizmoVisible = false;
+        ApplyDragType();
+    }
+
+    public void ApplyProperties(bool active = false, bool visible = false, bool gizmo = false)
+    {
+        collider.enabled = active;
+        renderer.enabled = visible;
+
+        if (Gizmo)
+            Gizmo.GizmoVisible = gizmo;
+    }
+
+    protected abstract void UpdateDragType();
+
+    protected abstract void Drag();
+
+    protected virtual void ApplyDragType()
+    {
+    }
+
+    protected virtual void Update()
+    {
+        transform.position = position();
+        transform.eulerAngles = rotation();
+
+        UpdateDragType();
+    }
+
+    protected virtual void OnMouseDown()
+    {
+        screenPoint = camera.WorldToScreenPoint(transform.position);
+        startMousePosition = Utility.MousePosition;
+        startOffset = transform.position
+            - camera.ScreenToWorldPoint(new(startMousePosition.x, startMousePosition.y, screenPoint.z));
+        newOffset = transform.position - MyObject.position;
+    }
+
+    protected virtual void OnMouseDrag()
+    {
+        if (reinitializeDrag)
         {
-            ApplyColour(new Color(r, g, b, a));
+            reinitializeDrag = false;
+            OnMouseDown();
         }
 
-        protected Vector3 MouseDelta() => Utility.MousePosition - startMousePosition;
+        if (collider.enabled && startMousePosition != Utility.MousePosition)
+            Drag();
+    }
 
-        protected bool OtherDragType()
+    protected virtual void OnMouseUp()
+    {
+        if (Time.time - startDoubleClick < DoubleClickSensitivity)
         {
-            return InputManager.GetKey(MpsKey.DragSelect) || InputManager.GetKey(MpsKey.DragDelete)
-                || InputManager.GetKey(MpsKey.DragMove) || InputManager.GetKey(MpsKey.DragRotate)
-                || InputManager.GetKey(MpsKey.DragScale) || InputManager.GetKey(MpsKey.DragFinger);
+            startDoubleClick = -1f;
+            OnDoubleClick();
         }
-
-        protected Vector3 CursorPosition()
+        else
         {
-            Vector3 mousePosition = Utility.MousePosition;
-            return camera.ScreenToWorldPoint(new Vector3(mousePosition.x, mousePosition.y, screenPoint.z))
-                + startOffset - newOffset;
+            startDoubleClick = Time.time;
         }
+    }
 
-        protected virtual void Update()
-        {
-            transform.position = position();
-            transform.eulerAngles = rotation();
+    protected virtual void OnDoubleClick()
+    {
+    }
 
-            UpdateDragType();
-        }
+    protected virtual void OnDestroy() =>
+        Destroy(GizmoGo);
 
-        protected virtual void OnMouseDown()
-        {
-            screenPoint = camera.WorldToScreenPoint(transform.position);
-            startMousePosition = Utility.MousePosition;
-            startOffset = transform.position - camera.ScreenToWorldPoint(
-                new Vector3(startMousePosition.x, startMousePosition.y, screenPoint.z)
-            );
-            newOffset = transform.position - MyObject.position;
-        }
+    protected void ApplyColour(Color colour) =>
+        renderer.material.color = colour;
 
-        protected virtual void OnMouseDrag()
-        {
-            if (reinitializeDrag)
-            {
-                reinitializeDrag = false;
-                OnMouseDown();
-            }
+    protected void ApplyColour(float r, float g, float b, float a = DefaultAlpha) =>
+        ApplyColour(new(r, g, b, a));
 
-            if (collider.enabled && startMousePosition != Utility.MousePosition) Drag();
-        }
+    protected Vector3 MouseDelta() =>
+        Utility.MousePosition - startMousePosition;
 
-        protected abstract void UpdateDragType();
-        protected abstract void Drag();
+    protected bool OtherDragType() =>
+        InputManager.GetKey(MpsKey.DragSelect) || InputManager.GetKey(MpsKey.DragDelete)
+        || InputManager.GetKey(MpsKey.DragMove) || InputManager.GetKey(MpsKey.DragRotate)
+        || InputManager.GetKey(MpsKey.DragScale) || InputManager.GetKey(MpsKey.DragFinger);
 
-        protected virtual void OnMouseUp()
-        {
-            if ((Time.time - startDoubleClick) < doubleClickSensitivity)
-            {
-                startDoubleClick = -1f;
-                OnDoubleClick();
-            }
-            else
-            {
-                startDoubleClick = Time.time;
-            }
-        }
+    protected Vector3 CursorPosition()
+    {
+        var mousePosition = Utility.MousePosition;
 
-        protected virtual void OnDoubleClick() { }
+        return camera.ScreenToWorldPoint(new(mousePosition.x, mousePosition.y, screenPoint.z)) + startOffset
+            - newOffset;
+    }
 
-        private void OnEnable()
-        {
-            if (position != null)
-            {
-                transform.position = position();
-                transform.eulerAngles = rotation();
-            }
-            if (GizmoGo) GizmoGo.SetActive(true);
-            ApplyDragType();
-        }
+    private static GameObject DragPointParent() =>
+        dragPointParent ? dragPointParent : (dragPointParent = new("[MPS DragPoint Parent]"));
+
+    private void Awake()
+    {
+        BaseScale = OriginalScale = transform.localScale;
+        collider = GetComponent<Collider>();
+        renderer = GetComponent<Renderer>();
+        ApplyDragType();
+    }
 
-        private void OnDisable()
+    private void OnEnable()
+    {
+        if (position is not null)
         {
-            if (GizmoGo) GizmoGo.SetActive(false);
+            transform.position = position();
+            transform.eulerAngles = rotation();
         }
 
-        protected virtual void OnDestroy() => Destroy(GizmoGo);
+        if (GizmoGo)
+            GizmoGo.SetActive(true);
+
+        ApplyDragType();
+    }
+
+    private void OnDisable()
+    {
+        if (GizmoGo)
+            GizmoGo.SetActive(false);
     }
 }

+ 19 - 0
src/MeidoPhotoStudio.Plugin/DragPoint/DragPointBG.cs

@@ -0,0 +1,19 @@
+using UnityEngine;
+
+namespace MeidoPhotoStudio.Plugin;
+
+public class DragPointBG : DragPointGeneral
+{
+    public override void Set(Transform myObject)
+    {
+        base.Set(myObject);
+
+        DefaultPosition = myObject.position;
+    }
+
+    protected override void ApplyDragType()
+    {
+        ApplyProperties(Transforming, Transforming, Rotating);
+        ApplyColours();
+    }
+}

+ 32 - 0
src/MeidoPhotoStudio.Plugin/DragPoint/DragPointBody.cs

@@ -0,0 +1,32 @@
+namespace MeidoPhotoStudio.Plugin;
+
+public class DragPointBody : DragPointGeneral
+{
+    public bool IsCube;
+
+    private bool isIK;
+
+    public bool IsIK
+    {
+        get => isIK;
+        set
+        {
+            if (isIK == value)
+                return;
+
+            isIK = value;
+            ApplyDragType();
+        }
+    }
+
+    protected override void ApplyDragType()
+    {
+        var enabled = !IsIK && (Transforming || Selecting);
+        var select = IsIK && Selecting;
+
+        ApplyProperties(enabled || select, IsCube && enabled, false);
+
+        if (IsCube)
+            ApplyColours();
+    }
+}

+ 236 - 181
src/MeidoPhotoStudio.Plugin/DragPoint/DragPointGeneral.cs

@@ -1,214 +1,269 @@
 using System;
+
 using UnityEngine;
 
-namespace MeidoPhotoStudio.Plugin
+using static MeidoPhotoStudio.Plugin.CustomGizmo;
+
+using Input = MeidoPhotoStudio.Plugin.InputManager;
+
+namespace MeidoPhotoStudio.Plugin;
+
+public abstract class DragPointGeneral : DragPoint
 {
-    using static CustomGizmo;
-    using Input = InputManager;
+    public const float SmallCube = 0.5f;
+
+    public static readonly Color MoveColour = new(0.2f, 0.5f, 0.95f, DefaultAlpha);
+    public static readonly Color RotateColour = new(0.2f, 0.75f, 0.3f, DefaultAlpha);
+    public static readonly Color ScaleColour = new(0.8f, 0.7f, 0.3f, DefaultAlpha);
+    public static readonly Color SelectColour = new(0.9f, 0.5f, 1f, DefaultAlpha);
+    public static readonly Color DeleteColour = new(1f, 0.1f, 0.1f, DefaultAlpha);
+
+    private float currentScale;
+    private bool scaling;
+    private Quaternion currentRotation;
+
+    public event EventHandler Move;
+
+    public event EventHandler Rotate;
+
+    public event EventHandler Scale;
+
+    public event EventHandler EndScale;
+
+    public event EventHandler Delete;
+
+    public event EventHandler Select;
+
+    public Quaternion DefaultRotation { get; set; } = Quaternion.identity;
 
-    public abstract class DragPointGeneral : DragPoint
+    public Vector3 DefaultPosition { get; set; } = Vector3.zero;
+
+    public Vector3 DefaultScale { get; set; } = Vector3.one;
+
+    public float ScaleFactor { get; set; } = 1f;
+
+    public bool ConstantScale { get; set; }
+
+    public override void AddGizmo(float scale = 0.35f, GizmoMode mode = GizmoMode.Local)
+    {
+        base.AddGizmo(scale, mode);
+
+        Gizmo.GizmoDrag += (_, _) =>
+        {
+            if (Gizmo.CurrentGizmoType is GizmoType.Rotate)
+                OnRotate();
+        };
+    }
+
+    protected override void Update()
     {
-        public const float smallCube = 0.5f;
-        private float currentScale;
-        private bool scaling;
-        private Quaternion currentRotation;
-        public Quaternion DefaultRotation { get; set; } = Quaternion.identity;
-        public Vector3 DefaultPosition { get; set; } = Vector3.zero;
-        public Vector3 DefaultScale { get; set; } = Vector3.one;
-        public float ScaleFactor { get; set; } = 1f;
-        public bool ConstantScale { get; set; }
-        public static readonly Color moveColour = new Color(0.2f, 0.5f, 0.95f, defaultAlpha);
-        public static readonly Color rotateColour = new Color(0.2f, 0.75f, 0.3f, defaultAlpha);
-        public static readonly Color scaleColour = new Color(0.8f, 0.7f, 0.3f, defaultAlpha);
-        public static readonly Color selectColour = new Color(0.9f, 0.5f, 1f, defaultAlpha);
-        public static readonly Color deleteColour = new Color(1f, 0.1f, 0.1f, defaultAlpha);
-        public event EventHandler Move;
-        public event EventHandler Rotate;
-        public event EventHandler Scale;
-        public event EventHandler EndScale;
-        public event EventHandler Delete;
-        public event EventHandler Select;
-
-        public override void AddGizmo(float scale = 0.35f, GizmoMode mode = GizmoMode.Local)
-        {
-            base.AddGizmo(scale, mode);
-            Gizmo.GizmoDrag += (s, a) =>
-            {
-                if (Gizmo.CurrentGizmoType == GizmoType.Rotate) OnRotate();
-            };
-        }
-
-        protected virtual void ApplyColours()
-        {
-            Color colour = moveColour;
-            if (Rotating) colour = rotateColour;
-            else if (Scaling) colour = scaleColour;
-            else if (Selecting) colour = selectColour;
-            else if (Deleting) colour = deleteColour;
-            ApplyColour(colour);
-        }
-
-        protected override void Update()
-        {
-            base.Update();
-
-            if (ConstantScale)
-            {
-                float distance = Vector3.Distance(camera.transform.position, transform.position);
-                transform.localScale = Vector3.one * (0.4f * BaseScale.x * DragPointScale * distance);
-            }
-        }
-
-        protected override void UpdateDragType()
-        {
-            bool shift = Input.Shift;
-            if (Input.GetKey(MpsKey.DragSelect))
-            {
-                CurrentDragType = DragType.Select;
-            }
-            else if (Input.GetKey(MpsKey.DragDelete))
-            {
-                CurrentDragType = DragType.Delete;
-            }
-            else if (Input.GetKey(MpsKey.DragMove))
-            {
-                if (Input.Control) CurrentDragType = DragType.MoveY;
-                else CurrentDragType = shift ? DragType.RotY : DragType.MoveXZ;
-            }
-            else if (Input.GetKey(MpsKey.DragRotate))
-            {
-                CurrentDragType = shift ? DragType.RotLocalY : DragType.RotLocalXZ;
-            }
-            else if (Input.GetKey(MpsKey.DragScale))
-            {
-                CurrentDragType = DragType.Scale;
-            }
+        base.Update();
+
+        if (!ConstantScale)
+            return;
+
+        var distance = Vector3.Distance(camera.transform.position, transform.position);
+
+        transform.localScale = Vector3.one * (0.4f * BaseScale.x * DragPointScale * distance);
+    }
+
+    protected override void UpdateDragType()
+    {
+        var shift = Input.Shift;
+
+        if (Input.GetKey(MpsKey.DragSelect))
+        {
+            CurrentDragType = DragType.Select;
+        }
+        else if (Input.GetKey(MpsKey.DragDelete))
+        {
+            CurrentDragType = DragType.Delete;
+        }
+        else if (Input.GetKey(MpsKey.DragMove))
+        {
+            if (Input.Control)
+                CurrentDragType = DragType.MoveY;
             else
-            {
-                CurrentDragType = DragType.None;
-            }
+                CurrentDragType = shift ? DragType.RotY : DragType.MoveXZ;
+        }
+        else if (Input.GetKey(MpsKey.DragRotate))
+        {
+            CurrentDragType = shift ? DragType.RotLocalY : DragType.RotLocalXZ;
         }
+        else if (Input.GetKey(MpsKey.DragScale))
+        {
+            CurrentDragType = DragType.Scale;
+        }
+        else
+        {
+            CurrentDragType = DragType.None;
+        }
+    }
 
-        protected override void OnMouseDown()
+    protected override void OnMouseDown()
+    {
+        if (Deleting)
         {
-            if (Deleting)
-            {
-                OnDelete();
-                return;
-            }
+            OnDelete();
 
-            if (Selecting)
-            {
-                OnSelect();
-                return;
-            }
+            return;
+        }
 
-            base.OnMouseDown();
+        if (Selecting)
+        {
+            OnSelect();
 
-            currentScale = MyObject.localScale.x;
-            currentRotation = MyObject.rotation;
+            return;
         }
 
-        protected override void OnDoubleClick()
+        base.OnMouseDown();
+
+        currentScale = MyObject.localScale.x;
+        currentRotation = MyObject.rotation;
+    }
+
+    protected override void OnDoubleClick()
+    {
+        if (Scaling)
         {
-            if (Scaling)
-            {
-                MyObject.localScale = DefaultScale;
-                OnScale();
-                OnEndScale();
-            }
+            MyObject.localScale = DefaultScale;
+            OnScale();
+            OnEndScale();
+        }
 
-            if (Rotating)
-            {
-                ResetRotation();
-                OnRotate();
-            }
+        if (Rotating)
+        {
+            ResetRotation();
+            OnRotate();
+        }
+
+        if (Moving)
+        {
+            ResetPosition();
+            OnMove();
+        }
+    }
+
+    protected override void OnMouseUp()
+    {
+        base.OnMouseUp();
 
-            if (Moving)
-            {
-                ResetPosition();
-                OnMove();
-            }
+        if (scaling)
+        {
+            scaling = false;
+            OnScale();
+            OnEndScale();
         }
+    }
 
-        protected virtual void ResetPosition() => MyObject.position = DefaultPosition;
+    protected override void Drag()
+    {
+        if (CurrentDragType is DragType.Select or DragType.Delete)
+            return;
+
+        var cursorPosition = CursorPosition();
+        var mouseDelta = MouseDelta();
+
+        // CurrentDragType can only be one thing at a time afaik so maybe refactor to else if chain
+        if (CurrentDragType is DragType.MoveXZ)
+        {
+            MyObject.position = new(cursorPosition.x, MyObject.position.y, cursorPosition.z);
 
-        protected virtual void ResetRotation() => MyObject.rotation = DefaultRotation;
+            OnMove();
+        }
 
-        protected override void OnMouseUp()
+        if (CurrentDragType is DragType.MoveY)
         {
-            base.OnMouseUp();
-            if (scaling)
-            {
-                scaling = false;
-                OnScale();
-                OnEndScale();
-            }
+            MyObject.position = new(MyObject.position.x, cursorPosition.y, MyObject.position.z);
+
+            OnMove();
         }
 
-        protected override void Drag()
+        if (CurrentDragType is DragType.RotY)
         {
-            if (CurrentDragType == DragType.Select || CurrentDragType == DragType.Delete) return;
+            MyObject.rotation = currentRotation;
+            MyObject.Rotate(Vector3.up, -mouseDelta.x / 3f, Space.World);
+            OnRotate();
+        }
 
-            Vector3 cursorPosition = CursorPosition();
-            Vector3 mouseDelta = MouseDelta();
+        if (CurrentDragType is DragType.RotLocalXZ)
+        {
+            MyObject.rotation = currentRotation;
 
-            if (CurrentDragType == DragType.MoveXZ)
-            {
-                MyObject.position = new Vector3(cursorPosition.x, MyObject.position.y, cursorPosition.z);
-                OnMove();
-            }
+            var forward = camera.transform.forward;
+            var right = camera.transform.right;
 
-            if (CurrentDragType == DragType.MoveY)
-            {
-                MyObject.position = new Vector3(
-                    MyObject.position.x, cursorPosition.y, MyObject.position.z
-                );
-                OnMove();
-            }
+            forward.y = 0f;
+            right.y = 0f;
+            MyObject.Rotate(forward, -mouseDelta.x / 6f, Space.World);
+            MyObject.Rotate(right, mouseDelta.y / 4f, Space.World);
 
-            if (CurrentDragType == DragType.RotY)
-            {
-                MyObject.rotation = currentRotation;
-                MyObject.Rotate(Vector3.up, -mouseDelta.x / 3f, Space.World);
-                OnRotate();
-            }
-
-            if (CurrentDragType == DragType.RotLocalXZ)
-            {
-                MyObject.rotation = currentRotation;
-                Vector3 forward = camera.transform.forward;
-                Vector3 right = camera.transform.right;
-                forward.y = 0f;
-                right.y = 0f;
-                MyObject.Rotate(forward, -mouseDelta.x / 6f, Space.World);
-                MyObject.Rotate(right, mouseDelta.y / 4f, Space.World);
-                OnRotate();
-            }
+            OnRotate();
+        }
 
-            if (CurrentDragType == DragType.RotLocalY)
-            {
-                MyObject.rotation = currentRotation;
-                MyObject.Rotate(Vector3.up * -mouseDelta.x / 2.2f);
-                OnRotate();
-            }
-
-            if (CurrentDragType == DragType.Scale)
-            {
-                scaling = true;
-                float scale = currentScale + (mouseDelta.y / 200f * ScaleFactor);
-                if (scale < 0f) scale = 0f;
-                MyObject.localScale = new Vector3(scale, scale, scale);
-                OnScale();
-            }
-        }
-
-        protected virtual void OnEndScale() => OnEvent(EndScale);
-        protected virtual void OnScale() => OnEvent(Scale);
-        protected virtual void OnMove() => OnEvent(Move);
-        protected virtual void OnRotate() => OnEvent(Rotate);
-        protected virtual void OnSelect() => OnEvent(Select);
-        protected virtual void OnDelete() => OnEvent(Delete);
-        private void OnEvent(EventHandler handler) => handler?.Invoke(this, EventArgs.Empty);
+        if (CurrentDragType is DragType.RotLocalY)
+        {
+            MyObject.rotation = currentRotation;
+            MyObject.Rotate(Vector3.up * -mouseDelta.x / 2.2f);
+
+            OnRotate();
+        }
+
+        if (CurrentDragType is DragType.Scale)
+        {
+            scaling = true;
+
+            var scale = currentScale + mouseDelta.y / 200f * ScaleFactor;
+
+            if (scale < 0f)
+                scale = 0f;
+
+            MyObject.localScale = new(scale, scale, scale);
+
+            OnScale();
+        }
+    }
+
+    protected virtual void ApplyColours()
+    {
+        var colour = MoveColour;
+
+        if (Rotating)
+            colour = RotateColour;
+        else if (Scaling)
+            colour = ScaleColour;
+        else if (Selecting)
+            colour = SelectColour;
+        else if (Deleting)
+            colour = DeleteColour;
+
+        ApplyColour(colour);
     }
+
+    protected virtual void ResetPosition() =>
+        MyObject.position = DefaultPosition;
+
+    protected virtual void ResetRotation() =>
+        MyObject.rotation = DefaultRotation;
+
+    protected virtual void OnEndScale() =>
+        OnEvent(EndScale);
+
+    protected virtual void OnScale() =>
+        OnEvent(Scale);
+
+    protected virtual void OnMove() =>
+        OnEvent(Move);
+
+    protected virtual void OnRotate() =>
+        OnEvent(Rotate);
+
+    protected virtual void OnSelect() =>
+        OnEvent(Select);
+
+    protected virtual void OnDelete() =>
+        OnEvent(Delete);
+
+    private void OnEvent(EventHandler handler) =>
+        handler?.Invoke(this, EventArgs.Empty);
 }

+ 86 - 68
src/MeidoPhotoStudio.Plugin/DragPoint/DragPointGravity.cs

@@ -1,86 +1,104 @@
 using UnityEngine;
 
-namespace MeidoPhotoStudio.Plugin
+using static TBody;
+
+namespace MeidoPhotoStudio.Plugin;
+
+public class DragPointGravity : DragPointGeneral
 {
-    using static TBody;
-    public class DragPointGravity : DragPointGeneral
+    private static readonly SlotID[] SkirtSlots = { SlotID.skirt, SlotID.onepiece, SlotID.mizugi, SlotID.panz };
+    private static readonly SlotID[] HairSlots = { SlotID.hairF, SlotID.hairR, SlotID.hairS, SlotID.hairT };
+
+    public GravityTransformControl Control { get; private set; }
+
+    public bool Valid =>
+        Control.isValid;
+
+    public bool Active =>
+        Valid && gameObject.activeSelf;
+
+    public static GravityTransformControl MakeGravityControl(Maid maid, bool skirt = false)
     {
-        private static readonly SlotID[] skirtSlots = { SlotID.skirt, SlotID.onepiece, SlotID.mizugi, SlotID.panz };
-        private static readonly SlotID[] hairSlots = { SlotID.hairF, SlotID.hairR, SlotID.hairS, SlotID.hairT };
-        public GravityTransformControl Control { get; private set; }
-        public bool Valid => Control.isValid;
-        public bool Active => Valid && gameObject.activeSelf;
+        var category = skirt ? "skirt" : "hair";
+        var bone = maid.body0.GetBone("Bip01");
+        var gravityGoName = $"GravityDatas_{maid.status.guid}_{category}";
+        var gravityTransform = maid.gameObject.transform.Find(gravityGoName);
 
-        public static GravityTransformControl MakeGravityControl(Maid maid, bool skirt = false)
+        if (!gravityTransform)
         {
-            string category = skirt ? "skirt" : "hair";
-
-            Transform bone = maid.body0.GetBone("Bip01");
-            string gravityGoName = $"GravityDatas_{maid.status.guid}_{category}";
-            Transform gravityTransform = maid.gameObject.transform.Find(gravityGoName);
-            if (gravityTransform == null)
-            {
-                GameObject go = new GameObject(gravityGoName);
-                go.transform.SetParent(bone, false);
-                go.transform.SetParent(maid.transform, true);
-                go.transform.localScale = Vector3.one;
-                go.transform.rotation = Quaternion.identity;
-                GameObject go2 = new GameObject(gravityGoName);
-                go2.transform.SetParent(go.transform, false);
-                gravityTransform = go2.transform;
-            }
-            else
-            {
-                gravityTransform = gravityTransform.GetChild(0);
-                GravityTransformControl control = gravityTransform.GetComponent<GravityTransformControl>();
-                if (control != null) GameObject.Destroy(control);
-            }
-
-            GravityTransformControl gravityControl = gravityTransform.gameObject.AddComponent<GravityTransformControl>();
-
-            SlotID[] slots = skirt ? skirtSlots : hairSlots;
-
-            gravityControl.SetTargetSlods(slots);
-            gravityControl.forceRate = 0.1f;
-
-            return gravityControl;
-        }
+            var go = new GameObject(gravityGoName);
 
-        public override void Set(Transform myObject)
-        {
-            base.Set(myObject);
-            Control = myObject.GetComponent<GravityTransformControl>();
-            gameObject.SetActive(false);
-        }
+            go.transform.SetParent(bone, false);
+            go.transform.SetParent(maid.transform, true);
+            go.transform.localScale = Vector3.one;
+            go.transform.rotation = Quaternion.identity;
 
-        protected override void ResetPosition() => Control.transform.localPosition = DefaultPosition;
+            var go2 = new GameObject(gravityGoName);
 
-        protected override void ApplyDragType()
-        {
-            ApplyProperties(Moving, Moving, false);
-            ApplyColours();
+            go2.transform.SetParent(go.transform, false);
+            gravityTransform = go2.transform;
         }
-
-        protected override void OnDestroy()
+        else
         {
-            if (Control.isValid)
-            {
-                Control.transform.localPosition = Vector3.zero;
-                Control.Update();
-            }
-            GameObject.Destroy(Control.transform.parent.gameObject);
-            base.OnDestroy();
+            gravityTransform = gravityTransform.GetChild(0);
+
+            var control = gravityTransform.GetComponent<GravityTransformControl>();
+
+            if (control)
+                Destroy(control);
         }
 
-        private void OnDisable() => Control.isEnabled = false;
+        var gravityControl = gravityTransform.gameObject.AddComponent<GravityTransformControl>();
+        var slots = skirt ? SkirtSlots : HairSlots;
 
-        private void OnEnable()
+        gravityControl.SetTargetSlods(slots);
+        gravityControl.forceRate = 0.1f;
+
+        return gravityControl;
+    }
+
+    public override void Set(Transform myObject)
+    {
+        base.Set(myObject);
+
+        Control = myObject.GetComponent<GravityTransformControl>();
+        gameObject.SetActive(false);
+    }
+
+    protected override void ResetPosition() =>
+        Control.transform.localPosition = DefaultPosition;
+
+    protected override void ApplyDragType()
+    {
+        ApplyProperties(Moving, Moving, false);
+        ApplyColours();
+    }
+
+    protected override void OnDestroy()
+    {
+        if (Control.isValid)
         {
-            if (Control)
-            {
-                Control.isEnabled = true;
-                if (!Control.isEnabled) gameObject.SetActive(false);
-            }
+            Control.transform.localPosition = Vector3.zero;
+            Control.Update();
         }
+
+        Destroy(Control.transform.parent.gameObject);
+
+        base.OnDestroy();
+    }
+
+    private void OnDisable() =>
+        Control.isEnabled = false;
+
+    private void OnEnable()
+    {
+        if (!Control)
+            return;
+
+        // TODO: WTF?
+        Control.isEnabled = true;
+
+        if (!Control.isEnabled)
+            gameObject.SetActive(false);
     }
 }

+ 285 - 222
src/MeidoPhotoStudio.Plugin/DragPoint/DragPointLight.cs

@@ -1,267 +1,330 @@
+using System;
+
 using UnityEngine;
 
-namespace MeidoPhotoStudio.Plugin
+namespace MeidoPhotoStudio.Plugin;
+
+public class DragPointLight : DragPointGeneral
 {
-    public class DragPointLight : DragPointGeneral
+    private readonly LightProperty[] lightProperties = new LightProperty[]
     {
-        public static EnvironmentManager EnvironmentManager { private get; set; }
-        private Light light;
-        public enum MPSLightType
-        {
-            Normal, Spot, Point, Disabled
-        }
-        public enum LightProp
-        {
-            LightRotX, LightRotY, Intensity, ShadowStrength, SpotAngle, Range, Red, Green, Blue
-        }
+        new(), new(), new(),
+    };
 
-        public bool IsActiveLight { get; set; }
-        public string Name { get; private set; } = string.Empty;
-        public bool IsMain { get; set; }
-        public MPSLightType SelectedLightType { get; private set; }
-        public LightProperty CurrentLightProperty => LightProperties[(int)SelectedLightType];
-        private readonly LightProperty[] LightProperties = new LightProperty[]
-        {
-            new LightProperty(),
-            new LightProperty(),
-            new LightProperty()
-        };
-        private bool isDisabled;
-        public bool IsDisabled
-        {
-            get => isDisabled;
-            set
-            {
-                isDisabled = value;
-                light.gameObject.SetActive(!isDisabled);
-            }
-        }
-        private bool isColourMode;
-        public bool IsColourMode
-        {
-            get => IsMain && isColourMode && SelectedLightType == MPSLightType.Normal;
-            set
-            {
-                if (!IsMain) return;
-                light.color = value ? Color.white : LightColour;
-                camera.backgroundColor = value ? LightColour : Color.black;
-                isColourMode = value;
-                LightColour = isColourMode ? camera.backgroundColor : light.color;
-                EnvironmentManager.BGVisible = !IsColourMode;
-            }
-        }
-        public Quaternion Rotation
+    private Light light;
+    private bool isDisabled;
+    private bool isColourMode;
+
+    public enum MPSLightType
+    {
+        Normal,
+        Spot,
+        Point,
+        Disabled,
+    }
+
+    public enum LightProp
+    {
+        LightRotX,
+        LightRotY,
+        Intensity,
+        ShadowStrength,
+        SpotAngle,
+        Range,
+        Red,
+        Green,
+        Blue,
+    }
+
+    public static EnvironmentManager EnvironmentManager { private get; set; }
+
+    public bool IsActiveLight { get; set; }
+
+    public string Name { get; private set; } = string.Empty;
+
+    public bool IsMain { get; set; }
+
+    public MPSLightType SelectedLightType { get; private set; }
+
+    public LightProperty CurrentLightProperty =>
+        lightProperties[(int)SelectedLightType];
+
+    public bool IsDisabled
+    {
+        get => isDisabled;
+        set
         {
-            get => CurrentLightProperty.Rotation;
-            set => light.transform.rotation = CurrentLightProperty.Rotation = value;
+            isDisabled = value;
+            light.gameObject.SetActive(!isDisabled);
         }
-        public float Intensity
+    }
+
+    public bool IsColourMode
+    {
+        get => IsMain && isColourMode && SelectedLightType is MPSLightType.Normal;
+        set
         {
-            get => CurrentLightProperty.Intensity;
-            set => light.intensity = CurrentLightProperty.Intensity = value;
+            if (!IsMain)
+                return;
+
+            light.color = value ? Color.white : LightColour;
+            camera.backgroundColor = value ? LightColour : Color.black;
+            isColourMode = value;
+            LightColour = isColourMode ? camera.backgroundColor : light.color;
+            EnvironmentManager.BGVisible = !IsColourMode;
         }
-        public float Range
+    }
+
+    public Quaternion Rotation
+    {
+        get => CurrentLightProperty.Rotation;
+        set => light.transform.rotation = CurrentLightProperty.Rotation = value;
+    }
+
+    public float Intensity
+    {
+        get => CurrentLightProperty.Intensity;
+        set => light.intensity = CurrentLightProperty.Intensity = value;
+    }
+
+    public float Range
+    {
+        get => CurrentLightProperty.Range;
+        set => light.range = CurrentLightProperty.Range = value;
+    }
+
+    public float SpotAngle
+    {
+        get => CurrentLightProperty.SpotAngle;
+        set
         {
-            get => CurrentLightProperty.Range;
-            set => light.range = CurrentLightProperty.Range = value;
+            light.spotAngle = CurrentLightProperty.SpotAngle = value;
+            light.transform.localScale = Vector3.one * value;
         }
-        public float SpotAngle
+    }
+
+    public float ShadowStrength
+    {
+        get => CurrentLightProperty.ShadowStrength;
+        set => light.shadowStrength = CurrentLightProperty.ShadowStrength = value;
+    }
+
+    public float LightColorRed
+    {
+        get => IsColourMode ? camera.backgroundColor.r : CurrentLightProperty.LightColour.r;
+        set
         {
-            get => CurrentLightProperty.SpotAngle;
-            set
-            {
-                light.spotAngle = CurrentLightProperty.SpotAngle = value;
-                light.transform.localScale = Vector3.one * value;
-            }
+            var color = IsColourMode ? camera.backgroundColor : light.color;
+
+            LightColour = new(value, color.g, color.b);
         }
-        public float ShadowStrength
+    }
+
+    public float LightColorGreen
+    {
+        get => IsColourMode ? camera.backgroundColor.g : CurrentLightProperty.LightColour.r;
+        set
         {
-            get => CurrentLightProperty.ShadowStrength;
-            set => light.shadowStrength = CurrentLightProperty.ShadowStrength = value;
+            var color = IsColourMode ? camera.backgroundColor : light.color;
+
+            LightColour = new(color.r, value, color.b);
         }
-        public float LightColorRed
+    }
+
+    public float LightColorBlue
+    {
+        get => IsColourMode ? camera.backgroundColor.b : CurrentLightProperty.LightColour.r;
+        set
         {
-            get => IsColourMode ? camera.backgroundColor.r : CurrentLightProperty.LightColour.r;
-            set
-            {
-                Color color = IsColourMode ? camera.backgroundColor : light.color;
-                LightColour = new Color(value, color.g, color.b);
-            }
+            var color = IsColourMode ? camera.backgroundColor : light.color;
+
+            LightColour = new(color.r, color.g, value);
         }
-        public float LightColorGreen
+    }
+
+    public Color LightColour
+    {
+        get => IsColourMode ? camera.backgroundColor : CurrentLightProperty.LightColour;
+        set
         {
-            get => IsColourMode ? camera.backgroundColor.g : CurrentLightProperty.LightColour.r;
-            set
-            {
-                Color color = IsColourMode ? camera.backgroundColor : light.color;
-                LightColour = new Color(color.r, value, color.b);
-            }
+            var colour = CurrentLightProperty.LightColour = value;
+
+            if (IsColourMode)
+                camera.backgroundColor = colour;
+            else
+                light.color = colour;
         }
-        public float LightColorBlue
+    }
+
+    public static void SetLightProperties(Light light, LightProperty prop)
+    {
+        light.transform.rotation = prop.Rotation;
+        light.intensity = prop.Intensity;
+        light.range = prop.Range;
+        light.spotAngle = prop.SpotAngle;
+        light.shadowStrength = prop.ShadowStrength;
+        light.color = prop.LightColour;
+
+        if (light.type is LightType.Spot)
+            light.transform.localScale = Vector3.one * prop.SpotAngle;
+        else if (light.type is LightType.Point)
+            light.transform.localScale = Vector3.one * prop.Range;
+    }
+
+    public override void Set(Transform myObject)
+    {
+        base.Set(myObject);
+
+        light = myObject.gameObject.GetOrAddComponent<Light>();
+
+        // TODO: Use trasnform.SetPositionAndRotation
+        light.transform.position = LightProperty.DefaultPosition;
+        light.transform.rotation = LightProperty.DefaultRotation;
+
+        SetLightType(MPSLightType.Normal);
+
+        ScaleFactor = 50f;
+        DefaultRotation = LightProperty.DefaultRotation;
+        DefaultPosition = LightProperty.DefaultPosition;
+    }
+
+    public void SetLightType(MPSLightType type)
+    {
+        const string spotName = "spot";
+        const string normalName = "normal";
+        const string pointName = "point";
+        const string mainName = "name";
+
+        var lightType = LightType.Directional;
+        var name = normalName;
+
+        SelectedLightType = type;
+
+        if (type is MPSLightType.Spot)
         {
-            get => IsColourMode ? camera.backgroundColor.b : CurrentLightProperty.LightColour.r;
-            set
-            {
-                Color color = IsColourMode ? camera.backgroundColor : light.color;
-                LightColour = new Color(color.r, color.g, value);
-            }
+            lightType = LightType.Spot;
+            name = spotName;
         }
-        public Color LightColour
+        else if (type is MPSLightType.Point)
         {
-            get => IsColourMode ? camera.backgroundColor : CurrentLightProperty.LightColour;
-            set
-            {
-                Color colour = CurrentLightProperty.LightColour = value;
-                if (IsColourMode) camera.backgroundColor = colour;
-                else light.color = colour;
-            }
+            lightType = LightType.Point;
+            name = pointName;
         }
 
-        public static void SetLightProperties(Light light, LightProperty prop)
-        {
-            light.transform.rotation = prop.Rotation;
-            light.intensity = prop.Intensity;
-            light.range = prop.Range;
-            light.spotAngle = prop.SpotAngle;
-            light.shadowStrength = prop.ShadowStrength;
-            light.color = prop.LightColour;
-            if (light.type == LightType.Spot) light.transform.localScale = Vector3.one * prop.SpotAngle;
-            else if (light.type == LightType.Point) light.transform.localScale = Vector3.one * prop.Range;
-        }
+        light.type = lightType;
+        Name = IsMain ? mainName : name;
 
-        public override void Set(Transform myObject)
-        {
-            base.Set(myObject);
-            light = myObject.gameObject.GetOrAddComponent<Light>();
+        if (IsMain)
+            EnvironmentManager.BGVisible = !(IsColourMode && SelectedLightType is MPSLightType.Normal);
 
-            light.transform.position = LightProperty.DefaultPosition;
-            light.transform.rotation = LightProperty.DefaultRotation;
+        SetProps();
+        ApplyDragType();
+    }
 
-            SetLightType(MPSLightType.Normal);
-            ScaleFactor = 50f;
-            DefaultRotation = LightProperty.DefaultRotation;
-            DefaultPosition = LightProperty.DefaultPosition;
-        }
+    public void SetRotation(float x, float y) =>
+        Rotation = Quaternion.Euler(x, y, Rotation.eulerAngles.z);
 
-        protected override void OnDestroy()
+    public void SetProp(LightProp prop, float value)
+    {
+        switch (prop)
         {
-            if (!IsMain) Destroy(light.gameObject);
-            base.OnDestroy();
-        }
+            case LightProp.Intensity:
+                Intensity = value;
 
-        protected override void OnRotate()
-        {
-            CurrentLightProperty.Rotation = light.transform.rotation;
-            base.OnRotate();
-        }
+                break;
+            case LightProp.ShadowStrength:
+                ShadowStrength = value;
 
-        protected override void OnScale()
-        {
-            float value = light.transform.localScale.x;
-            if (SelectedLightType == MPSLightType.Point) Range = value;
-            else if (SelectedLightType == MPSLightType.Spot) SpotAngle = value;
-            base.OnScale();
-        }
+                break;
+            case LightProp.SpotAngle:
+                SpotAngle = value;
 
-        protected override void ApplyDragType()
-        {
-            if (Selecting || Moving) ApplyProperties(true, true, false);
-            else if (SelectedLightType != MPSLightType.Point && Rotating) ApplyProperties(true, true, false);
-            else if (SelectedLightType != MPSLightType.Normal && Scaling) ApplyProperties(true, true, false);
-            else if (!IsMain && Deleting) ApplyProperties(true, true, false);
-            else ApplyProperties(false, false, false);
+                break;
+            case LightProp.Range:
+                Range = value;
 
-            ApplyColours();
-        }
+                break;
+            case LightProp.Red:
+                LightColorRed = value;
 
-        public void SetLightType(MPSLightType type)
-        {
-            LightType lightType = LightType.Directional;
-
-            string name = "normal";
-            SelectedLightType = type;
-
-            if (type == MPSLightType.Spot)
-            {
-                lightType = LightType.Spot;
-                name = "spot";
-            }
-            else if (type == MPSLightType.Point)
-            {
-                lightType = LightType.Point;
-                name = "point";
-            }
-
-            light.type = lightType;
-            Name = IsMain ? "main" : name;
-
-            if (IsMain)
-            {
-                EnvironmentManager.BGVisible = !(IsColourMode && SelectedLightType == MPSLightType.Normal);
-            }
-
-            SetProps();
-            ApplyDragType();
-        }
+                break;
+            case LightProp.Green:
+                LightColorGreen = value;
 
-        public void SetRotation(float x, float y) => Rotation = Quaternion.Euler(x, y, Rotation.eulerAngles.z);
+                break;
+            case LightProp.Blue:
+                LightColorBlue = value;
 
-        public void SetProp(LightProp prop, float value)
-        {
-            switch (prop)
-            {
-                case LightProp.Intensity:
-                    Intensity = value;
-                    break;
-                case LightProp.ShadowStrength:
-                    ShadowStrength = value;
-                    break;
-                case LightProp.SpotAngle:
-                    SpotAngle = value;
-                    break;
-                case LightProp.Range:
-                    Range = value;
-                    break;
-                case LightProp.Red:
-                    LightColorRed = value;
-                    break;
-                case LightProp.Green:
-                    LightColorGreen = value;
-                    break;
-                case LightProp.Blue:
-                    LightColorBlue = value;
-                    break;
-            }
+                break;
+            case LightProp.LightRotX:
+            case LightProp.LightRotY:
+                // Do nothing
+                break;
+            default:
+                throw new ArgumentOutOfRangeException(nameof(prop));
         }
+    }
 
-        public void ResetLightProps()
-        {
-            LightProperties[(int)SelectedLightType] = new LightProperty();
-            SetProps();
-        }
+    public void ResetLightProps()
+    {
+        lightProperties[(int)SelectedLightType] = new();
+        SetProps();
+    }
 
-        public void ResetLightPosition() => light.transform.position = LightProperty.DefaultPosition;
+    public void ResetLightPosition() =>
+        light.transform.position = LightProperty.DefaultPosition;
 
-        private void SetProps()
-        {
-            SetLightProperties(light, CurrentLightProperty);
-            if (IsColourMode)
-            {
-                light.color = Color.white;
-                camera.backgroundColor = CurrentLightProperty.LightColour;
-            }
-        }
+    protected override void OnDestroy()
+    {
+        if (!IsMain)
+            Destroy(light.gameObject);
+
+        base.OnDestroy();
     }
 
-    public class LightProperty
+    protected override void OnRotate()
     {
-        public static readonly Vector3 DefaultPosition = new(0f, 1.9f, 0.4f);
-        public static readonly Quaternion DefaultRotation = Quaternion.Euler(40f, 180f, 0f);
-        public Quaternion Rotation { get; set; } = DefaultRotation;
-        public float Intensity { get; set; } = 0.95f;
-        public float Range { get; set; } = GameMain.Instance.MainLight.GetComponent<Light>().range;
-        public float SpotAngle { get; set; } = 50f;
-        public float ShadowStrength { get; set; } = 0.10f;
-        public Color LightColour { get; set; } = Color.white;
+        CurrentLightProperty.Rotation = light.transform.rotation;
+
+        base.OnRotate();
+    }
+
+    protected override void OnScale()
+    {
+        var value = light.transform.localScale.x;
+
+        if (SelectedLightType is MPSLightType.Point)
+            Range = value;
+        else if (SelectedLightType is MPSLightType.Spot)
+            SpotAngle = value;
+
+        base.OnScale();
+    }
+
+    protected override void ApplyDragType()
+    {
+        if (Selecting || Moving)
+            ApplyProperties(true, true, false);
+        else if (SelectedLightType is not MPSLightType.Point && Rotating)
+            ApplyProperties(true, true, false);
+        else if (SelectedLightType is not MPSLightType.Normal && Scaling)
+            ApplyProperties(true, true, false);
+        else if (!IsMain && Deleting)
+            ApplyProperties(true, true, false);
+        else
+            ApplyProperties(false, false, false);
+
+        ApplyColours();
+    }
+
+    private void SetProps()
+    {
+        SetLightProperties(light, CurrentLightProperty);
+
+        if (!IsColourMode)
+            return;
+
+        light.color = Color.white;
+        camera.backgroundColor = CurrentLightProperty.LightColour;
     }
 }

+ 55 - 50
src/MeidoPhotoStudio.Plugin/DragPoint/DragPointMeido.cs

@@ -1,65 +1,70 @@
 using System;
+
 using UnityEngine;
 
-namespace MeidoPhotoStudio.Plugin
+using static MeidoPhotoStudio.Plugin.CustomGizmo;
+
+namespace MeidoPhotoStudio.Plugin;
+
+public abstract class DragPointMeido : DragPoint
 {
-    using static CustomGizmo;
-    public abstract class DragPointMeido : DragPoint
+    public static readonly Vector3 BoneScale = Vector3.one * 0.04f;
+
+    protected const int JointUpper = 0;
+    protected const int JointMiddle = 1;
+    protected const int JointLower = 2;
+
+    protected Meido meido;
+    protected Maid maid;
+    protected bool isPlaying;
+    protected bool isBone;
+
+    public virtual bool IsBone
     {
-        public static readonly Vector3 boneScale = Vector3.one * 0.04f;
-        protected const int jointUpper = 0;
-        protected const int jointMiddle = 1;
-        protected const int jointLower = 2;
-        protected Meido meido;
-        protected Maid maid;
-        protected IKCtrlData IkCtrlData => meido.Body.IKCtrl.GetIKData("左手");
-        protected bool isPlaying;
-        protected bool isBone;
-        public virtual bool IsBone
+        get => isBone;
+        set
         {
-            get => isBone;
-            set
-            {
-                if (value != isBone)
-                {
-                    isBone = value;
-                    ApplyDragType();
-                }
-            }
-        }
+            if (value == isBone)
+                return;
 
-        public virtual void Initialize(Meido meido, Func<Vector3> position, Func<Vector3> rotation)
-        {
-            base.Initialize(position, rotation);
-            this.meido = meido;
-            maid = meido.Maid;
-            isPlaying = !meido.Stop;
+            isBone = value;
+            ApplyDragType();
         }
+    }
 
-        public override void AddGizmo(float scale = 0.25f, GizmoMode mode = GizmoMode.Local)
-        {
-            base.AddGizmo(scale, mode);
-            Gizmo.GizmoDrag += (s, a) =>
-            {
-                meido.Stop = true;
-                isPlaying = false;
-            };
-        }
+    protected IKCtrlData IkCtrlData =>
+        meido.Body.IKCtrl.GetIKData("左手");
 
-        protected override void OnMouseDown()
-        {
-            base.OnMouseDown();
-            isPlaying = !meido.Stop;
-        }
+    public virtual void Initialize(Meido meido, Func<Vector3> position, Func<Vector3> rotation)
+    {
+        Initialize(position, rotation);
 
-        protected void InitializeIK(TBody.IKCMO iKCmo, Transform upper, Transform middle, Transform lower)
-        {
-            iKCmo.Init(upper, middle, lower, maid.body0);
-        }
+        this.meido = meido;
+        maid = meido.Maid;
+        isPlaying = !meido.Stop;
+    }
 
-        protected void Porc(TBody.IKCMO ikCmo, IKCtrlData ikData, Transform upper, Transform middle, Transform lower)
+    public override void AddGizmo(float scale = 0.25f, GizmoMode mode = GizmoMode.Local)
+    {
+        base.AddGizmo(scale, mode);
+
+        Gizmo.GizmoDrag += (_, _) =>
         {
-            ikCmo.Porc(upper, middle, lower, CursorPosition(), Vector3.zero, ikData);
-        }
+            meido.Stop = true;
+            isPlaying = false;
+        };
     }
+
+    protected override void OnMouseDown()
+    {
+        base.OnMouseDown();
+
+        isPlaying = !meido.Stop;
+    }
+
+    protected void InitializeIK(TBody.IKCMO iKCmo, Transform upper, Transform middle, Transform lower) =>
+        iKCmo.Init(upper, middle, lower, maid.body0);
+
+    protected void Porc(TBody.IKCMO ikCmo, IKCtrlData ikData, Transform upper, Transform middle, Transform lower) =>
+        ikCmo.Porc(upper, middle, lower, CursorPosition(), Vector3.zero, ikData);
 }

+ 0 - 47
src/MeidoPhotoStudio.Plugin/DragPoint/DragPointOther.cs

@@ -1,47 +0,0 @@
-using System.Collections.Generic;
-using UnityEngine;
-using UnityEngine.Rendering;
-
-namespace MeidoPhotoStudio.Plugin
-{
-    public class DragPointBody : DragPointGeneral
-    {
-        public bool IsCube;
-        private bool isIK;
-        public bool IsIK
-        {
-            get => isIK;
-            set
-            {
-                if (isIK != value)
-                {
-                    isIK = value;
-                    ApplyDragType();
-                }
-            }
-        }
-        protected override void ApplyDragType()
-        {
-            bool enabled = !IsIK && (Transforming || Selecting);
-            bool select = IsIK && Selecting;
-            ApplyProperties(enabled || select, IsCube && enabled, false);
-
-            if (IsCube) ApplyColours();
-        }
-    }
-
-    public class DragPointBG : DragPointGeneral
-    {
-        public override void Set(Transform myObject)
-        {
-            base.Set(myObject);
-            DefaultPosition = myObject.position;
-        }
-
-        protected override void ApplyDragType()
-        {
-            ApplyProperties(Transforming, Transforming, Rotating);
-            ApplyColours();
-        }
-    }
-}

+ 64 - 83
src/MeidoPhotoStudio.Plugin/DragPoint/DragPointProp.cs

@@ -1,113 +1,94 @@
-using System.Collections.Generic;
-using System.IO;
+using System.Collections.Generic;
 using System.Linq;
+
 using UnityEngine;
 using UnityEngine.Rendering;
 
-namespace MeidoPhotoStudio.Plugin
+namespace MeidoPhotoStudio.Plugin;
+
+public class DragPointProp : DragPointGeneral
 {
-    public class DragPointProp : DragPointGeneral
-    {
-        private List<Renderer> renderers;
-        public AttachPointInfo AttachPointInfo { get; private set; } = AttachPointInfo.Empty;
-        public string Name => MyGameObject.name;
-        public string assetName = string.Empty;
-        public PropInfo Info { get; set; }
+    public string AssetName = string.Empty;
 
-        public bool ShadowCasting
-        {
-            get => renderers.Count != 0 && renderers.Any(r => r.shadowCastingMode == ShadowCastingMode.On);
-            set
-            {
-                foreach (var renderer in renderers)
-                    renderer.shadowCastingMode = value ? ShadowCastingMode.On : ShadowCastingMode.Off;
-            }
-        }
+    private List<Renderer> renderers;
 
-        public override void Set(Transform myObject)
-        {
-            base.Set(myObject);
-            DefaultRotation = MyObject.rotation;
-            DefaultPosition = MyObject.position;
-            DefaultScale = MyObject.localScale;
-            renderers = new List<Renderer>(MyObject.GetComponentsInChildren<Renderer>());
-        }
+    public AttachPointInfo AttachPointInfo { get; private set; } = AttachPointInfo.Empty;
 
-        public void AttachTo(Meido meido, AttachPoint point, bool keepWorldPosition = true)
-        {
-            var attachPoint = meido?.IKManager.GetAttachPointTransform(point);
+    public PropInfo Info { get; set; }
 
-            AttachPointInfo = meido == null ? AttachPointInfo.Empty : new AttachPointInfo(point, meido);
+    public string Name =>
+        MyGameObject.name;
 
-            var position = MyObject.position;
-            var rotation = MyObject.rotation;
-            var scale = MyObject.localScale;
+    public bool ShadowCasting
+    {
+        get => renderers.Count is not 0 && renderers.Any(r => r.shadowCastingMode is ShadowCastingMode.On);
+        set
+        {
+            foreach (var renderer in renderers)
+                renderer.shadowCastingMode = value ? ShadowCastingMode.On : ShadowCastingMode.Off;
+        }
+    }
 
-            MyObject.transform.SetParent(attachPoint, keepWorldPosition);
+    public override void Set(Transform myObject)
+    {
+        base.Set(myObject);
 
-            if (keepWorldPosition)
-            {
-                MyObject.position = position;
-                MyObject.rotation = rotation;
-            }
-            else
-            {
-                MyObject.localPosition = Vector3.zero;
-                MyObject.rotation = Quaternion.identity;
-            }
+        DefaultRotation = MyObject.rotation;
+        DefaultPosition = MyObject.position;
+        DefaultScale = MyObject.localScale;
+        renderers = new(MyObject.GetComponentsInChildren<Renderer>());
+    }
 
-            MyObject.localScale = scale;
+    public void AttachTo(Meido meido, AttachPoint point, bool keepWorldPosition = true)
+    {
+        var attachPoint = meido?.IKManager.GetAttachPointTransform(point);
 
-            if (attachPoint == null) Utility.FixGameObjectScale(MyGameObject);
-        }
+        AttachPointInfo = meido is null ? AttachPointInfo.Empty : new(point, meido);
 
-        public void DetachFrom(bool keepWorldPosition = true) => AttachTo(null, AttachPoint.None, keepWorldPosition);
+        // TODO: Use transform.SetPositionAndRotation MyObject.position = position;
+        var position = MyObject.position;
+        var rotation = MyObject.rotation;
+        var scale = MyObject.localScale;
 
-        public void DetachTemporary()
-        {
-            MyObject.transform.SetParent(null, true);
-            Utility.FixGameObjectScale(MyGameObject);
-        }
+        MyObject.transform.SetParent(attachPoint, keepWorldPosition);
 
-        protected override void ApplyDragType()
+        if (keepWorldPosition)
         {
-            var active = DragPointEnabled && Transforming || Special;
-            ApplyProperties(active, active, GizmoEnabled && Rotating);
-            ApplyColours();
+            MyObject.rotation = rotation;
         }
-
-        protected override void OnDestroy()
+        else
         {
-            Destroy(MyGameObject);
-            base.OnDestroy();
+            MyObject.localPosition = Vector3.zero;
+            MyObject.rotation = Quaternion.identity;
         }
-    }
 
-    public class PropInfo
-    {
-        public enum PropType { Mod, MyRoom, Bg, Odogu }
+        MyObject.localScale = scale;
+
+        if (!attachPoint)
+            Utility.FixGameObjectScale(MyGameObject);
+    }
 
-        public PropType Type { get; }
-        public string IconFile { get; set; }
-        public string Filename { get; set; }
-        public string SubFilename { get; set; }
-        public int MyRoomID { get; set; }
+    public void DetachFrom(bool keepWorldPosition = true) =>
+        AttachTo(null, AttachPoint.None, keepWorldPosition);
 
-        public PropInfo(PropType type) => Type = type;
+    public void DetachTemporary()
+    {
+        MyObject.transform.SetParent(null, true);
+        Utility.FixGameObjectScale(MyGameObject);
+    }
 
-        public static PropInfo FromModItem(ModItem modItem) => new(PropType.Mod)
-        {
-            Filename = modItem.IsOfficialMod ? Path.GetFileName(modItem.MenuFile) : modItem.MenuFile,
-            SubFilename = modItem.BaseMenuFile
-        };
+    protected override void ApplyDragType()
+    {
+        var active = DragPointEnabled && Transforming || Special;
 
-        public static PropInfo FromMyRoom(MyRoomItem myRoomItem) => new(PropType.MyRoom)
-        {
-            MyRoomID = myRoomItem.ID, Filename = myRoomItem.PrefabName
-        };
+        ApplyProperties(active, active, GizmoEnabled && Rotating);
+        ApplyColours();
+    }
 
-        public static PropInfo FromBg(string name) => new(PropType.Bg) { Filename = name };
+    protected override void OnDestroy()
+    {
+        Destroy(MyGameObject);
 
-        public static PropInfo FromGameProp(string name) => new(PropType.Odogu) { Filename = name };
+        base.OnDestroy();
     }
 }

+ 21 - 0
src/MeidoPhotoStudio.Plugin/DragPoint/LightProperty.cs

@@ -0,0 +1,21 @@
+using UnityEngine;
+
+namespace MeidoPhotoStudio.Plugin;
+
+public class LightProperty
+{
+    public static readonly Vector3 DefaultPosition = new(0f, 1.9f, 0.4f);
+    public static readonly Quaternion DefaultRotation = Quaternion.Euler(40f, 180f, 0f);
+
+    public Quaternion Rotation { get; set; } = DefaultRotation;
+
+    public float Intensity { get; set; } = 0.95f;
+
+    public float Range { get; set; } = GameMain.Instance.MainLight.GetComponent<Light>().range;
+
+    public float SpotAngle { get; set; } = 50f;
+
+    public float ShadowStrength { get; set; } = 0.10f;
+
+    public Color LightColour { get; set; } = Color.white;
+}

+ 47 - 0
src/MeidoPhotoStudio.Plugin/DragPoint/PropInfo.cs

@@ -0,0 +1,47 @@
+using System.IO;
+
+namespace MeidoPhotoStudio.Plugin;
+
+public class PropInfo
+{
+    public PropInfo(PropType type) =>
+        Type = type;
+
+    public enum PropType
+    {
+        Mod,
+        MyRoom,
+        Bg,
+        Odogu,
+    }
+
+    public PropType Type { get; }
+
+    public string IconFile { get; set; }
+
+    public string Filename { get; set; }
+
+    public string SubFilename { get; set; }
+
+    public int MyRoomID { get; set; }
+
+    public static PropInfo FromModItem(ModItem modItem) =>
+        new(PropType.Mod)
+        {
+            Filename = modItem.IsOfficialMod ? Path.GetFileName(modItem.MenuFile) : modItem.MenuFile,
+            SubFilename = modItem.BaseMenuFile,
+        };
+
+    public static PropInfo FromMyRoom(MyRoomItem myRoomItem) =>
+        new(PropType.MyRoom)
+        {
+            MyRoomID = myRoomItem.ID,
+            Filename = myRoomItem.PrefabName,
+        };
+
+    public static PropInfo FromBg(string name) =>
+        new(PropType.Bg) { Filename = name };
+
+    public static PropInfo FromGameProp(string name) =>
+        new(PropType.Odogu) { Filename = name };
+}

+ 10 - 5
src/MeidoPhotoStudio.Plugin/GUI/Controls/BaseControl.cs

@@ -1,12 +1,17 @@
 using System;
+
 using UnityEngine;
 
-namespace MeidoPhotoStudio.Plugin
+namespace MeidoPhotoStudio.Plugin;
+
+public abstract class BaseControl
 {
-    public abstract class BaseControl
+    public event EventHandler ControlEvent;
+
+    public virtual void Draw(params GUILayoutOption[] layoutOptions)
     {
-        public event EventHandler ControlEvent;
-        public virtual void Draw(params GUILayoutOption[] layoutOptions) { }
-        public virtual void OnControlEvent(EventArgs args) => ControlEvent?.Invoke(this, args);
     }
+
+    public virtual void OnControlEvent(EventArgs args) =>
+        ControlEvent?.Invoke(this, args);
 }

+ 18 - 13
src/MeidoPhotoStudio.Plugin/GUI/Controls/Button.cs

@@ -1,21 +1,26 @@
 using System;
+
 using UnityEngine;
 
-namespace MeidoPhotoStudio.Plugin
+namespace MeidoPhotoStudio.Plugin;
+
+public class Button : BaseControl
 {
-    public class Button : BaseControl
+    public Button(string label) =>
+        Label = label;
+
+    public string Label { get; set; }
+
+    public override void Draw(params GUILayoutOption[] layoutOptions)
     {
-        public string Label { get; set; }
-        public Button(string label) => Label = label;
-        public void Draw(GUIStyle buttonStyle, params GUILayoutOption[] layoutOptions)
-        {
-            if (GUILayout.Button(Label, buttonStyle, layoutOptions)) OnControlEvent(EventArgs.Empty);
-        }
+        var buttonStyle = new GUIStyle(GUI.skin.button);
 
-        public override void Draw(params GUILayoutOption[] layoutOptions)
-        {
-            GUIStyle buttonStyle = new GUIStyle(GUI.skin.button);
-            Draw(buttonStyle, layoutOptions);
-        }
+        Draw(buttonStyle, layoutOptions);
+    }
+
+    public void Draw(GUIStyle buttonStyle, params GUILayoutOption[] layoutOptions)
+    {
+        if (GUILayout.Button(Label, buttonStyle, layoutOptions))
+            OnControlEvent(EventArgs.Empty);
     }
 }

+ 44 - 34
src/MeidoPhotoStudio.Plugin/GUI/Controls/ComboBox.cs

@@ -1,47 +1,57 @@
 using UnityEngine;
 
-namespace MeidoPhotoStudio.Plugin
+namespace MeidoPhotoStudio.Plugin;
+
+public class ComboBox : BaseControl
 {
-    public class ComboBox : BaseControl
+    private readonly TextField textField = new();
+
+    public ComboBox(string[] itemList)
     {
-        private readonly TextField textField = new TextField();
-        public Dropdown BaseDropDown { get; }
-        public string Value
-        {
-            get => textField.Value;
-            set => textField.Value = value;
-        }
+        BaseDropDown = new("▾", itemList);
+        BaseDropDown.SelectionChange += (_, _) =>
+            textField.Value = BaseDropDown.SelectedItem;
 
-        public ComboBox(string[] itemList)
-        {
-            BaseDropDown = new Dropdown("▾", itemList);
-            BaseDropDown.SelectionChange += (s, a) => textField.Value = BaseDropDown.SelectedItem;
-            Value = itemList[0];
-        }
+        Value = itemList[0];
+    }
+
+    public Dropdown BaseDropDown { get; }
+
+    public string Value
+    {
+        get => textField.Value;
+        set => textField.Value = value;
+    }
 
-        public void SetDropdownItems(string[] itemList)
+    public override void Draw(params GUILayoutOption[] layoutOptions)
+    {
+        var buttonStyle = new GUIStyle(GUI.skin.button)
         {
-            string oldValue = Value;
-            BaseDropDown.SetDropdownItems(itemList);
-            Value = oldValue;
-        }
+            alignment = TextAnchor.MiddleCenter,
+        };
 
-        public void SetDropdownItem(int index, string newItem) => BaseDropDown.SetDropdownItem(index, newItem);
+        Draw(buttonStyle, layoutOptions);
+    }
 
-        public void SetDropdownItem(string newItem) => BaseDropDown.SetDropdownItem(newItem);
+    public void SetDropdownItems(string[] itemList)
+    {
+        var oldValue = Value;
 
-        public override void Draw(params GUILayoutOption[] layoutOptions)
-        {
-            GUIStyle buttonStyle = new GUIStyle(GUI.skin.button) { alignment = TextAnchor.MiddleCenter };
-            Draw(buttonStyle, layoutOptions);
-        }
+        BaseDropDown.SetDropdownItems(itemList);
+        Value = oldValue;
+    }
 
-        public void Draw(GUIStyle style, params GUILayoutOption[] layoutOptions)
-        {
-            GUILayout.BeginHorizontal();
-            textField.Draw(new GUIStyle(GUI.skin.textField), layoutOptions);
-            BaseDropDown.Draw(style, GUILayout.ExpandWidth(false));
-            GUILayout.EndHorizontal();
-        }
+    public void SetDropdownItem(int index, string newItem) =>
+        BaseDropDown.SetDropdownItem(index, newItem);
+
+    public void SetDropdownItem(string newItem) =>
+        BaseDropDown.SetDropdownItem(newItem);
+
+    public void Draw(GUIStyle style, params GUILayoutOption[] layoutOptions)
+    {
+        GUILayout.BeginHorizontal();
+        textField.Draw(new(GUI.skin.textField), layoutOptions);
+        BaseDropDown.Draw(style, GUILayout.ExpandWidth(false));
+        GUILayout.EndHorizontal();
     }
 }

+ 137 - 310
src/MeidoPhotoStudio.Plugin/GUI/Controls/DropDown.cs

@@ -1,359 +1,186 @@
 using System;
+
 using UnityEngine;
-namespace MeidoPhotoStudio.Plugin
+
+using DropdownCloseArgs = MeidoPhotoStudio.Plugin.DropdownHelper.DropdownCloseArgs;
+using DropdownSelectArgs = MeidoPhotoStudio.Plugin.DropdownHelper.DropdownSelectArgs;
+
+namespace MeidoPhotoStudio.Plugin;
+
+public class Dropdown : BaseControl
 {
-    using DropdownSelectArgs = DropdownHelper.DropdownSelectArgs;
-    using DropdownCloseArgs = DropdownHelper.DropdownCloseArgs;
-    public class Dropdown : BaseControl
+    private readonly string label;
+    private readonly bool isMenu;
+
+    private Vector2 elementSize;
+    private int selectedItemIndex;
+    private bool clickedYou;
+    private bool showDropdown;
+    private Vector2 scrollPos;
+    private Rect buttonRect;
+
+    public Dropdown(string label, string[] itemList, int selectedItemIndex = 0)
+        : this(itemList, selectedItemIndex)
     {
-        public event EventHandler SelectionChange;
-        public event EventHandler DropdownOpen;
-        public event EventHandler DropdownClose;
-        private bool clickedYou;
-        private bool showDropdown;
-        private readonly string label;
-        private readonly bool isMenu;
-        public string[] DropdownList { get; private set; }
-        public int DropdownID { get; }
-        private Vector2 scrollPos;
-        public Vector2 ScrollPos => scrollPos;
-        private Rect buttonRect;
-        public Rect ButtonRect
-        {
-            get => buttonRect;
-            private set => buttonRect = value;
-        }
-        private Vector2 elementSize;
-        public Vector2 ElementSize => elementSize;
-        private int selectedItemIndex;
-        public int SelectedItemIndex
-        {
-            get => selectedItemIndex;
-            set
-            {
-                selectedItemIndex = Mathf.Clamp(value, 0, DropdownList.Length - 1);
-                OnDropdownEvent(SelectionChange);
-            }
-        }
-        public string SelectedItem => DropdownList[SelectedItemIndex];
+        this.label = label;
 
-        public Dropdown(string label, string[] itemList, int selectedItemIndex = 0)
-            : this(itemList, selectedItemIndex)
-        {
-            isMenu = true;
-            this.label = label;
-        }
+        isMenu = true;
+    }
 
-        public Dropdown(string[] itemList, int selectedItemIndex = 0)
-        {
-            DropdownID = DropdownHelper.DropdownID;
-            SetDropdownItems(itemList, selectedItemIndex);
+    public Dropdown(string[] itemList, int selectedItemIndex = 0)
+    {
+        DropdownID = DropdownHelper.DropdownID;
+        SetDropdownItems(itemList, selectedItemIndex);
 
-            DropdownHelper.SelectionChange += OnChangeSelection;
-            DropdownHelper.DropdownClose += OnCloseDropdown;
-        }
+        DropdownHelper.SelectionChange += OnChangeSelection;
+        DropdownHelper.DropdownClose += OnCloseDropdown;
+    }
 
-        // TODO: I don't think this works the way I think it does
-        ~Dropdown()
-        {
-            DropdownHelper.SelectionChange -= OnChangeSelection;
-            DropdownHelper.DropdownClose -= OnCloseDropdown;
-        }
+    // TODO: I don't think this works the way I think it does
+    ~Dropdown()
+    {
+        DropdownHelper.SelectionChange -= OnChangeSelection;
+        DropdownHelper.DropdownClose -= OnCloseDropdown;
+    }
 
-        public void SetDropdownItems(string[] itemList, int selectedItemIndex = -1)
-        {
-            if (selectedItemIndex < 0) selectedItemIndex = SelectedItemIndex;
-            elementSize = Vector2.zero;
-
-            // TODO: Calculate scrollpos position maybe
-            if ((selectedItemIndex != this.selectedItemIndex) || (itemList.Length != DropdownList?.Length))
-            {
-                scrollPos = Vector2.zero;
-            }
-            DropdownList = itemList;
-            SelectedItemIndex = selectedItemIndex;
-        }
+    public event EventHandler SelectionChange;
 
-        public void SetDropdownItem(int index, string newItem)
-        {
-            if (index < 0 || index >= DropdownList.Length) return;
+    public event EventHandler DropdownOpen;
 
-            Vector2 itemSize = DropdownHelper.CalculateElementSize(newItem);
+    public event EventHandler DropdownClose;
 
-            if (itemSize.x > ElementSize.x) elementSize = itemSize;
+    public int DropdownID { get; }
 
-            DropdownList[index] = newItem;
-        }
+    public string[] DropdownList { get; private set; }
 
-        public void SetDropdownItem(string newItem)
-        {
-            SetDropdownItem(SelectedItemIndex, newItem);
-        }
+    public Vector2 ScrollPos =>
+        scrollPos;
 
-        public void Step(int dir)
-        {
-            dir = (int)Mathf.Sign(dir);
-            SelectedItemIndex = Utility.Wrap(SelectedItemIndex + dir, 0, DropdownList.Length);
-        }
+    public string SelectedItem =>
+        DropdownList[SelectedItemIndex];
+
+    public Rect ButtonRect
+    {
+        get => buttonRect;
+        private set => buttonRect = value;
+    }
 
-        public void Draw(GUIStyle buttonStyle, params GUILayoutOption[] layoutOptions)
+    public Vector2 ElementSize =>
+        elementSize;
+
+    public int SelectedItemIndex
+    {
+        get => selectedItemIndex;
+        set
         {
-            Draw(buttonStyle, null, layoutOptions);
+            selectedItemIndex = Mathf.Clamp(value, 0, DropdownList.Length - 1);
+            OnDropdownEvent(SelectionChange);
         }
+    }
 
-        public void Draw(GUIStyle buttonStyle, GUIStyle dropdownStyle = null, params GUILayoutOption[] layoutOptions)
-        {
-            bool clicked = GUILayout.Button(
-                isMenu ? label : DropdownList[selectedItemIndex], buttonStyle, layoutOptions
-            );
+    public void SetDropdownItems(string[] itemList, int selectedItemIndex = -1)
+    {
+        if (selectedItemIndex < 0)
+            selectedItemIndex = SelectedItemIndex;
 
-            if (clicked)
-            {
-                showDropdown = !clickedYou;
-                clickedYou = false;
-            }
+        elementSize = Vector2.zero;
 
-            if (showDropdown && Event.current.type == EventType.Repaint) InitializeDropdown(dropdownStyle);
-        }
+        // TODO: Calculate scrollpos position maybe
+        if (selectedItemIndex != this.selectedItemIndex || itemList.Length != DropdownList?.Length)
+            scrollPos = Vector2.zero;
 
-        public override void Draw(params GUILayoutOption[] layoutOptions)
-        {
-            GUIStyle buttonStyle = new GUIStyle(GUI.skin.button) { alignment = TextAnchor.MiddleLeft };
-            Draw(buttonStyle, layoutOptions);
-        }
+        DropdownList = itemList;
+        SelectedItemIndex = selectedItemIndex;
+    }
 
-        private void OnChangeSelection(object sender, DropdownSelectArgs args)
-        {
-            if (args.DropdownID == DropdownID)
-            {
-                SelectedItemIndex = args.SelectedItemIndex;
-            }
-        }
+    public void SetDropdownItem(int index, string newItem)
+    {
+        if (index < 0 || index >= DropdownList.Length)
+            return;
 
-        private void OnCloseDropdown(object sender, DropdownCloseArgs args)
-        {
-            if (args.DropdownID == DropdownID)
-            {
-                scrollPos = args.ScrollPos;
-                clickedYou = args.ClickedYou;
+        var itemSize = DropdownHelper.CalculateElementSize(newItem);
 
-                if (clickedYou) OnDropdownEvent(SelectionChange);
+        if (itemSize.x > ElementSize.x)
+            elementSize = itemSize;
 
-                OnDropdownEvent(DropdownClose);
-            }
-        }
+        DropdownList[index] = newItem;
+    }
 
-        private void InitializeDropdown(GUIStyle dropdownStyle)
-        {
-            showDropdown = false;
-
-            buttonRect = GUILayoutUtility.GetLastRect();
-            Vector2 rectPos = GUIUtility.GUIToScreenPoint(new Vector2(buttonRect.x, buttonRect.y));
-            buttonRect.x = rectPos.x;
-            buttonRect.y = rectPos.y;
-            if (elementSize == Vector2.zero)
-            {
-                elementSize = DropdownHelper.CalculateElementSize(DropdownList, dropdownStyle);
-            }
-            DropdownHelper.Set(this, dropdownStyle);
-
-            OnDropdownEvent(DropdownOpen);
-        }
+    public void SetDropdownItem(string newItem) =>
+        SetDropdownItem(SelectedItemIndex, newItem);
 
-        private void OnDropdownEvent(EventHandler handler)
-        {
-            handler?.Invoke(this, EventArgs.Empty);
-        }
+    public void Step(int dir)
+    {
+        dir = (int)Mathf.Sign(dir);
+        SelectedItemIndex = Utility.Wrap(SelectedItemIndex + dir, 0, DropdownList.Length);
     }
 
-    public static class DropdownHelper
+    public void Draw(GUIStyle buttonStyle, params GUILayoutOption[] layoutOptions) =>
+        Draw(buttonStyle, null, layoutOptions);
+
+    public void Draw(GUIStyle buttonStyle, GUIStyle dropdownStyle = null, params GUILayoutOption[] layoutOptions)
     {
-        public static event EventHandler<DropdownSelectArgs> SelectionChange;
-        public static event EventHandler<DropdownCloseArgs> DropdownClose;
-        private static int dropdownID = 100;
-        public static int DropdownID => dropdownID++;
-        private static GUIStyle defaultDropdownStyle;
-        public static GUIStyle DefaultDropdownStyle
+        var clicked = GUILayout.Button(isMenu ? label : DropdownList[selectedItemIndex], buttonStyle, layoutOptions);
+
+        if (clicked)
         {
-            get
-            {
-                if (!initialized) InitializeStyle();
-                return defaultDropdownStyle;
-            }
+            showDropdown = !clickedYou;
+            clickedYou = false;
         }
-        private static GUIStyle dropdownStyle;
-        private static GUIStyle windowStyle;
-        private static Rect buttonRect;
-        private static string[] dropdownList;
-        private static Vector2 scrollPos;
-        private static int currentDropdownID;
-        private static int selectedItemIndex;
-        private static bool initialized;
-        public static bool Visible { get; set; }
-        public static bool DropdownOpen { get; private set; }
-        private static bool onScrollBar;
-        public static Rect dropdownWindow;
-        private static Rect dropdownScrollRect;
-        private static Rect dropdownRect;
-
-        public static Vector2 CalculateElementSize(string item, GUIStyle style = null)
+
+        if (showDropdown && Event.current.type is EventType.Repaint)
+            InitializeDropdown(dropdownStyle);
+    }
+
+    public override void Draw(params GUILayoutOption[] layoutOptions)
+    {
+        var buttonStyle = new GUIStyle(GUI.skin.button)
         {
-            if (!initialized) InitializeStyle();
+            alignment = TextAnchor.MiddleLeft,
+        };
 
-            style ??= DefaultDropdownStyle;
+        Draw(buttonStyle, layoutOptions);
+    }
 
-            return style.CalcSize(new GUIContent(item));
-        }
+    private void OnChangeSelection(object sender, DropdownSelectArgs args)
+    {
+        if (args.DropdownID == DropdownID)
+            SelectedItemIndex = args.SelectedItemIndex;
+    }
 
-        public static Vector2 CalculateElementSize(string[] list, GUIStyle style = null)
-        {
-            if (!initialized) InitializeStyle();
+    private void OnCloseDropdown(object sender, DropdownCloseArgs args)
+    {
+        if (args.DropdownID != DropdownID)
+            return;
 
-            style ??= DefaultDropdownStyle;
+        scrollPos = args.ScrollPos;
+        clickedYou = args.ClickedYou;
 
-            GUIContent content = new GUIContent(list[0]);
-            Vector2 calculatedSize = style.CalcSize(content);
-            for (int i = 1; i < list.Length; i++)
-            {
-                content.text = list[i];
-                Vector2 calcSize = style.CalcSize(content);
-                if (calcSize.x > calculatedSize.x) calculatedSize = calcSize;
-            }
+        if (clickedYou)
+            OnDropdownEvent(SelectionChange);
 
-            return calculatedSize;
-        }
+        OnDropdownEvent(DropdownClose);
+    }
 
-        public static void Set(Dropdown dropdown, GUIStyle style = null)
-        {
-            dropdownStyle = style ?? DefaultDropdownStyle;
-            currentDropdownID = dropdown.DropdownID;
-            dropdownList = dropdown.DropdownList;
-            scrollPos = dropdown.ScrollPos;
-            selectedItemIndex = dropdown.SelectedItemIndex;
-            scrollPos = dropdown.ScrollPos;
-            buttonRect = dropdown.ButtonRect;
-            Vector2 calculatedSize = dropdown.ElementSize;
-
-            float calculatedListHeight = calculatedSize.y * dropdownList.Length;
-
-            float heightAbove = buttonRect.y;
-            float heightBelow = Screen.height - heightAbove - buttonRect.height;
-
-            float rectWidth = Mathf.Max(calculatedSize.x + 5, buttonRect.width);
-            float rectHeight = Mathf.Min(calculatedListHeight, Mathf.Max(heightAbove, heightBelow));
-
-            if (calculatedListHeight > heightBelow && heightAbove > heightBelow)
-            {
-                dropdownWindow = new Rect(buttonRect.x, buttonRect.y - rectHeight, rectWidth + 18, rectHeight);
-            }
-            else
-            {
-                if (calculatedListHeight > heightBelow) rectHeight -= calculatedSize.y;
-                dropdownWindow = new Rect(buttonRect.x, buttonRect.y + buttonRect.height, rectWidth + 18, rectHeight);
-            }
-
-            dropdownWindow.x = Mathf.Clamp(dropdownWindow.x, 0, Screen.width - rectWidth - 18);
-
-            dropdownScrollRect = new Rect(0, 0, dropdownWindow.width, dropdownWindow.height);
-            dropdownRect = new Rect(0, 0, dropdownWindow.width - 18, calculatedListHeight);
-
-            DropdownOpen = true;
-            Visible = true;
-        }
+    private void InitializeDropdown(GUIStyle dropdownStyle)
+    {
+        showDropdown = false;
 
-        public static void HandleDropdown()
-        {
-            dropdownWindow = GUI.Window(Constants.dropdownWindowID, dropdownWindow, GUIFunc, "", windowStyle);
-            if (Input.mouseScrollDelta.y != 0f && Visible && dropdownWindow.Contains(Event.current.mousePosition))
-            {
-                Input.ResetInputAxes();
-            }
-        }
+        buttonRect = GUILayoutUtility.GetLastRect();
 
-        private static void GUIFunc(int id)
-        {
-            bool clicked = false;
-
-            if (Event.current.type == EventType.MouseUp) clicked = true;
-
-            scrollPos = GUI.BeginScrollView(dropdownScrollRect, scrollPos, dropdownRect);
-            int selection = GUI.SelectionGrid(dropdownRect, selectedItemIndex, dropdownList, 1, dropdownStyle);
-            GUI.EndScrollView();
-
-            bool clickedYou = false;
-            if (Utility.AnyMouseDown())
-            {
-                Vector2 mousePos = GUIUtility.GUIToScreenPoint(Event.current.mousePosition);
-                bool clickedMe = dropdownWindow.Contains(mousePos);
-                onScrollBar = mousePos.x > dropdownWindow.x + dropdownWindow.width - 12f;
-                if (buttonRect.Contains(mousePos)) clickedYou = true;
-                if (!clickedMe) DropdownOpen = false;
-            }
-
-            if (selection != selectedItemIndex || (clicked && !onScrollBar))
-            {
-                SelectionChange?.Invoke(null, new DropdownSelectArgs(currentDropdownID, selection));
-                DropdownOpen = false;
-            }
-
-            if (!DropdownOpen)
-            {
-                Visible = false;
-                DropdownClose?.Invoke(null, new DropdownCloseArgs(currentDropdownID, scrollPos, clickedYou));
-            }
-        }
+        var rectPos = GUIUtility.GUIToScreenPoint(new(buttonRect.x, buttonRect.y));
 
-        private static void InitializeStyle()
-        {
-            defaultDropdownStyle = new GUIStyle(GUI.skin.button)
-            {
-                alignment = TextAnchor.MiddleLeft,
-                margin = new RectOffset(0, 0, 0, 0)
-            };
-            defaultDropdownStyle.padding.top = defaultDropdownStyle.padding.bottom = 2;
-            defaultDropdownStyle.normal.background = Utility.MakeTex(2, 2, new Color(0f, 0f, 0f, 0.5f));
-            Texture2D whiteBackground = new Texture2D(2, 2);
-            defaultDropdownStyle.onHover.background
-                = defaultDropdownStyle.hover.background
-                = defaultDropdownStyle.onNormal.background
-                = whiteBackground;
-            defaultDropdownStyle.onHover.textColor
-                = defaultDropdownStyle.onNormal.textColor
-                = defaultDropdownStyle.hover.textColor
-                = Color.black;
-
-            windowStyle = new GUIStyle(GUI.skin.box)
-            {
-                padding = new RectOffset(0, 0, 0, 0),
-                alignment = TextAnchor.UpperRight
-            };
-            initialized = true;
-        }
+        buttonRect.x = rectPos.x;
+        buttonRect.y = rectPos.y;
 
-        public class DropdownEventArgs : EventArgs
-        {
-            public int DropdownID { get; }
-            public DropdownEventArgs(int dropdownID) => DropdownID = dropdownID;
-        }
+        if (elementSize == Vector2.zero)
+            elementSize = DropdownHelper.CalculateElementSize(DropdownList, dropdownStyle);
 
-        public class DropdownSelectArgs : DropdownEventArgs
-        {
-            public int SelectedItemIndex { get; }
-            public DropdownSelectArgs(int dropdownID, int selection) : base(dropdownID)
-            {
-                SelectedItemIndex = selection;
-            }
-        }
+        DropdownHelper.Set(this, dropdownStyle);
 
-        public class DropdownCloseArgs : DropdownEventArgs
-        {
-            public Vector2 ScrollPos { get; }
-            public bool ClickedYou { get; }
-            public DropdownCloseArgs(int dropdownID, Vector2 scrollPos, bool clickedYou = false) : base(dropdownID)
-            {
-                ScrollPos = scrollPos;
-                ClickedYou = clickedYou;
-            }
-        }
+        OnDropdownEvent(DropdownOpen);
     }
+
+    private void OnDropdownEvent(EventHandler handler) =>
+        handler?.Invoke(this, EventArgs.Empty);
 }

+ 230 - 0
src/MeidoPhotoStudio.Plugin/GUI/Controls/DropdownHelper.cs

@@ -0,0 +1,230 @@
+using System;
+
+using UnityEngine;
+
+namespace MeidoPhotoStudio.Plugin;
+
+public static class DropdownHelper
+{
+    public static Rect DropdownWindow;
+
+    private static int dropdownID = 100;
+    private static GUIStyle defaultDropdownStyle;
+    private static bool onScrollBar;
+    private static Rect dropdownScrollRect;
+    private static Rect dropdownRect;
+    private static GUIStyle dropdownStyle;
+    private static GUIStyle windowStyle;
+    private static Rect buttonRect;
+    private static string[] dropdownList;
+    private static Vector2 scrollPos;
+    private static int currentDropdownID;
+    private static int selectedItemIndex;
+    private static bool initialized;
+
+    public static event EventHandler<DropdownSelectArgs> SelectionChange;
+
+    public static event EventHandler<DropdownCloseArgs> DropdownClose;
+
+    public static int DropdownID =>
+        dropdownID++;
+
+    public static GUIStyle DefaultDropdownStyle
+    {
+        get
+        {
+            if (!initialized)
+                InitializeStyle();
+
+            return defaultDropdownStyle;
+        }
+    }
+
+    public static bool Visible { get; set; }
+
+    public static bool DropdownOpen { get; private set; }
+
+    public static Vector2 CalculateElementSize(string item, GUIStyle style = null)
+    {
+        if (!initialized)
+            InitializeStyle();
+
+        style ??= DefaultDropdownStyle;
+
+        return style.CalcSize(new(item));
+    }
+
+    public static Vector2 CalculateElementSize(string[] list, GUIStyle style = null)
+    {
+        if (!initialized)
+            InitializeStyle();
+
+        style ??= DefaultDropdownStyle;
+
+        var content = new GUIContent(list[0]);
+        var calculatedSize = style.CalcSize(content);
+
+        for (var i = 1; i < list.Length; i++)
+        {
+            content.text = list[i];
+
+            var calcSize = style.CalcSize(content);
+
+            if (calcSize.x > calculatedSize.x)
+                calculatedSize = calcSize;
+        }
+
+        return calculatedSize;
+    }
+
+    public static void Set(Dropdown dropdown, GUIStyle style = null)
+    {
+        dropdownStyle = style ?? DefaultDropdownStyle;
+        currentDropdownID = dropdown.DropdownID;
+        dropdownList = dropdown.DropdownList;
+        scrollPos = dropdown.ScrollPos;
+        selectedItemIndex = dropdown.SelectedItemIndex;
+        scrollPos = dropdown.ScrollPos;
+        buttonRect = dropdown.ButtonRect;
+
+        var calculatedSize = dropdown.ElementSize;
+        var calculatedListHeight = calculatedSize.y * dropdownList.Length;
+        var heightAbove = buttonRect.y;
+        var heightBelow = Screen.height - heightAbove - buttonRect.height;
+        var rectWidth = Mathf.Max(calculatedSize.x + 5, buttonRect.width);
+        var rectHeight = Mathf.Min(calculatedListHeight, Mathf.Max(heightAbove, heightBelow));
+
+        if (calculatedListHeight > heightBelow && heightAbove > heightBelow)
+        {
+            DropdownWindow = new(buttonRect.x, buttonRect.y - rectHeight, rectWidth + 18, rectHeight);
+        }
+        else
+        {
+            if (calculatedListHeight > heightBelow)
+                rectHeight -= calculatedSize.y;
+
+            DropdownWindow = new(buttonRect.x, buttonRect.y + buttonRect.height, rectWidth + 18, rectHeight);
+        }
+
+        DropdownWindow.x = Mathf.Clamp(DropdownWindow.x, 0, Screen.width - rectWidth - 18);
+
+        dropdownScrollRect = new(0, 0, DropdownWindow.width, DropdownWindow.height);
+        dropdownRect = new(0, 0, DropdownWindow.width - 18, calculatedListHeight);
+
+        DropdownOpen = true;
+        Visible = true;
+    }
+
+    public static void HandleDropdown()
+    {
+        DropdownWindow = GUI.Window(Constants.DropdownWindowID, DropdownWindow, GUIFunc, string.Empty, windowStyle);
+
+        if (Input.mouseScrollDelta.y is not 0f && Visible && DropdownWindow.Contains(Event.current.mousePosition))
+            Input.ResetInputAxes();
+    }
+
+    private static void GUIFunc(int id)
+    {
+        var clicked = false;
+
+        if (Event.current.type is EventType.MouseUp)
+            clicked = true;
+
+        scrollPos = GUI.BeginScrollView(dropdownScrollRect, scrollPos, dropdownRect);
+
+        var selection = GUI.SelectionGrid(dropdownRect, selectedItemIndex, dropdownList, 1, dropdownStyle);
+
+        GUI.EndScrollView();
+
+        var clickedYou = false;
+
+        if (Utility.AnyMouseDown())
+        {
+            var mousePos = GUIUtility.GUIToScreenPoint(Event.current.mousePosition);
+            var clickedMe = DropdownWindow.Contains(mousePos);
+
+            onScrollBar = mousePos.x > DropdownWindow.x + DropdownWindow.width - 12f;
+
+            if (buttonRect.Contains(mousePos))
+                clickedYou = true;
+
+            if (!clickedMe)
+                DropdownOpen = false;
+        }
+
+        if (selection != selectedItemIndex || clicked && !onScrollBar)
+        {
+            SelectionChange?.Invoke(null, new(currentDropdownID, selection));
+            DropdownOpen = false;
+        }
+
+        if (!DropdownOpen)
+        {
+            Visible = false;
+            DropdownClose?.Invoke(null, new(currentDropdownID, scrollPos, clickedYou));
+        }
+    }
+
+    private static void InitializeStyle()
+    {
+        defaultDropdownStyle = new(GUI.skin.button)
+        {
+            alignment = TextAnchor.MiddleLeft,
+            margin = new(0, 0, 0, 0),
+        };
+
+        defaultDropdownStyle.padding.top = defaultDropdownStyle.padding.bottom = 2;
+        defaultDropdownStyle.normal.background = Utility.MakeTex(2, 2, new(0f, 0f, 0f, 0.5f));
+
+        var whiteBackground = new Texture2D(2, 2);
+
+        defaultDropdownStyle.onHover.background
+            = defaultDropdownStyle.hover.background
+            = defaultDropdownStyle.onNormal.background
+            = whiteBackground;
+
+        defaultDropdownStyle.onHover.textColor
+            = defaultDropdownStyle.onNormal.textColor
+            = defaultDropdownStyle.hover.textColor
+            = Color.black;
+
+        windowStyle = new(GUI.skin.box)
+        {
+            padding = new(0, 0, 0, 0),
+            alignment = TextAnchor.UpperRight,
+        };
+
+        initialized = true;
+    }
+
+    public class DropdownEventArgs : EventArgs
+    {
+        public DropdownEventArgs(int dropdownID) =>
+            DropdownID = dropdownID;
+
+        public int DropdownID { get; }
+    }
+
+    public class DropdownSelectArgs : DropdownEventArgs
+    {
+        public DropdownSelectArgs(int dropdownID, int selection)
+            : base(dropdownID) =>
+            SelectedItemIndex = selection;
+
+        public int SelectedItemIndex { get; }
+    }
+
+    public class DropdownCloseArgs : DropdownEventArgs
+    {
+        public DropdownCloseArgs(int dropdownID, Vector2 scrollPos, bool clickedYou = false)
+            : base(dropdownID)
+        {
+            ScrollPos = scrollPos;
+            ClickedYou = clickedYou;
+        }
+
+        public Vector2 ScrollPos { get; }
+
+        public bool ClickedYou { get; }
+    }
+}

+ 50 - 43
src/MeidoPhotoStudio.Plugin/GUI/Controls/KeyRebindButton.cs

@@ -1,56 +1,63 @@
 using System;
+
 using UnityEngine;
 
-namespace MeidoPhotoStudio.Plugin
+namespace MeidoPhotoStudio.Plugin;
+
+public class KeyRebindButton : BaseControl
 {
-    public class KeyRebindButton : BaseControl
+    private readonly Button button;
+
+    private bool listening;
+    private KeyCode keyCode;
+
+    public KeyRebindButton(KeyCode code)
     {
-        private readonly Button button;
-        private bool listening;
-        private KeyCode keyCode;
-        public KeyCode KeyCode
-        {
-            get => keyCode;
-            set
-            {
-                keyCode = value;
-                button.Label = keyCode.ToString();
-            }
-        }
-        public KeyRebindButton(KeyCode code)
-        {
-            button = new Button(code.ToString());
-            button.ControlEvent += (s, a) => StartListening();
-        }
+        button = new(code.ToString());
+        button.ControlEvent += (_, _) =>
+            StartListening();
+    }
 
-        public void Draw(GUIStyle buttonStyle, params GUILayoutOption[] layoutOptions)
+    public KeyCode KeyCode
+    {
+        get => keyCode;
+        set
         {
-            GUI.enabled = !listening && !InputManager.Listening;
-            button.Draw(buttonStyle, layoutOptions);
-            GUI.enabled = true;
+            keyCode = value;
+            button.Label = keyCode.ToString();
         }
+    }
 
-        public override void Draw(params GUILayoutOption[] layoutOptions)
-        {
-            GUIStyle buttonStyle = new GUIStyle(GUI.skin.button);
-            Draw(buttonStyle, layoutOptions);
-        }
+    public override void Draw(params GUILayoutOption[] layoutOptions)
+    {
+        var buttonStyle = new GUIStyle(GUI.skin.button);
 
-        private void StartListening()
-        {
-            listening = true;
-            button.Label = string.Empty;
-            InputManager.StartListening();
-            InputManager.KeyChange += KeyChange;
-        }
+        Draw(buttonStyle, layoutOptions);
+    }
 
-        private void KeyChange(object sender, EventArgs args)
-        {
-            listening = false;
-            if (InputManager.CurrentKeyCode != KeyCode.Escape) KeyCode = InputManager.CurrentKeyCode;
-            else KeyCode = KeyCode;
-            InputManager.KeyChange -= KeyChange;
-            OnControlEvent(EventArgs.Empty);
-        }
+    public void Draw(GUIStyle buttonStyle, params GUILayoutOption[] layoutOptions)
+    {
+        GUI.enabled = !listening && !InputManager.Listening;
+        button.Draw(buttonStyle, layoutOptions);
+        GUI.enabled = true;
+    }
+
+    private void StartListening()
+    {
+        listening = true;
+        button.Label = string.Empty;
+        InputManager.StartListening();
+        InputManager.KeyChange += KeyChange;
+    }
+
+    private void KeyChange(object sender, EventArgs args)
+    {
+        listening = false;
+
+        KeyCode = InputManager.CurrentKeyCode is not KeyCode.Escape ? InputManager.CurrentKeyCode : KeyCode;
+
+        InputManager.KeyChange -= KeyChange;
+
+        OnControlEvent(EventArgs.Empty);
     }
 }

+ 32 - 28
src/MeidoPhotoStudio.Plugin/GUI/Controls/Modal.cs

@@ -1,39 +1,43 @@
 using UnityEngine;
 
-namespace MeidoPhotoStudio.Plugin
+namespace MeidoPhotoStudio.Plugin;
+
+public static class Modal
 {
-    public static class Modal
+    private static BaseWindow currentModal;
+
+    public static bool Visible
     {
-        private static BaseWindow currentModal;
-        public static bool Visible
+        get => currentModal?.Visible ?? false;
+        set
         {
-            get => currentModal?.Visible ?? false;
-            set
-            {
-                if (currentModal == null) return;
-                currentModal.Visible = value;
-            }
-        }
+            if (currentModal is null)
+                return;
 
-        public static void Show(BaseWindow modalWindow)
-        {
-            if (currentModal != null) Close();
-            currentModal = modalWindow;
-            Visible = true;
+            currentModal.Visible = value;
         }
+    }
 
-        public static void Close()
-        {
-            Visible = false;
-            currentModal = null;
-        }
+    public static void Show(BaseWindow modalWindow)
+    {
+        if (currentModal is not null)
+            Close();
 
-        public static void Draw()
-        {
-            GUIStyle windowStyle = new GUIStyle(GUI.skin.box);
-            currentModal.WindowRect = GUI.ModalWindow(
-                currentModal.windowID, currentModal.WindowRect, currentModal.GUIFunc, "", windowStyle
-            );
-        }
+        currentModal = modalWindow;
+        Visible = true;
+    }
+
+    public static void Close()
+    {
+        Visible = false;
+        currentModal = null;
+    }
+
+    public static void Draw()
+    {
+        var windowStyle = new GUIStyle(GUI.skin.box);
+
+        currentModal.WindowRect =
+            GUI.ModalWindow(currentModal.WindowID, currentModal.WindowRect, currentModal.GUIFunc, string.Empty, windowStyle);
     }
 }

+ 90 - 74
src/MeidoPhotoStudio.Plugin/GUI/Controls/SelectionGrid.cs

@@ -1,102 +1,118 @@
 using System;
+
 using UnityEngine;
 
-namespace MeidoPhotoStudio.Plugin
+namespace MeidoPhotoStudio.Plugin;
+
+public class SelectionGrid : BaseControl
 {
-    public class SelectionGrid : BaseControl
+    private SimpleToggle[] toggles;
+    private int selectedItemIndex;
+
+    public SelectionGrid(string[] items, int selected = 0)
     {
-        private SimpleToggle[] toggles;
-        private int selectedItemIndex;
-        public int SelectedItemIndex
-        {
-            get => selectedItemIndex;
-            set
-            {
-                selectedItemIndex = Mathf.Clamp(value, 0, toggles.Length - 1);
-                foreach (SimpleToggle toggle in toggles)
-                {
-                    toggle.value = toggle.toggleIndex == selectedItemIndex;
-                }
-                OnControlEvent(EventArgs.Empty);
-            }
-        }
+        selectedItemIndex = Mathf.Clamp(selected, 0, items.Length - 1);
+        toggles = MakeToggles(items);
+    }
 
-        public SelectionGrid(string[] items, int selected = 0)
+    public int SelectedItemIndex
+    {
+        get => selectedItemIndex;
+        set
         {
-            selectedItemIndex = Mathf.Clamp(selected, 0, items.Length - 1);
-            toggles = MakeToggles(items);
+            selectedItemIndex = Mathf.Clamp(value, 0, toggles.Length - 1);
+
+            foreach (var toggle in toggles)
+                toggle.Value = toggle.ToggleIndex == selectedItemIndex;
+
+            OnControlEvent(EventArgs.Empty);
         }
+    }
 
-        private SimpleToggle[] MakeToggles(string[] items)
-        {
-            SimpleToggle[] toggles = new SimpleToggle[items.Length];
-            for (int i = 0; i < items.Length; i++)
+    public override void Draw(params GUILayoutOption[] layoutOptions)
+    {
+        GUILayout.BeginHorizontal();
+
+        foreach (var toggle in toggles)
+            toggle.Draw(layoutOptions);
+
+        GUILayout.EndHorizontal();
+    }
+
+    public void SetItems(string[] items, int selectedItemIndex = -1)
+    {
+        if (selectedItemIndex < 0)
+            selectedItemIndex = SelectedItemIndex;
+
+        if (items.Length != toggles.Length)
+            toggles = MakeToggles(items);
+        else
+            for (var i = 0; i < items.Length; i++)
             {
-                SimpleToggle toggle = new SimpleToggle(items[i], i == SelectedItemIndex) { toggleIndex = i };
-                toggle.ControlEvent += (s, a) =>
-                {
-                    int value = (s as SimpleToggle).toggleIndex;
-                    if (value != SelectedItemIndex) SelectedItemIndex = value;
-                };
-                toggles[i] = toggle;
+                var item = items[i];
+
+                toggles[i].Value = i == SelectedItemIndex;
+                toggles[i].Label = item;
             }
-            return toggles;
-        }
 
-        public void SetItems(string[] items, int selectedItemIndex = -1)
+        SelectedItemIndex = Mathf.Clamp(selectedItemIndex, 0, items.Length - 1);
+    }
+
+    private SimpleToggle[] MakeToggles(string[] items)
+    {
+        var toggles = new SimpleToggle[items.Length];
+
+        for (var i = 0; i < items.Length; i++)
         {
-            if (selectedItemIndex < 0) selectedItemIndex = SelectedItemIndex;
-            if (items.Length != toggles.Length)
+            var toggle = new SimpleToggle(items[i], i == SelectedItemIndex)
             {
-                toggles = MakeToggles(items);
-            }
-            else
+                ToggleIndex = i,
+            };
+
+            toggle.ControlEvent += (sender, _) =>
             {
-                for (int i = 0; i < items.Length; i++)
-                {
-                    string item = items[i];
-                    toggles[i].value = i == SelectedItemIndex;
-                    toggles[i].label = item;
-                }
-            }
-            SelectedItemIndex = Mathf.Clamp(selectedItemIndex, 0, items.Length - 1);
+                var value = (sender as SimpleToggle).ToggleIndex;
+
+                if (value != SelectedItemIndex)
+                    SelectedItemIndex = value;
+            };
+
+            toggles[i] = toggle;
         }
 
-        public override void Draw(params GUILayoutOption[] layoutOptions)
+        return toggles;
+    }
+
+    private class SimpleToggle
+    {
+        public int ToggleIndex;
+        public bool Value;
+        public string Label;
+
+        public SimpleToggle(string label, bool value = false)
         {
-            GUILayout.BeginHorizontal();
-            foreach (SimpleToggle toggle in toggles)
-            {
-                toggle.Draw(layoutOptions);
-            }
-            GUILayout.EndHorizontal();
+            Label = label;
+            Value = value;
         }
 
-        private class SimpleToggle
+        public event EventHandler ControlEvent;
+
+        public void Draw(params GUILayoutOption[] layoutOptions)
         {
-            public int toggleIndex;
-            public bool value;
-            public string label;
-            public event EventHandler ControlEvent;
+            var value = GUILayout.Toggle(Value, Label, layoutOptions);
+
+            if (value == Value)
+                return;
 
-            public SimpleToggle(string label, bool value = false)
+            if (!value)
             {
-                this.label = label;
-                this.value = value;
+                Value = true;
             }
-
-            public void Draw(params GUILayoutOption[] layoutOptions)
+            else
             {
-                bool value = GUILayout.Toggle(this.value, label, layoutOptions);
-                if (value != this.value)
-                {
-                    if (!value) this.value = true;
-                    else
-                    {
-                        this.value = value;
-                        ControlEvent?.Invoke(this, EventArgs.Empty);
-                    }
-                }
+                Value = value;
+
+                ControlEvent?.Invoke(this, EventArgs.Empty);
             }
         }
     }

+ 119 - 122
src/MeidoPhotoStudio.Plugin/GUI/Controls/Slider.cs

@@ -1,168 +1,165 @@
 using System;
 using System.Globalization;
+
 using UnityEngine;
 
-namespace MeidoPhotoStudio.Plugin
+namespace MeidoPhotoStudio.Plugin;
+
+public class Slider : BaseControl
 {
-    public class Slider : BaseControl
+    private bool hasLabel;
+    private string label;
+    private float value;
+    private float left;
+    private float right;
+    private float defaultValue;
+    private string textFieldValue;
+    private bool hasTextField;
+
+    public Slider(string label, float left, float right, float value = 0, float defaultValue = 0)
     {
-        private bool hasLabel;
-        private string label;
-        public string Label
-        {
-            get => label;
-            set
-            {
-                label = value;
-                hasLabel = !string.IsNullOrEmpty(label);
-            }
-        }
+        Label = label;
+        this.left = left;
+        this.right = right;
+        this.value = Utility.Bound(value, left, right);
+        textFieldValue = FormatValue(this.value);
+        DefaultValue = defaultValue;
+    }
 
-        private float value;
+    public Slider(string label, SliderProp prop)
+        : this(label, prop.Left, prop.Right, prop.Initial, prop.Default)
+    {
+    }
 
-        public float Value
-        {
-            get => value;
-            set
-            {
-                this.value = Utility.Bound(value, Left, Right);
-                if (hasTextField) textFieldValue = FormatValue(value);
-                OnControlEvent(EventArgs.Empty);
-            }
-        }
+    public Slider(SliderProp prop)
+        : this(string.Empty, prop.Left, prop.Right, prop.Initial, prop.Default)
+    {
+    }
 
-        private float left;
+    public bool HasReset { get; set; }
 
-        public float Left
+    public string Label
+    {
+        get => label;
+        set
         {
-            get => left;
-            set
-            {
-                left = value;
-                this.value = Utility.Bound(value, left, right);
-            }
+            label = value;
+            hasLabel = !string.IsNullOrEmpty(label);
         }
+    }
 
-        private float right;
-
-        public float Right
-        {
-            get => right;
-            set
-            {
-                right = value;
-                this.value = Utility.Bound(value, left, right);
-            }
-        }
-        private float defaultValue;
-        public float DefaultValue
+    public float Value
+    {
+        get => value;
+        set
         {
-            get => defaultValue;
-            set => defaultValue = Utility.Bound(value, Left, Right);
+            this.value = Utility.Bound(value, Left, Right);
+
+            if (hasTextField)
+                textFieldValue = FormatValue(value);
+
+            OnControlEvent(EventArgs.Empty);
         }
+    }
 
-        private string textFieldValue;
-        private bool hasTextField;
-        public bool HasTextField
+    public float Left
+    {
+        get => left;
+        set
         {
-            get => hasTextField;
-            set
-            {
-                hasTextField = value;
-                if (hasTextField) textFieldValue = FormatValue(Value);
-            }
+            left = value;
+            this.value = Utility.Bound(value, left, right);
         }
-        public bool HasReset { get; set; }
+    }
 
-        public Slider(string label, float left, float right, float value = 0, float defaultValue = 0)
+    public float Right
+    {
+        get => right;
+        set
         {
-            Label = label;
-            this.left = left;
-            this.right = right;
+            right = value;
             this.value = Utility.Bound(value, left, right);
-            textFieldValue = FormatValue(this.value);
-            DefaultValue = defaultValue;
         }
+    }
 
-        public Slider(string label, SliderProp prop) : this(label, prop.Left, prop.Right, prop.Initial, prop.Default) { }
-
-        public Slider(SliderProp prop) : this(string.Empty, prop.Left, prop.Right, prop.Initial, prop.Default) { }
+    public float DefaultValue
+    {
+        get => defaultValue;
+        set => defaultValue = Utility.Bound(value, Left, Right);
+    }
 
-        public void SetBounds(float left, float right)
+    public bool HasTextField
+    {
+        get => hasTextField;
+        set
         {
-            this.left = left;
-            this.right = right;
-            value = Utility.Bound(value, left, right);
+            hasTextField = value;
+
+            if (hasTextField)
+                textFieldValue = FormatValue(Value);
         }
+    }
+
+    public override void Draw(params GUILayoutOption[] layoutOptions)
+    {
+        var hasUpper = hasLabel || HasTextField || HasReset;
+        var tempText = string.Empty;
 
-        public override void Draw(params GUILayoutOption[] layoutOptions)
+        if (hasUpper)
         {
-            var hasUpper = hasLabel || HasTextField || HasReset;
+            GUILayout.BeginVertical(GUILayout.ExpandWidth(false));
+            GUILayout.BeginHorizontal();
 
-            var tempText = string.Empty;
+            if (hasLabel)
+            {
+                GUILayout.Label(Label, MpsGui.SliderLabelStyle, GUILayout.ExpandWidth(false));
+                GUILayout.FlexibleSpace();
+            }
 
-            if (hasUpper)
+            if (HasTextField)
+                tempText = GUILayout.TextField(textFieldValue, MpsGui.SliderTextBoxStyle, GUILayout.Width(60f));
+
+            if (HasReset && GUILayout.Button("|", MpsGui.SliderResetButtonStyle, GUILayout.Width(15f)))
             {
-                GUILayout.BeginVertical(GUILayout.ExpandWidth(false));
-                GUILayout.BeginHorizontal();
-
-                if (hasLabel)
-                {
-                    GUILayout.Label(Label, MpsGui.SliderLabelStyle, GUILayout.ExpandWidth(false));
-                    GUILayout.FlexibleSpace();
-                }
-
-                if (HasTextField)
-                {
-                    tempText = GUILayout.TextField(textFieldValue, MpsGui.SliderTextBoxStyle, GUILayout.Width(60f));
-                }
-
-                if (HasReset && GUILayout.Button("|", MpsGui.SliderResetButtonStyle, GUILayout.Width(15f)))
-                {
-                    Value = DefaultValue;
-                    tempText = textFieldValue = FormatValue(Value);
-                }
-                GUILayout.EndHorizontal();
+                Value = DefaultValue;
+                tempText = textFieldValue = FormatValue(Value);
             }
 
-            GUIStyle sliderStyle = hasUpper ? MpsGui.SliderStyle : MpsGui.SliderStyleNoLabel;
+            GUILayout.EndHorizontal();
+        }
 
-            var tempValue = GUILayout.HorizontalSlider(
-                Value, Left, Right, sliderStyle, MpsGui.SliderThumbStyle, layoutOptions
-            );
+        var sliderStyle = hasUpper ? MpsGui.SliderStyle : MpsGui.SliderStyleNoLabel;
+        var tempValue =
+            GUILayout.HorizontalSlider(Value, Left, Right, sliderStyle, MpsGui.SliderThumbStyle, layoutOptions);
 
-            if (hasUpper) GUILayout.EndVertical();
+        if (hasUpper)
+            GUILayout.EndVertical();
 
-            if (HasTextField)
+        if (HasTextField)
+        {
+            if (tempValue != Value)
+                tempText = textFieldValue = FormatValue(tempValue);
+
+            if (tempText != textFieldValue)
             {
-                if (tempValue != Value) tempText = textFieldValue = FormatValue(tempValue);
+                textFieldValue = tempText;
 
-                if (tempText != textFieldValue)
-                {
-                    textFieldValue = tempText;
-                    if (float.TryParse(tempText, out var newValue)) tempValue = newValue;
-                }
+                if (float.TryParse(tempText, out var newValue))
+                    tempValue = newValue;
             }
-
-            if (tempValue != Value) Value = tempValue;
         }
 
-        private static string FormatValue(float value) => value.ToString("0.####", CultureInfo.InvariantCulture);
+        if (tempValue != Value)
+            Value = tempValue;
     }
 
-    public readonly struct SliderProp
+    public void SetBounds(float left, float right)
     {
-        public float Left { get; }
-        public float Right { get; }
-        public float Initial { get; }
-        public float Default { get; }
-
-        public SliderProp(float left, float right, float initial = 0f, float @default = 0f)
-        {
-            Left = left;
-            Right = right;
-            Initial = Utility.Bound(initial, left, right);
-            Default = Utility.Bound(@default, left, right);
-        }
+        this.left = left;
+        this.right = right;
+        value = Utility.Bound(value, left, right);
     }
+
+    private static string FormatValue(float value) =>
+        value.ToString("0.####", CultureInfo.InvariantCulture);
 }

+ 20 - 0
src/MeidoPhotoStudio.Plugin/GUI/Controls/SliderProp.cs

@@ -0,0 +1,20 @@
+namespace MeidoPhotoStudio.Plugin;
+
+public readonly struct SliderProp
+{
+    public SliderProp(float left, float right, float initial = 0f, float @default = 0f)
+    {
+        Left = left;
+        Right = right;
+        Initial = Utility.Bound(initial, left, right);
+        Default = Utility.Bound(@default, left, right);
+    }
+
+    public float Left { get; }
+
+    public float Right { get; }
+
+    public float Initial { get; }
+
+    public float Default { get; }
+}

+ 10 - 13
src/MeidoPhotoStudio.Plugin/GUI/Controls/TextArea.cs

@@ -1,17 +1,14 @@
 using UnityEngine;
 
-namespace MeidoPhotoStudio.Plugin
+namespace MeidoPhotoStudio.Plugin;
+
+public class TextArea : BaseControl
 {
-    public class TextArea : BaseControl
-    {
-        public string Value { get; set; } = string.Empty;
-        public void Draw(GUIStyle textAreaStyle, params GUILayoutOption[] layoutOptions)
-        {
-            Value = GUILayout.TextArea(Value, textAreaStyle, layoutOptions);
-        }
-        public override void Draw(params GUILayoutOption[] layoutOptions)
-        {
-            Draw(new GUIStyle(GUI.skin.textArea), layoutOptions);
-        }
-    }
+    public string Value { get; set; } = string.Empty;
+
+    public override void Draw(params GUILayoutOption[] layoutOptions) =>
+        Draw(new(GUI.skin.textArea), layoutOptions);
+
+    public void Draw(GUIStyle textAreaStyle, params GUILayoutOption[] layoutOptions) =>
+        Value = GUILayout.TextArea(Value, textAreaStyle, layoutOptions);
 }

+ 22 - 17
src/MeidoPhotoStudio.Plugin/GUI/Controls/TextField.cs

@@ -1,24 +1,29 @@
 using System;
+
 using UnityEngine;
 
-namespace MeidoPhotoStudio.Plugin
+namespace MeidoPhotoStudio.Plugin;
+
+public class TextField : BaseControl
 {
-    public class TextField : BaseControl
+    private static int textFieldID = 961;
+
+    private readonly string controlName = $"textField{ID}";
+
+    public string Value { get; set; } = string.Empty;
+
+    private static int ID =>
+        ++textFieldID;
+
+    public override void Draw(params GUILayoutOption[] layoutOptions) =>
+        Draw(new(GUI.skin.textField), layoutOptions);
+
+    public void Draw(GUIStyle textFieldStyle, params GUILayoutOption[] layoutOptions)
     {
-        private static int textFieldID = 961;
-        private static int ID => ++textFieldID;
-        private readonly string controlName = $"textField{ID}";
-        public string Value { get; set; } = string.Empty;
-        public void Draw(GUIStyle textFieldStyle, params GUILayoutOption[] layoutOptions)
-        {
-            GUI.SetNextControlName(controlName);
-            Value = GUILayout.TextField(Value, textFieldStyle, layoutOptions);
-            if (Event.current.isKey && Event.current.keyCode == KeyCode.Return) OnControlEvent(EventArgs.Empty);
-        }
-
-        public override void Draw(params GUILayoutOption[] layoutOptions)
-        {
-            Draw(new GUIStyle(GUI.skin.textField), layoutOptions);
-        }
+        GUI.SetNextControlName(controlName);
+        Value = GUILayout.TextField(Value, textFieldStyle, layoutOptions);
+
+        if (Event.current.isKey && Event.current.keyCode is KeyCode.Return)
+            OnControlEvent(EventArgs.Empty);
     }
 }

+ 34 - 30
src/MeidoPhotoStudio.Plugin/GUI/Controls/Toggle.cs

@@ -1,44 +1,48 @@
 using System;
+
 using UnityEngine;
 
-namespace MeidoPhotoStudio.Plugin
+namespace MeidoPhotoStudio.Plugin;
+
+public class Toggle : BaseControl
 {
-    public class Toggle : BaseControl
+    private bool value;
+
+    public Toggle(string label, bool state = false)
     {
-        private bool value;
-        public bool Value
-        {
-            get => value;
-            set
-            {
-                this.value = value;
-                OnControlEvent(EventArgs.Empty);
-            }
-        }
+        Label = label;
+        value = state;
+    }
 
-        public string Label { get; set; }
+    public string Label { get; set; }
 
-        public Toggle(string label, bool state = false)
+    public bool Value
+    {
+        get => value;
+        set
         {
-            Label = label;
-            value = state;
-        }
+            this.value = value;
 
-        public override void Draw(params GUILayoutOption[] layoutOptions)
-        {
-            Draw(new GUIStyle(GUI.skin.toggle), layoutOptions);
+            OnControlEvent(EventArgs.Empty);
         }
+    }
 
-        public void Draw(GUIStyle toggleStyle, params GUILayoutOption[] layoutOptions)
-        {
-            bool value = GUILayout.Toggle(Value, Label, toggleStyle, layoutOptions);
-            if (value != Value) Value = value;
-        }
+    public override void Draw(params GUILayoutOption[] layoutOptions) =>
+        Draw(new(GUI.skin.toggle), layoutOptions);
 
-        public void Draw(Rect rect)
-        {
-            bool value = GUI.Toggle(rect, Value, Label);
-            if (value != Value) Value = value;
-        }
+    public void Draw(GUIStyle toggleStyle, params GUILayoutOption[] layoutOptions)
+    {
+        var value = GUILayout.Toggle(Value, Label, toggleStyle, layoutOptions);
+
+        if (value != Value)
+            Value = value;
+    }
+
+    public void Draw(Rect rect)
+    {
+        var value = GUI.Toggle(rect, Value, Label);
+
+        if (value != Value)
+            Value = value;
     }
 }

+ 178 - 145
src/MeidoPhotoStudio.Plugin/GUI/Panes/BackgroundWindow2Panes/AttachPropPane.cs

@@ -1,188 +1,221 @@
 using System;
-using System.Linq;
 using System.Collections.Generic;
+using System.Linq;
+
 using UnityEngine;
 
-namespace MeidoPhotoStudio.Plugin
+namespace MeidoPhotoStudio.Plugin;
+
+public class AttachPropPane : BasePane
 {
-    public class AttachPropPane : BasePane
+    private static readonly Dictionary<AttachPoint, string> ToggleTranslation =
+        new()
+        {
+            [AttachPoint.Head] = "head",
+            [AttachPoint.Neck] = "neck",
+            [AttachPoint.UpperArmL] = "upperArmL",
+            [AttachPoint.UpperArmR] = "upperArmR",
+            [AttachPoint.ForearmL] = "forearmL",
+            [AttachPoint.ForearmR] = "forearmR",
+            [AttachPoint.MuneL] = "muneL",
+            [AttachPoint.MuneR] = "muneR",
+            [AttachPoint.HandL] = "handL",
+            [AttachPoint.HandR] = "handR",
+            [AttachPoint.Pelvis] = "pelvis",
+            [AttachPoint.ThighL] = "thighL",
+            [AttachPoint.ThighR] = "thighR",
+            [AttachPoint.CalfL] = "calfL",
+            [AttachPoint.CalfR] = "calfR",
+            [AttachPoint.FootL] = "footL",
+            [AttachPoint.FootR] = "footR",
+            [AttachPoint.Spine1a] = "spine1a",
+            [AttachPoint.Spine1] = "spine1",
+            [AttachPoint.Spine0a] = "spine0a",
+            [AttachPoint.Spine0] = "spine0",
+        };
+
+    private readonly PropManager propManager;
+    private readonly MeidoManager meidoManager;
+    private readonly Dictionary<AttachPoint, Toggle> toggles = new();
+    private readonly Toggle keepWorldPositionToggle;
+    private readonly Dropdown meidoDropdown;
+
+    private Toggle activeToggle;
+    private bool meidoDropdownActive;
+    private bool doguDropdownActive;
+    private string header;
+
+    public AttachPropPane(MeidoManager meidoManager, PropManager propManager)
     {
-        private readonly PropManager propManager;
-        private readonly MeidoManager meidoManager;
-        private readonly Dictionary<AttachPoint, Toggle> toggles = new Dictionary<AttachPoint, Toggle>();
+        header = Translation.Get("attachPropPane", "header");
 
-        private static readonly Dictionary<AttachPoint, string> toggleTranslation =
-            new Dictionary<AttachPoint, string>()
-            {
-                [AttachPoint.Head] = "head",
-                [AttachPoint.Neck] = "neck",
-                [AttachPoint.UpperArmL] = "upperArmL",
-                [AttachPoint.UpperArmR] = "upperArmR",
-                [AttachPoint.ForearmL] = "forearmL",
-                [AttachPoint.ForearmR] = "forearmR",
-                [AttachPoint.MuneL] = "muneL",
-                [AttachPoint.MuneR] = "muneR",
-                [AttachPoint.HandL] = "handL",
-                [AttachPoint.HandR] = "handR",
-                [AttachPoint.Pelvis] = "pelvis",
-                [AttachPoint.ThighL] = "thighL",
-                [AttachPoint.ThighR] = "thighR",
-                [AttachPoint.CalfL] = "calfL",
-                [AttachPoint.CalfR] = "calfR",
-                [AttachPoint.FootL] = "footL",
-                [AttachPoint.FootR] = "footR",
-                [AttachPoint.Spine1a] = "spine1a",
-                [AttachPoint.Spine1] = "spine1",
-                [AttachPoint.Spine0a] = "spine0a",
-                [AttachPoint.Spine0] = "spine0"
-            };
-
-        private readonly Toggle keepWorldPositionToggle;
-        private readonly Dropdown meidoDropdown;
-        private Toggle activeToggle;
-        private bool meidoDropdownActive;
-        private bool doguDropdownActive;
-        private string header;
-        private bool PaneActive => meidoDropdownActive && doguDropdownActive;
-        private Meido SelectedMeido => meidoManager.ActiveMeidoList[meidoDropdown.SelectedItemIndex];
-        private DragPointProp SelectedProp => propManager.CurrentProp;
-        private bool KeepWoldPosition => keepWorldPositionToggle.Value;
-
-        public AttachPropPane(MeidoManager meidoManager, PropManager propManager)
+        this.propManager = propManager;
+        this.meidoManager = meidoManager;
+        this.meidoManager.EndCallMeidos += (_, _) =>
+            SetMeidoDropdown();
+
+        this.propManager.PropSelectionChange += (_, _) =>
+            UpdateToggles();
+
+        this.propManager.PropListChange += (_, _) =>
         {
-            header = Translation.Get("attachPropPane", "header");
-            this.propManager = propManager;
-            this.meidoManager = meidoManager;
+            doguDropdownActive = this.propManager.PropCount > 0;
+            UpdateToggles();
+        };
 
-            this.meidoManager.EndCallMeidos += (s, a) => SetMeidoDropdown();
-            this.propManager.PropSelectionChange += (s, a) => UpdateToggles();
+        meidoDropdown = new(new[] { Translation.Get("systemMessage", "noMaids") });
+        meidoDropdown.SelectionChange += (_, _) =>
+            UpdateToggles();
 
-            this.propManager.PropListChange += (s, a) =>
-            {
-                doguDropdownActive = this.propManager.PropCount > 0;
-                UpdateToggles();
-            };
+        keepWorldPositionToggle = new(Translation.Get("attachPropPane", "keepWorldPosition"));
 
-            meidoDropdown = new Dropdown(new[] { Translation.Get("systemMessage", "noMaids") });
-            meidoDropdown.SelectionChange += (s, a) => UpdateToggles();
+        foreach (var attachPoint in Enum.GetValues(typeof(AttachPoint)).Cast<AttachPoint>())
+        {
+            if (attachPoint is AttachPoint.None)
+                continue;
 
-            keepWorldPositionToggle = new Toggle(Translation.Get("attachPropPane", "keepWorldPosition"));
+            var point = attachPoint;
+            var toggle = new Toggle(Translation.Get("attachPropPane", ToggleTranslation[point]));
 
-            foreach (AttachPoint attachPoint in Enum.GetValues(typeof(AttachPoint)))
-            {
-                if (attachPoint == AttachPoint.None) continue;
+            toggle.ControlEvent += (_, _) =>
+                OnToggleChange(point);
 
-                var point = attachPoint;
-                var toggle = new Toggle(Translation.Get("attachPropPane", toggleTranslation[point]));
-                toggle.ControlEvent += (s, a) => OnToggleChange(point);
-                toggles[point] = toggle;
-            }
+            toggles[point] = toggle;
         }
+    }
 
-        protected override void ReloadTranslation()
-        {
-            header = Translation.Get("attachPropPane", "header");
-            keepWorldPositionToggle.Label = Translation.Get("attachPropPane", "keepWorldPosition");
-            foreach (AttachPoint attachPoint in Enum.GetValues(typeof(AttachPoint)))
-            {
-                if (attachPoint == AttachPoint.None) continue;
+    private bool PaneActive =>
+        meidoDropdownActive && doguDropdownActive;
 
-                toggles[attachPoint].Label = Translation.Get("attachPropPane", toggleTranslation[attachPoint]);
-            }
-        }
+    private Meido SelectedMeido =>
+        meidoManager.ActiveMeidoList[meidoDropdown.SelectedItemIndex];
+
+    private DragPointProp SelectedProp =>
+        propManager.CurrentProp;
+
+    private bool KeepWoldPosition =>
+        keepWorldPositionToggle.Value;
 
-        public override void Draw()
+    public override void Draw()
+    {
+        const float dropdownButtonHeight = 30;
+        const float dropdownButtonWidth = 153f;
+
+        var dropdownLayoutOptions = new[]
         {
-            const float dropdownButtonHeight = 30;
-            const float dropdownButtonWidth = 153f;
-            GUILayoutOption[] dropdownLayoutOptions =
-            {
-                GUILayout.Height(dropdownButtonHeight), GUILayout.Width(dropdownButtonWidth)
-            };
+            GUILayout.Height(dropdownButtonHeight),
+            GUILayout.Width(dropdownButtonWidth),
+        };
 
-            MpsGui.Header(header);
-            MpsGui.WhiteLine();
+        MpsGui.Header(header);
+        MpsGui.WhiteLine();
 
-            GUI.enabled = PaneActive;
+        GUI.enabled = PaneActive;
 
-            meidoDropdown.Draw(dropdownLayoutOptions);
+        meidoDropdown.Draw(dropdownLayoutOptions);
 
-            keepWorldPositionToggle.Draw();
+        keepWorldPositionToggle.Draw();
 
-            DrawToggleGroup(AttachPoint.Head, AttachPoint.Neck);
-            DrawToggleGroup(AttachPoint.UpperArmR, AttachPoint.Spine1a, AttachPoint.UpperArmL);
-            DrawToggleGroup(AttachPoint.ForearmR, AttachPoint.Spine1, AttachPoint.ForearmL);
-            DrawToggleGroup(AttachPoint.MuneR, AttachPoint.Spine0a, AttachPoint.MuneL);
-            DrawToggleGroup(AttachPoint.HandR, AttachPoint.Spine0, AttachPoint.HandL);
-            DrawToggleGroup(AttachPoint.ThighR, AttachPoint.Pelvis, AttachPoint.ThighL);
-            DrawToggleGroup(AttachPoint.CalfR, AttachPoint.CalfL);
-            DrawToggleGroup(AttachPoint.FootR, AttachPoint.FootL);
+        DrawToggleGroup(AttachPoint.Head, AttachPoint.Neck);
+        DrawToggleGroup(AttachPoint.UpperArmR, AttachPoint.Spine1a, AttachPoint.UpperArmL);
+        DrawToggleGroup(AttachPoint.ForearmR, AttachPoint.Spine1, AttachPoint.ForearmL);
+        DrawToggleGroup(AttachPoint.MuneR, AttachPoint.Spine0a, AttachPoint.MuneL);
+        DrawToggleGroup(AttachPoint.HandR, AttachPoint.Spine0, AttachPoint.HandL);
+        DrawToggleGroup(AttachPoint.ThighR, AttachPoint.Pelvis, AttachPoint.ThighL);
+        DrawToggleGroup(AttachPoint.CalfR, AttachPoint.CalfL);
+        DrawToggleGroup(AttachPoint.FootR, AttachPoint.FootL);
 
-            GUI.enabled = true;
-        }
+        GUI.enabled = true;
+    }
+
+    protected override void ReloadTranslation()
+    {
+        header = Translation.Get("attachPropPane", "header");
+        keepWorldPositionToggle.Label = Translation.Get("attachPropPane", "keepWorldPosition");
 
-        private void DrawToggleGroup(params AttachPoint[] attachPoints)
+        foreach (var attachPoint in Enum.GetValues(typeof(AttachPoint)).Cast<AttachPoint>())
         {
-            GUILayout.BeginHorizontal();
-            GUILayout.FlexibleSpace();
-            foreach (var point in attachPoints) toggles[point].Draw();
-            GUILayout.FlexibleSpace();
-            GUILayout.EndHorizontal();
+            if (attachPoint is AttachPoint.None)
+                continue;
+
+            toggles[attachPoint].Label = Translation.Get("attachPropPane", ToggleTranslation[attachPoint]);
         }
+    }
 
-        private void OnToggleChange(AttachPoint point)
-        {
-            if (updating) return;
+    private void DrawToggleGroup(params AttachPoint[] attachPoints)
+    {
+        GUILayout.BeginHorizontal();
+        GUILayout.FlexibleSpace();
 
-            var toggle = toggles[point];
-            if (toggle.Value)
-            {
-                if (activeToggle != null)
-                {
-                    updating = true;
-                    activeToggle.Value = false;
-                    updating = false;
-                }
-
-                activeToggle = toggle;
-                SelectedProp.AttachTo(SelectedMeido, point, KeepWoldPosition);
-            }
-            else
+        foreach (var point in attachPoints)
+            toggles[point].Draw();
+
+        GUILayout.FlexibleSpace();
+        GUILayout.EndHorizontal();
+    }
+
+    private void OnToggleChange(AttachPoint point)
+    {
+        if (updating)
+            return;
+
+        var toggle = toggles[point];
+
+        if (toggle.Value)
+        {
+            if (activeToggle is not null)
             {
-                SelectedProp.DetachFrom(KeepWoldPosition);
-                activeToggle = null;
+                updating = true;
+                activeToggle.Value = false;
+                updating = false;
             }
-        }
 
-        private void UpdateToggles()
+            activeToggle = toggle;
+            SelectedProp.AttachTo(SelectedMeido, point, KeepWoldPosition);
+        }
+        else
         {
-            updating = true;
-            if (activeToggle != null) activeToggle.Value = false;
+            SelectedProp.DetachFrom(KeepWoldPosition);
             activeToggle = null;
-            updating = false;
+        }
+    }
 
-            if (!meidoManager.HasActiveMeido || propManager.PropCount == 0) return;
+    private void UpdateToggles()
+    {
+        updating = true;
 
-            var info = SelectedProp.AttachPointInfo;
+        if (activeToggle is not null)
+            activeToggle.Value = false;
 
-            if (SelectedMeido.Maid.status.guid != info.MaidGuid) return;
+        activeToggle = null;
+        updating = false;
 
-            updating = true;
-            var toggle = toggles[info.AttachPoint];
-            toggle.Value = true;
-            activeToggle = toggle;
-            updating = false;
-        }
+        if (!meidoManager.HasActiveMeido || propManager.PropCount is 0)
+            return;
 
-        private void SetMeidoDropdown()
-        {
-            meidoDropdownActive = meidoManager.HasActiveMeido;
-            string[] dropdownList = meidoManager.ActiveMeidoList.Count == 0
-                ? new[] { Translation.Get("systemMessage", "noMaids") }
-                : meidoManager.ActiveMeidoList.Select(meido => $"{meido.Slot + 1}: {meido.FirstName} {meido.LastName}")
-                    .ToArray();
+        var info = SelectedProp.AttachPointInfo;
 
-            meidoDropdown.SetDropdownItems(dropdownList, 0);
-        }
+        if (SelectedMeido.Maid.status.guid != info.MaidGuid)
+            return;
+
+        updating = true;
+
+        var toggle = toggles[info.AttachPoint];
+
+        toggle.Value = true;
+        activeToggle = toggle;
+        updating = false;
+    }
+
+    private void SetMeidoDropdown()
+    {
+        meidoDropdownActive = meidoManager.HasActiveMeido;
+
+        var dropdownList = meidoManager.ActiveMeidoList.Count is 0
+            ? new[] { Translation.Get("systemMessage", "noMaids") }
+            : meidoManager.ActiveMeidoList.Select(meido => $"{meido.Slot + 1}: {meido.FirstName} {meido.LastName}")
+                .ToArray();
+
+        meidoDropdown.SetDropdownItems(dropdownList, 0);
     }
 }

+ 165 - 147
src/MeidoPhotoStudio.Plugin/GUI/Panes/BackgroundWindow2Panes/ModPropsPane.cs

@@ -1,196 +1,214 @@
 using System.Collections.Generic;
 using System.Linq;
+
 using UnityEngine;
 
-namespace MeidoPhotoStudio.Plugin
+using static MeidoPhotoStudio.Plugin.MenuFileUtility;
+
+namespace MeidoPhotoStudio.Plugin;
+
+public class ModPropsPane : BasePane
 {
-    using static MenuFileUtility;
-    public class ModPropsPane : BasePane
+    private readonly PropManager propManager;
+    private readonly Dropdown propCategoryDropdown;
+    private readonly Toggle modFilterToggle;
+    private readonly Toggle baseFilterToggle;
+    private readonly bool isModsOnly = PropManager.ModItemsOnly;
+
+    private Vector2 propListScrollPos;
+    private List<ModItem> modPropList;
+    private string currentCategory;
+    private bool modItemsReady;
+    private bool shouldDraw;
+    private int categoryIndex;
+    private bool modFilter;
+    private bool baseFilter;
+    private int currentListCount;
+
+    public ModPropsPane(PropManager propManager)
     {
-        private readonly PropManager propManager;
-        private readonly Dropdown propCategoryDropdown;
-        private readonly Toggle modFilterToggle;
-        private readonly Toggle baseFilterToggle;
-        private Vector2 propListScrollPos;
-        private string SelectedCategory => MenuCategories[propCategoryDropdown.SelectedItemIndex];
-        private List<ModItem> modPropList;
-        private string currentCategory;
-        private bool modItemsReady;
-        private bool shouldDraw;
-        private int categoryIndex;
-        private bool modFilter;
-        private bool baseFilter;
-        private int currentListCount;
-        private readonly bool isModsOnly = PropManager.ModItemsOnly;
-        private enum FilterType
+        this.propManager = propManager;
+
+        modItemsReady = MenuFilesReady || PropManager.ModItemsOnly;
+
+        var listItems = Translation.GetArray("clothing", MenuCategories);
+
+        if (!modItemsReady)
         {
-            None, Mod, Base
+            listItems[0] = Translation.Get("systemMessage", "initializing");
+
+            MenuFilesReadyChange += (_, _) =>
+            {
+                modItemsReady = true;
+                propCategoryDropdown.SetDropdownItems(Translation.GetArray("clothing", MenuCategories));
+            };
         }
 
-        public ModPropsPane(PropManager propManager)
+        propCategoryDropdown = new(listItems);
+        propCategoryDropdown.SelectionChange += (_, _) =>
         {
-            this.propManager = propManager;
+            if (!modItemsReady)
+                return;
 
-            modItemsReady = MenuFilesReady || PropManager.ModItemsOnly;
+            ChangePropCategory();
+        };
 
-            string[] listItems = Translation.GetArray("clothing", MenuCategories);
+        if (isModsOnly)
+            return;
 
-            if (!modItemsReady)
-            {
-                listItems[0] = Translation.Get("systemMessage", "initializing");
-
-                MenuFilesReadyChange += (s, a) =>
-                {
-                    modItemsReady = true;
-                    propCategoryDropdown.SetDropdownItems(
-                        Translation.GetArray("clothing", MenuCategories)
-                    );
-                };
-            }
+        modFilterToggle = new(Translation.Get("background2Window", "modsToggle"));
+        modFilterToggle.ControlEvent += (_, _) =>
+            ChangeFilter(FilterType.Mod);
 
-            propCategoryDropdown = new Dropdown(listItems);
+        baseFilterToggle = new(Translation.Get("background2Window", "baseToggle"));
+        baseFilterToggle.ControlEvent += (_, _) =>
+            ChangeFilter(FilterType.Base);
+    }
 
-            propCategoryDropdown.SelectionChange += (s, a) =>
-            {
-                if (!modItemsReady) return;
-                ChangePropCategory();
-            };
+    private enum FilterType
+    {
+        None,
+        Mod,
+        Base,
+    }
 
-            if (!isModsOnly)
-            {
-                modFilterToggle = new Toggle(Translation.Get("background2Window", "modsToggle"));
-                modFilterToggle.ControlEvent += (s, a) => ChangeFilter(FilterType.Mod);
+    private string SelectedCategory =>
+        MenuCategories[propCategoryDropdown.SelectedItemIndex];
 
-                baseFilterToggle = new Toggle(Translation.Get("background2Window", "baseToggle"));
-                baseFilterToggle.ControlEvent += (s, a) => ChangeFilter(FilterType.Base);
-            }
-        }
+    public override void Draw()
+    {
+        const float dropdownButtonHeight = 30f;
+
+        var dropdownButtonWidth = isModsOnly ? 120f : 90f;
 
-        protected override void ReloadTranslation()
+        var dropdownLayoutOptions = new[]
         {
-            string[] listItems = Translation.GetArray("clothing", MenuCategories);
+            GUILayout.Height(dropdownButtonHeight),
+            GUILayout.Width(dropdownButtonWidth),
+        };
 
-            if (!modItemsReady) listItems[0] = Translation.Get("systemMessage", "initializing");
+        GUILayout.BeginHorizontal();
 
-            propCategoryDropdown.SetDropdownItems(listItems);
+        if (isModsOnly)
+        {
+            GUILayout.FlexibleSpace();
+            propCategoryDropdown.Draw(dropdownLayoutOptions);
+            GUILayout.FlexibleSpace();
+        }
+        else
+        {
+            GUI.enabled = modItemsReady;
+            propCategoryDropdown.Draw(dropdownLayoutOptions);
 
-            if (!isModsOnly)
-            {
-                modFilterToggle.Label = Translation.Get("background2Window", "modsToggle");
-                baseFilterToggle.Label = Translation.Get("background2Window", "baseToggle");
-            }
+            GUI.enabled = shouldDraw;
+            modFilterToggle.Draw();
+            baseFilterToggle.Draw();
+            GUI.enabled = true;
         }
 
-        public float buttonSize = 54f;
-        public override void Draw()
+        GUILayout.EndHorizontal();
+
+        if (shouldDraw)
         {
-            const float dropdownButtonHeight = 30f;
-            float dropdownButtonWidth = isModsOnly ? 120f : 90f;
-            GUILayoutOption[] dropdownLayoutOptions = new GUILayoutOption[] {
-                GUILayout.Height(dropdownButtonHeight),
-                GUILayout.Width(dropdownButtonWidth)
-            };
+            var windowRect = parent.WindowRect;
+            var windowHeight = windowRect.height;
+            var windowWidth = windowRect.width;
 
-            GUILayout.BeginHorizontal();
+            const float offsetTop = 80f;
+            const int columns = 4;
 
-            if (isModsOnly)
-            {
-                GUILayout.FlexibleSpace();
-                propCategoryDropdown.Draw(dropdownLayoutOptions);
-                GUILayout.FlexibleSpace();
-            }
-            else
-            {
-                GUI.enabled = modItemsReady;
-                propCategoryDropdown.Draw(dropdownLayoutOptions);
+            var buttonSize = windowWidth / columns - 10f;
+            var positionRect = new Rect(5f, offsetTop + dropdownButtonHeight, windowWidth - 10f, windowHeight - 145f);
 
-                GUI.enabled = shouldDraw;
-                modFilterToggle.Draw();
-                baseFilterToggle.Draw();
-                GUI.enabled = true;
-            }
+            var viewRect = new Rect(
+                0f, 0f, buttonSize * columns, buttonSize * Mathf.Ceil(currentListCount / (float)columns) + 5);
 
-            GUILayout.EndHorizontal();
+            propListScrollPos = GUI.BeginScrollView(positionRect, propListScrollPos, viewRect);
 
-            if (shouldDraw)
+            var modIndex = 0;
+
+            foreach (var modItem in modPropList)
             {
-                Rect windowRect = parent.WindowRect;
-                float windowHeight = windowRect.height;
-                float windowWidth = windowRect.width;
-
-                // const float buttonSize = 50f;
-                const float offsetTop = 80f;
-                const int columns = 4;
-                float buttonSize = (windowWidth / columns) - 10f;
-
-                Rect positionRect = new Rect(
-                    5f, offsetTop + dropdownButtonHeight, windowWidth - 10f, windowHeight - 145f
-                );
-                Rect viewRect = new Rect(
-                    0f, 0f, buttonSize * columns, (buttonSize * Mathf.Ceil(currentListCount / (float)columns)) + 5
-                );
-                propListScrollPos = GUI.BeginScrollView(positionRect, propListScrollPos, viewRect);
-
-                int modIndex = 0;
-                foreach (ModItem modItem in modPropList)
-                {
-                    if ((modFilter && !modItem.IsMod) || (baseFilter && modItem.IsMod)) continue;
-
-                    float x = modIndex % columns * buttonSize;
-                    float y = modIndex / columns * buttonSize;
-                    Rect iconRect = new Rect(x, y, buttonSize, buttonSize);
-                    if (GUI.Button(iconRect, "")) propManager.AddModProp(modItem);
-                    GUI.DrawTexture(iconRect, modItem.Icon);
-                    modIndex++;
-                }
-
-                GUI.EndScrollView();
+                if (modFilter && !modItem.IsMod || baseFilter && modItem.IsMod)
+                    continue;
+
+                var x = modIndex % columns * buttonSize;
+                var y = modIndex / columns * buttonSize;
+                var iconRect = new Rect(x, y, buttonSize, buttonSize);
+
+                if (GUI.Button(iconRect, string.Empty))
+                    propManager.AddModProp(modItem);
+
+                GUI.DrawTexture(iconRect, modItem.Icon);
+                modIndex++;
             }
+
+            GUI.EndScrollView();
         }
+    }
 
-        private void ChangeFilter(FilterType filterType)
-        {
-            if (updating) return;
+    protected override void ReloadTranslation()
+    {
+        var listItems = Translation.GetArray("clothing", MenuCategories);
 
-            if (modFilterToggle.Value && baseFilterToggle.Value)
-            {
-                updating = true;
-                modFilterToggle.Value = filterType == FilterType.Mod;
-                baseFilterToggle.Value = filterType == FilterType.Base;
-                updating = false;
-            }
+        if (!modItemsReady)
+            listItems[0] = Translation.Get("systemMessage", "initializing");
 
-            modFilter = modFilterToggle.Value;
-            baseFilter = baseFilterToggle.Value;
+        propCategoryDropdown.SetDropdownItems(listItems);
 
-            SetListCount();
-        }
+        if (isModsOnly)
+            return;
 
-        private void ChangePropCategory()
+        modFilterToggle.Label = Translation.Get("background2Window", "modsToggle");
+        baseFilterToggle.Label = Translation.Get("background2Window", "baseToggle");
+    }
+
+    private void ChangeFilter(FilterType filterType)
+    {
+        if (updating)
+            return;
+
+        if (modFilterToggle.Value && baseFilterToggle.Value)
         {
-            string category = SelectedCategory;
+            updating = true;
+            modFilterToggle.Value = filterType is FilterType.Mod;
+            baseFilterToggle.Value = filterType is FilterType.Base;
+            updating = false;
+        }
+
+        modFilter = modFilterToggle.Value;
+        baseFilter = baseFilterToggle.Value;
+
+        SetListCount();
+    }
 
-            if (currentCategory == category) return;
-            currentCategory = category;
+    private void ChangePropCategory()
+    {
+        var category = SelectedCategory;
 
-            categoryIndex = propCategoryDropdown.SelectedItemIndex;
+        if (currentCategory == category)
+            return;
 
-            shouldDraw = categoryIndex > 0;
+        currentCategory = category;
 
-            if (!shouldDraw) return;
+        categoryIndex = propCategoryDropdown.SelectedItemIndex;
 
-            propListScrollPos = Vector2.zero;
+        shouldDraw = categoryIndex > 0;
 
-            modPropList = Constants.GetModPropList(category);
+        if (!shouldDraw)
+            return;
 
-            SetListCount();
-        }
+        propListScrollPos = Vector2.zero;
 
-        private void SetListCount()
-        {
-            if (modFilter) currentListCount = modPropList.Count(mod => mod.IsMod);
-            else if (baseFilter) currentListCount = modPropList.Count(mod => !mod.IsMod);
-            else currentListCount = modPropList.Count;
-        }
+        modPropList = Constants.GetModPropList(category);
+
+        SetListCount();
     }
+
+    private void SetListCount() =>
+        currentListCount = modFilter
+            ? modPropList.Count(mod => mod.IsMod)
+            : baseFilter
+                ? modPropList.Count(mod => !mod.IsMod)
+                : modPropList.Count;
 }

+ 78 - 78
src/MeidoPhotoStudio.Plugin/GUI/Panes/BackgroundWindow2Panes/MyRoomPropsPane.cs

@@ -1,93 +1,93 @@
 using System.Collections.Generic;
+
 using UnityEngine;
 
-namespace MeidoPhotoStudio.Plugin
+namespace MeidoPhotoStudio.Plugin;
+
+public class MyRoomPropsPane : BasePane
 {
-    public class MyRoomPropsPane : BasePane
+    private readonly PropManager propManager;
+    private readonly Dropdown propCategoryDropdown;
+
+    private Vector2 propListScrollPos;
+    private List<MyRoomItem> myRoomPropList;
+    private string currentCategory;
+
+    public MyRoomPropsPane(PropManager propManager)
     {
-        private readonly PropManager propManager;
-        private readonly Dropdown propCategoryDropdown;
-        private Vector2 propListScrollPos;
-        private string SelectedCategory => Constants.MyRoomPropCategories[propCategoryDropdown.SelectedItemIndex];
-        private List<MyRoomItem> myRoomPropList;
-        private string currentCategory;
-
-        public MyRoomPropsPane(PropManager propManager)
-        {
-            this.propManager = propManager;
+        this.propManager = propManager;
 
-            propCategoryDropdown = new Dropdown(Translation.GetArray("doguCategories", Constants.MyRoomPropCategories));
-            propCategoryDropdown.SelectionChange += (s, a) => ChangePropCategory(SelectedCategory);
+        propCategoryDropdown = new(Translation.GetArray("doguCategories", Constants.MyRoomPropCategories));
+        propCategoryDropdown.SelectionChange += (_, _) =>
             ChangePropCategory(SelectedCategory);
-        }
 
-        protected override void ReloadTranslation()
-        {
-            propCategoryDropdown.SetDropdownItems(
-                Translation.GetArray("doguCategories", Constants.MyRoomPropCategories)
-            );
-        }
+        ChangePropCategory(SelectedCategory);
+    }
 
-        public override void Draw()
+    private string SelectedCategory =>
+        Constants.MyRoomPropCategories[propCategoryDropdown.SelectedItemIndex];
+
+    public override void Draw()
+    {
+        const float dropdownButtonHeight = 30f;
+        const float dropdownButtonWidth = 120f;
+
+        var dropdownLayoutOptions = new[]
         {
-            const float dropdownButtonHeight = 30f;
-            const float dropdownButtonWidth = 120f;
-            GUILayoutOption[] dropdownLayoutOptions = new GUILayoutOption[] {
-                GUILayout.Height(dropdownButtonHeight),
-                GUILayout.Width(dropdownButtonWidth)
-            };
-
-            GUILayout.BeginHorizontal();
-            GUILayout.FlexibleSpace();
-            propCategoryDropdown.Draw(dropdownLayoutOptions);
-            GUILayout.FlexibleSpace();
-            GUILayout.EndHorizontal();
-
-            Rect windowRect = parent.WindowRect;
-
-            float windowHeight = windowRect.height;
-            float windowWidth = windowRect.width;
-
-            const float offsetTop = 80f;
-            const int columns = 3;
-            float buttonSize = (windowWidth / columns) - 10f;
-
-            int listCount = myRoomPropList.Count;
-
-            Rect positionRect = new Rect(
-                5f, offsetTop + dropdownButtonHeight, windowWidth - 10f, windowHeight - 145f
-            );
-            Rect viewRect = new Rect(
-                0f, 0f, buttonSize * columns, (buttonSize * Mathf.Ceil(listCount / (float)columns)) + 5f
-            );
-            propListScrollPos = GUI.BeginScrollView(positionRect, propListScrollPos, viewRect);
-
-            for (int i = 0; i < listCount; i++)
-            {
-                float x = i % columns * buttonSize;
-                float y = i / columns * buttonSize;
-                MyRoomItem myRoomItem = myRoomPropList[i];
-                Rect iconRect = new Rect(x, y, buttonSize, buttonSize);
-                if (GUI.Button(iconRect, "")) propManager.AddMyRoomProp(myRoomItem);
-                GUI.DrawTexture(iconRect, myRoomItem.Icon);
-            }
-
-            GUI.EndScrollView();
-        }
+            GUILayout.Height(dropdownButtonHeight),
+            GUILayout.Width(dropdownButtonWidth),
+        };
+
+        GUILayout.BeginHorizontal();
+        GUILayout.FlexibleSpace();
+        propCategoryDropdown.Draw(dropdownLayoutOptions);
+        GUILayout.FlexibleSpace();
+        GUILayout.EndHorizontal();
 
-        private void ChangePropCategory(string category)
+        var windowRect = parent.WindowRect;
+        var windowHeight = windowRect.height;
+        var windowWidth = windowRect.width;
+
+        const float offsetTop = 80f;
+        const int columns = 3;
+
+        var buttonSize = windowWidth / columns - 10f;
+        var listCount = myRoomPropList.Count;
+        var positionRect = new Rect(5f, offsetTop + dropdownButtonHeight, windowWidth - 10f, windowHeight - 145f);
+        var viewRect = new Rect(0f, 0f, buttonSize * columns, buttonSize * Mathf.Ceil(listCount / (float)columns) + 5f);
+
+        propListScrollPos = GUI.BeginScrollView(positionRect, propListScrollPos, viewRect);
+
+        for (var i = 0; i < listCount; i++)
         {
-            if (currentCategory == category) return;
-            currentCategory = category;
-            propListScrollPos = Vector2.zero;
-            myRoomPropList = Constants.MyRoomPropDict[category];
-            if (myRoomPropList[0].Icon == null)
-            {
-                foreach (MyRoomItem item in myRoomPropList)
-                {
-                    item.Icon = (Texture2D)MyRoomCustom.PlacementData.GetData(item.ID).GetThumbnail();
-                }
-            }
+            var x = i % columns * buttonSize;
+            var y = i / columns * buttonSize;
+            var myRoomItem = myRoomPropList[i];
+            var iconRect = new Rect(x, y, buttonSize, buttonSize);
+
+            if (GUI.Button(iconRect, string.Empty))
+                propManager.AddMyRoomProp(myRoomItem);
+
+            GUI.DrawTexture(iconRect, myRoomItem.Icon);
         }
+
+        GUI.EndScrollView();
+    }
+
+    protected override void ReloadTranslation() =>
+        propCategoryDropdown.SetDropdownItems(Translation.GetArray("doguCategories", Constants.MyRoomPropCategories));
+
+    private void ChangePropCategory(string category)
+    {
+        if (currentCategory == category)
+            return;
+
+        currentCategory = category;
+        propListScrollPos = Vector2.zero;
+        myRoomPropList = Constants.MyRoomPropDict[category];
+
+        if (!myRoomPropList[0].Icon)
+            foreach (var item in myRoomPropList)
+                item.Icon = (Texture2D)MyRoomCustom.PlacementData.GetData(item.ID).GetThumbnail();
     }
 }

+ 158 - 138
src/MeidoPhotoStudio.Plugin/GUI/Panes/BackgroundWindow2Panes/PropManagerPane.cs

@@ -1,151 +1,171 @@
 using UnityEngine;
 
-namespace MeidoPhotoStudio.Plugin
+namespace MeidoPhotoStudio.Plugin;
+
+public class PropManagerPane : BasePane
 {
-    public class PropManagerPane : BasePane
+    private readonly PropManager propManager;
+    private readonly Dropdown propDropdown;
+    private readonly Button previousPropButton;
+    private readonly Button nextPropButton;
+    private readonly Toggle dragPointToggle;
+    private readonly Toggle gizmoToggle;
+    private readonly Toggle shadowCastingToggle;
+    private readonly Button deletePropButton;
+    private readonly Button copyPropButton;
+
+    private string propManagerHeader;
+
+    public PropManagerPane(PropManager propManager)
     {
-        private readonly PropManager propManager;
-        private readonly Dropdown propDropdown;
-        private readonly Button previousPropButton;
-        private readonly Button nextPropButton;
-        private readonly Toggle dragPointToggle;
-        private readonly Toggle gizmoToggle;
-        private readonly Toggle shadowCastingToggle;
-        private readonly Button deletePropButton;
-        private readonly Button copyPropButton;
-        private string propManagerHeader;
-
-        private int CurrentDoguIndex => propManager.CurrentPropIndex;
-
-        public PropManagerPane(PropManager propManager)
-        {
-            this.propManager = propManager;
-            this.propManager.PropListChange += (s, a) =>
-            {
-                UpdatePropList();
-                UpdateToggles();
-            };
-
-            this.propManager.FromPropSelect += (s, a) =>
-            {
-                updating = true;
-                propDropdown.SelectedItemIndex = CurrentDoguIndex;
-                updating = false;
-                UpdateToggles();
-            };
-
-            propDropdown = new Dropdown(this.propManager.PropNameList);
-            propDropdown.SelectionChange += (s, a) =>
-            {
-                if (updating) return;
-                this.propManager.CurrentPropIndex = propDropdown.SelectedItemIndex;
-                UpdateToggles();
-            };
-
-            previousPropButton = new Button("<");
-            previousPropButton.ControlEvent += (s, a) => propDropdown.Step(-1);
-
-            nextPropButton = new Button(">");
-            nextPropButton.ControlEvent += (s, a) => propDropdown.Step(1);
-
-            dragPointToggle = new Toggle(Translation.Get("propManagerPane", "dragPointToggle"));
-            dragPointToggle.ControlEvent += (s, a) =>
-            {
-                if (updating || this.propManager.PropCount == 0) return;
-                this.propManager.CurrentProp.DragPointEnabled = dragPointToggle.Value;
-            };
-
-            gizmoToggle = new Toggle(Translation.Get("propManagerPane", "gizmoToggle"));
-            gizmoToggle.ControlEvent += (s, a) =>
-            {
-                if (updating || this.propManager.PropCount == 0) return;
-                this.propManager.CurrentProp.GizmoEnabled = gizmoToggle.Value;
-            };
-
-            shadowCastingToggle = new Toggle(Translation.Get("propManagerPane", "shadowCastingToggle"));
-            shadowCastingToggle.ControlEvent += (s, a) =>
-            {
-                if (updating || this.propManager.PropCount == 0) return;
-                this.propManager.CurrentProp.ShadowCasting = shadowCastingToggle.Value;
-            };
-
-            copyPropButton = new Button(Translation.Get("propManagerPane", "copyButton"));
-            copyPropButton.ControlEvent += (s, a) => this.propManager.CopyProp(CurrentDoguIndex);
-
-            deletePropButton = new Button(Translation.Get("propManagerPane", "deleteButton"));
-            deletePropButton.ControlEvent += (s, a) => this.propManager.RemoveProp(CurrentDoguIndex);
-
-            propManagerHeader = Translation.Get("propManagerPane", "header");
-        }
-
-        protected override void ReloadTranslation()
-        {
-            dragPointToggle.Label = Translation.Get("propManagerPane", "dragPointToggle");
-            gizmoToggle.Label = Translation.Get("propManagerPane", "gizmoToggle");
-            shadowCastingToggle.Label = Translation.Get("propManagerPane", "shadowCastingToggle");
-            copyPropButton.Label = Translation.Get("propManagerPane", "copyButton");
-            deletePropButton.Label = Translation.Get("propManagerPane", "deleteButton");
-            propManagerHeader = Translation.Get("propManagerPane", "header");
-        }
-
-        public override void Draw()
+        this.propManager = propManager;
+        this.propManager.PropListChange += (_, _) =>
         {
-            const float buttonHeight = 30;
-            GUILayoutOption[] arrowLayoutOptions = {
-                GUILayout.Width(buttonHeight),
-                GUILayout.Height(buttonHeight)
-            };
-
-            const float dropdownButtonWidth = 140f;
-            GUILayoutOption[] dropdownLayoutOptions = new GUILayoutOption[] {
-                GUILayout.Height(buttonHeight),
-                GUILayout.Width(dropdownButtonWidth)
-            };
-
-            MpsGui.Header(propManagerHeader);
-            MpsGui.WhiteLine();
-
-            GUI.enabled = propManager.PropCount > 0;
-
-            GUILayout.BeginHorizontal();
-            propDropdown.Draw(dropdownLayoutOptions);
-            previousPropButton.Draw(arrowLayoutOptions);
-            nextPropButton.Draw(arrowLayoutOptions);
-            GUILayout.EndHorizontal();
-
-            GUILayoutOption noExpandWidth = GUILayout.ExpandWidth(false);
-
-            GUILayout.BeginHorizontal();
-            dragPointToggle.Draw(noExpandWidth);
-            gizmoToggle.Draw(noExpandWidth);
-            copyPropButton.Draw(noExpandWidth);
-            deletePropButton.Draw(noExpandWidth);
-            GUILayout.EndHorizontal();
-
-            GUILayout.BeginHorizontal();
-            shadowCastingToggle.Draw(noExpandWidth);
-            GUILayout.EndHorizontal();
-
-            GUI.enabled = true;
-        }
-
-        private void UpdatePropList()
+            UpdatePropList();
+            UpdateToggles();
+        };
+
+        this.propManager.FromPropSelect += (_, _) =>
         {
             updating = true;
-            propDropdown.SetDropdownItems(propManager.PropNameList, CurrentDoguIndex);
+            propDropdown.SelectedItemIndex = CurrentDoguIndex;
             updating = false;
-        }
+            UpdateToggles();
+        };
 
-        private void UpdateToggles()
+        propDropdown = new(this.propManager.PropNameList);
+        propDropdown.SelectionChange += (_, _) =>
         {
-            DragPointProp prop = propManager.CurrentProp;
-            if (prop == null) return;
+            if (updating)
+                return;
 
-            updating = true;
-            dragPointToggle.Value = prop.DragPointEnabled;
-            gizmoToggle.Value = prop.GizmoEnabled;
-            shadowCastingToggle.Value = prop.ShadowCasting;
-            updating = false;
-        }
+            this.propManager.CurrentPropIndex = propDropdown.SelectedItemIndex;
+
+            UpdateToggles();
+        };
+
+        previousPropButton = new("<");
+        previousPropButton.ControlEvent += (_, _) =>
+            propDropdown.Step(-1);
+
+        nextPropButton = new(">");
+        nextPropButton.ControlEvent += (_, _) =>
+            propDropdown.Step(1);
+
+        dragPointToggle = new(Translation.Get("propManagerPane", "dragPointToggle"));
+        dragPointToggle.ControlEvent += (_, _) =>
+        {
+            if (updating || this.propManager.PropCount is 0)
+                return;
+
+            this.propManager.CurrentProp.DragPointEnabled = dragPointToggle.Value;
+        };
+
+        gizmoToggle = new(Translation.Get("propManagerPane", "gizmoToggle"));
+        gizmoToggle.ControlEvent += (_, _) =>
+        {
+            if (updating || this.propManager.PropCount is 0)
+                return;
+
+            this.propManager.CurrentProp.GizmoEnabled = gizmoToggle.Value;
+        };
+
+        shadowCastingToggle = new(Translation.Get("propManagerPane", "shadowCastingToggle"));
+        shadowCastingToggle.ControlEvent += (_, _) =>
+        {
+            if (updating || this.propManager.PropCount is 0)
+                return;
+
+            this.propManager.CurrentProp.ShadowCasting = shadowCastingToggle.Value;
+        };
+
+        copyPropButton = new(Translation.Get("propManagerPane", "copyButton"));
+        copyPropButton.ControlEvent += (_, _) =>
+            this.propManager.CopyProp(CurrentDoguIndex);
+
+        deletePropButton = new(Translation.Get("propManagerPane", "deleteButton"));
+        deletePropButton.ControlEvent += (_, _) =>
+            this.propManager.RemoveProp(CurrentDoguIndex);
+
+        propManagerHeader = Translation.Get("propManagerPane", "header");
+    }
+
+    private int CurrentDoguIndex =>
+        propManager.CurrentPropIndex;
+
+    public override void Draw()
+    {
+        const float buttonHeight = 30;
+
+        var arrowLayoutOptions = new[]
+        {
+            GUILayout.Width(buttonHeight),
+            GUILayout.Height(buttonHeight),
+        };
+
+        const float dropdownButtonWidth = 140f;
+
+        var dropdownLayoutOptions = new[]
+        {
+            GUILayout.Height(buttonHeight),
+            GUILayout.Width(dropdownButtonWidth),
+        };
+
+        MpsGui.Header(propManagerHeader);
+        MpsGui.WhiteLine();
+
+        GUI.enabled = propManager.PropCount > 0;
+
+        GUILayout.BeginHorizontal();
+        propDropdown.Draw(dropdownLayoutOptions);
+        previousPropButton.Draw(arrowLayoutOptions);
+        nextPropButton.Draw(arrowLayoutOptions);
+        GUILayout.EndHorizontal();
+
+        var noExpandWidth = GUILayout.ExpandWidth(false);
+
+        GUILayout.BeginHorizontal();
+        dragPointToggle.Draw(noExpandWidth);
+        gizmoToggle.Draw(noExpandWidth);
+        copyPropButton.Draw(noExpandWidth);
+        deletePropButton.Draw(noExpandWidth);
+        GUILayout.EndHorizontal();
+
+        GUILayout.BeginHorizontal();
+        shadowCastingToggle.Draw(noExpandWidth);
+        GUILayout.EndHorizontal();
+
+        GUI.enabled = true;
+    }
+
+    protected override void ReloadTranslation()
+    {
+        dragPointToggle.Label = Translation.Get("propManagerPane", "dragPointToggle");
+        gizmoToggle.Label = Translation.Get("propManagerPane", "gizmoToggle");
+        shadowCastingToggle.Label = Translation.Get("propManagerPane", "shadowCastingToggle");
+        copyPropButton.Label = Translation.Get("propManagerPane", "copyButton");
+        deletePropButton.Label = Translation.Get("propManagerPane", "deleteButton");
+        propManagerHeader = Translation.Get("propManagerPane", "header");
+    }
+
+    private void UpdatePropList()
+    {
+        updating = true;
+        propDropdown.SetDropdownItems(propManager.PropNameList, CurrentDoguIndex);
+        updating = false;
+    }
+
+    private void UpdateToggles()
+    {
+        var prop = propManager.CurrentProp;
+
+        if (!prop)
+            return;
+
+        updating = true;
+        dragPointToggle.Value = prop.DragPointEnabled;
+        gizmoToggle.Value = prop.GizmoEnabled;
+        shadowCastingToggle.Value = prop.ShadowCasting;
+        updating = false;
     }
 }

+ 147 - 135
src/MeidoPhotoStudio.Plugin/GUI/Panes/BackgroundWindow2Panes/PropsPane.cs

@@ -1,168 +1,180 @@
-using UnityEngine;
 using System.Collections.Generic;
 using System.Linq;
 
-namespace MeidoPhotoStudio.Plugin
+using UnityEngine;
+
+namespace MeidoPhotoStudio.Plugin;
+
+public class PropsPane : BasePane
 {
-    public class PropsPane : BasePane
-    {
-        private readonly PropManager propManager;
-        private string currentCategory;
-        private string SelectedCategory => Constants.DoguCategories[doguCategoryDropdown.SelectedItemIndex];
-        private readonly Dropdown doguCategoryDropdown;
-        private readonly Dropdown doguDropdown;
-        private readonly Button addDoguButton;
-        private readonly Button nextDoguButton;
-        private readonly Button prevDoguButton;
-        private readonly Button nextDoguCategoryButton;
-        private readonly Button prevDoguCategoryButton;
-        private static bool handItemsReady;
-        private bool itemSelectorEnabled = true;
-
-        public PropsPane(PropManager propManager)
-        {
-            this.propManager = propManager;
+    private static bool handItemsReady;
 
-            handItemsReady = Constants.HandItemsInitialized;
-            if (!handItemsReady) Constants.MenuFilesChange += InitializeHandItems;
+    private readonly PropManager propManager;
+    private readonly Dropdown doguCategoryDropdown;
+    private readonly Dropdown doguDropdown;
+    private readonly Button addDoguButton;
+    private readonly Button nextDoguButton;
+    private readonly Button prevDoguButton;
+    private readonly Button nextDoguCategoryButton;
+    private readonly Button prevDoguCategoryButton;
 
-            doguCategoryDropdown = new Dropdown(Translation.GetArray("doguCategories", Constants.DoguCategories));
-            doguCategoryDropdown.SelectionChange += (s, a) => ChangeDoguCategory(SelectedCategory);
+    private string currentCategory;
+    private bool itemSelectorEnabled = true;
 
-            doguDropdown = new Dropdown(new[] { string.Empty });
+    public PropsPane(PropManager propManager)
+    {
+        this.propManager = propManager;
 
-            addDoguButton = new Button("+");
-            addDoguButton.ControlEvent += (s, a) => SpawnObject();
+        handItemsReady = Constants.HandItemsInitialized;
 
-            nextDoguButton = new Button(">");
-            nextDoguButton.ControlEvent += (s, a) => doguDropdown.Step(1);
+        if (!handItemsReady)
+            Constants.MenuFilesChange += InitializeHandItems;
 
-            prevDoguButton = new Button("<");
-            prevDoguButton.ControlEvent += (s, a) => doguDropdown.Step(-1);
+        doguCategoryDropdown = new(Translation.GetArray("doguCategories", Constants.DoguCategories));
+        doguCategoryDropdown.SelectionChange += (_, _) =>
+            ChangeDoguCategory(SelectedCategory);
 
-            nextDoguCategoryButton = new Button(">");
-            nextDoguCategoryButton.ControlEvent += (s, a) => doguCategoryDropdown.Step(1);
+        doguDropdown = new(new[] { string.Empty });
 
-            prevDoguCategoryButton = new Button("<");
-            prevDoguCategoryButton.ControlEvent += (s, a) => doguCategoryDropdown.Step(-1);
+        addDoguButton = new("+");
+        addDoguButton.ControlEvent += (_, _) =>
+            SpawnObject();
 
-            ChangeDoguCategory(SelectedCategory);
-        }
+        nextDoguButton = new(">");
+        nextDoguButton.ControlEvent += (_, _) =>
+            doguDropdown.Step(1);
 
-        protected override void ReloadTranslation()
-        {
-            doguCategoryDropdown.SetDropdownItems(
-                Translation.GetArray("doguCategories", Constants.DoguCategories)
-            );
+        prevDoguButton = new("<");
+        prevDoguButton.ControlEvent += (_, _) =>
+            doguDropdown.Step(-1);
 
-            string category = SelectedCategory;
+        nextDoguCategoryButton = new(">");
+        nextDoguCategoryButton.ControlEvent += (_, _) =>
+            doguCategoryDropdown.Step(1);
 
-            string[] translationArray;
+        prevDoguCategoryButton = new("<");
+        prevDoguCategoryButton.ControlEvent += (_, _) =>
+            doguCategoryDropdown.Step(-1);
 
-            if (category == Constants.customDoguCategories[Constants.DoguCategory.HandItem] && !handItemsReady)
-            {
-                translationArray = new[] { Translation.Get("systemMessage", "initializing") };
-            }
-            else translationArray = GetTranslations(category);
+        ChangeDoguCategory(SelectedCategory);
+    }
 
-            doguDropdown.SetDropdownItems(translationArray);
-        }
+    private string SelectedCategory =>
+        Constants.DoguCategories[doguCategoryDropdown.SelectedItemIndex];
+
+    public override void Draw()
+    {
+        const float buttonHeight = 30;
 
-        public override void Draw()
+        var arrowLayoutOptions = new[]
         {
-            const float buttonHeight = 30;
-            GUILayoutOption[] arrowLayoutOptions = {
-                GUILayout.Width(buttonHeight),
-                GUILayout.Height(buttonHeight)
-            };
-
-            const float dropdownButtonWidth = 120f;
-            GUILayoutOption[] dropdownLayoutOptions = new GUILayoutOption[] {
-                GUILayout.Height(buttonHeight),
-                GUILayout.Width(dropdownButtonWidth)
-            };
-
-            GUILayout.BeginHorizontal();
-            prevDoguCategoryButton.Draw(arrowLayoutOptions);
-            doguCategoryDropdown.Draw(dropdownLayoutOptions);
-            nextDoguCategoryButton.Draw(arrowLayoutOptions);
-            GUILayout.EndHorizontal();
-
-            GUI.enabled = itemSelectorEnabled;
-            GUILayout.BeginHorizontal();
-            doguDropdown.Draw(dropdownLayoutOptions);
-            prevDoguButton.Draw(arrowLayoutOptions);
-            nextDoguButton.Draw(arrowLayoutOptions);
-            addDoguButton.Draw(arrowLayoutOptions);
-            GUILayout.EndHorizontal();
-            GUI.enabled = true;
-        }
+            GUILayout.Width(buttonHeight),
+            GUILayout.Height(buttonHeight),
+        };
 
-        private void InitializeHandItems(object sender, MenuFilesEventArgs args)
+        const float dropdownButtonWidth = 120f;
+
+        var dropdownLayoutOptions = new[]
         {
-            if (args.Type == MenuFilesEventArgs.EventType.HandItems)
-            {
-                handItemsReady = true;
-                string selectedCategory = SelectedCategory;
-                if (selectedCategory == Constants.customDoguCategories[Constants.DoguCategory.HandItem])
-                {
-                    ChangeDoguCategory(selectedCategory, true);
-                }
-            }
-        }
+            GUILayout.Height(buttonHeight),
+            GUILayout.Width(dropdownButtonWidth),
+        };
+
+        GUILayout.BeginHorizontal();
+        prevDoguCategoryButton.Draw(arrowLayoutOptions);
+        doguCategoryDropdown.Draw(dropdownLayoutOptions);
+        nextDoguCategoryButton.Draw(arrowLayoutOptions);
+        GUILayout.EndHorizontal();
+
+        GUI.enabled = itemSelectorEnabled;
+        GUILayout.BeginHorizontal();
+        doguDropdown.Draw(dropdownLayoutOptions);
+        prevDoguButton.Draw(arrowLayoutOptions);
+        nextDoguButton.Draw(arrowLayoutOptions);
+        addDoguButton.Draw(arrowLayoutOptions);
+        GUILayout.EndHorizontal();
+        GUI.enabled = true;
+    }
+
+    protected override void ReloadTranslation()
+    {
+        doguCategoryDropdown.SetDropdownItems(Translation.GetArray("doguCategories", Constants.DoguCategories));
+
+        var category = SelectedCategory;
+
+        var translationArray =
+            category == Constants.CustomDoguCategories[Constants.DoguCategory.HandItem] && !handItemsReady
+                ? new[] { Translation.Get("systemMessage", "initializing") }
+                : GetTranslations(category);
+
+        doguDropdown.SetDropdownItems(translationArray);
+    }
+
+    private void InitializeHandItems(object sender, MenuFilesEventArgs args)
+    {
+        if (args.Type is not MenuFilesEventArgs.EventType.HandItems)
+            return;
+
+        handItemsReady = true;
+
+        var selectedCategory = SelectedCategory;
+
+        if (selectedCategory == Constants.CustomDoguCategories[Constants.DoguCategory.HandItem])
+            ChangeDoguCategory(selectedCategory, true);
+    }
 
-        private void ChangeDoguCategory(string category, bool force = false)
+    private void ChangeDoguCategory(string category, bool force = false)
+    {
+        if (category == currentCategory && !force)
+            return;
+
+        currentCategory = category;
+
+        string[] translationArray;
+
+        if (category == Constants.CustomDoguCategories[Constants.DoguCategory.HandItem] && !handItemsReady)
         {
-            if (category != currentCategory || force)
-            {
-                currentCategory = category;
-
-                string[] translationArray;
-
-                if (category == Constants.customDoguCategories[Constants.DoguCategory.HandItem] && !handItemsReady)
-                {
-                    translationArray = new[] { Translation.Get("systemMessage", "initializing") };
-                    itemSelectorEnabled = false;
-                }
-                else
-                {
-                    translationArray = GetTranslations(category);
-                    itemSelectorEnabled = true;
-                }
-                doguDropdown.SetDropdownItems(translationArray, 0);
-            }
+            translationArray = new[] { Translation.Get("systemMessage", "initializing") };
+            itemSelectorEnabled = false;
         }
-
-        private string[] GetTranslations(string category)
+        else
         {
-            IEnumerable<string> itemList = Constants.DoguDict[category];
-            if (category == Constants.customDoguCategories[Constants.DoguCategory.HandItem])
-            {
-                itemList = itemList.Select(item =>
-                {
-                    string handItemAsOdogu = Utility.HandItemToOdogu(item);
-                    return Translation.Has("propNames", handItemAsOdogu) ? handItemAsOdogu : item;
-                });
-            }
-
-            string translationCategory = category == Constants.customDoguCategories[Constants.DoguCategory.BGSmall]
-                ? "bgNames"
-                : "propNames";
-
-            return Translation.GetArray(translationCategory, itemList);
+            translationArray = GetTranslations(category);
+            itemSelectorEnabled = true;
         }
 
-        private void SpawnObject()
+        doguDropdown.SetDropdownItems(translationArray, 0);
+    }
+
+    private string[] GetTranslations(string category)
+    {
+        IEnumerable<string> itemList = Constants.DoguDict[category];
+
+        if (category == Constants.CustomDoguCategories[Constants.DoguCategory.HandItem])
         {
-            string assetName = Constants.DoguDict[SelectedCategory][doguDropdown.SelectedItemIndex];
-            if (SelectedCategory == Constants.customDoguCategories[Constants.DoguCategory.BGSmall])
-            {
-                propManager.AddBgProp(assetName);
-            }
-            else
+            // TODO: itemList should not be reused
+            itemList = itemList.Select(item =>
             {
-                propManager.AddGameProp(assetName);
-            }
+                var handItemAsOdogu = Utility.HandItemToOdogu(item);
+
+                return Translation.Has("propNames", handItemAsOdogu) ? handItemAsOdogu : item;
+            });
         }
+
+        var translationCategory = category == Constants.CustomDoguCategories[Constants.DoguCategory.BGSmall]
+            ? "bgNames"
+            : "propNames";
+
+        return Translation.GetArray(translationCategory, itemList);
+    }
+
+    private void SpawnObject()
+    {
+        var assetName = Constants.DoguDict[SelectedCategory][doguDropdown.SelectedItemIndex];
+
+        if (SelectedCategory == Constants.CustomDoguCategories[Constants.DoguCategory.BGSmall])
+            propManager.AddBgProp(assetName);
+        else
+            propManager.AddGameProp(assetName);
     }
 }

+ 74 - 67
src/MeidoPhotoStudio.Plugin/GUI/Panes/BackgroundWindowPanes/BackgroundSelectorPane.cs

@@ -1,82 +1,89 @@
-using UnityEngine;
-using System.Linq;
 using System.Collections.Generic;
+using System.Linq;
 
-namespace MeidoPhotoStudio.Plugin
+using UnityEngine;
+
+namespace MeidoPhotoStudio.Plugin;
+
+public class BackgroundSelectorPane : BasePane
 {
-    public class BackgroundSelectorPane : BasePane
+    private readonly EnvironmentManager environmentManager;
+    private readonly Dropdown bgDropdown;
+    private readonly Button prevBGButton;
+    private readonly Button nextBGButton;
+
+    public BackgroundSelectorPane(EnvironmentManager environmentManager)
     {
-        private readonly EnvironmentManager environmentManager;
-        private readonly Dropdown bgDropdown;
-        private readonly Button prevBGButton;
-        private readonly Button nextBGButton;
+        this.environmentManager = environmentManager;
 
-        public BackgroundSelectorPane(EnvironmentManager environmentManager)
-        {
-            this.environmentManager = environmentManager;
+        var theaterIndex = Constants.BGList.FindIndex(bg => bg == EnvironmentManager.DefaultBg);
+        var bgList = new List<string>(Translation.GetList("bgNames", Constants.BGList));
 
-            int theaterIndex = Constants.BGList.FindIndex(bg => bg == EnvironmentManager.defaultBg);
+        if (Constants.MyRoomCustomBGIndex >= 0)
+            bgList.AddRange(Constants.MyRoomCustomBGList.Select(kvp => kvp.Value));
 
-            List<string> bgList = new List<string>(Translation.GetList("bgNames", Constants.BGList));
-            if (Constants.MyRoomCustomBGIndex >= 0)
-            {
-                bgList.AddRange(Constants.MyRoomCustomBGList.Select(kvp => kvp.Value));
-            }
+        bgDropdown = new(bgList.ToArray(), theaterIndex);
+        bgDropdown.SelectionChange += (_, _) =>
+            ChangeBackground();
 
-            bgDropdown = new Dropdown(bgList.ToArray(), theaterIndex);
-            bgDropdown.SelectionChange += (s, a) => ChangeBackground();
+        prevBGButton = new("<");
+        prevBGButton.ControlEvent += (_, _) =>
+            bgDropdown.Step(-1);
 
-            prevBGButton = new Button("<");
-            prevBGButton.ControlEvent += (s, a) => bgDropdown.Step(-1);
+        nextBGButton = new(">");
+        nextBGButton.ControlEvent += (_, _) =>
+            bgDropdown.Step(1);
+    }
 
-            nextBGButton = new Button(">");
-            nextBGButton.ControlEvent += (s, a) => bgDropdown.Step(1);
-        }
+    public override void Draw()
+    {
+        const float buttonHeight = 30;
 
-        protected override void ReloadTranslation()
+        var arrowLayoutOptions = new[]
         {
-            List<string> bgList = new List<string>(Translation.GetList("bgNames", Constants.BGList));
-            if (Constants.MyRoomCustomBGIndex >= 0)
-            {
-                bgList.AddRange(Constants.MyRoomCustomBGList.Select(kvp => kvp.Value));
-            }
-
-            updating = true;
-            bgDropdown.SetDropdownItems(bgList.ToArray());
-            updating = false;
-        }
-
-        public override void Draw()
-        {
-            const float buttonHeight = 30;
-            GUILayoutOption[] arrowLayoutOptions = {
-                GUILayout.Width(buttonHeight),
-                GUILayout.Height(buttonHeight)
-            };
-
-            const float dropdownButtonWidth = 153f;
-            GUILayoutOption[] dropdownLayoutOptions = new GUILayoutOption[] {
-                GUILayout.Height(buttonHeight),
-                GUILayout.Width(dropdownButtonWidth)
-            };
-
-            GUILayout.BeginHorizontal();
-            prevBGButton.Draw(arrowLayoutOptions);
-            bgDropdown.Draw(dropdownLayoutOptions);
-            nextBGButton.Draw(arrowLayoutOptions);
-            GUILayout.EndHorizontal();
-        }
-
-        private void ChangeBackground()
+            GUILayout.Width(buttonHeight),
+            GUILayout.Height(buttonHeight),
+        };
+
+        const float dropdownButtonWidth = 153f;
+
+        var dropdownLayoutOptions = new[]
         {
-            if (updating) return;
-            int selectedIndex = bgDropdown.SelectedItemIndex;
-            bool isCreative = bgDropdown.SelectedItemIndex >= Constants.MyRoomCustomBGIndex;
-            string bg = isCreative
-                ? Constants.MyRoomCustomBGList[selectedIndex - Constants.MyRoomCustomBGIndex].Key
-                : Constants.BGList[selectedIndex];
-
-            environmentManager.ChangeBackground(bg, isCreative);
-        }
+            GUILayout.Height(buttonHeight),
+            GUILayout.Width(dropdownButtonWidth),
+        };
+
+        GUILayout.BeginHorizontal();
+        prevBGButton.Draw(arrowLayoutOptions);
+        bgDropdown.Draw(dropdownLayoutOptions);
+        nextBGButton.Draw(arrowLayoutOptions);
+        GUILayout.EndHorizontal();
+    }
+
+    protected override void ReloadTranslation()
+    {
+        var bgList = new List<string>(Translation.GetList("bgNames", Constants.BGList));
+
+        if (Constants.MyRoomCustomBGIndex >= 0)
+            bgList.AddRange(Constants.MyRoomCustomBGList.Select(kvp => kvp.Value));
+
+        updating = true;
+        bgDropdown.SetDropdownItems(bgList.ToArray());
+        updating = false;
+    }
+
+    private void ChangeBackground()
+    {
+        if (updating)
+            return;
+
+        var selectedIndex = bgDropdown.SelectedItemIndex;
+        var isCreative = bgDropdown.SelectedItemIndex >= Constants.MyRoomCustomBGIndex;
+
+        var bg = isCreative
+            ? Constants.MyRoomCustomBGList[selectedIndex - Constants.MyRoomCustomBGIndex].Key
+            : Constants.BGList[selectedIndex];
+
+        environmentManager.ChangeBackground(bg, isCreative);
     }
 }

+ 83 - 70
src/MeidoPhotoStudio.Plugin/GUI/Panes/BackgroundWindowPanes/CameraPane.cs

@@ -1,86 +1,99 @@
-using System.Linq;
+using System.Linq;
+
 using UnityEngine;
 
-namespace MeidoPhotoStudio.Plugin
+namespace MeidoPhotoStudio.Plugin;
+
+public class CameraPane : BasePane
 {
-    public class CameraPane : BasePane
+    private readonly CameraManager cameraManager;
+    private readonly SelectionGrid cameraGrid;
+    private readonly Slider zRotationSlider;
+    private readonly Slider fovSlider;
+
+    private string header;
+
+    public CameraPane(CameraManager cameraManager)
     {
-        private readonly CameraManager cameraManager;
-        private readonly SelectionGrid cameraGrid;
-        private readonly Slider zRotationSlider;
-        private readonly Slider fovSlider;
-        private string header;
+        this.cameraManager = cameraManager;
+        this.cameraManager.CameraChange += (_, _) =>
+            UpdatePane();
+
+        var camera = CameraUtility.MainCamera.camera;
+        var eulerAngles = camera.transform.eulerAngles;
+
+        zRotationSlider = new(Translation.Get("cameraPane", "zRotation"), 0f, 360f, eulerAngles.z)
+        {
+            HasReset = true,
+            HasTextField = true,
+        };
 
-        public CameraPane(CameraManager cameraManager)
+        zRotationSlider.ControlEvent += (_, _) =>
         {
-            this.cameraManager = cameraManager;
-            this.cameraManager.CameraChange += (s, a) => UpdatePane();
-
-            Camera camera = CameraUtility.MainCamera.camera;
-            Vector3 eulerAngles = camera.transform.eulerAngles;
-
-            zRotationSlider = new Slider(Translation.Get("cameraPane", "zRotation"), 0f, 360f, eulerAngles.z)
-            {
-                HasReset = true, HasTextField = true
-            };
-            zRotationSlider.ControlEvent += (s, a) =>
-            {
-                if (updating) return;
-                Vector3 newRotation = camera.transform.eulerAngles;
-                newRotation.z = zRotationSlider.Value;
-                camera.transform.rotation = Quaternion.Euler(newRotation);
-            };
-
-            var fieldOfView = camera.fieldOfView;
-            fovSlider = new Slider(Translation.Get("cameraPane", "fov"), 20f, 150f, fieldOfView, fieldOfView)
-            {
-                HasReset = true, HasTextField = true
-            };
-            fovSlider.ControlEvent += (s, a) =>
-            {
-                if (updating) return;
-                camera.fieldOfView = fovSlider.Value;
-            };
-            cameraGrid = new SelectionGrid(
-                Enumerable.Range(1, cameraManager.CameraCount).Select(x => x.ToString()).ToArray()
-            );
-            cameraGrid.ControlEvent += (s, a) =>
-            {
-                if (updating) return;
-                cameraManager.CurrentCameraIndex = cameraGrid.SelectedItemIndex;
-            };
-
-            header = Translation.Get("cameraPane", "header");
-        }
-
-        protected override void ReloadTranslation()
+            if (updating)
+                return;
+
+            var newRotation = camera.transform.eulerAngles;
+
+            newRotation.z = zRotationSlider.Value;
+            camera.transform.rotation = Quaternion.Euler(newRotation);
+        };
+
+        var fieldOfView = camera.fieldOfView;
+
+        fovSlider = new(Translation.Get("cameraPane", "fov"), 20f, 150f, fieldOfView, fieldOfView)
         {
-            zRotationSlider.Label = Translation.Get("cameraPane", "zRotation");
-            fovSlider.Label = Translation.Get("cameraPane", "fov");
-            header = Translation.Get("cameraPane", "header");
-        }
+            HasReset = true,
+            HasTextField = true,
+        };
 
-        public override void Draw()
+        fovSlider.ControlEvent += (_, _) =>
         {
-            MpsGui.Header(header);
-            MpsGui.WhiteLine();
-            cameraGrid.Draw();
-            zRotationSlider.Draw();
-            fovSlider.Draw();
-        }
-
-        public override void UpdatePane()
+            if (updating)
+                return;
+
+            camera.fieldOfView = fovSlider.Value;
+        };
+
+        cameraGrid = new(Enumerable.Range(1, cameraManager.CameraCount).Select(x => x.ToString()).ToArray());
+        cameraGrid.ControlEvent += (_, _) =>
         {
-            updating = true;
+            if (updating)
+                return;
+
+            cameraManager.CurrentCameraIndex = cameraGrid.SelectedItemIndex;
+        };
+
+        header = Translation.Get("cameraPane", "header");
+    }
+
+    public override void Draw()
+    {
+        MpsGui.Header(header);
+        MpsGui.WhiteLine();
+        cameraGrid.Draw();
+        zRotationSlider.Draw();
+        fovSlider.Draw();
+    }
+
+    public override void UpdatePane()
+    {
+        updating = true;
 
-            Camera camera = CameraUtility.MainCamera.camera;
+        var camera = CameraUtility.MainCamera.camera;
 
-            zRotationSlider.Value = camera.transform.eulerAngles.z;
-            fovSlider.Value = camera.fieldOfView;
+        zRotationSlider.Value = camera.transform.eulerAngles.z;
+        fovSlider.Value = camera.fieldOfView;
 
-            cameraGrid.SelectedItemIndex = cameraManager.CurrentCameraIndex;
+        cameraGrid.SelectedItemIndex = cameraManager.CurrentCameraIndex;
 
-            updating = false;
-        }
+        updating = false;
+    }
+
+    protected override void ReloadTranslation()
+    {
+        zRotationSlider.Label = Translation.Get("cameraPane", "zRotation");
+        fovSlider.Label = Translation.Get("cameraPane", "fov");
+        header = Translation.Get("cameraPane", "header");
     }
 }

+ 79 - 62
src/MeidoPhotoStudio.Plugin/GUI/Panes/BackgroundWindowPanes/DragPointPane.cs

@@ -1,74 +1,91 @@
 using UnityEngine;
 
-namespace MeidoPhotoStudio.Plugin
+namespace MeidoPhotoStudio.Plugin;
+
+public class DragPointPane : BasePane
 {
-    public class DragPointPane : BasePane
+    private readonly Toggle propsCubeToggle;
+    private readonly Toggle smallCubeToggle;
+    private readonly Toggle maidCubeToggle;
+    private readonly Toggle bgCubeToggle;
+
+    private string header;
+
+    public DragPointPane()
     {
-        private string header;
-        private readonly Toggle propsCubeToggle;
-        private readonly Toggle smallCubeToggle;
-        private readonly Toggle maidCubeToggle;
-        private readonly Toggle bgCubeToggle;
-        private enum Setting
-        {
-            Prop, Maid, Background, Size
-        }
+        header = Translation.Get("movementCube", "header");
 
-        public DragPointPane()
-        {
-            header = Translation.Get("movementCube", "header");
-            propsCubeToggle = new Toggle(Translation.Get("movementCube", "props"), PropManager.CubeActive);
-            smallCubeToggle = new Toggle(Translation.Get("movementCube", "small"));
-            maidCubeToggle = new Toggle(Translation.Get("movementCube", "maid"), MeidoDragPointManager.CubeActive);
-            bgCubeToggle = new Toggle(Translation.Get("movementCube", "bg"), EnvironmentManager.CubeActive);
-
-            propsCubeToggle.ControlEvent += (s, a) => ChangeDragPointSetting(Setting.Prop, propsCubeToggle.Value);
-            smallCubeToggle.ControlEvent += (s, a) => ChangeDragPointSetting(Setting.Size, smallCubeToggle.Value);
-            maidCubeToggle.ControlEvent += (s, a) => ChangeDragPointSetting(Setting.Maid, maidCubeToggle.Value);
-            bgCubeToggle.ControlEvent += (s, a) => ChangeDragPointSetting(Setting.Background, bgCubeToggle.Value);
-        }
+        propsCubeToggle = new(Translation.Get("movementCube", "props"), PropManager.CubeActive);
+        propsCubeToggle.ControlEvent += (_, _) =>
+            ChangeDragPointSetting(Setting.Prop, propsCubeToggle.Value);
 
-        protected override void ReloadTranslation()
-        {
-            header = Translation.Get("movementCube", "header");
-            propsCubeToggle.Label = Translation.Get("movementCube", "props");
-            smallCubeToggle.Label = Translation.Get("movementCube", "small");
-            maidCubeToggle.Label = Translation.Get("movementCube", "maid");
-            bgCubeToggle.Label = Translation.Get("movementCube", "bg");
-        }
+        smallCubeToggle = new(Translation.Get("movementCube", "small"));
+        smallCubeToggle.ControlEvent += (_, _) =>
+            ChangeDragPointSetting(Setting.Size, smallCubeToggle.Value);
 
-        public override void Draw()
-        {
-            MpsGui.Header(header);
-            MpsGui.WhiteLine();
-
-            GUILayout.BeginHorizontal();
-            propsCubeToggle.Draw();
-            smallCubeToggle.Draw();
-            maidCubeToggle.Draw();
-            bgCubeToggle.Draw();
-            GUILayout.EndHorizontal();
-        }
+        maidCubeToggle = new(Translation.Get("movementCube", "maid"), MeidoDragPointManager.CubeActive);
+        maidCubeToggle.ControlEvent += (_, _) =>
+            ChangeDragPointSetting(Setting.Maid, maidCubeToggle.Value);
+
+        bgCubeToggle = new(Translation.Get("movementCube", "bg"), EnvironmentManager.CubeActive);
+        bgCubeToggle.ControlEvent += (_, _) =>
+            ChangeDragPointSetting(Setting.Background, bgCubeToggle.Value);
+    }
+
+    private enum Setting
+    {
+        Prop,
+        Maid,
+        Background,
+        Size,
+    }
+
+    public override void Draw()
+    {
+        MpsGui.Header(header);
+        MpsGui.WhiteLine();
+
+        GUILayout.BeginHorizontal();
+        propsCubeToggle.Draw();
+        smallCubeToggle.Draw();
+        maidCubeToggle.Draw();
+        bgCubeToggle.Draw();
+        GUILayout.EndHorizontal();
+    }
+
+    protected override void ReloadTranslation()
+    {
+        header = Translation.Get("movementCube", "header");
+        propsCubeToggle.Label = Translation.Get("movementCube", "props");
+        smallCubeToggle.Label = Translation.Get("movementCube", "small");
+        maidCubeToggle.Label = Translation.Get("movementCube", "maid");
+        bgCubeToggle.Label = Translation.Get("movementCube", "bg");
+    }
 
-        private void ChangeDragPointSetting(Setting setting, bool value)
+    private void ChangeDragPointSetting(Setting setting, bool value)
+    {
+        switch (setting)
         {
-            switch (setting)
-            {
-                case Setting.Prop:
-                    PropManager.CubeActive = value;
-                    break;
-                case Setting.Background:
-                    EnvironmentManager.CubeActive = value;
-                    break;
-                case Setting.Maid:
-                    MeidoDragPointManager.CubeActive = value;
-                    break;
-                case Setting.Size:
-                    MeidoDragPointManager.CubeSmall = value;
-                    EnvironmentManager.CubeSmall = value;
-                    PropManager.CubeSmall = value;
-                    break;
-            }
+            case Setting.Prop:
+                PropManager.CubeActive = value;
+
+                break;
+            case Setting.Background:
+                EnvironmentManager.CubeActive = value;
+
+                break;
+            case Setting.Maid:
+                MeidoDragPointManager.CubeActive = value;
+
+                break;
+            case Setting.Size:
+                MeidoDragPointManager.CubeSmall = value;
+                EnvironmentManager.CubeSmall = value;
+                PropManager.CubeSmall = value;
+
+                break;
+            default:
+                break;
         }
     }
 }

+ 106 - 94
src/MeidoPhotoStudio.Plugin/GUI/Panes/BackgroundWindowPanes/EffectsPanes/BloomPane.cs

@@ -1,104 +1,116 @@
 using UnityEngine;
 
-namespace MeidoPhotoStudio.Plugin
+namespace MeidoPhotoStudio.Plugin;
+
+public class BloomPane : EffectPane<BloomEffectManager>
 {
-    public class BloomPane : EffectPane<BloomEffectManager>
+    private readonly Slider intensitySlider;
+    private readonly Slider blurSlider;
+    private readonly Slider redSlider;
+    private readonly Slider greenSlider;
+    private readonly Slider blueSlider;
+    private readonly Toggle hdrToggle;
+
+    public BloomPane(EffectManager effectManager)
+        : base(effectManager)
     {
-        protected override BloomEffectManager EffectManager { get; set; }
-        private readonly Slider intensitySlider;
-        private readonly Slider blurSlider;
-        private readonly Slider redSlider;
-        private readonly Slider greenSlider;
-        private readonly Slider blueSlider;
-        private readonly Toggle hdrToggle;
-
-        public BloomPane(EffectManager effectManager) : base(effectManager)
+        intensitySlider = new(Translation.Get("effectBloom", "intensity"), 0f, 100f, EffectManager.BloomValue);
+        intensitySlider.ControlEvent += (_, _) =>
         {
-            intensitySlider = new Slider(
-                Translation.Get("effectBloom", "intensity"), 0f, 100f, EffectManager.BloomValue
-            );
-            intensitySlider.ControlEvent += (s, a) =>
-            {
-                if (updating) return;
-                EffectManager.BloomValue = intensitySlider.Value;
-            };
-            blurSlider = new Slider(Translation.Get("effectBloom", "blur"), 0f, 15f, EffectManager.BlurIterations);
-            blurSlider.ControlEvent += (s, a) =>
-            {
-                if (updating) return;
-                EffectManager.BlurIterations = (int)blurSlider.Value;
-            };
-            redSlider = new Slider(
-                Translation.Get("backgroundWindow", "red"), 1f, 0.5f, EffectManager.BloomThresholdColorRed
-            );
-            redSlider.ControlEvent += (s, a) =>
-            {
-                if (updating) return;
-                EffectManager.BloomThresholdColorRed = redSlider.Value;
-            };
-            greenSlider = new Slider(
-                Translation.Get("backgroundWindow", "green"), 1f, 0.5f, EffectManager.BloomThresholdColorGreen
-            );
-            greenSlider.ControlEvent += (s, a) =>
-            {
-                if (updating) return;
-                EffectManager.BloomThresholdColorGreen = greenSlider.Value;
-            };
-            blueSlider = new Slider(
-                Translation.Get("backgroundWindow", "blue"), 1f, 0.5f, EffectManager.BloomThresholdColorBlue
-            );
-            blueSlider.ControlEvent += (s, a) =>
-            {
-                if (updating) return;
-                EffectManager.BloomThresholdColorBlue = blueSlider.Value;
-            };
-            hdrToggle = new Toggle(Translation.Get("effectBloom", "hdrToggle"), EffectManager.BloomHDR);
-            hdrToggle.ControlEvent += (s, a) =>
-            {
-                if (updating) return;
-                EffectManager.BloomHDR = hdrToggle.Value;
-            };
-        }
-
-        protected override void TranslatePane()
+            if (updating)
+                return;
+
+            EffectManager.BloomValue = intensitySlider.Value;
+        };
+
+        blurSlider = new(Translation.Get("effectBloom", "blur"), 0f, 15f, EffectManager.BlurIterations);
+        blurSlider.ControlEvent += (_, _) =>
         {
-            intensitySlider.Label = Translation.Get("effectBloom", "intensity");
-            blurSlider.Label = Translation.Get("effectBloom", "blur");
-            redSlider.Label = Translation.Get("backgroundWindow", "red");
-            greenSlider.Label = Translation.Get("backgroundWindow", "green");
-            blueSlider.Label = Translation.Get("backgroundWindow", "blue");
-            hdrToggle.Label = Translation.Get("effectBloom", "hdrToggle");
-        }
-
-        protected override void UpdateControls()
+            if (updating)
+                return;
+
+            EffectManager.BlurIterations = (int)blurSlider.Value;
+        };
+
+        redSlider = new(Translation.Get("backgroundWindow", "red"), 1f, 0.5f, EffectManager.BloomThresholdColorRed);
+        redSlider.ControlEvent += (_, _) =>
         {
-            intensitySlider.Value = EffectManager.BloomValue;
-            blurSlider.Value = EffectManager.BlurIterations;
-            redSlider.Value = EffectManager.BloomThresholdColorRed;
-            greenSlider.Value = EffectManager.BloomThresholdColorGreen;
-            blueSlider.Value = EffectManager.BloomThresholdColorBlue;
-            hdrToggle.Value = EffectManager.BloomHDR;
-        }
-
-        protected override void DrawPane()
+            if (updating)
+                return;
+
+            EffectManager.BloomThresholdColorRed = redSlider.Value;
+        };
+
+        greenSlider =
+            new(Translation.Get("backgroundWindow", "green"), 1f, 0.5f, EffectManager.BloomThresholdColorGreen);
+
+        greenSlider.ControlEvent += (_, _) =>
+        {
+            if (updating)
+                return;
+
+            EffectManager.BloomThresholdColorGreen = greenSlider.Value;
+        };
+
+        blueSlider = new(Translation.Get("backgroundWindow", "blue"), 1f, 0.5f, EffectManager.BloomThresholdColorBlue);
+        blueSlider.ControlEvent += (_, _) =>
+        {
+            if (updating)
+                return;
+
+            EffectManager.BloomThresholdColorBlue = blueSlider.Value;
+        };
+
+        hdrToggle = new(Translation.Get("effectBloom", "hdrToggle"), EffectManager.BloomHDR);
+        hdrToggle.ControlEvent += (_, _) =>
         {
-            GUILayoutOption sliderWidth = MpsGui.HalfSlider;
-
-            GUILayout.BeginHorizontal();
-            intensitySlider.Draw(sliderWidth);
-            blurSlider.Draw(sliderWidth);
-            GUILayout.EndHorizontal();
-
-            GUILayout.BeginHorizontal();
-            redSlider.Draw(sliderWidth);
-            greenSlider.Draw(sliderWidth);
-            GUILayout.EndHorizontal();
-
-            GUILayout.BeginHorizontal();
-            blueSlider.Draw(sliderWidth);
-            GUILayout.FlexibleSpace();
-            hdrToggle.Draw(GUILayout.ExpandWidth(false));
-            GUILayout.EndHorizontal();
-        }
+            if (updating)
+                return;
+
+            EffectManager.BloomHDR = hdrToggle.Value;
+        };
+    }
+
+    protected override BloomEffectManager EffectManager { get; set; }
+
+    protected override void TranslatePane()
+    {
+        intensitySlider.Label = Translation.Get("effectBloom", "intensity");
+        blurSlider.Label = Translation.Get("effectBloom", "blur");
+        redSlider.Label = Translation.Get("backgroundWindow", "red");
+        greenSlider.Label = Translation.Get("backgroundWindow", "green");
+        blueSlider.Label = Translation.Get("backgroundWindow", "blue");
+        hdrToggle.Label = Translation.Get("effectBloom", "hdrToggle");
+    }
+
+    protected override void UpdateControls()
+    {
+        intensitySlider.Value = EffectManager.BloomValue;
+        blurSlider.Value = EffectManager.BlurIterations;
+        redSlider.Value = EffectManager.BloomThresholdColorRed;
+        greenSlider.Value = EffectManager.BloomThresholdColorGreen;
+        blueSlider.Value = EffectManager.BloomThresholdColorBlue;
+        hdrToggle.Value = EffectManager.BloomHDR;
+    }
+
+    protected override void DrawPane()
+    {
+        var sliderWidth = MpsGui.HalfSlider;
+
+        GUILayout.BeginHorizontal();
+        intensitySlider.Draw(sliderWidth);
+        blurSlider.Draw(sliderWidth);
+        GUILayout.EndHorizontal();
+
+        GUILayout.BeginHorizontal();
+        redSlider.Draw(sliderWidth);
+        greenSlider.Draw(sliderWidth);
+        GUILayout.EndHorizontal();
+
+        GUILayout.BeginHorizontal();
+        blueSlider.Draw(sliderWidth);
+        GUILayout.FlexibleSpace();
+        hdrToggle.Draw(GUILayout.ExpandWidth(false));
+        GUILayout.EndHorizontal();
     }
 }

+ 92 - 77
src/MeidoPhotoStudio.Plugin/GUI/Panes/BackgroundWindowPanes/EffectsPanes/DepthOfFieldPane.cs

@@ -1,87 +1,102 @@
 using UnityEngine;
 
-namespace MeidoPhotoStudio.Plugin
+namespace MeidoPhotoStudio.Plugin;
+
+public class DepthOfFieldPane : EffectPane<DepthOfFieldEffectManager>
 {
-    public class DepthOfFieldPane : EffectPane<DepthOfFieldEffectManager>
+    private readonly Slider focalLengthSlider;
+    private readonly Slider focalSizeSlider;
+    private readonly Slider apertureSlider;
+    private readonly Slider blurSlider;
+    private readonly Toggle thicknessToggle;
+
+    public DepthOfFieldPane(EffectManager effectManager)
+        : base(effectManager)
     {
-        protected override DepthOfFieldEffectManager EffectManager { get; set; }
-        private readonly Slider focalLengthSlider;
-        private readonly Slider focalSizeSlider;
-        private readonly Slider apertureSlider;
-        private readonly Slider blurSlider;
-        private readonly Toggle thicknessToggle;
-
-        public DepthOfFieldPane(EffectManager effectManager) : base(effectManager)
+        focalLengthSlider =
+            new(Translation.Get("effectDof", "focalLength"), 0f, 10f, EffectManager.FocalLength);
+
+        focalLengthSlider.ControlEvent += (_, _) =>
         {
-            focalLengthSlider = new Slider(
-                Translation.Get("effectDof", "focalLength"), 0f, 10f, EffectManager.FocalLength
-            );
-            focalSizeSlider = new Slider(Translation.Get("effectDof", "focalArea"), 0f, 2f, EffectManager.FocalSize);
-            apertureSlider = new Slider(Translation.Get("effectDof", "aperture"), 0f, 60f, EffectManager.Aperture);
-            blurSlider = new Slider(Translation.Get("effectDof", "blur"), 0f, 10f, EffectManager.MaxBlurSize);
-            thicknessToggle = new Toggle(Translation.Get("effectDof", "thicknessToggle"), EffectManager.VisualizeFocus);
-            focalLengthSlider.ControlEvent += (s, a) =>
-            {
-                if (updating) return;
-                EffectManager.FocalLength = focalLengthSlider.Value;
-            };
-            focalSizeSlider.ControlEvent += (s, a) =>
-            {
-                if (updating) return;
-                EffectManager.FocalSize = focalSizeSlider.Value;
-            };
-            apertureSlider.ControlEvent += (s, a) =>
-            {
-                if (updating) return;
-                EffectManager.Aperture = apertureSlider.Value;
-            };
-            blurSlider.ControlEvent += (s, a) =>
-            {
-                if (updating) return;
-                EffectManager.MaxBlurSize = blurSlider.Value;
-            };
-            thicknessToggle.ControlEvent += (s, a) =>
-            {
-                if (updating) return;
-                EffectManager.VisualizeFocus = thicknessToggle.Value;
-            };
-        }
-
-        protected override void TranslatePane()
+            if (updating)
+                return;
+
+            EffectManager.FocalLength = focalLengthSlider.Value;
+        };
+
+        focalSizeSlider = new(Translation.Get("effectDof", "focalArea"), 0f, 2f, EffectManager.FocalSize);
+        focalSizeSlider.ControlEvent += (_, _) =>
         {
-            focalLengthSlider.Label = Translation.Get("effectDof", "focalLength");
-            focalSizeSlider.Label = Translation.Get("effectDof", "focalArea");
-            apertureSlider.Label = Translation.Get("effectDof", "aperture");
-            blurSlider.Label = Translation.Get("effectDof", "blur");
-            thicknessToggle.Label = Translation.Get("effectDof", "thicknessToggle");
-        }
-
-        protected override void UpdateControls()
+            if (updating)
+                return;
+
+            EffectManager.FocalSize = focalSizeSlider.Value;
+        };
+
+        apertureSlider = new(Translation.Get("effectDof", "aperture"), 0f, 60f, EffectManager.Aperture);
+        apertureSlider.ControlEvent += (_, _) =>
         {
-            focalLengthSlider.Value = EffectManager.FocalLength;
-            focalSizeSlider.Value = EffectManager.FocalSize;
-            apertureSlider.Value = EffectManager.Aperture;
-            blurSlider.Value = EffectManager.MaxBlurSize;
-            thicknessToggle.Value = EffectManager.VisualizeFocus;
-        }
-
-        protected override void DrawPane()
+            if (updating)
+                return;
+
+            EffectManager.Aperture = apertureSlider.Value;
+        };
+
+        blurSlider = new(Translation.Get("effectDof", "blur"), 0f, 10f, EffectManager.MaxBlurSize);
+        blurSlider.ControlEvent += (_, _) =>
+        {
+            if (updating)
+                return;
+
+            EffectManager.MaxBlurSize = blurSlider.Value;
+        };
+
+        thicknessToggle = new(Translation.Get("effectDof", "thicknessToggle"), EffectManager.VisualizeFocus);
+        thicknessToggle.ControlEvent += (_, _) =>
         {
-            focalLengthSlider.Draw();
-
-            GUILayoutOption sliderWidth = MpsGui.HalfSlider;
-
-            GUILayout.BeginHorizontal();
-            focalSizeSlider.Draw(sliderWidth);
-            apertureSlider.Draw(sliderWidth);
-            GUILayout.EndHorizontal();
-
-            GUILayout.BeginHorizontal();
-            blurSlider.Draw(sliderWidth);
-            GUILayout.FlexibleSpace();
-            thicknessToggle.Draw();
-            GUILayout.EndHorizontal();
-            GUI.enabled = true;
-        }
+            if (updating)
+                return;
+
+            EffectManager.VisualizeFocus = thicknessToggle.Value;
+        };
+    }
+
+    protected override DepthOfFieldEffectManager EffectManager { get; set; }
+
+    protected override void TranslatePane()
+    {
+        focalLengthSlider.Label = Translation.Get("effectDof", "focalLength");
+        focalSizeSlider.Label = Translation.Get("effectDof", "focalArea");
+        apertureSlider.Label = Translation.Get("effectDof", "aperture");
+        blurSlider.Label = Translation.Get("effectDof", "blur");
+        thicknessToggle.Label = Translation.Get("effectDof", "thicknessToggle");
+    }
+
+    protected override void UpdateControls()
+    {
+        focalLengthSlider.Value = EffectManager.FocalLength;
+        focalSizeSlider.Value = EffectManager.FocalSize;
+        apertureSlider.Value = EffectManager.Aperture;
+        blurSlider.Value = EffectManager.MaxBlurSize;
+        thicknessToggle.Value = EffectManager.VisualizeFocus;
+    }
+
+    protected override void DrawPane()
+    {
+        focalLengthSlider.Draw();
+
+        var sliderWidth = MpsGui.HalfSlider;
+
+        GUILayout.BeginHorizontal();
+        focalSizeSlider.Draw(sliderWidth);
+        apertureSlider.Draw(sliderWidth);
+        GUILayout.EndHorizontal();
+
+        GUILayout.BeginHorizontal();
+        blurSlider.Draw(sliderWidth);
+        GUILayout.FlexibleSpace();
+        thicknessToggle.Draw();
+        GUILayout.EndHorizontal();
+        GUI.enabled = true;
     }
 }

+ 70 - 58
src/MeidoPhotoStudio.Plugin/GUI/Panes/BackgroundWindowPanes/EffectsPanes/EffectPane.cs

@@ -1,74 +1,86 @@
 using UnityEngine;
 
-namespace MeidoPhotoStudio.Plugin
+namespace MeidoPhotoStudio.Plugin;
+
+public abstract class EffectPane<T> : BasePane
+    where T : IEffectManager
 {
-    public abstract class EffectPane<T> : BasePane where T : IEffectManager
+    protected readonly Toggle effectToggle;
+    protected readonly Button resetEffectButton;
+
+    private bool enabled;
+
+    protected EffectPane(EffectManager effectManager)
     {
-        protected abstract T EffectManager { get; set; }
-        protected readonly Toggle effectToggle;
-        protected readonly Button resetEffectButton;
-        private bool enabled;
-        public override bool Enabled
-        {
-            get => enabled;
-            set
-            {
-                enabled = value;
-                if (updating) return;
-                EffectManager.SetEffectActive(enabled);
-            }
-        }
+        EffectManager = effectManager.Get<T>();
 
-        protected EffectPane(EffectManager effectManager)
-        {
-            EffectManager = effectManager.Get<T>();
-            resetEffectButton = new Button(Translation.Get("effectsPane", "reset"));
-            resetEffectButton.ControlEvent += (s, a) => ResetEffect();
-            effectToggle = new Toggle(Translation.Get("effectsPane", "onToggle"));
-            effectToggle.ControlEvent += (s, a) => Enabled = effectToggle.Value;
-        }
+        resetEffectButton = new(Translation.Get("effectsPane", "reset"));
+        resetEffectButton.ControlEvent += (_, _) =>
+            ResetEffect();
 
-        protected override void ReloadTranslation()
+        effectToggle = new(Translation.Get("effectsPane", "onToggle"));
+        effectToggle.ControlEvent += (_, _) =>
+            Enabled = effectToggle.Value;
+    }
+
+    public override bool Enabled
+    {
+        get => enabled;
+        set
         {
-            updating = true;
-            effectToggle.Label = Translation.Get("effectsPane", "onToggle");
-            resetEffectButton.Label = Translation.Get("effectsPane", "reset");
-            TranslatePane();
-            updating = false;
-        }
+            enabled = value;
 
-        protected abstract void TranslatePane();
+            if (updating)
+                return;
 
-        public override void UpdatePane()
-        {
-            if (!EffectManager.Ready) return;
-            updating = true;
-            effectToggle.Value = EffectManager.Active;
-            UpdateControls();
-            updating = false;
+            EffectManager.SetEffectActive(enabled);
         }
+    }
 
-        protected abstract void UpdateControls();
+    protected abstract T EffectManager { get; set; }
 
-        public override void Draw()
-        {
-            GUILayout.BeginHorizontal();
-            effectToggle.Draw();
-            GUILayout.FlexibleSpace();
-            GUI.enabled = Enabled;
-            resetEffectButton.Draw();
-            GUILayout.EndHorizontal();
-            DrawPane();
-            GUI.enabled = true;
-        }
+    public override void UpdatePane()
+    {
+        if (!EffectManager.Ready)
+            return;
 
-        protected abstract void DrawPane();
+        updating = true;
+        effectToggle.Value = EffectManager.Active;
+        UpdateControls();
+        updating = false;
+    }
 
-        private void ResetEffect()
-        {
-            EffectManager.Deactivate();
-            EffectManager.SetEffectActive(true);
-            UpdatePane();
-        }
+    public override void Draw()
+    {
+        GUILayout.BeginHorizontal();
+        effectToggle.Draw();
+        GUILayout.FlexibleSpace();
+        GUI.enabled = Enabled;
+        resetEffectButton.Draw();
+        GUILayout.EndHorizontal();
+        DrawPane();
+        GUI.enabled = true;
+    }
+
+    protected override void ReloadTranslation()
+    {
+        updating = true;
+        effectToggle.Label = Translation.Get("effectsPane", "onToggle");
+        resetEffectButton.Label = Translation.Get("effectsPane", "reset");
+        TranslatePane();
+        updating = false;
+    }
+
+    protected abstract void TranslatePane();
+
+    protected abstract void UpdateControls();
+
+    protected abstract void DrawPane();
+
+    private void ResetEffect()
+    {
+        EffectManager.Deactivate();
+        EffectManager.SetEffectActive(true);
+        UpdatePane();
     }
 }

+ 38 - 38
src/MeidoPhotoStudio.Plugin/GUI/Panes/BackgroundWindowPanes/EffectsPanes/EffectsPane.cs

@@ -1,51 +1,51 @@
 using System.Collections.Generic;
 
-namespace MeidoPhotoStudio.Plugin
+namespace MeidoPhotoStudio.Plugin;
+
+public class EffectsPane : BasePane
 {
-    public class EffectsPane : BasePane
-    {
-        private readonly Dictionary<string, BasePane> effectPanes = new Dictionary<string, BasePane>();
-        private readonly List<string> effectList = new List<string>();
-        private readonly SelectionGrid effectToggles;
-        private BasePane currentEffectPane;
+    private readonly Dictionary<string, BasePane> effectPanes = new();
+    private readonly List<string> effectList = new();
+    private readonly SelectionGrid effectToggles;
 
-        public BasePane this[string effectUI]
-        {
-            private get => effectPanes[effectUI];
-            set
-            {
-                effectPanes[effectUI] = value;
-                effectList.Add(effectUI);
-                effectToggles.SetItems(Translation.GetArray("effectsPane", effectList), 0);
-            }
-        }
+    private BasePane currentEffectPane;
 
-        public EffectsPane()
-        {
-            effectToggles = new SelectionGrid(new[] { "dummy" /* thicc */ });
-            effectToggles.ControlEvent += (s, a) => SetEffectPane(effectList[effectToggles.SelectedItemIndex]);
-        }
+    public EffectsPane()
+    {
+        effectToggles = new(new[] { "dummy" /* thicc */ });
+        effectToggles.ControlEvent += (_, _) =>
+            SetEffectPane(effectList[effectToggles.SelectedItemIndex]);
+    }
 
-        protected override void ReloadTranslation()
+    public BasePane this[string effectUI]
+    {
+        private get => effectPanes[effectUI];
+        set
         {
-            effectToggles.SetItems(Translation.GetArray("effectsPane", effectList));
+            effectPanes[effectUI] = value;
+            effectList.Add(effectUI);
+            effectToggles.SetItems(Translation.GetArray("effectsPane", effectList), 0);
         }
+    }
 
-        private void SetEffectPane(string effectUI)
-        {
-            currentEffectPane = effectPanes[effectUI];
-            currentEffectPane.UpdatePane();
-        }
+    public override void UpdatePane() =>
+        currentEffectPane.UpdatePane();
 
-        public override void UpdatePane() => currentEffectPane.UpdatePane();
+    public override void Draw()
+    {
+        MpsGui.Header("Effects");
+        MpsGui.WhiteLine();
+        effectToggles.Draw();
+        MpsGui.BlackLine();
+        currentEffectPane.Draw();
+    }
 
-        public override void Draw()
-        {
-            MpsGui.Header("Effects");
-            MpsGui.WhiteLine();
-            effectToggles.Draw();
-            MpsGui.BlackLine();
-            currentEffectPane.Draw();
-        }
+    protected override void ReloadTranslation() =>
+        effectToggles.SetItems(Translation.GetArray("effectsPane", effectList));
+
+    private void SetEffectPane(string effectUI)
+    {
+        currentEffectPane = effectPanes[effectUI];
+        currentEffectPane.UpdatePane();
     }
 }

+ 115 - 101
src/MeidoPhotoStudio.Plugin/GUI/Panes/BackgroundWindowPanes/EffectsPanes/FogPane.cs

@@ -1,115 +1,129 @@
 using UnityEngine;
 
-namespace MeidoPhotoStudio.Plugin
+namespace MeidoPhotoStudio.Plugin;
+
+public class FogPane : EffectPane<FogEffectManager>
 {
-    public class FogPane : EffectPane<FogEffectManager>
+    private readonly Slider distanceSlider;
+    private readonly Slider densitySlider;
+    private readonly Slider heightScaleSlider;
+    private readonly Slider heightSlider;
+    private readonly Slider redSlider;
+    private readonly Slider greenSlider;
+    private readonly Slider blueSlider;
+
+    public FogPane(EffectManager effectManager)
+        : base(effectManager)
     {
-        protected override FogEffectManager EffectManager { get; set; }
-        private readonly Slider distanceSlider;
-        private readonly Slider densitySlider;
-        private readonly Slider heightScaleSlider;
-        private readonly Slider heightSlider;
-        private readonly Slider redSlider;
-        private readonly Slider greenSlider;
-        private readonly Slider blueSlider;
-
-        public FogPane(EffectManager effectManager) : base(effectManager)
+        distanceSlider = new(Translation.Get("effectFog", "distance"), 0f, 30f, EffectManager.Distance);
+        distanceSlider.ControlEvent += (_, _) =>
         {
-            distanceSlider = new Slider(
-                Translation.Get("effectFog", "distance"), 0f, 30f, EffectManager.Distance
-            );
-            densitySlider = new Slider(
-                Translation.Get("effectFog", "density"), 0f, 10f, EffectManager.Density
-            );
-            heightScaleSlider = new Slider(
-                Translation.Get("effectFog", "strength"), -5f, 20f, EffectManager.HeightScale
-            );
-            heightSlider = new Slider(
-                Translation.Get("effectFog", "height"), -10f, 10f, EffectManager.Height
-            );
-            Color initialFogColour = EffectManager.FogColour;
-            redSlider = new Slider(Translation.Get("backgroundWIndow", "red"), 0f, 1f, initialFogColour.r);
-            greenSlider = new Slider(Translation.Get("backgroundWIndow", "green"), 0f, 1f, initialFogColour.g);
-            blueSlider = new Slider(Translation.Get("backgroundWIndow", "blue"), 0f, 1f, initialFogColour.b);
-            distanceSlider.ControlEvent += (s, a) =>
-            {
-                if (updating) return;
-                EffectManager.Distance = distanceSlider.Value;
-            };
-            densitySlider.ControlEvent += (s, a) =>
-            {
-                if (updating) return;
-                EffectManager.Density = densitySlider.Value;
-            };
-            heightScaleSlider.ControlEvent += (s, a) =>
-            {
-                if (updating) return;
-                EffectManager.HeightScale = heightScaleSlider.Value;
-            };
-            heightSlider.ControlEvent += (s, a) =>
-            {
-                if (updating) return;
-                EffectManager.Height = heightSlider.Value;
-            };
-            redSlider.ControlEvent += (s, a) =>
-            {
-                if (updating) return;
-                EffectManager.FogColourRed = redSlider.Value;
-            };
-            greenSlider.ControlEvent += (s, a) =>
-            {
-                if (updating) return;
-                EffectManager.FogColourGreen = greenSlider.Value;
-            };
-            blueSlider.ControlEvent += (s, a) =>
-            {
-                if (updating) return;
-                EffectManager.FogColourBlue = blueSlider.Value;
-            };
-        }
-
-        protected override void TranslatePane()
+            if (updating)
+                return;
+
+            EffectManager.Distance = distanceSlider.Value;
+        };
+
+        densitySlider = new(Translation.Get("effectFog", "density"), 0f, 10f, EffectManager.Density);
+        densitySlider.ControlEvent += (_, _) =>
         {
-            distanceSlider.Label = Translation.Get("effectFog", "distance");
-            densitySlider.Label = Translation.Get("effectFog", "density");
-            heightScaleSlider.Label = Translation.Get("effectFog", "strength");
-            heightSlider.Label = Translation.Get("effectFog", "height");
-            redSlider.Label = Translation.Get("backgroundWIndow", "red");
-            greenSlider.Label = Translation.Get("backgroundWIndow", "green");
-            blueSlider.Label = Translation.Get("backgroundWIndow", "blue");
-        }
-
-        protected override void UpdateControls()
+            if (updating)
+                return;
+
+            EffectManager.Density = densitySlider.Value;
+        };
+
+        heightScaleSlider = new(Translation.Get("effectFog", "strength"), -5f, 20f, EffectManager.HeightScale);
+        heightScaleSlider.ControlEvent += (_, _) =>
         {
-            distanceSlider.Value = EffectManager.Distance;
-            densitySlider.Value = EffectManager.Density;
-            heightScaleSlider.Value = EffectManager.HeightScale;
-            heightSlider.Value = EffectManager.Height;
-            redSlider.Value = EffectManager.FogColourRed;
-            greenSlider.Value = EffectManager.FogColourGreen;
-            blueSlider.Value = EffectManager.FogColourBlue;
-        }
-
-        protected override void DrawPane()
+            if (updating)
+                return;
+
+            EffectManager.HeightScale = heightScaleSlider.Value;
+        };
+
+        heightSlider = new(Translation.Get("effectFog", "height"), -10f, 10f, EffectManager.Height);
+        heightSlider.ControlEvent += (_, _) =>
+        {
+            if (updating)
+                return;
+
+            EffectManager.Height = heightSlider.Value;
+        };
+
+        var initialFogColour = EffectManager.FogColour;
+
+        redSlider = new(Translation.Get("backgroundWIndow", "red"), 0f, 1f, initialFogColour.r);
+        redSlider.ControlEvent += (_, _) =>
+        {
+            if (updating)
+                return;
+
+            EffectManager.FogColourRed = redSlider.Value;
+        };
+
+        greenSlider = new(Translation.Get("backgroundWIndow", "green"), 0f, 1f, initialFogColour.g);
+        greenSlider.ControlEvent += (_, _) =>
         {
-            GUILayoutOption sliderWidth = MpsGui.HalfSlider;
+            if (updating)
+                return;
+
+            EffectManager.FogColourGreen = greenSlider.Value;
+        };
+
+        blueSlider = new(Translation.Get("backgroundWIndow", "blue"), 0f, 1f, initialFogColour.b);
+        blueSlider.ControlEvent += (_, _) =>
+        {
+            if (updating)
+                return;
+
+            EffectManager.FogColourBlue = blueSlider.Value;
+        };
+    }
+
+    protected override FogEffectManager EffectManager { get; set; }
+
+    protected override void TranslatePane()
+    {
+        distanceSlider.Label = Translation.Get("effectFog", "distance");
+        densitySlider.Label = Translation.Get("effectFog", "density");
+        heightScaleSlider.Label = Translation.Get("effectFog", "strength");
+        heightSlider.Label = Translation.Get("effectFog", "height");
+        redSlider.Label = Translation.Get("backgroundWIndow", "red");
+        greenSlider.Label = Translation.Get("backgroundWIndow", "green");
+        blueSlider.Label = Translation.Get("backgroundWIndow", "blue");
+    }
+
+    protected override void UpdateControls()
+    {
+        distanceSlider.Value = EffectManager.Distance;
+        densitySlider.Value = EffectManager.Density;
+        heightScaleSlider.Value = EffectManager.HeightScale;
+        heightSlider.Value = EffectManager.Height;
+        redSlider.Value = EffectManager.FogColourRed;
+        greenSlider.Value = EffectManager.FogColourGreen;
+        blueSlider.Value = EffectManager.FogColourBlue;
+    }
+
+    protected override void DrawPane()
+    {
+        var sliderWidth = MpsGui.HalfSlider;
 
-            GUILayout.BeginHorizontal();
-            distanceSlider.Draw(sliderWidth);
-            densitySlider.Draw(sliderWidth);
-            GUILayout.EndHorizontal();
+        GUILayout.BeginHorizontal();
+        distanceSlider.Draw(sliderWidth);
+        densitySlider.Draw(sliderWidth);
+        GUILayout.EndHorizontal();
 
-            GUILayout.BeginHorizontal();
-            heightScaleSlider.Draw(sliderWidth);
-            heightSlider.Draw(sliderWidth);
-            GUILayout.EndHorizontal();
+        GUILayout.BeginHorizontal();
+        heightScaleSlider.Draw(sliderWidth);
+        heightSlider.Draw(sliderWidth);
+        GUILayout.EndHorizontal();
 
-            GUILayout.BeginHorizontal();
-            redSlider.Draw(sliderWidth);
-            greenSlider.Draw(sliderWidth);
-            GUILayout.EndHorizontal();
+        GUILayout.BeginHorizontal();
+        redSlider.Draw(sliderWidth);
+        greenSlider.Draw(sliderWidth);
+        GUILayout.EndHorizontal();
 
-            blueSlider.Draw(sliderWidth);
-        }
+        blueSlider.Draw(sliderWidth);
     }
 }

+ 53 - 52
src/MeidoPhotoStudio.Plugin/GUI/Panes/BackgroundWindowPanes/EffectsPanes/OtherEffectsPane.cs

@@ -1,71 +1,72 @@
 using UnityEngine;
 
-namespace MeidoPhotoStudio.Plugin
+namespace MeidoPhotoStudio.Plugin;
+
+public class OtherEffectsPane : BasePane
 {
-    public class OtherEffectsPane : BasePane
+    private readonly EffectManager effectManager;
+    private readonly SepiaToneEffectManager sepiaToneEffectManger;
+    private readonly BlurEffectManager blurEffectManager;
+    private readonly Toggle sepiaToggle;
+    private readonly Slider blurSlider;
+
+    public OtherEffectsPane(EffectManager effectManager)
     {
-        private readonly EffectManager effectManager;
-        private readonly SepiaToneEffectManger sepiaToneEffectManger;
-        private readonly BlurEffectManager blurEffectManager;
-        private readonly Toggle sepiaToggle;
-        private readonly Slider blurSlider;
+        this.effectManager = effectManager;
 
-        public OtherEffectsPane(EffectManager effectManager)
-        {
-            this.effectManager = effectManager;
+        sepiaToneEffectManger = this.effectManager.Get<SepiaToneEffectManager>();
+        blurEffectManager = this.effectManager.Get<BlurEffectManager>();
 
-            sepiaToneEffectManger = this.effectManager.Get<SepiaToneEffectManger>();
-            blurEffectManager = this.effectManager.Get<BlurEffectManager>();
+        sepiaToggle = new(Translation.Get("otherEffectsPane", "sepiaToggle"));
+        sepiaToggle.ControlEvent += (_, _) =>
+        {
+            if (updating)
+                return;
 
-            sepiaToggle = new Toggle(Translation.Get("otherEffectsPane", "sepiaToggle"));
-            sepiaToggle.ControlEvent += (s, a) =>
-            {
-                if (updating) return;
-                sepiaToneEffectManger.SetEffectActive(sepiaToggle.Value);
-            };
+            sepiaToneEffectManger.SetEffectActive(sepiaToggle.Value);
+        };
 
-            blurSlider = new Slider(Translation.Get("otherEffectsPane", "blurSlider"), 0f, 18f);
-            blurSlider.ControlEvent += (s, a) =>
-            {
-                if (updating)
-                    return;
+        blurSlider = new(Translation.Get("otherEffectsPane", "blurSlider"), 0f, 18f);
+        blurSlider.ControlEvent += (_, _) =>
+        {
+            if (updating)
+                return;
 
-                var value = 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);
+            if (!blurEffectManager.Active && value > 0f)
+                blurEffectManager.SetEffectActive(true);
+            else if (blurEffectManager.Active && Mathf.Approximately(value, 0f))
+                blurEffectManager.SetEffectActive(false);
 
-                blurEffectManager.BlurSize = value;
-            };
-        }
+            blurEffectManager.BlurSize = value;
+        };
+    }
 
-        protected override void ReloadTranslation()
-        {
-            sepiaToggle.Label = Translation.Get("otherEffectsPane", "sepiaToggle");
-            blurSlider.Label = Translation.Get("otherEffectsPane", "blurSlider");
-        }
+    public override void Draw()
+    {
+        GUILayout.BeginHorizontal();
+        sepiaToggle.Draw();
+        blurSlider.Draw();
+        GUILayout.EndHorizontal();
+    }
 
-        public override void Draw()
-        {
-            GUILayout.BeginHorizontal();
-            sepiaToggle.Draw();
-            blurSlider.Draw();
-            GUILayout.EndHorizontal();
-        }
+    public override void UpdatePane()
+    {
+        updating = true;
 
-        public override void UpdatePane()
-        {
-            updating = true;
+        if (sepiaToneEffectManger.Ready)
+            sepiaToggle.Value = sepiaToneEffectManger.Active;
 
-            if (sepiaToneEffectManger.Ready)
-                sepiaToggle.Value = sepiaToneEffectManger.Active;
+        if (blurEffectManager.Ready)
+            blurSlider.Value = blurEffectManager.BlurSize;
 
-            if (blurEffectManager.Ready)
-                blurSlider.Value = blurEffectManager.BlurSize;
+        updating = false;
+    }
 
-            updating = false;
-        }
+    protected override void ReloadTranslation()
+    {
+        sepiaToggle.Label = Translation.Get("otherEffectsPane", "sepiaToggle");
+        blurSlider.Label = Translation.Get("otherEffectsPane", "blurSlider");
     }
 }

+ 72 - 60
src/MeidoPhotoStudio.Plugin/GUI/Panes/BackgroundWindowPanes/EffectsPanes/VignettePane.cs

@@ -1,72 +1,84 @@
 using UnityEngine;
 
-namespace MeidoPhotoStudio.Plugin
+namespace MeidoPhotoStudio.Plugin;
+
+public class VignettePane : EffectPane<VignetteEffectManager>
 {
-    public class VignettePane : EffectPane<VignetteEffectManager>
-    {
-        protected override VignetteEffectManager EffectManager { get; set; }
-        private readonly Slider intensitySlider;
-        private readonly Slider blurSlider;
-        private readonly Slider blurSpreadSlider;
-        private readonly Slider aberrationSlider;
+    private readonly Slider intensitySlider;
+    private readonly Slider blurSlider;
+    private readonly Slider blurSpreadSlider;
+    private readonly Slider aberrationSlider;
 
-        public VignettePane(EffectManager effectManager) : base(effectManager)
+    public VignettePane(EffectManager effectManager)
+        : base(effectManager)
+    {
+        intensitySlider = new(Translation.Get("effectVignette", "intensity"), -40f, 70f);
+        intensitySlider.ControlEvent += (_, _) =>
         {
-            intensitySlider = new Slider(Translation.Get("effectVignette", "intensity"), -40f, 70f);
-            intensitySlider.ControlEvent += (s, a) =>
-            {
-                if (updating) return;
-                EffectManager.Intensity = intensitySlider.Value;
-            };
-            blurSlider = new Slider(Translation.Get("effectVignette", "blur"), 0f, 5f);
-            blurSlider.ControlEvent += (s, a) =>
-            {
-                if (updating) return;
-                EffectManager.Blur = blurSlider.Value;
-            };
-            blurSpreadSlider = new Slider(Translation.Get("effectVignette", "blurSpread"), 0f, 40f);
-            blurSpreadSlider.ControlEvent += (s, a) =>
-            {
-                if (updating) return;
-                EffectManager.BlurSpread = blurSpreadSlider.Value;
-            };
-            aberrationSlider = new Slider(Translation.Get("effectVignette", "aberration"), -30f, 30f);
-            aberrationSlider.ControlEvent += (s, a) =>
-            {
-                if (updating) return;
-                EffectManager.ChromaticAberration = aberrationSlider.Value;
-            };
-        }
-
-        protected override void TranslatePane()
+            if (updating)
+                return;
+
+            EffectManager.Intensity = intensitySlider.Value;
+        };
+
+        blurSlider = new(Translation.Get("effectVignette", "blur"), 0f, 5f);
+        blurSlider.ControlEvent += (_, _) =>
         {
-            intensitySlider.Label = Translation.Get("effectVignette", "intensity");
-            blurSlider.Label = Translation.Get("effectVignette", "blur");
-            blurSpreadSlider.Label = Translation.Get("effectVignette", "blurSpread");
-            aberrationSlider.Label = Translation.Get("effectVignette", "aberration");
-        }
+            if (updating)
+                return;
 
-        protected override void UpdateControls()
+            EffectManager.Blur = blurSlider.Value;
+        };
+
+        blurSpreadSlider = new(Translation.Get("effectVignette", "blurSpread"), 0f, 40f);
+        blurSpreadSlider.ControlEvent += (_, _) =>
         {
-            intensitySlider.Value = EffectManager.Intensity;
-            blurSlider.Value = EffectManager.Blur;
-            blurSpreadSlider.Value = EffectManager.BlurSpread;
-            aberrationSlider.Value = EffectManager.ChromaticAberration;
-        }
+            if (updating)
+                return;
+
+            EffectManager.BlurSpread = blurSpreadSlider.Value;
+        };
 
-        protected override void DrawPane()
+        aberrationSlider = new(Translation.Get("effectVignette", "aberration"), -30f, 30f);
+        aberrationSlider.ControlEvent += (_, _) =>
         {
-            GUILayoutOption sliderWidth = MpsGui.HalfSlider;
-
-            GUILayout.BeginHorizontal();
-            intensitySlider.Draw(sliderWidth);
-            blurSlider.Draw(sliderWidth);
-            GUILayout.EndHorizontal();
-
-            GUILayout.BeginHorizontal();
-            blurSpreadSlider.Draw(sliderWidth);
-            aberrationSlider.Draw(sliderWidth);
-            GUILayout.EndHorizontal();
-        }
+            if (updating)
+                return;
+
+            EffectManager.ChromaticAberration = aberrationSlider.Value;
+        };
+    }
+
+    protected override VignetteEffectManager EffectManager { get; set; }
+
+    protected override void TranslatePane()
+    {
+        intensitySlider.Label = Translation.Get("effectVignette", "intensity");
+        blurSlider.Label = Translation.Get("effectVignette", "blur");
+        blurSpreadSlider.Label = Translation.Get("effectVignette", "blurSpread");
+        aberrationSlider.Label = Translation.Get("effectVignette", "aberration");
+    }
+
+    protected override void UpdateControls()
+    {
+        intensitySlider.Value = EffectManager.Intensity;
+        blurSlider.Value = EffectManager.Blur;
+        blurSpreadSlider.Value = EffectManager.BlurSpread;
+        aberrationSlider.Value = EffectManager.ChromaticAberration;
+    }
+
+    protected override void DrawPane()
+    {
+        var sliderWidth = MpsGui.HalfSlider;
+
+        GUILayout.BeginHorizontal();
+        intensitySlider.Draw(sliderWidth);
+        blurSlider.Draw(sliderWidth);
+        GUILayout.EndHorizontal();
+
+        GUILayout.BeginHorizontal();
+        blurSpreadSlider.Draw(sliderWidth);
+        aberrationSlider.Draw(sliderWidth);
+        GUILayout.EndHorizontal();
     }
 }

+ 298 - 238
src/MeidoPhotoStudio.Plugin/GUI/Panes/BackgroundWindowPanes/LightsPane.cs

@@ -1,297 +1,357 @@
 using System;
 using System.Collections.Generic;
+
 using UnityEngine;
 
-namespace MeidoPhotoStudio.Plugin
+using static MeidoPhotoStudio.Plugin.DragPointLight;
+
+namespace MeidoPhotoStudio.Plugin;
+
+public class LightsPane : BasePane
 {
-    using static DragPointLight;
-    public class LightsPane : BasePane
+    private static readonly string[] LightTypes = { "normal", "spot", "point" };
+    private static readonly Dictionary<LightProp, SliderProp> LightSliderProp;
+    private static readonly string[,] SliderNames =
+    {
+        { "lights", "x" },
+        { "lights", "y" },
+        { "lights", "intensity" },
+        { "lights", "shadow" },
+        { "lights", "spot" },
+        { "lights", "range" },
+        { "backgroundWindow", "red" },
+        { "backgroundWindow", "green" },
+        { "backgroundWindow", "blue" },
+    };
+
+    private readonly LightManager lightManager;
+    private readonly Dictionary<LightProp, Slider> lightSlider;
+    private readonly Dropdown lightDropdown;
+    private readonly Button addLightButton;
+    private readonly Button deleteLightButton;
+    private readonly Button clearLightsButton;
+    private readonly Button resetPropsButton;
+    private readonly Button resetPositionButton;
+    private readonly SelectionGrid lightTypeGrid;
+    private readonly Toggle colorToggle;
+    private readonly Toggle disableToggle;
+
+    private MPSLightType currentLightType;
+    private string lightHeader;
+    private string resetLabel;
+
+    static LightsPane()
     {
-        private static readonly string[] lightTypes = { "normal", "spot", "point" };
-        private readonly LightManager lightManager;
-        private readonly Dictionary<LightProp, Slider> lightSlider;
-        private readonly Dropdown lightDropdown;
-        private readonly Button addLightButton;
-        private readonly Button deleteLightButton;
-        private readonly Button clearLightsButton;
-        private readonly Button resetPropsButton;
-        private readonly Button resetPositionButton;
-        private readonly SelectionGrid lightTypeGrid;
-        private readonly Toggle colorToggle;
-        private readonly Toggle disableToggle;
-        private MPSLightType currentLightType;
-        private string lightHeader;
-        private string resetLabel;
-
-        private static readonly Dictionary<LightProp, SliderProp> lightSliderProp;
-        private static readonly string[,] sliderNames = {
-            { "lights", "x" }, { "lights", "y" }, { "lights", "intensity" }, { "lights", "shadow" },
-            { "lights", "spot" }, { "lights", "range" }, { "backgroundWindow", "red" }, { "backgroundWindow", "green" },
-            { "backgroundWindow", "blue" }
+        var rotation = LightProperty.DefaultRotation.eulerAngles;
+        var range = GameMain.Instance.MainLight.GetComponent<Light>().range;
+
+        LightSliderProp = new()
+        {
+            [LightProp.LightRotX] = new(0f, 360f, rotation.x, rotation.x),
+            [LightProp.LightRotY] = new(0f, 360f, rotation.y, rotation.y),
+            [LightProp.Intensity] = new(0f, 2f, 0.95f, 0.95f),
+            [LightProp.ShadowStrength] = new(0f, 1f, 0.098f, 0.098f),
+            [LightProp.Range] = new(0f, 150f, range, range),
+            [LightProp.SpotAngle] = new(0f, 150f, 50f, 50f),
+            [LightProp.Red] = new(0f, 1f, 1f, 1f),
+            [LightProp.Green] = new(0f, 1f, 1f, 1f),
+            [LightProp.Blue] = new(0f, 1f, 1f, 1f),
         };
+    }
+
+    public LightsPane(LightManager lightManager)
+    {
+        this.lightManager = lightManager;
+        this.lightManager.Rotate += (_, _) =>
+            UpdateRotation();
+
+        this.lightManager.Scale += (_, _) =>
+            UpdateScale();
+
+        this.lightManager.Select += (_, _) =>
+            UpdateCurrentLight();
+
+        this.lightManager.ListModified += (_, _) =>
+            UpdateList();
+
+        lightTypeGrid = new(Translation.GetArray("lightType", LightTypes));
+        lightTypeGrid.ControlEvent += (_, _) =>
+            SetCurrentLightType();
 
-        static LightsPane()
+        lightDropdown = new(new[] { "Main" });
+        lightDropdown.SelectionChange += (_, _) =>
+            SetCurrentLight();
+
+        addLightButton = new("+");
+        addLightButton.ControlEvent += (_, _) =>
+            lightManager.AddLight();
+
+        deleteLightButton = new(Translation.Get("lightsPane", "delete"));
+        deleteLightButton.ControlEvent += (_, _) =>
+            lightManager.DeleteActiveLight();
+
+        disableToggle = new(Translation.Get("lightsPane", "disable"));
+        disableToggle.ControlEvent += (_, _) =>
+            lightManager.CurrentLight.IsDisabled = disableToggle.Value;
+
+        clearLightsButton = new(Translation.Get("lightsPane", "clear"));
+        clearLightsButton.ControlEvent += (_, _) =>
+            ClearLights();
+
+        var numberOfLightProps = Enum.GetNames(typeof(LightProp)).Length;
+
+        lightSlider = new(numberOfLightProps);
+
+        for (var i = 0; i < numberOfLightProps; i++)
         {
-            Vector3 rotation = LightProperty.DefaultRotation.eulerAngles;
-            var range = GameMain.Instance.MainLight.GetComponent<Light>().range;
-            lightSliderProp = new Dictionary<LightProp, SliderProp>
+            var lightProp = (LightProp)i;
+            var sliderProp = LightSliderProp[lightProp];
+
+            var slider = new Slider(Translation.Get(SliderNames[i, 0], SliderNames[i, 1]), sliderProp)
             {
-                [LightProp.LightRotX] = new SliderProp(0f, 360f, rotation.x, rotation.x),
-                [LightProp.LightRotY] = new SliderProp(0f, 360f, rotation.y, rotation.y),
-                [LightProp.Intensity] = new SliderProp(0f, 2f, 0.95f, 0.95f),
-                [LightProp.ShadowStrength] = new SliderProp(0f, 1f, 0.098f, 0.098f),
-                [LightProp.Range] = new SliderProp(0f, 150f, range, range),
-                [LightProp.SpotAngle] = new SliderProp(0f, 150f, 50f, 50f),
-                [LightProp.Red] = new SliderProp(0f, 1f, 1f, 1f),
-                [LightProp.Green] = new SliderProp(0f, 1f, 1f, 1f),
-                [LightProp.Blue] = new SliderProp(0f, 1f, 1f, 1f),
+                HasTextField = true,
+                HasReset = true,
             };
+
+            if (lightProp <= LightProp.LightRotY)
+                slider.ControlEvent += (_, _) =>
+                    SetLightRotation();
+            else
+                slider.ControlEvent += (_, _) =>
+                    SetLightProp(lightProp, slider.Value);
+
+            lightSlider[lightProp] = slider;
         }
 
-        public LightsPane(LightManager lightManager)
-        {
-            this.lightManager = lightManager;
-            this.lightManager.Rotate += (s, a) => UpdateRotation();
-            this.lightManager.Scale += (s, a) => UpdateScale();
-            this.lightManager.Select += (s, a) => UpdateCurrentLight();
-            this.lightManager.ListModified += (s, a) => UpdateList();
+        colorToggle = new(Translation.Get("lightsPane", "colour"));
+        colorToggle.ControlEvent += (_, _) =>
+            SetColourMode();
+
+        resetPropsButton = new(Translation.Get("lightsPane", "resetProperties"));
+        resetPropsButton.ControlEvent += (_, _) =>
+            ResetLightProps();
 
-            lightTypeGrid = new SelectionGrid(Translation.GetArray("lightType", lightTypes));
-            lightTypeGrid.ControlEvent += (s, a) => SetCurrentLightType();
+        resetPositionButton = new(Translation.Get("lightsPane", "resetPosition"));
+        resetPositionButton.ControlEvent += (_, _) =>
+            lightManager.CurrentLight.ResetLightPosition();
 
-            lightDropdown = new Dropdown(new[] { "Main" });
-            lightDropdown.SelectionChange += (s, a) => SetCurrentLight();
+        lightHeader = Translation.Get("lightsPane", "header");
+        resetLabel = Translation.Get("lightsPane", "resetLabel");
+    }
 
-            addLightButton = new Button("+");
-            addLightButton.ControlEvent += (s, a) => lightManager.AddLight();
+    public override void UpdatePane()
+    {
+        updating = true;
+
+        var currentLight = lightManager.CurrentLight;
+
+        currentLightType = currentLight.SelectedLightType;
+        lightTypeGrid.SelectedItemIndex = (int)currentLightType;
+        disableToggle.Value = currentLight.IsDisabled;
+        lightSlider[LightProp.LightRotX].Value = currentLight.Rotation.eulerAngles.x;
+        lightSlider[LightProp.LightRotY].Value = currentLight.Rotation.eulerAngles.y;
+        lightSlider[LightProp.Intensity].Value = currentLight.Intensity;
+        lightSlider[LightProp.ShadowStrength].Value = currentLight.ShadowStrength;
+        lightSlider[LightProp.Range].Value = currentLight.Range;
+        lightSlider[LightProp.SpotAngle].Value = currentLight.SpotAngle;
+        lightSlider[LightProp.Red].Value = currentLight.LightColour.r;
+        lightSlider[LightProp.Green].Value = currentLight.LightColour.g;
+        lightSlider[LightProp.Blue].Value = currentLight.LightColour.b;
+
+        updating = false;
+    }
 
-            deleteLightButton = new Button(Translation.Get("lightsPane", "delete"));
-            deleteLightButton.ControlEvent += (s, a) => lightManager.DeleteActiveLight();
+    public override void Draw()
+    {
+        var isMain = lightManager.SelectedLightIndex is 0;
+        var noExpandWidth = GUILayout.ExpandWidth(false);
 
-            disableToggle = new Toggle(Translation.Get("lightsPane", "disable"));
-            disableToggle.ControlEvent += (s, a) => lightManager.CurrentLight.IsDisabled = disableToggle.Value;
+        MpsGui.Header(lightHeader);
+        MpsGui.WhiteLine();
 
-            clearLightsButton = new Button(Translation.Get("lightsPane", "clear"));
-            clearLightsButton.ControlEvent += (s, a) => ClearLights();
+        GUILayout.BeginHorizontal();
+        lightDropdown.Draw(GUILayout.Width(84));
+        addLightButton.Draw(noExpandWidth);
 
-            var numberOfLightProps = Enum.GetNames(typeof(LightProp)).Length;
-            lightSlider = new Dictionary<LightProp, Slider>(numberOfLightProps);
+        GUILayout.FlexibleSpace();
+        GUI.enabled = !isMain;
+        deleteLightButton.Draw(noExpandWidth);
+        GUI.enabled = true;
+        clearLightsButton.Draw(noExpandWidth);
+        GUILayout.EndHorizontal();
 
-            for (var i = 0; i < numberOfLightProps; i++)
-            {
-                var lightProp = (LightProp)i;
-                SliderProp sliderProp = lightSliderProp[lightProp];
-                var slider = new Slider(Translation.Get(sliderNames[i, 0], sliderNames[i, 1]), sliderProp)
-                {
-                    HasTextField = true,
-                    HasReset = true
-                };
-                if (lightProp <= LightProp.LightRotY) slider.ControlEvent += (s, a) => SetLightRotation();
-                else slider.ControlEvent += (s, a) => SetLightProp(lightProp, slider.Value);
-                lightSlider[lightProp] = slider;
-            }
-
-            colorToggle = new Toggle(Translation.Get("lightsPane", "colour"));
-            colorToggle.ControlEvent += (s, a) => SetColourMode();
-
-            resetPropsButton = new Button(Translation.Get("lightsPane", "resetProperties"));
-            resetPropsButton.ControlEvent += (s, a) => ResetLightProps();
-
-            resetPositionButton = new Button(Translation.Get("lightsPane", "resetPosition"));
-            resetPositionButton.ControlEvent += (s, a) => lightManager.CurrentLight.ResetLightPosition();
-
-            lightHeader = Translation.Get("lightsPane", "header");
-            resetLabel = Translation.Get("lightsPane", "resetLabel");
-        }
+        var isDisabled = !isMain && lightManager.CurrentLight.IsDisabled;
 
-        protected override void ReloadTranslation()
-        {
-            updating = true;
-            lightTypeGrid.SetItems(Translation.GetArray("lightType", lightTypes));
-            lightDropdown.SetDropdownItems(lightManager.LightNameList);
-            deleteLightButton.Label = Translation.Get("lightsPane", "delete");
-            disableToggle.Label = Translation.Get("lightsPane", "disable");
-            clearLightsButton.Label = Translation.Get("lightsPane", "clear");
-            for (var lightProp = LightProp.LightRotX; lightProp <= LightProp.Blue; lightProp++)
-            {
-                lightSlider[lightProp].Label =
-                    Translation.Get(sliderNames[(int)lightProp, 0], sliderNames[(int)lightProp, 1]);
-            }
-            colorToggle.Label = Translation.Get("lightsPane", "colour");
-            resetPropsButton.Label = Translation.Get("lightsPane", "resetProperties");
-            resetPositionButton.Label = Translation.Get("lightsPane", "resetPosition");
-            lightHeader = Translation.Get("lightsPane", "header");
-            resetLabel = Translation.Get("lightsPane", "resetLabel");
-            updating = false;
-        }
+        GUILayout.BeginHorizontal();
+        GUI.enabled = !isDisabled;
+        lightTypeGrid.Draw(noExpandWidth);
 
-        private void SetColourMode()
+        if (!isMain)
         {
-            lightManager.SetColourModeActive(colorToggle.Value);
-            UpdatePane();
+            GUI.enabled = true;
+            disableToggle.Draw();
         }
 
-        private void ClearLights()
-        {
-            lightManager.ClearLights();
-            UpdatePane();
-        }
+        if (lightManager.SelectedLightIndex is 0 && currentLightType is MPSLightType.Normal)
+            colorToggle.Draw();
 
-        private void SetCurrentLight()
-        {
-            if (updating) return;
-            lightManager.SelectedLightIndex = lightDropdown.SelectedItemIndex;
-            UpdatePane();
-        }
+        GUILayout.EndHorizontal();
+
+        GUI.enabled = !isDisabled;
 
-        private void ResetLightProps()
+        if (currentLightType is not MPSLightType.Point)
         {
-            lightManager.CurrentLight.ResetLightProps();
-            UpdatePane();
+            lightSlider[LightProp.LightRotX].Draw();
+            lightSlider[LightProp.LightRotY].Draw();
         }
 
-        private void SetCurrentLightType()
-        {
-            if (updating) return;
+        lightSlider[LightProp.Intensity].Draw();
 
-            currentLightType = (MPSLightType)lightTypeGrid.SelectedItemIndex;
+        if (currentLightType is MPSLightType.Normal)
+            lightSlider[LightProp.ShadowStrength].Draw();
+        else
+            lightSlider[LightProp.Range].Draw();
 
-            DragPointLight currentLight = lightManager.CurrentLight;
+        if (currentLightType is MPSLightType.Spot)
+            lightSlider[LightProp.SpotAngle].Draw();
 
-            currentLight.SetLightType(currentLightType);
+        MpsGui.BlackLine();
 
-            lightDropdown.SetDropdownItem(lightManager.ActiveLightName);
-            UpdatePane();
-        }
+        lightSlider[LightProp.Red].Draw();
+        lightSlider[LightProp.Green].Draw();
+        lightSlider[LightProp.Blue].Draw();
 
-        private void SetLightProp(LightProp prop, float value)
-        {
-            if (updating) return;
-            lightManager.CurrentLight.SetProp(prop, value);
-        }
+        GUILayout.BeginHorizontal();
+        GUILayout.Label(resetLabel, noExpandWidth);
+        resetPropsButton.Draw(noExpandWidth);
+        resetPositionButton.Draw(noExpandWidth);
+        GUILayout.EndHorizontal();
 
-        private void SetLightRotation()
-        {
-            if (updating) return;
-            var lightRotX = lightSlider[LightProp.LightRotX].Value;
-            var lightRotY = lightSlider[LightProp.LightRotY].Value;
-            lightManager.CurrentLight.SetRotation(lightRotX, lightRotY);
-        }
+        GUI.enabled = true;
+    }
 
-        private void UpdateList()
-        {
-            string[] newList = lightManager.LightNameList;
-            lightDropdown.SetDropdownItems(newList, lightManager.SelectedLightIndex);
-            UpdatePane();
-        }
+    protected override void ReloadTranslation()
+    {
+        updating = true;
 
-        private void UpdateRotation()
-        {
-            updating = true;
-            LightProperty prop = lightManager.CurrentLight.CurrentLightProperty;
-            lightSlider[LightProp.LightRotX].Value = prop.Rotation.eulerAngles.x;
-            lightSlider[LightProp.LightRotY].Value = prop.Rotation.eulerAngles.y;
-            updating = false;
-        }
+        lightTypeGrid.SetItems(Translation.GetArray("lightType", LightTypes));
+        lightDropdown.SetDropdownItems(lightManager.LightNameList);
+        deleteLightButton.Label = Translation.Get("lightsPane", "delete");
+        disableToggle.Label = Translation.Get("lightsPane", "disable");
+        clearLightsButton.Label = Translation.Get("lightsPane", "clear");
 
-        private void UpdateScale()
-        {
-            updating = true;
-            lightSlider[LightProp.SpotAngle].Value = lightManager.CurrentLight.CurrentLightProperty.SpotAngle;
-            lightSlider[LightProp.Range].Value = lightManager.CurrentLight.CurrentLightProperty.Range;
-            updating = false;
-        }
+        for (var lightProp = LightProp.LightRotX; lightProp <= LightProp.Blue; lightProp++)
+            lightSlider[lightProp].Label =
+                Translation.Get(SliderNames[(int)lightProp, 0], SliderNames[(int)lightProp, 1]);
 
-        private void UpdateCurrentLight()
-        {
-            updating = true;
-            lightDropdown.SelectedItemIndex = lightManager.SelectedLightIndex;
-            updating = false;
-            UpdatePane();
-        }
+        colorToggle.Label = Translation.Get("lightsPane", "colour");
+        resetPropsButton.Label = Translation.Get("lightsPane", "resetProperties");
+        resetPositionButton.Label = Translation.Get("lightsPane", "resetPosition");
+        lightHeader = Translation.Get("lightsPane", "header");
+        resetLabel = Translation.Get("lightsPane", "resetLabel");
 
-        public override void UpdatePane()
-        {
-            updating = true;
-            DragPointLight currentLight = lightManager.CurrentLight;
-            currentLightType = currentLight.SelectedLightType;
-            lightTypeGrid.SelectedItemIndex = (int)currentLightType;
-            disableToggle.Value = currentLight.IsDisabled;
-            lightSlider[LightProp.LightRotX].Value = currentLight.Rotation.eulerAngles.x;
-            lightSlider[LightProp.LightRotY].Value = currentLight.Rotation.eulerAngles.y;
-            lightSlider[LightProp.Intensity].Value = currentLight.Intensity;
-            lightSlider[LightProp.ShadowStrength].Value = currentLight.ShadowStrength;
-            lightSlider[LightProp.Range].Value = currentLight.Range;
-            lightSlider[LightProp.SpotAngle].Value = currentLight.SpotAngle;
-            lightSlider[LightProp.Red].Value = currentLight.LightColour.r;
-            lightSlider[LightProp.Green].Value = currentLight.LightColour.g;
-            lightSlider[LightProp.Blue].Value = currentLight.LightColour.b;
-            updating = false;
-        }
+        updating = false;
+    }
 
-        public override void Draw()
-        {
-            var isMain = lightManager.SelectedLightIndex == 0;
+    private void SetColourMode()
+    {
+        lightManager.SetColourModeActive(colorToggle.Value);
+        UpdatePane();
+    }
 
-            GUILayoutOption noExpandWidth = GUILayout.ExpandWidth(false);
+    private void ClearLights()
+    {
+        lightManager.ClearLights();
+        UpdatePane();
+    }
 
-            MpsGui.Header(lightHeader);
-            MpsGui.WhiteLine();
+    private void SetCurrentLight()
+    {
+        if (updating)
+            return;
 
-            GUILayout.BeginHorizontal();
-            lightDropdown.Draw(GUILayout.Width(84));
-            addLightButton.Draw(noExpandWidth);
+        lightManager.SelectedLightIndex = lightDropdown.SelectedItemIndex;
+        UpdatePane();
+    }
 
-            GUILayout.FlexibleSpace();
-            GUI.enabled = !isMain;
-            deleteLightButton.Draw(noExpandWidth);
-            GUI.enabled = true;
-            clearLightsButton.Draw(noExpandWidth);
-            GUILayout.EndHorizontal();
-
-            bool isDisabled = !isMain && lightManager.CurrentLight.IsDisabled;
-            GUILayout.BeginHorizontal();
-            GUI.enabled = !isDisabled;
-            lightTypeGrid.Draw(noExpandWidth);
-            if (!isMain)
-            {
-                GUI.enabled = true;
-                disableToggle.Draw();
-            }
+    private void ResetLightProps()
+    {
+        lightManager.CurrentLight.ResetLightProps();
+        UpdatePane();
+    }
 
-            if (lightManager.SelectedLightIndex == 0 && currentLightType == MPSLightType.Normal) 
-                colorToggle.Draw();
-            
-            GUILayout.EndHorizontal();
+    private void SetCurrentLightType()
+    {
+        if (updating)
+            return;
 
-            GUI.enabled = !isDisabled;
+        currentLightType = (MPSLightType)lightTypeGrid.SelectedItemIndex;
 
-            if (currentLightType != MPSLightType.Point)
-            {
-                lightSlider[LightProp.LightRotX].Draw();
-                lightSlider[LightProp.LightRotY].Draw();
-            }
+        var currentLight = lightManager.CurrentLight;
 
-            lightSlider[LightProp.Intensity].Draw();
+        currentLight.SetLightType(currentLightType);
 
-            if (currentLightType == MPSLightType.Normal) lightSlider[LightProp.ShadowStrength].Draw();
-            else lightSlider[LightProp.Range].Draw();
+        lightDropdown.SetDropdownItem(lightManager.ActiveLightName);
+        UpdatePane();
+    }
 
-            if (currentLightType == MPSLightType.Spot) lightSlider[LightProp.SpotAngle].Draw();
+    private void SetLightProp(LightProp prop, float value)
+    {
+        if (updating)
+            return;
 
-            MpsGui.BlackLine();
+        lightManager.CurrentLight.SetProp(prop, value);
+    }
 
-            lightSlider[LightProp.Red].Draw();
-            lightSlider[LightProp.Green].Draw();
-            lightSlider[LightProp.Blue].Draw();
+    private void SetLightRotation()
+    {
+        if (updating)
+            return;
 
-            GUILayout.BeginHorizontal();
-            GUILayout.Label(resetLabel, noExpandWidth);
-            resetPropsButton.Draw(noExpandWidth);
-            resetPositionButton.Draw(noExpandWidth);
-            GUILayout.EndHorizontal();
+        var lightRotX = lightSlider[LightProp.LightRotX].Value;
+        var lightRotY = lightSlider[LightProp.LightRotY].Value;
 
-            GUI.enabled = true;
-        }
+        lightManager.CurrentLight.SetRotation(lightRotX, lightRotY);
+    }
+
+    private void UpdateList()
+    {
+        var newList = lightManager.LightNameList;
+
+        lightDropdown.SetDropdownItems(newList, lightManager.SelectedLightIndex);
+        UpdatePane();
+    }
+
+    private void UpdateRotation()
+    {
+        updating = true;
+
+        var prop = lightManager.CurrentLight.CurrentLightProperty;
+
+        lightSlider[LightProp.LightRotX].Value = prop.Rotation.eulerAngles.x;
+        lightSlider[LightProp.LightRotY].Value = prop.Rotation.eulerAngles.y;
+
+        updating = false;
+    }
+
+    private void UpdateScale()
+    {
+        updating = true;
+
+        lightSlider[LightProp.SpotAngle].Value = lightManager.CurrentLight.CurrentLightProperty.SpotAngle;
+        lightSlider[LightProp.Range].Value = lightManager.CurrentLight.CurrentLightProperty.Range;
+
+        updating = false;
+    }
+
+    private void UpdateCurrentLight()
+    {
+        updating = true;
+
+        lightDropdown.SelectedItemIndex = lightManager.SelectedLightIndex;
+
+        updating = false;
+
+        UpdatePane();
     }
 }

+ 32 - 16
src/MeidoPhotoStudio.Plugin/GUI/Panes/BasePane.cs

@@ -1,30 +1,46 @@
 using System;
 
-namespace MeidoPhotoStudio.Plugin
+namespace MeidoPhotoStudio.Plugin;
+
+public abstract class BasePane
 {
-    public abstract class BasePane
-    {
-        protected BaseWindow parent;
-        protected bool updating;
-        public virtual bool Visible { get; set; }
-        public virtual bool Enabled { get; set; }
+    protected BaseWindow parent;
+    protected bool updating;
 
-        protected BasePane() => Translation.ReloadTranslationEvent += OnReloadTranslation;
+    protected BasePane() =>
+        Translation.ReloadTranslationEvent += OnReloadTranslation;
 
-        ~BasePane() => Translation.ReloadTranslationEvent -= OnReloadTranslation;
+    // TODO: This does not work how I think it works. Probably just remove entirely.
+    ~BasePane() =>
+        Translation.ReloadTranslationEvent -= OnReloadTranslation;
 
-        private void OnReloadTranslation(object sender, EventArgs args) => ReloadTranslation();
+    public virtual bool Visible { get; set; }
 
-        public virtual void SetParent(BaseWindow window) => parent = window;
+    public virtual bool Enabled { get; set; }
 
-        protected virtual void ReloadTranslation() { }
+    public virtual void SetParent(BaseWindow window) =>
+        parent = window;
 
-        public virtual void UpdatePane() { }
+    public virtual void UpdatePane()
+    {
+    }
 
-        public virtual void Draw() { }
+    public virtual void Draw()
+    {
+    }
 
-        public virtual void Activate() { }
+    public virtual void Activate()
+    {
+    }
+
+    public virtual void Deactivate()
+    {
+    }
 
-        public virtual void Deactivate() { }
+    protected virtual void ReloadTranslation()
+    {
     }
+
+    private void OnReloadTranslation(object sender, EventArgs args) =>
+        ReloadTranslation();
 }

+ 103 - 96
src/MeidoPhotoStudio.Plugin/GUI/Panes/CallWindowPanes/MaidSelectorPane.cs

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

+ 151 - 129
src/MeidoPhotoStudio.Plugin/GUI/Panes/FaceWindowPanes/MaidFaceBlendPane.cs

@@ -1,159 +1,181 @@
 using System.Collections.Generic;
 using System.IO;
 using System.Linq;
+
 using UnityEngine;
 
-namespace MeidoPhotoStudio.Plugin
+namespace MeidoPhotoStudio.Plugin;
+
+public class MaidFaceBlendPane : BasePane
 {
-    public class MaidFaceBlendPane : BasePane
+    private static readonly string[] TabTranslations = { "baseTab", "customTab" };
+
+    private readonly MeidoManager meidoManager;
+    private readonly SelectionGrid faceBlendSourceGrid;
+    private readonly Dropdown faceBlendCategoryDropdown;
+    private readonly Button prevCategoryButton;
+    private readonly Button nextCategoryButton;
+    private readonly Dropdown faceBlendDropdown;
+    private readonly Button facePrevButton;
+    private readonly Button faceNextButton;
+
+    private bool facePresetMode;
+    private bool faceListEnabled;
+
+    public MaidFaceBlendPane(MeidoManager meidoManager)
     {
-        private readonly MeidoManager meidoManager;
-        private readonly SelectionGrid faceBlendSourceGrid;
-        private readonly Dropdown faceBlendCategoryDropdown;
-        private readonly Button prevCategoryButton;
-        private readonly Button nextCategoryButton;
-        private readonly Dropdown faceBlendDropdown;
-        private readonly Button facePrevButton;
-        private readonly Button faceNextButton;
-        private static readonly string[] tabTranslations = { "baseTab", "customTab" };
-        private bool facePresetMode;
-        private bool faceListEnabled;
-        private Dictionary<string, List<string>> CurrentFaceDict => facePresetMode
-            ? Constants.CustomFaceDict : Constants.FaceDict;
-        private List<string> CurrentFaceGroupList => facePresetMode
-            ? Constants.CustomFaceGroupList : Constants.FaceGroupList;
-        private string SelectedFaceGroup => CurrentFaceGroupList[faceBlendCategoryDropdown.SelectedItemIndex];
-        private List<string> CurrentFaceList => CurrentFaceDict[SelectedFaceGroup];
-        private int SelectedFaceIndex => faceBlendDropdown.SelectedItemIndex;
-        private string SelectedFace => CurrentFaceList[SelectedFaceIndex];
-
-        public MaidFaceBlendPane(MeidoManager meidoManager)
-        {
-            Constants.CustomFaceChange += OnPresetChange;
-            this.meidoManager = meidoManager;
+        Constants.CustomFaceChange += OnPresetChange;
 
-            faceBlendSourceGrid = new SelectionGrid(Translation.GetArray("maidFaceWindow", tabTranslations));
-            faceBlendSourceGrid.ControlEvent += (s, a) =>
-            {
-                facePresetMode = faceBlendSourceGrid.SelectedItemIndex == 1;
-                if (updating) return;
-                string[] list = facePresetMode
-                    ? CurrentFaceGroupList.ToArray()
-                    : Translation.GetArray("faceBlendCategory", Constants.FaceGroupList);
-                faceBlendCategoryDropdown.SetDropdownItems(list, 0);
-            };
-
-            faceBlendCategoryDropdown = new Dropdown(
-                Translation.GetArray("faceBlendCategory", Constants.FaceGroupList)
-            );
-            faceBlendCategoryDropdown.SelectionChange += (s, a) =>
-            {
-                faceListEnabled = CurrentFaceList.Count > 0;
-                faceBlendDropdown.SetDropdownItems(UIFaceList(), 0);
-            };
+        this.meidoManager = meidoManager;
 
-            prevCategoryButton = new Button("<");
-            prevCategoryButton.ControlEvent += (s, a) => faceBlendCategoryDropdown.Step(-1);
+        faceBlendSourceGrid = new(Translation.GetArray("maidFaceWindow", TabTranslations));
 
-            nextCategoryButton = new Button(">");
-            nextCategoryButton.ControlEvent += (s, a) => faceBlendCategoryDropdown.Step(1);
+        faceBlendSourceGrid.ControlEvent += (_, _) =>
+        {
+            facePresetMode = faceBlendSourceGrid.SelectedItemIndex is 1;
 
-            faceBlendDropdown = new Dropdown(UIFaceList());
-            faceBlendDropdown.SelectionChange += (s, a) =>
-            {
-                if (!faceListEnabled || updating) return;
-                this.meidoManager.ActiveMeido.SetFaceBlendSet(SelectedFace);
-            };
+            if (updating)
+                return;
 
-            facePrevButton = new Button("<");
-            facePrevButton.ControlEvent += (s, a) => faceBlendDropdown.Step(-1);
+            var list = facePresetMode
+                ? CurrentFaceGroupList.ToArray()
+                : Translation.GetArray("faceBlendCategory", Constants.FaceGroupList);
 
-            faceNextButton = new Button(">");
-            faceNextButton.ControlEvent += (s, a) => faceBlendDropdown.Step(1);
+            faceBlendCategoryDropdown.SetDropdownItems(list, 0);
+        };
 
+        faceBlendCategoryDropdown = new(Translation.GetArray("faceBlendCategory", Constants.FaceGroupList));
+        faceBlendCategoryDropdown.SelectionChange += (_, _) =>
+        {
             faceListEnabled = CurrentFaceList.Count > 0;
-        }
+            faceBlendDropdown.SetDropdownItems(UIFaceList(), 0);
+        };
+
+        prevCategoryButton = new("<");
+        prevCategoryButton.ControlEvent += (_, _) =>
+            faceBlendCategoryDropdown.Step(-1);
+
+        nextCategoryButton = new(">");
+        nextCategoryButton.ControlEvent += (_, _) =>
+            faceBlendCategoryDropdown.Step(1);
 
-        protected override void ReloadTranslation()
+        faceBlendDropdown = new(UIFaceList());
+        faceBlendDropdown.SelectionChange += (_, _) =>
         {
-            updating = true;
-            faceBlendSourceGrid.SetItems(Translation.GetArray("maidFaceWindow", tabTranslations));
-            if (!facePresetMode)
-            {
-                faceBlendCategoryDropdown.SetDropdownItems(
-                    Translation.GetArray("faceBlendCategory", Constants.FaceGroupList)
-                );
-            }
-            updating = false;
-        }
+            if (!faceListEnabled || updating)
+                return;
+
+            this.meidoManager.ActiveMeido.SetFaceBlendSet(SelectedFace);
+        };
+
+        facePrevButton = new("<");
+        facePrevButton.ControlEvent += (_, _) =>
+            faceBlendDropdown.Step(-1);
+
+        faceNextButton = new(">");
+        faceNextButton.ControlEvent += (_, _) =>
+            faceBlendDropdown.Step(1);
+
+        faceListEnabled = CurrentFaceList.Count > 0;
+    }
+
+    private Dictionary<string, List<string>> CurrentFaceDict =>
+        facePresetMode ? Constants.CustomFaceDict : Constants.FaceDict;
+
+    private List<string> CurrentFaceGroupList =>
+        facePresetMode ? Constants.CustomFaceGroupList : Constants.FaceGroupList;
+
+    private string SelectedFaceGroup =>
+        CurrentFaceGroupList[faceBlendCategoryDropdown.SelectedItemIndex];
+
+    private List<string> CurrentFaceList =>
+        CurrentFaceDict[SelectedFaceGroup];
 
-        public override void Draw()
+    private int SelectedFaceIndex =>
+        faceBlendDropdown.SelectedItemIndex;
+
+    private string SelectedFace =>
+        CurrentFaceList[SelectedFaceIndex];
+
+    public override void Draw()
+    {
+        const float buttonHeight = 30;
+
+        var arrowLayoutOptions = new[]
         {
-            const float buttonHeight = 30;
-            GUILayoutOption[] arrowLayoutOptions = {
-                GUILayout.Width(buttonHeight),
-                GUILayout.Height(buttonHeight)
-            };
-
-            const float dropdownButtonWidth = 153f;
-            GUILayoutOption[] dropdownLayoutOptions = new GUILayoutOption[] {
-                GUILayout.Height(buttonHeight),
-                GUILayout.Width(dropdownButtonWidth)
-            };
-
-            GUI.enabled = meidoManager.HasActiveMeido;
-
-            faceBlendSourceGrid.Draw();
-
-            MpsGui.WhiteLine();
-
-            GUILayout.BeginHorizontal();
-            prevCategoryButton.Draw(arrowLayoutOptions);
-            faceBlendCategoryDropdown.Draw(dropdownLayoutOptions);
-            nextCategoryButton.Draw(arrowLayoutOptions);
-            GUILayout.EndHorizontal();
-
-            GUILayout.BeginHorizontal();
-            GUI.enabled = GUI.enabled && faceListEnabled;
-            facePrevButton.Draw(arrowLayoutOptions);
-            faceBlendDropdown.Draw(dropdownLayoutOptions);
-            faceNextButton.Draw(arrowLayoutOptions);
-            GUILayout.EndHorizontal();
-            GUI.enabled = true;
-        }
+            GUILayout.Width(buttonHeight),
+            GUILayout.Height(buttonHeight),
+        };
+
+        const float dropdownButtonWidth = 153f;
 
-        private string[] UIFaceList()
+        var dropdownLayoutOptions = new[]
         {
-            return CurrentFaceList.Count == 0
-                ? (new[] { "No Face Presets" })
-                : CurrentFaceList.Select(face => facePresetMode
-                    ? Path.GetFileNameWithoutExtension(face)
-                    : Translation.Get("faceBlendPresetsDropdown", face)
-                ).ToArray();
-        }
+            GUILayout.Height(buttonHeight),
+            GUILayout.Width(dropdownButtonWidth),
+        };
+
+        GUI.enabled = meidoManager.HasActiveMeido;
+
+        faceBlendSourceGrid.Draw();
+
+        MpsGui.WhiteLine();
+
+        GUILayout.BeginHorizontal();
+        prevCategoryButton.Draw(arrowLayoutOptions);
+        faceBlendCategoryDropdown.Draw(dropdownLayoutOptions);
+        nextCategoryButton.Draw(arrowLayoutOptions);
+        GUILayout.EndHorizontal();
+
+        GUILayout.BeginHorizontal();
+        GUI.enabled = GUI.enabled && faceListEnabled;
+        facePrevButton.Draw(arrowLayoutOptions);
+        faceBlendDropdown.Draw(dropdownLayoutOptions);
+        faceNextButton.Draw(arrowLayoutOptions);
+        GUILayout.EndHorizontal();
+        GUI.enabled = true;
+    }
+
+    protected override void ReloadTranslation()
+    {
+        updating = true;
+        faceBlendSourceGrid.SetItems(Translation.GetArray("maidFaceWindow", TabTranslations));
+
+        if (!facePresetMode)
+            faceBlendCategoryDropdown.SetDropdownItems(
+                Translation.GetArray("faceBlendCategory", Constants.FaceGroupList));
+
+        updating = false;
+    }
 
-        private void OnPresetChange(object sender, PresetChangeEventArgs args)
+    private string[] UIFaceList() =>
+        CurrentFaceList.Count is 0
+            ? new[] { "No Face Presets" }
+            : CurrentFaceList.Select(face => facePresetMode
+                ? Path.GetFileNameWithoutExtension(face)
+                : Translation.Get("faceBlendPresetsDropdown", face)).ToArray();
+
+    private void OnPresetChange(object sender, PresetChangeEventArgs args)
+    {
+        if (args == PresetChangeEventArgs.Empty)
         {
-            if (args == PresetChangeEventArgs.Empty)
-            {
-                if (facePresetMode)
-                {
-                    updating = true;
-                    faceBlendCategoryDropdown.SetDropdownItems(CurrentFaceGroupList.ToArray(), 0);
-                    faceBlendDropdown.SetDropdownItems(UIFaceList(), 0);
-                    updating = false;
-                }
-            }
-            else
+            if (facePresetMode)
             {
                 updating = true;
-                faceBlendSourceGrid.SelectedItemIndex = 1;
-                faceBlendCategoryDropdown.SetDropdownItems(
-                    CurrentFaceGroupList.ToArray(), CurrentFaceGroupList.IndexOf(args.Category)
-                );
+                faceBlendCategoryDropdown.SetDropdownItems(CurrentFaceGroupList.ToArray(), 0);
+                faceBlendDropdown.SetDropdownItems(UIFaceList(), 0);
                 updating = false;
-                faceBlendDropdown.SetDropdownItems(UIFaceList(), CurrentFaceList.IndexOf(args.Path));
             }
         }
+        else
+        {
+            updating = true;
+            faceBlendSourceGrid.SelectedItemIndex = 1;
+            faceBlendCategoryDropdown.SetDropdownItems(
+                CurrentFaceGroupList.ToArray(), CurrentFaceGroupList.IndexOf(args.Category));
+
+            updating = false;
+            faceBlendDropdown.SetDropdownItems(UIFaceList(), CurrentFaceList.IndexOf(args.Path));
+        }
     }
 }

+ 190 - 173
src/MeidoPhotoStudio.Plugin/GUI/Panes/FaceWindowPanes/MaidFaceSliderPane.cs

@@ -1,214 +1,231 @@
 using System;
-using System.IO;
 using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+
 using Newtonsoft.Json;
 using UnityEngine;
-using System.Linq;
 
-namespace MeidoPhotoStudio.Plugin
+using static MeidoPhotoStudio.Plugin.Meido;
+
+namespace MeidoPhotoStudio.Plugin;
+
+public class MaidFaceSliderPane : BasePane
 {
-    using static Meido;
-    public class MaidFaceSliderPane : BasePane
+    private static readonly Dictionary<string, float> SliderLimits = new()
     {
-        private static readonly Dictionary<string, float> SliderLimits = new Dictionary<string, float>()
-        {
-            // Eye Shut
-            ["eyeclose"] = 1f,
-            // Eye Smile
-            ["eyeclose2"] = 1f,
-            // Glare
-            ["eyeclose3"] = 1f,
-            // Wide Eyes
-            ["eyebig"] = 1f,
-            // Wink 1
-            ["eyeclose6"] = 1f,
-            // Wink 2
-            ["eyeclose5"] = 1f,
-            // Highlight
-            ["hitomih"] = 2f,
-            // Pupil Size
-            ["hitomis"] = 3f,
-            // Brow 1
-            ["mayuha"] = 1f,
-            // Brow 2
-            ["mayuw"] = 1f,
-            // Brow Up
-            ["mayuup"] = 1f,
-            // Brow Down 1
-            ["mayuv"] = 1f,
-            // Brow Down 2
-            ["mayuvhalf"] = 1f,
-            // Mouth Open 1
-            ["moutha"] = 1f,
-            // Mouth Open 2
-            ["mouths"] = 1f,
-            // Mouth Narrow
-            ["mouthc"] = 1f,
-            // Mouth Widen
-            ["mouthi"] = 1f,
-            // Smile
-            ["mouthup"] = 1.4f,
-            // Frown
-            ["mouthdw"] = 1f,
-            // Mouth Pucker
-            ["mouthhe"] = 1f,
-            // Grin
-            ["mouthuphalf"] = 2f,
-            // Tongue Out
-            ["tangout"] = 1f,
-            // Tongue Up
-            ["tangup"] = 1f,
-            // Tongue Base
-            ["tangopen"] = 1f
-        };
-        private readonly MeidoManager meidoManager;
-        private readonly Dictionary<string, BaseControl> faceControls;
-        private bool hasTangOpen;
-
-        public MaidFaceSliderPane(MeidoManager meidoManager)
-        {
-            this.meidoManager = meidoManager;
-            faceControls = new Dictionary<string, BaseControl>();
+        ["eyeclose"] = 1f, // Eye Shut
+        ["eyeclose2"] = 1f, // Eye Smile
+        ["eyeclose3"] = 1f, // Glare
+        ["eyebig"] = 1f, // Wide Eyes
+        ["eyeclose6"] = 1f, // Wink 1
+        ["eyeclose5"] = 1f, // Wink 2
+        ["hitomih"] = 2f, // Highlight
+        ["hitomis"] = 3f, // Pupil Size
+        ["mayuha"] = 1f, // Brow 1
+        ["mayuw"] = 1f, // Brow 2
+        ["mayuup"] = 1f, // Brow Up
+        ["mayuv"] = 1f, // Brow Down 1
+        ["mayuvhalf"] = 1f, // Brow Down 2
+        ["moutha"] = 1f, // Mouth Open 1
+        ["mouths"] = 1f, // Mouth Open 2
+        ["mouthc"] = 1f, // Mouth Narrow
+        ["mouthi"] = 1f, // Mouth Widen
+        ["mouthup"] = 1.4f, // Smile
+        ["mouthdw"] = 1f, // Frown
+        ["mouthhe"] = 1f, // Mouth Pucker
+        ["mouthuphalf"] = 2f, // Grin
+        ["tangout"] = 1f, // Tongue Out
+        ["tangup"] = 1f, // Tongue Up
+        ["tangopen"] = 1f, // Tongue Base
+    };
+
+    private readonly MeidoManager meidoManager;
+    private readonly Dictionary<string, BaseControl> faceControls;
+
+    private bool hasTangOpen;
+
+    public MaidFaceSliderPane(MeidoManager meidoManager)
+    {
+        this.meidoManager = meidoManager;
 
-            foreach (string key in faceKeys)
-            {
-                string uiName = Translation.Get("faceBlendValues", key);
-                Slider slider = new Slider(uiName, 0f, SliderLimits[key]);
-                string myKey = key;
-                slider.ControlEvent += (s, a) => SetFaceValue(myKey, slider.Value);
-                faceControls[key] = slider;
-            }
+        faceControls = new();
 
-            foreach (string key in faceToggleKeys)
-            {
-                string uiName = Translation.Get("faceBlendValues", key);
-                Toggle toggle = new Toggle(uiName);
-                string myKey = key;
-                toggle.ControlEvent += (s, a) => SetFaceValue(myKey, toggle.Value);
-                faceControls[key] = toggle;
-            }
+        foreach (var key in FaceKeys)
+        {
+            var uiName = Translation.Get("faceBlendValues", key);
+            var slider = new Slider(uiName, 0f, SliderLimits[key]);
+            var sliderKey = key;
+
+            slider.ControlEvent += (_, _) =>
+                SetFaceValue(sliderKey, slider.Value);
 
-            InitializeSliderLimits(faceControls);
+            faceControls[key] = slider;
         }
 
-        private static void InitializeSliderLimits(Dictionary<string, BaseControl> controls)
+        foreach (var key in FaceToggleKeys)
         {
-            try
-            {
-                string sliderLimitsPath = Path.Combine(Constants.databasePath, "face_slider_limits.json");
-                string sliderLimitsJson = File.ReadAllText(sliderLimitsPath);
+            var uiName = Translation.Get("faceBlendValues", key);
+            var toggle = new Toggle(uiName);
+            var sliderKey = key;
 
-                foreach (var kvp in JsonConvert.DeserializeObject<Dictionary<string, float>>(sliderLimitsJson))
-                {
-                    string key = kvp.Key;
-                    if (faceKeys.Contains(key) && controls.ContainsKey(key))
-                    {
-                        float limit = kvp.Value;
-                        limit = kvp.Value >= 1f ? limit : SliderLimits[key];
-                        Slider slider = (Slider)controls[kvp.Key];
-                        slider.SetBounds(slider.Left, limit);
-                    }
-                    else Utility.LogWarning($"'{key}' is not a valid face key");
-                }
-            }
-            catch (IOException e)
-            {
-                Utility.LogWarning($"Could not open face slider limit database because {e.Message}");
-            }
-            catch (Exception e)
-            {
-                Utility.LogError($"Could not apply face slider limit database because {e.Message}");
-            }
+            toggle.ControlEvent += (_, _) =>
+                SetFaceValue(sliderKey, toggle.Value);
+
+            faceControls[key] = toggle;
         }
 
-        protected override void ReloadTranslation()
+        InitializeSliderLimits(faceControls);
+    }
+
+    public override void UpdatePane()
+    {
+        updating = true;
+        var meido = meidoManager.ActiveMeido;
+
+        for (var i = 0; i < FaceKeys.Length; i++)
         {
-            for (int i = 0; i < faceKeys.Length; i++)
+            var slider = (Slider)faceControls[FaceKeys[i]];
+
+            try
             {
-                Slider slider = (Slider)faceControls[faceKeys[i]];
-                slider.Label = Translation.Get("faceBlendValues", faceKeys[i]);
+                slider.Value = meido.GetFaceBlendValue(FaceKeys[i]);
             }
-
-            for (int i = 0; i < faceToggleKeys.Length; i++)
+            catch
             {
-                Toggle toggle = (Toggle)faceControls[faceToggleKeys[i]];
-                toggle.Label = Translation.Get("faceBlendValues", faceToggleKeys[i]);
+                // Ignored
             }
         }
 
-        public override void UpdatePane()
+        for (var i = 0; i < FaceToggleKeys.Length; i++)
         {
-            updating = true;
-            Meido meido = meidoManager.ActiveMeido;
-            for (int i = 0; i < faceKeys.Length; i++)
-            {
-                Slider slider = (Slider)faceControls[faceKeys[i]];
-                try
-                {
-                    slider.Value = meido.GetFaceBlendValue(faceKeys[i]);
-                }
-                catch { }
-            }
+            var hash = FaceToggleKeys[i];
+            var toggle = (Toggle)faceControls[hash];
 
-            for (int i = 0; i < faceToggleKeys.Length; i++)
-            {
-                string hash = faceToggleKeys[i];
-                Toggle toggle = (Toggle)faceControls[hash];
-                toggle.Value = meido.GetFaceBlendValue(hash) > 0f;
-                if (hash == "toothoff") toggle.Value = !toggle.Value;
-            }
-            hasTangOpen = meido.Body.Face.morph.Contains("tangopen");
-            updating = false;
+            toggle.Value = meido.GetFaceBlendValue(hash) > 0f;
+
+            if (hash is "toothoff")
+                toggle.Value = !toggle.Value;
         }
 
-        public override void Draw()
+        hasTangOpen = meido.Body.Face.morph.Contains("tangopen");
+        updating = false;
+    }
+
+    public override void Draw()
+    {
+        GUI.enabled = meidoManager.HasActiveMeido;
+        DrawSliders("eyeclose", "eyeclose2");
+        DrawSliders("eyeclose3", "eyebig");
+        DrawSliders("eyeclose6", "eyeclose5");
+        DrawSliders("hitomih", "hitomis");
+        DrawSliders("mayuha", "mayuw");
+        DrawSliders("mayuup", "mayuv");
+        DrawSliders("mayuvhalf");
+        DrawSliders("moutha", "mouths");
+        DrawSliders("mouthc", "mouthi");
+        DrawSliders("mouthup", "mouthdw");
+        DrawSliders("mouthhe", "mouthuphalf");
+        DrawSliders("tangout", "tangup");
+
+        if (hasTangOpen)
+            DrawSliders("tangopen");
+
+        MpsGui.WhiteLine();
+        DrawToggles("hoho2", "shock", "nosefook");
+        DrawToggles("namida", "yodare", "toothoff");
+        DrawToggles("tear1", "tear2", "tear3");
+        DrawToggles("hohos", "hoho", "hohol");
+        GUI.enabled = true;
+    }
+
+    protected override void ReloadTranslation()
+    {
+        for (var i = 0; i < FaceKeys.Length; i++)
         {
-            GUI.enabled = meidoManager.HasActiveMeido;
-            DrawSliders("eyeclose", "eyeclose2");
-            DrawSliders("eyeclose3", "eyebig");
-            DrawSliders("eyeclose6", "eyeclose5");
-            DrawSliders("hitomih", "hitomis");
-            DrawSliders("mayuha", "mayuw");
-            DrawSliders("mayuup", "mayuv");
-            DrawSliders("mayuvhalf");
-            DrawSliders("moutha", "mouths");
-            DrawSliders("mouthc", "mouthi");
-            DrawSliders("mouthup", "mouthdw");
-            DrawSliders("mouthhe", "mouthuphalf");
-            DrawSliders("tangout", "tangup");
-            if (hasTangOpen) DrawSliders("tangopen");
-            MpsGui.WhiteLine();
-            DrawToggles("hoho2", "shock", "nosefook");
-            DrawToggles("namida", "yodare", "toothoff");
-            DrawToggles("tear1", "tear2", "tear3");
-            DrawToggles("hohos", "hoho", "hohol");
-            GUI.enabled = true;
+            var slider = (Slider)faceControls[FaceKeys[i]];
+
+            slider.Label = Translation.Get("faceBlendValues", FaceKeys[i]);
         }
 
-        private void DrawSliders(params string[] keys)
+        for (var i = 0; i < FaceToggleKeys.Length; i++)
         {
-            GUILayout.BeginHorizontal();
-            foreach (string key in keys) faceControls[key].Draw(MpsGui.HalfSlider);
-            GUILayout.EndHorizontal();
+            var toggle = (Toggle)faceControls[FaceToggleKeys[i]];
+
+            toggle.Label = Translation.Get("faceBlendValues", FaceToggleKeys[i]);
         }
+    }
 
-        private void DrawToggles(params string[] keys)
+    private static void InitializeSliderLimits(Dictionary<string, BaseControl> controls)
+    {
+        try
         {
-            GUILayout.BeginHorizontal();
-            foreach (string key in keys) faceControls[key].Draw();
-            GUILayout.EndHorizontal();
-        }
+            var sliderLimitsPath = Path.Combine(Constants.DatabasePath, "face_slider_limits.json");
+            var sliderLimitsJson = File.ReadAllText(sliderLimitsPath);
 
-        private void SetFaceValue(string key, float value)
+            foreach (var kvp in JsonConvert.DeserializeObject<Dictionary<string, float>>(sliderLimitsJson))
+            {
+                var key = kvp.Key;
+
+                if (FaceKeys.Contains(key) && controls.ContainsKey(key))
+                {
+                    var limit = kvp.Value;
+
+                    limit = kvp.Value >= 1f ? limit : SliderLimits[key];
+
+                    var slider = (Slider)controls[kvp.Key];
+
+                    slider.SetBounds(slider.Left, limit);
+                }
+                else
+                {
+                    Utility.LogWarning($"'{key}' is not a valid face key");
+                }
+            }
+        }
+        catch (IOException e)
         {
-            if (updating) return;
-            meidoManager.ActiveMeido.SetFaceBlendValue(key, value);
+            Utility.LogWarning($"Could not open face slider limit database because {e.Message}");
         }
-
-        private void SetFaceValue(string key, bool value)
+        catch (Exception e)
         {
-            if (key == "toothoff") value = !value;
-            SetFaceValue(key, value ? 1f : 0f);
+            Utility.LogError($"Could not apply face slider limit database because {e.Message}");
         }
     }
+
+    private void DrawSliders(params string[] keys)
+    {
+        GUILayout.BeginHorizontal();
+
+        foreach (var key in keys)
+            faceControls[key].Draw(MpsGui.HalfSlider);
+
+        GUILayout.EndHorizontal();
+    }
+
+    private void DrawToggles(params string[] keys)
+    {
+        GUILayout.BeginHorizontal();
+
+        foreach (var key in keys)
+            faceControls[key].Draw();
+
+        GUILayout.EndHorizontal();
+    }
+
+    private void SetFaceValue(string key, float value)
+    {
+        if (updating)
+            return;
+
+        meidoManager.ActiveMeido.SetFaceBlendValue(key, value);
+    }
+
+    private void SetFaceValue(string key, bool value)
+    {
+        if (key is "toothoff")
+            value = !value;
+
+        SetFaceValue(key, value ? 1f : 0f);
+    }
 }

+ 61 - 57
src/MeidoPhotoStudio.Plugin/GUI/Panes/FaceWindowPanes/SaveFacePane.cs

@@ -1,63 +1,67 @@
 using UnityEngine;
 
-namespace MeidoPhotoStudio.Plugin
+namespace MeidoPhotoStudio.Plugin;
+
+public class SaveFacePane : BasePane
 {
-    public class SaveFacePane : BasePane
+    private readonly MeidoManager meidoManager;
+    private readonly ComboBox categoryComboBox;
+    private readonly TextField faceNameTextField;
+    private readonly Button saveFaceButton;
+
+    private string categoryHeader;
+    private string nameHeader;
+
+    public SaveFacePane(MeidoManager meidoManager)
+    {
+        Constants.CustomFaceChange += (_, _) =>
+            categoryComboBox.SetDropdownItems(Constants.CustomFaceGroupList.ToArray());
+
+        this.meidoManager = meidoManager;
+
+        categoryHeader = Translation.Get("faceSave", "categoryHeader");
+        nameHeader = Translation.Get("faceSave", "nameHeader");
+
+        saveFaceButton = new(Translation.Get("faceSave", "saveButton"));
+        saveFaceButton.ControlEvent += (_, _) =>
+            SaveFace();
+
+        categoryComboBox = new(Constants.CustomFaceGroupList.ToArray());
+
+        faceNameTextField = new();
+    }
+
+    public override void Draw()
     {
-        private readonly MeidoManager meidoManager;
-        private readonly ComboBox categoryComboBox;
-        private readonly TextField faceNameTextField;
-        private readonly Button saveFaceButton;
-        private string categoryHeader;
-        private string nameHeader;
-
-        public SaveFacePane(MeidoManager meidoManager)
-        {
-            Constants.CustomFaceChange += (s, a)
-                => categoryComboBox.SetDropdownItems(Constants.CustomFaceGroupList.ToArray());
-
-            this.meidoManager = meidoManager;
-
-            categoryHeader = Translation.Get("faceSave", "categoryHeader");
-            nameHeader = Translation.Get("faceSave", "nameHeader");
-
-            saveFaceButton = new Button(Translation.Get("faceSave", "saveButton"));
-            saveFaceButton.ControlEvent += (s, a) => SaveFace();
-
-            categoryComboBox = new ComboBox(Constants.CustomFaceGroupList.ToArray());
-            faceNameTextField = new TextField();
-        }
-
-        protected override void ReloadTranslation()
-        {
-            categoryHeader = Translation.Get("faceSave", "categoryHeader");
-            nameHeader = Translation.Get("faceSave", "nameHeader");
-            saveFaceButton.Label = Translation.Get("faceSave", "saveButton");
-        }
-
-        public override void Draw()
-        {
-            GUI.enabled = meidoManager.HasActiveMeido;
-
-            MpsGui.Header(categoryHeader);
-            categoryComboBox.Draw(GUILayout.Width(165f));
-
-            MpsGui.Header(nameHeader);
-            GUILayout.BeginHorizontal();
-            faceNameTextField.Draw(GUILayout.Width(160f));
-            saveFaceButton.Draw(GUILayout.ExpandWidth(false));
-            GUILayout.EndHorizontal();
-
-            GUI.enabled = true;
-        }
-
-        private void SaveFace()
-        {
-            if (!meidoManager.HasActiveMeido) return;
-
-            Meido meido = meidoManager.ActiveMeido;
-            Constants.AddFacePreset(meido.SerializeFace(), faceNameTextField.Value, categoryComboBox.Value);
-            faceNameTextField.Value = string.Empty;
-        }
+        GUI.enabled = meidoManager.HasActiveMeido;
+
+        MpsGui.Header(categoryHeader);
+        categoryComboBox.Draw(GUILayout.Width(165f));
+
+        MpsGui.Header(nameHeader);
+        GUILayout.BeginHorizontal();
+        faceNameTextField.Draw(GUILayout.Width(160f));
+        saveFaceButton.Draw(GUILayout.ExpandWidth(false));
+        GUILayout.EndHorizontal();
+
+        GUI.enabled = true;
+    }
+
+    protected override void ReloadTranslation()
+    {
+        categoryHeader = Translation.Get("faceSave", "categoryHeader");
+        nameHeader = Translation.Get("faceSave", "nameHeader");
+        saveFaceButton.Label = Translation.Get("faceSave", "saveButton");
+    }
+
+    private void SaveFace()
+    {
+        if (!meidoManager.HasActiveMeido)
+            return;
+
+        var meido = meidoManager.ActiveMeido;
+
+        Constants.AddFacePreset(meido.SerializeFace(), faceNameTextField.Value, categoryComboBox.Value);
+        faceNameTextField.Value = string.Empty;
     }
 }

+ 57 - 53
src/MeidoPhotoStudio.Plugin/GUI/Panes/MainWindowPanes/BG2WindowPane.cs

@@ -1,59 +1,63 @@
 using UnityEngine;
 
-namespace MeidoPhotoStudio.Plugin
+namespace MeidoPhotoStudio.Plugin;
+
+public class BG2WindowPane : BaseMainWindowPane
 {
-    public class BG2WindowPane : BaseMainWindowPane
+    private static readonly string[] TabNames = { "props", "myRoom", "mod" };
+
+    private readonly MeidoManager meidoManager;
+    private readonly PropManager propManager;
+    private readonly AttachPropPane attachPropPane;
+    private readonly PropManagerPane propManagerPane;
+    private readonly SelectionGrid propTabs;
+
+    private BasePane currentPropsPane;
+
+    public BG2WindowPane(MeidoManager meidoManager, PropManager propManager)
     {
-        private static readonly string[] tabNames = { "props", "myRoom", "mod" };
-        private readonly MeidoManager meidoManager;
-        private readonly PropManager propManager;
-        private readonly AttachPropPane attachPropPane;
-        private readonly PropManagerPane propManagerPane;
-        private readonly SelectionGrid propTabs;
-        private BasePane currentPropsPane;
-
-        public BG2WindowPane(MeidoManager meidoManager, PropManager propManager)
-        {
-            this.meidoManager = meidoManager;
-            this.propManager = propManager;
-            this.propManager.FromPropSelect += (s, a) => propTabs.SelectedItemIndex = 0;
-
-            // should be added in this order
-            AddPane(new PropsPane(propManager));
-            AddPane(new MyRoomPropsPane(propManager));
-            AddPane(new ModPropsPane(propManager));
-
-            attachPropPane = AddPane(new AttachPropPane(this.meidoManager, propManager));
-            propManagerPane = AddPane(new PropManagerPane(propManager));
-
-            propTabs = new SelectionGrid(Translation.GetArray("propsPaneTabs", tabNames));
-            propTabs.ControlEvent += (s, a) => currentPropsPane = Panes[propTabs.SelectedItemIndex];
-            currentPropsPane = Panes[0];
-        }
-
-        protected override void ReloadTranslation()
-        {
-            propTabs.SetItems(Translation.GetArray("propsPaneTabs", tabNames));
-        }
-
-        public override void Draw()
-        {
-            tabsPane.Draw();
-            propTabs.Draw();
-            MpsGui.WhiteLine();
-            currentPropsPane.Draw();
-
-            if (propTabs.SelectedItemIndex != 0) return;
-
-            propManagerPane.Draw();
-            scrollPos = GUILayout.BeginScrollView(scrollPos);
-            attachPropPane.Draw();
-            GUILayout.EndScrollView();
-        }
-
-        public override void UpdatePanes()
-        {
-            if (ActiveWindow) base.UpdatePanes();
-        }
+        this.meidoManager = meidoManager;
+        this.propManager = propManager;
+        this.propManager.FromPropSelect += (_, _) =>
+            propTabs.SelectedItemIndex = 0;
+
+        // should be added in this order
+        AddPane(new PropsPane(propManager));
+        AddPane(new MyRoomPropsPane(propManager));
+        AddPane(new ModPropsPane(propManager));
+
+        attachPropPane = AddPane(new AttachPropPane(this.meidoManager, propManager));
+        propManagerPane = AddPane(new PropManagerPane(propManager));
+
+        propTabs = new(Translation.GetArray("propsPaneTabs", TabNames));
+        propTabs.ControlEvent += (_, _) =>
+            currentPropsPane = Panes[propTabs.SelectedItemIndex];
+
+        currentPropsPane = Panes[0];
     }
+
+    public override void Draw()
+    {
+        tabsPane.Draw();
+        propTabs.Draw();
+        MpsGui.WhiteLine();
+        currentPropsPane.Draw();
+
+        if (propTabs.SelectedItemIndex is not 0)
+            return;
+
+        propManagerPane.Draw();
+        scrollPos = GUILayout.BeginScrollView(scrollPos);
+        attachPropPane.Draw();
+        GUILayout.EndScrollView();
+    }
+
+    public override void UpdatePanes()
+    {
+        if (ActiveWindow)
+            base.UpdatePanes();
+    }
+
+    protected override void ReloadTranslation() =>
+        propTabs.SetItems(Translation.GetArray("propsPaneTabs", TabNames));
 }

+ 52 - 52
src/MeidoPhotoStudio.Plugin/GUI/Panes/MainWindowPanes/BGWindowPane.cs

@@ -1,66 +1,66 @@
 using UnityEngine;
 
-namespace MeidoPhotoStudio.Plugin
+namespace MeidoPhotoStudio.Plugin;
+
+public class BGWindowPane : BaseMainWindowPane
 {
-    public class BGWindowPane : BaseMainWindowPane
+    private readonly BackgroundSelectorPane backgroundSelectorPane;
+    private readonly CameraPane cameraPane;
+    private readonly LightsPane lightsPane;
+    private readonly EffectsPane effectsPane;
+    private readonly DragPointPane dragPointPane;
+    private readonly OtherEffectsPane otherEffectsPane;
+    private readonly Button sceneManagerButton;
+
+    public BGWindowPane(
+        EnvironmentManager environmentManager,
+        LightManager lightManager,
+        EffectManager effectManager,
+        SceneWindow sceneWindow,
+        CameraManager cameraManager)
     {
-        private readonly BackgroundSelectorPane backgroundSelectorPane;
-        private readonly CameraPane cameraPane;
-        private readonly LightsPane lightsPane;
-        private readonly EffectsPane effectsPane;
-        private readonly DragPointPane dragPointPane;
-        private readonly OtherEffectsPane otherEffectsPane;
-        private readonly Button sceneManagerButton;
+        sceneManagerButton = new(Translation.Get("backgroundWindow", "manageScenesButton"));
+        sceneManagerButton.ControlEvent += (_, _) =>
+            sceneWindow.Visible = !sceneWindow.Visible;
 
-        public BGWindowPane(
-            EnvironmentManager environmentManager, LightManager lightManager, EffectManager effectManager,
-            SceneWindow sceneWindow, CameraManager cameraManager
-        )
+        backgroundSelectorPane = AddPane(new BackgroundSelectorPane(environmentManager));
+        cameraPane = AddPane(new CameraPane(cameraManager));
+        dragPointPane = AddPane(new DragPointPane());
+        lightsPane = AddPane(new LightsPane(lightManager));
+        effectsPane = AddPane(new EffectsPane()
         {
-            sceneManagerButton = new Button(Translation.Get("backgroundWindow", "manageScenesButton"));
-            sceneManagerButton.ControlEvent += (s, a) => sceneWindow.Visible = !sceneWindow.Visible;
-
-            backgroundSelectorPane = AddPane(new BackgroundSelectorPane(environmentManager));
-            cameraPane = AddPane(new CameraPane(cameraManager));
-            dragPointPane = AddPane(new DragPointPane());
-            lightsPane = AddPane(new LightsPane(lightManager));
-
-            effectsPane = AddPane(new EffectsPane()
-            {
-                ["bloom"] = new BloomPane(effectManager),
-                ["dof"] = new DepthOfFieldPane(effectManager),
-                ["vignette"] = new VignettePane(effectManager),
-                ["fog"] = new FogPane(effectManager)
-            });
+            ["bloom"] = new BloomPane(effectManager),
+            ["dof"] = new DepthOfFieldPane(effectManager),
+            ["vignette"] = new VignettePane(effectManager),
+            ["fog"] = new FogPane(effectManager),
+        });
 
-            otherEffectsPane = AddPane(new OtherEffectsPane(effectManager));
-        }
-
-        protected override void ReloadTranslation()
-        {
-            sceneManagerButton.Label = Translation.Get("backgroundWindow", "manageScenesButton");
-        }
+        otherEffectsPane = AddPane(new OtherEffectsPane(effectManager));
+    }
 
-        public override void Draw()
-        {
-            tabsPane.Draw();
-            sceneManagerButton.Draw();
-            backgroundSelectorPane.Draw();
-            dragPointPane.Draw();
+    public override void Draw()
+    {
+        tabsPane.Draw();
+        sceneManagerButton.Draw();
+        backgroundSelectorPane.Draw();
+        dragPointPane.Draw();
 
-            scrollPos = GUILayout.BeginScrollView(scrollPos);
+        scrollPos = GUILayout.BeginScrollView(scrollPos);
 
-            cameraPane.Draw();
-            lightsPane.Draw();
-            effectsPane.Draw();
-            otherEffectsPane.Draw();
+        cameraPane.Draw();
+        lightsPane.Draw();
+        effectsPane.Draw();
+        otherEffectsPane.Draw();
 
-            GUILayout.EndScrollView();
-        }
+        GUILayout.EndScrollView();
+    }
 
-        public override void UpdatePanes()
-        {
-            if (ActiveWindow) base.UpdatePanes();
-        }
+    public override void UpdatePanes()
+    {
+        if (ActiveWindow)
+            base.UpdatePanes();
     }
+
+    protected override void ReloadTranslation() =>
+        sceneManagerButton.Label = Translation.Get("backgroundWindow", "manageScenesButton");
 }

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

@@ -1,8 +1,9 @@
-namespace MeidoPhotoStudio.Plugin
+namespace MeidoPhotoStudio.Plugin;
+
+public abstract class BaseMainWindowPane : BaseWindow
 {
-    public abstract class BaseMainWindowPane : BaseWindow
-    {
-        protected TabsPane tabsPane;
-        public void SetTabsPane(TabsPane tabsPane) => this.tabsPane = tabsPane;
-    }
+    protected TabsPane tabsPane;
+
+    public void SetTabsPane(TabsPane tabsPane) =>
+        this.tabsPane = tabsPane;
 }

+ 38 - 41
src/MeidoPhotoStudio.Plugin/GUI/Panes/MainWindowPanes/CallWindowPane.cs

@@ -1,47 +1,44 @@
 using UnityEngine;
 
-namespace MeidoPhotoStudio.Plugin
+namespace MeidoPhotoStudio.Plugin;
+
+public class CallWindowPane : BaseMainWindowPane
 {
-    public class CallWindowPane : BaseMainWindowPane
+    private readonly MeidoManager meidoManager;
+    private readonly MaidSelectorPane maidSelectorPane;
+    private readonly Dropdown placementDropdown;
+    private readonly Button placementOKButton;
+
+    public CallWindowPane(MeidoManager meidoManager)
+    {
+        this.meidoManager = meidoManager;
+
+        placementDropdown = new(Translation.GetArray("placementDropdown", MaidPlacementUtility.PlacementTypes));
+
+        placementOKButton = new(Translation.Get("maidCallWindow", "okButton"));
+        placementOKButton.ControlEvent += (_, _) =>
+            this.meidoManager.PlaceMeidos(MaidPlacementUtility.PlacementTypes[placementDropdown.SelectedItemIndex]);
+
+        maidSelectorPane = AddPane(new MaidSelectorPane(this.meidoManager));
+    }
+
+    public override void Draw()
     {
-        private readonly MeidoManager meidoManager;
-        private readonly MaidSelectorPane maidSelectorPane;
-        private readonly Dropdown placementDropdown;
-        private readonly Button placementOKButton;
-
-        public CallWindowPane(MeidoManager meidoManager)
-        {
-            this.meidoManager = meidoManager;
-            placementDropdown = new Dropdown(
-                Translation.GetArray("placementDropdown", MaidPlacementUtility.placementTypes)
-            );
-
-            placementOKButton = new Button(Translation.Get("maidCallWindow", "okButton"));
-            placementOKButton.ControlEvent += (o, a) => this.meidoManager.PlaceMeidos(
-                MaidPlacementUtility.placementTypes[placementDropdown.SelectedItemIndex]
-            );
-
-            maidSelectorPane = AddPane(new MaidSelectorPane(this.meidoManager));
-        }
-
-        protected override void ReloadTranslation()
-        {
-            placementDropdown.SetDropdownItems(
-                Translation.GetArray("placementDropdown", MaidPlacementUtility.placementTypes)
-            );
-            placementOKButton.Label = Translation.Get("maidCallWindow", "okButton");
-        }
-
-        public override void Draw()
-        {
-            tabsPane.Draw();
-
-            GUILayout.BeginHorizontal();
-            placementDropdown.Draw(GUILayout.Width(150));
-            placementOKButton.Draw();
-            GUILayout.EndHorizontal();
-
-            maidSelectorPane.Draw();
-        }
+        tabsPane.Draw();
+
+        GUILayout.BeginHorizontal();
+        placementDropdown.Draw(GUILayout.Width(150));
+        placementOKButton.Draw();
+        GUILayout.EndHorizontal();
+
+        maidSelectorPane.Draw();
+    }
+
+    protected override void ReloadTranslation()
+    {
+        placementDropdown.SetDropdownItems(
+            Translation.GetArray("placementDropdown", MaidPlacementUtility.PlacementTypes));
+
+        placementOKButton.Label = Translation.Get("maidCallWindow", "okButton");
     }
 }

+ 60 - 58
src/MeidoPhotoStudio.Plugin/GUI/Panes/MainWindowPanes/FaceWindowPane.cs

@@ -1,64 +1,66 @@
 using UnityEngine;
 
-namespace MeidoPhotoStudio.Plugin
+namespace MeidoPhotoStudio.Plugin;
+
+public class FaceWindowPane : BaseMainWindowPane
 {
-    public class FaceWindowPane : BaseMainWindowPane
+    private readonly MeidoManager meidoManager;
+    private readonly MaidFaceSliderPane maidFaceSliderPane;
+    private readonly MaidFaceBlendPane maidFaceBlendPane;
+    private readonly MaidSwitcherPane maidSwitcherPane;
+    private readonly SaveFacePane saveFacePane;
+    private readonly Toggle saveFaceToggle;
+
+    private bool saveFaceMode;
+
+    public FaceWindowPane(MeidoManager meidoManager, MaidSwitcherPane maidSwitcherPane)
     {
-        private readonly MeidoManager meidoManager;
-        private readonly MaidFaceSliderPane maidFaceSliderPane;
-        private readonly MaidFaceBlendPane maidFaceBlendPane;
-        private readonly MaidSwitcherPane maidSwitcherPane;
-        private readonly SaveFacePane saveFacePane;
-        private readonly Toggle saveFaceToggle;
-        private bool saveFaceMode;
-
-        public FaceWindowPane(MeidoManager meidoManager, MaidSwitcherPane maidSwitcherPane)
-        {
-            this.meidoManager = meidoManager;
-
-            this.maidSwitcherPane = maidSwitcherPane;
-
-            maidFaceSliderPane = AddPane(new MaidFaceSliderPane(this.meidoManager));
-            maidFaceBlendPane = AddPane(new MaidFaceBlendPane(this.meidoManager));
-            saveFacePane = AddPane(new SaveFacePane(this.meidoManager));
-
-            saveFaceToggle = new Toggle(Translation.Get("maidFaceWindow", "savePaneToggle"));
-            saveFaceToggle.ControlEvent += (s, a) => saveFaceMode = !saveFaceMode;
-        }
-
-        protected override void ReloadTranslation()
-        {
-            saveFaceToggle.Label = Translation.Get("maidFaceWindow", "savePaneToggle");
-        }
-
-        public override void Draw()
-        {
-            tabsPane.Draw();
-            maidSwitcherPane.Draw();
-
-            maidFaceBlendPane.Draw();
-
-            scrollPos = GUILayout.BeginScrollView(scrollPos);
-
-            maidFaceSliderPane.Draw();
-
-            GUI.enabled = meidoManager.HasActiveMeido;
-            saveFaceToggle.Draw();
-            GUI.enabled = true;
-
-            if (saveFaceMode) saveFacePane.Draw();
-
-            GUILayout.EndScrollView();
-        }
-
-        public override void UpdatePanes()
-        {
-            if (!meidoManager.HasActiveMeido) return;
-            if (ActiveWindow)
-            {
-                meidoManager.ActiveMeido.StopBlink();
-                base.UpdatePanes();
-            }
-        }
+        this.meidoManager = meidoManager;
+        this.maidSwitcherPane = maidSwitcherPane;
+
+        maidFaceSliderPane = AddPane(new MaidFaceSliderPane(this.meidoManager));
+        maidFaceBlendPane = AddPane(new MaidFaceBlendPane(this.meidoManager));
+        saveFacePane = AddPane(new SaveFacePane(this.meidoManager));
+
+        saveFaceToggle = new(Translation.Get("maidFaceWindow", "savePaneToggle"));
+        saveFaceToggle.ControlEvent += (_, _) =>
+            saveFaceMode = !saveFaceMode;
     }
+
+    public override void Draw()
+    {
+        tabsPane.Draw();
+        maidSwitcherPane.Draw();
+
+        maidFaceBlendPane.Draw();
+
+        scrollPos = GUILayout.BeginScrollView(scrollPos);
+
+        maidFaceSliderPane.Draw();
+
+        GUI.enabled = meidoManager.HasActiveMeido;
+        saveFaceToggle.Draw();
+        GUI.enabled = true;
+
+        if (saveFaceMode)
+            saveFacePane.Draw();
+
+        GUILayout.EndScrollView();
+    }
+
+    public override void UpdatePanes()
+    {
+        if (!meidoManager.HasActiveMeido)
+            return;
+
+        if (!ActiveWindow)
+            return;
+
+        meidoManager.ActiveMeido.StopBlink();
+
+        base.UpdatePanes();
+    }
+
+    protected override void ReloadTranslation() =>
+        saveFaceToggle.Label = Translation.Get("maidFaceWindow", "savePaneToggle");
 }

+ 120 - 110
src/MeidoPhotoStudio.Plugin/GUI/Panes/MainWindowPanes/PoseWindowPane.cs

@@ -1,147 +1,157 @@
 using UnityEngine;
 
-namespace MeidoPhotoStudio.Plugin
+namespace MeidoPhotoStudio.Plugin;
+
+public class PoseWindowPane : BaseMainWindowPane
 {
-    public class PoseWindowPane : BaseMainWindowPane
+    private readonly MeidoManager meidoManager;
+    private readonly MaidPoseSelectorPane maidPosePane;
+    private readonly SavePosePane savePosePane;
+    private readonly MaidSwitcherPane maidSwitcherPane;
+    private readonly MaidFreeLookPane maidFaceLookPane;
+    private readonly MpnAttachPropPane mpnAttachPropPane;
+    private readonly MaidDressingPane maidDressingPane;
+    private readonly GravityControlPane gravityControlPane;
+    private readonly CopyPosePane copyPosePane;
+    private readonly HandPresetPane handPresetPane;
+    private readonly SaveHandPane saveHandPane;
+    private readonly MaidIKPane maidIKPane;
+    private readonly Toggle freeLookToggle;
+    private readonly Toggle savePoseToggle;
+    private readonly Toggle saveHandToggle;
+    private readonly Button flipButton;
+
+    private bool savePoseMode;
+    private bool saveHandMode;
+    private string handPresetHeader;
+    private string flipIKHeader;
+
+    public PoseWindowPane(MeidoManager meidoManager, MaidSwitcherPane maidSwitcherPane)
     {
-        private readonly MeidoManager meidoManager;
-        private readonly MaidPoseSelectorPane maidPosePane;
-        private readonly SavePosePane savePosePane;
-        private readonly MaidSwitcherPane maidSwitcherPane;
-        private readonly MaidFaceLookPane maidFaceLookPane;
-        private readonly MpnAttachPropPane mpnAttachPropPane;
-        private readonly MaidDressingPane maidDressingPane;
-        private readonly GravityControlPane gravityControlPane;
-        private readonly CopyPosePane copyPosePane;
-        private readonly HandPresetPane handPresetPane;
-        private readonly SaveHandPane saveHandPane;
-        private readonly MaidIKPane maidIKPane;
-        private readonly Toggle freeLookToggle;
-        private readonly Toggle savePoseToggle;
-        private readonly Toggle saveHandToggle;
-        private readonly Button flipButton;
-        private bool savePoseMode;
-        private bool saveHandMode;
-        private string handPresetHeader;
-        private string flipIKHeader;
-
-        public PoseWindowPane(MeidoManager meidoManager, MaidSwitcherPane maidSwitcherPane)
-        {
-            this.meidoManager = meidoManager;
-            this.maidSwitcherPane = maidSwitcherPane;
+        this.meidoManager = meidoManager;
+        this.maidSwitcherPane = maidSwitcherPane;
 
-            maidPosePane = AddPane(new MaidPoseSelectorPane(meidoManager));
-            savePosePane = AddPane(new SavePosePane(meidoManager));
+        maidPosePane = AddPane(new MaidPoseSelectorPane(meidoManager));
+        savePosePane = AddPane(new SavePosePane(meidoManager));
 
-            maidFaceLookPane = AddPane(new MaidFaceLookPane(meidoManager));
-            maidFaceLookPane.Enabled = false;
+        maidFaceLookPane = AddPane(new MaidFreeLookPane(meidoManager));
+        maidFaceLookPane.Enabled = false;
 
-            freeLookToggle = new Toggle(Translation.Get("freeLookPane", "freeLookToggle"), false);
-            freeLookToggle.ControlEvent += (s, a) => SetMaidFreeLook();
+        freeLookToggle = new(Translation.Get("freeLookPane", "freeLookToggle"), false);
+        freeLookToggle.ControlEvent += (_, _) =>
+            SetMaidFreeLook();
 
-            savePoseToggle = new Toggle(Translation.Get("posePane", "saveToggle"));
-            savePoseToggle.ControlEvent += (s, a) => savePoseMode = !savePoseMode;
+        savePoseToggle = new(Translation.Get("posePane", "saveToggle"));
+        savePoseToggle.ControlEvent += (_, _) =>
+            savePoseMode = !savePoseMode;
 
-            mpnAttachPropPane = new MpnAttachPropPane(this.meidoManager);
+        // TODO: Why are only some panes added to the pane list?
+        mpnAttachPropPane = new(this.meidoManager);
 
-            maidDressingPane = AddPane(new MaidDressingPane(this.meidoManager));
+        maidDressingPane = AddPane(new MaidDressingPane(this.meidoManager));
+        maidIKPane = AddPane(new MaidIKPane(this.meidoManager));
+        gravityControlPane = AddPane(new GravityControlPane(this.meidoManager));
+        copyPosePane = AddPane(new CopyPosePane(this.meidoManager));
 
-            maidIKPane = AddPane(new MaidIKPane(this.meidoManager));
+        saveHandToggle = new(Translation.Get("handPane", "saveToggle"));
+        saveHandToggle.ControlEvent += (_, _) =>
+            saveHandMode = !saveHandMode;
 
-            gravityControlPane = AddPane(new GravityControlPane(this.meidoManager));
+        handPresetPane = AddPane(new HandPresetPane(meidoManager));
+        saveHandPane = AddPane(new SaveHandPane(meidoManager));
 
-            copyPosePane = AddPane(new CopyPosePane(this.meidoManager));
+        flipButton = new(Translation.Get("flipIK", "flipButton"));
+        flipButton.ControlEvent += (_, _) =>
+            this.meidoManager.ActiveMeido.IKManager.Flip();
 
-            saveHandToggle = new Toggle(Translation.Get("handPane", "saveToggle"));
-            saveHandToggle.ControlEvent += (s, a) => saveHandMode = !saveHandMode;
+        handPresetHeader = Translation.Get("handPane", "header");
+        flipIKHeader = Translation.Get("flipIK", "header");
+    }
 
-            handPresetPane = AddPane(new HandPresetPane(meidoManager));
-            saveHandPane = AddPane(new SaveHandPane(meidoManager));
+    public override void Draw()
+    {
+        tabsPane.Draw();
 
-            flipButton = new Button(Translation.Get("flipIK", "flipButton"));
-            flipButton.ControlEvent += (s, a) => this.meidoManager.ActiveMeido.IKManager.Flip();
+        maidSwitcherPane.Draw();
+        maidPosePane.Draw();
 
-            handPresetHeader = Translation.Get("handPane", "header");
-            flipIKHeader = Translation.Get("flipIK", "header");
-        }
+        maidIKPane.Draw();
 
-        protected override void ReloadTranslation()
-        {
-            freeLookToggle.Label = Translation.Get("freeLookPane", "freeLookToggle");
-            savePoseToggle.Label = Translation.Get("posePane", "saveToggle");
-            saveHandToggle.Label = Translation.Get("handPane", "saveToggle");
-            flipButton.Label = Translation.Get("flipIK", "flipButton");
-            handPresetHeader = Translation.Get("handPane", "header");
-            flipIKHeader = Translation.Get("flipIK", "header");
-        }
-
-        public override void Draw()
-        {
-            tabsPane.Draw();
+        MpsGui.WhiteLine();
 
-            maidSwitcherPane.Draw();
-            maidPosePane.Draw();
+        scrollPos = GUILayout.BeginScrollView(scrollPos);
 
-            maidIKPane.Draw();
+        GUI.enabled = meidoManager.HasActiveMeido;
+        GUILayout.BeginHorizontal();
+        freeLookToggle.Draw();
+        savePoseToggle.Draw();
+        GUILayout.EndHorizontal();
+        GUI.enabled = true;
 
-            MpsGui.WhiteLine();
+        if (savePoseMode)
+            savePosePane.Draw();
+        else
+            maidFaceLookPane.Draw();
 
-            scrollPos = GUILayout.BeginScrollView(scrollPos);
+        mpnAttachPropPane.Draw();
 
-            GUI.enabled = meidoManager.HasActiveMeido;
-            GUILayout.BeginHorizontal();
-            freeLookToggle.Draw();
-            savePoseToggle.Draw();
-            GUILayout.EndHorizontal();
-            GUI.enabled = true;
+        maidDressingPane.Draw();
 
-            if (savePoseMode) savePosePane.Draw();
-            else maidFaceLookPane.Draw();
+        GUI.enabled = meidoManager.HasActiveMeido;
+        MpsGui.Header(handPresetHeader);
+        MpsGui.WhiteLine();
+        saveHandToggle.Draw();
+        GUI.enabled = true;
 
-            mpnAttachPropPane.Draw();
+        if (saveHandMode)
+            saveHandPane.Draw();
+        else
+            handPresetPane.Draw();
 
-            maidDressingPane.Draw();
+        gravityControlPane.Draw();
 
-            GUI.enabled = meidoManager.HasActiveMeido;
-            MpsGui.Header(handPresetHeader);
-            MpsGui.WhiteLine();
-            saveHandToggle.Draw();
-            GUI.enabled = true;
+        copyPosePane.Draw();
 
-            if (saveHandMode) saveHandPane.Draw();
-            else handPresetPane.Draw();
+        GUILayout.BeginHorizontal();
+        GUI.enabled = meidoManager.HasActiveMeido;
+        GUILayout.Label(flipIKHeader, GUILayout.ExpandWidth(false));
+        flipButton.Draw(GUILayout.ExpandWidth(false));
+        GUI.enabled = true;
+        GUILayout.EndHorizontal();
 
-            gravityControlPane.Draw();
+        GUILayout.EndScrollView();
+    }
 
-            copyPosePane.Draw();
+    public override void UpdatePanes()
+    {
+        if (meidoManager.ActiveMeido is null)
+            return;
 
-            GUILayout.BeginHorizontal();
-            GUI.enabled = meidoManager.HasActiveMeido;
-            GUILayout.Label(flipIKHeader, GUILayout.ExpandWidth(false));
-            flipButton.Draw(GUILayout.ExpandWidth(false));
-            GUI.enabled = true;
-            GUILayout.EndHorizontal();
+        if (ActiveWindow)
+        {
+            updating = true;
+            freeLookToggle.Value = meidoManager.ActiveMeido?.FreeLook ?? false;
+            updating = false;
 
-            GUILayout.EndScrollView();
+            base.UpdatePanes();
         }
+    }
 
-        private void SetMaidFreeLook()
-        {
-            if (updating) return;
-            meidoManager.ActiveMeido.FreeLook = freeLookToggle.Value;
-        }
+    protected override void ReloadTranslation()
+    {
+        freeLookToggle.Label = Translation.Get("freeLookPane", "freeLookToggle");
+        savePoseToggle.Label = Translation.Get("posePane", "saveToggle");
+        saveHandToggle.Label = Translation.Get("handPane", "saveToggle");
+        flipButton.Label = Translation.Get("flipIK", "flipButton");
+        handPresetHeader = Translation.Get("handPane", "header");
+        flipIKHeader = Translation.Get("flipIK", "header");
+    }
 
-        public override void UpdatePanes()
-        {
-            if (meidoManager.ActiveMeido == null) return;
-
-            if (ActiveWindow)
-            {
-                updating = true;
-                freeLookToggle.Value = meidoManager.ActiveMeido?.FreeLook ?? false;
-                updating = false;
-                base.UpdatePanes();
-            }
-        }
+    private void SetMaidFreeLook()
+    {
+        if (updating)
+            return;
+
+        meidoManager.ActiveMeido.FreeLook = freeLookToggle.Value;
     }
 }

+ 130 - 131
src/MeidoPhotoStudio.Plugin/GUI/Panes/MainWindowPanes/SettingsWindowPane.cs

@@ -1,150 +1,149 @@
 using System;
 using System.Collections.Generic;
 using System.Linq;
+
 using UnityEngine;
 
-namespace MeidoPhotoStudio.Plugin
+namespace MeidoPhotoStudio.Plugin;
+
+public class SettingsWindowPane : BaseMainWindowPane
 {
-    public class SettingsWindowPane : BaseMainWindowPane
+    private static readonly string[] HeaderTranslationKeys =
     {
-        private static readonly string[] headerTranslationKeys = {
-            "controls", "controlsGeneral", "controlsMaids", "controlsCamera", "controlsDragPoint", "controlsScene"
-        };
-        private static readonly Dictionary<string, string> headers = new Dictionary<string, string>();
-        private static readonly string[] actionTranslationKeys;
-        private static readonly string[] actionLabels;
-        private readonly Button reloadTranslationButton;
-        private readonly Button reloadAllPresetsButton;
-        private readonly KeyRebindButton[] rebindButtons;
-
-        static SettingsWindowPane()
-        {
-            actionTranslationKeys = Enum.GetNames(typeof(MpsKey))
-                .Select(action => char.ToLowerInvariant(action[0]) + action.Substring(1))
-                .ToArray();
-            actionLabels = new string[actionTranslationKeys.Length];
-        }
+        "controls", "controlsGeneral", "controlsMaids", "controlsCamera", "controlsDragPoint", "controlsScene",
+    };
 
-        public SettingsWindowPane()
-        {
-            rebindButtons = new KeyRebindButton[actionTranslationKeys.Length];
-
-            for (int i = 0; i < rebindButtons.Length; i++)
-            {
-                MpsKey action = (MpsKey)i;
-                KeyRebindButton button = new KeyRebindButton(KeyCode.None);
-                button.ControlEvent += (s, a) => InputManager.Rebind(action, button.KeyCode);
-                rebindButtons[i] = button;
-
-                actionLabels[i] = Translation.Get("controls", actionTranslationKeys[i]);
-            }
-
-            for (int i = 0; i < headerTranslationKeys.Length; i++)
-            {
-                headers[headerTranslationKeys[i]] = Translation.Get("settingsHeaders", headerTranslationKeys[i]);
-            }
-
-            reloadTranslationButton = new Button(Translation.Get("settingsLabels", "reloadTranslation"));
-            reloadTranslationButton.ControlEvent += (s, a) => Translation.ReinitializeTranslation();
-
-            reloadAllPresetsButton = new Button(Translation.Get("settingsLabels", "reloadAllPresets"));
-            reloadAllPresetsButton.ControlEvent += (s, a) =>
-            {
-                Constants.InitializeCustomFaceBlends();
-                Constants.InitializeHandPresets();
-                Constants.InitializeCustomPoses();
-            };
-        }
+    private static readonly Dictionary<string, string> Headers = new();
+    private static readonly string[] ActionTranslationKeys;
+    private static readonly string[] ActionLabels;
 
-        protected override void ReloadTranslation()
-        {
-            for (int i = 0; i < rebindButtons.Length; i++)
-            {
-                actionLabels[i] = Translation.Get("controls", actionTranslationKeys[i]);
-            }
-
-            for (int i = 0; i < headerTranslationKeys.Length; i++)
-            {
-                headers[headerTranslationKeys[i]] = Translation.Get("settingsHeaders", headerTranslationKeys[i]);
-            }
-
-            reloadTranslationButton.Label = Translation.Get("settingsLabels", "reloadTranslation");
-            reloadAllPresetsButton.Label = Translation.Get("settingsLabels", "reloadAllPresets");
-        }
+    private readonly Button reloadTranslationButton;
+    private readonly Button reloadAllPresetsButton;
+    private readonly KeyRebindButton[] rebindButtons;
+
+    static SettingsWindowPane()
+    {
+        ActionTranslationKeys = Enum.GetNames(typeof(MpsKey))
+            .Select(action => char.ToLowerInvariant(action[0]) + action.Substring(1))
+            .ToArray();
 
-        public override void Draw()
+        ActionLabels = new string[ActionTranslationKeys.Length];
+    }
+
+    public SettingsWindowPane()
+    {
+        rebindButtons = new KeyRebindButton[ActionTranslationKeys.Length];
+
+        for (var i = 0; i < rebindButtons.Length; i++)
         {
-            scrollPos = GUILayout.BeginScrollView(scrollPos);
-
-            MpsGui.Header(headers["controls"]);
-            MpsGui.WhiteLine();
-
-            MpsGui.Header(headers["controlsGeneral"]);
-            MpsGui.WhiteLine();
-            for (MpsKey key = MpsKey.Activate; key <= MpsKey.ToggleMessage; key++)
-            {
-                DrawSetting(key);
-            }
-
-            MpsGui.Header(headers["controlsMaids"]);
-            MpsGui.WhiteLine();
-            DrawSetting(MpsKey.MeidoUndressing);
-
-            MpsGui.Header(headers["controlsCamera"]);
-            MpsGui.WhiteLine();
-            for (MpsKey key = MpsKey.CameraLayer; key <= MpsKey.CameraLoad; key++)
-            {
-                DrawSetting(key);
-            }
-
-            MpsGui.Header(headers["controlsDragPoint"]);
-            MpsGui.WhiteLine();
-            for (MpsKey key = MpsKey.DragSelect; key <= MpsKey.DragFinger; key++)
-            {
-                DrawSetting(key);
-            }
-
-            MpsGui.Header(headers["controlsScene"]);
-            MpsGui.WhiteLine();
-            for (MpsKey key = MpsKey.SaveScene; key <= MpsKey.OpenSceneManager; key++)
-            {
-                DrawSetting(key);
-            }
-
-            GUI.enabled = !InputManager.Listening;
-
-            // Translation settings
-            MpsGui.WhiteLine();
-            reloadTranslationButton.Draw();
-
-            reloadAllPresetsButton.Draw();
-
-            GUILayout.EndScrollView();
-
-            GUI.enabled = true;
+            var action = (MpsKey)i;
+            var button = new KeyRebindButton(KeyCode.None);
+
+            button.ControlEvent += (_, _) =>
+                InputManager.Rebind(action, button.KeyCode);
+
+            rebindButtons[i] = button;
+
+            ActionLabels[i] = Translation.Get("controls", ActionTranslationKeys[i]);
         }
 
-        private void DrawSetting(MpsKey key)
+        for (var i = 0; i < HeaderTranslationKeys.Length; i++)
+            Headers[HeaderTranslationKeys[i]] = Translation.Get("settingsHeaders", HeaderTranslationKeys[i]);
+
+        reloadTranslationButton = new(Translation.Get("settingsLabels", "reloadTranslation"));
+        reloadTranslationButton.ControlEvent += (_, _) =>
+            Translation.ReinitializeTranslation();
+
+        reloadAllPresetsButton = new(Translation.Get("settingsLabels", "reloadAllPresets"));
+        reloadAllPresetsButton.ControlEvent += (_, _) =>
         {
-            int keyIndex = (int)key;
-            GUILayout.BeginHorizontal();
-            GUILayout.Label(actionLabels[keyIndex]);
-            GUILayout.FlexibleSpace();
-            rebindButtons[keyIndex].Draw(GUILayout.Width(90f));
-            if (GUILayout.Button("×", GUILayout.ExpandWidth(false)))
-            {
-                rebindButtons[keyIndex].KeyCode = KeyCode.None;
-                InputManager.Rebind(key, KeyCode.None);
-            }
-            GUILayout.EndHorizontal();
-        }
+            Constants.InitializeCustomFaceBlends();
+            Constants.InitializeHandPresets();
+            Constants.InitializeCustomPoses();
+        };
+    }
+
+    public override void Draw()
+    {
+        scrollPos = GUILayout.BeginScrollView(scrollPos);
+
+        MpsGui.Header(Headers["controls"]);
+        MpsGui.WhiteLine();
+
+        MpsGui.Header(Headers["controlsGeneral"]);
+        MpsGui.WhiteLine();
 
-        public override void UpdatePanes()
+        for (var key = MpsKey.Activate; key <= MpsKey.ToggleMessage; key++)
+            DrawSetting(key);
+
+        MpsGui.Header(Headers["controlsMaids"]);
+        MpsGui.WhiteLine();
+        DrawSetting(MpsKey.MeidoUndressing);
+
+        MpsGui.Header(Headers["controlsCamera"]);
+        MpsGui.WhiteLine();
+
+        for (var key = MpsKey.CameraLayer; key <= MpsKey.CameraLoad; key++)
+            DrawSetting(key);
+
+        MpsGui.Header(Headers["controlsDragPoint"]);
+        MpsGui.WhiteLine();
+
+        for (var key = MpsKey.DragSelect; key <= MpsKey.DragFinger; key++)
+            DrawSetting(key);
+
+        MpsGui.Header(Headers["controlsScene"]);
+        MpsGui.WhiteLine();
+
+        for (var key = MpsKey.SaveScene; key <= MpsKey.OpenSceneManager; key++)
+            DrawSetting(key);
+
+        GUI.enabled = !InputManager.Listening;
+
+        // Translation settings
+        MpsGui.WhiteLine();
+        reloadTranslationButton.Draw();
+
+        reloadAllPresetsButton.Draw();
+
+        GUILayout.EndScrollView();
+
+        GUI.enabled = true;
+    }
+
+    public override void UpdatePanes()
+    {
+        for (var i = 0; i < rebindButtons.Length; i++)
+            rebindButtons[i].KeyCode = InputManager.GetActionKey((MpsKey)i);
+    }
+
+    protected override void ReloadTranslation()
+    {
+        for (var i = 0; i < rebindButtons.Length; i++)
+            ActionLabels[i] = Translation.Get("controls", ActionTranslationKeys[i]);
+
+        for (var i = 0; i < HeaderTranslationKeys.Length; i++)
+            Headers[HeaderTranslationKeys[i]] = Translation.Get("settingsHeaders", HeaderTranslationKeys[i]);
+
+        reloadTranslationButton.Label = Translation.Get("settingsLabels", "reloadTranslation");
+        reloadAllPresetsButton.Label = Translation.Get("settingsLabels", "reloadAllPresets");
+    }
+
+    private void DrawSetting(MpsKey key)
+    {
+        var keyIndex = (int)key;
+
+        GUILayout.BeginHorizontal();
+        GUILayout.Label(ActionLabels[keyIndex]);
+        GUILayout.FlexibleSpace();
+        rebindButtons[keyIndex].Draw(GUILayout.Width(90f));
+
+        if (GUILayout.Button("×", GUILayout.ExpandWidth(false)))
         {
-            for (int i = 0; i < rebindButtons.Length; i++)
-            {
-                rebindButtons[i].KeyCode = InputManager.GetActionKey((MpsKey)i);
-            }
+            rebindButtons[keyIndex].KeyCode = KeyCode.None;
+            InputManager.Rebind(key, KeyCode.None);
         }
+
+        GUILayout.EndHorizontal();
     }
 }

+ 112 - 83
src/MeidoPhotoStudio.Plugin/GUI/Panes/OtherPanes/MaidSwitcherPane.cs

@@ -1,119 +1,148 @@
 using UnityEngine;
 
-namespace MeidoPhotoStudio.Plugin
+namespace MeidoPhotoStudio.Plugin;
+
+public class MaidSwitcherPane : BasePane
 {
-    public class MaidSwitcherPane : BasePane
+    private readonly MeidoManager meidoManager;
+    private readonly Button previousButton;
+    private readonly Button nextButton;
+    private readonly Toggle editToggle;
+
+    public MaidSwitcherPane(MeidoManager meidoManager)
     {
-        private readonly MeidoManager meidoManager;
-        private readonly Button previousButton;
-        private readonly Button nextButton;
-        private readonly Toggle editToggle;
+        this.meidoManager = meidoManager;
 
-        public MaidSwitcherPane(MeidoManager meidoManager)
-        {
-            this.meidoManager = meidoManager;
-            this.meidoManager.UpdateMeido += (s, a) => UpdatePane();
+        this.meidoManager.UpdateMeido += (_, _) =>
+            UpdatePane();
 
-            previousButton = new Button("<");
-            previousButton.ControlEvent += (s, a) => ChangeMaid(-1);
+        previousButton = new("<");
 
-            nextButton = new Button(">");
-            nextButton.ControlEvent += (s, a) => ChangeMaid(1);
+        previousButton.ControlEvent += (_, _) =>
+            ChangeMaid(-1);
 
-            editToggle = new Toggle("Edit", true);
-            editToggle.ControlEvent += (s, a) => SetEditMaid();
-        }
+        nextButton = new(">");
 
-        public override void Draw()
-        {
-            const float boxSize = 70;
-            const int margin = (int)(boxSize / 2.8f);
+        nextButton.ControlEvent += (_, _) =>
+            ChangeMaid(1);
+
+        editToggle = new("Edit", true);
+
+        editToggle.ControlEvent += (_, _) =>
+            SetEditMaid();
+    }
+
+    public override void Draw()
+    {
+        const float boxSize = 70;
+        const int margin = (int)(boxSize / 2.8f);
 
-            GUIStyle buttonStyle = new GUIStyle(GUI.skin.button);
-            buttonStyle.margin.top = margin;
+        var buttonStyle = new GUIStyle(GUI.skin.button);
 
-            GUIStyle labelStyle = new GUIStyle(GUI.skin.label);
-            labelStyle.margin.top = margin;
+        buttonStyle.margin.top = margin;
 
-            GUIStyle boxStyle = new GUIStyle(GUI.skin.box) { margin = new RectOffset(0, 0, 0, 0) };
-            GUIStyle horizontalStyle = new GUIStyle { padding = new RectOffset(4, 4, 0, 0) };
+        var labelStyle = new GUIStyle(GUI.skin.label);
 
-            GUILayoutOption[] buttonOptions = new[] { GUILayout.ExpandHeight(true), GUILayout.ExpandWidth(false) };
-            GUILayoutOption[] boxLayoutOptions = new[] { GUILayout.Height(boxSize), GUILayout.Width(boxSize) };
+        labelStyle.margin.top = margin;
 
-            GUI.enabled = meidoManager.HasActiveMeido;
-            Meido meido = meidoManager.ActiveMeido;
+        var boxStyle = new GUIStyle(GUI.skin.box)
+        {
+            margin = new RectOffset(0, 0, 0, 0),
+        };
 
-            GUILayout.BeginHorizontal(horizontalStyle, GUILayout.Height(boxSize));
+        var horizontalStyle = new GUIStyle
+        {
+            padding = new RectOffset(4, 4, 0, 0),
+        };
 
-            previousButton.Draw(buttonStyle, buttonOptions);
+        var buttonOptions = new[]
+        {
+            GUILayout.ExpandHeight(true), GUILayout.ExpandWidth(false),
+        };
 
-            GUILayout.Space(20);
+        var boxLayoutOptions = new[]
+        {
+            GUILayout.Height(boxSize), GUILayout.Width(boxSize),
+        };
 
-            if (meidoManager.HasActiveMeido && meido.Portrait) MpsGui.DrawTexture(meido.Portrait, boxLayoutOptions);
-            else GUILayout.Box(GUIContent.none, boxStyle, boxLayoutOptions);
+        GUI.enabled = meidoManager.HasActiveMeido;
 
-            string label = meidoManager.HasActiveMeido ? $"{meido.LastName}\n{meido.FirstName}" : string.Empty;
+        var meido = meidoManager.ActiveMeido;
 
-            GUILayout.Label(label, labelStyle, GUILayout.ExpandWidth(false));
+        GUILayout.BeginHorizontal(horizontalStyle, GUILayout.Height(boxSize));
 
-            GUILayout.FlexibleSpace();
+        previousButton.Draw(buttonStyle, buttonOptions);
 
-            nextButton.Draw(buttonStyle, buttonOptions);
+        GUILayout.Space(20);
 
-            GUILayout.EndHorizontal();
+        if (meidoManager.HasActiveMeido && meido.Portrait)
+            MpsGui.DrawTexture(meido.Portrait, boxLayoutOptions);
+        else
+            GUILayout.Box(GUIContent.none, boxStyle, boxLayoutOptions);
 
-            Rect previousRect = GUILayoutUtility.GetLastRect();
+        var label = meidoManager.HasActiveMeido ? $"{meido.LastName}\n{meido.FirstName}" : string.Empty;
 
-            if (MeidoPhotoStudio.EditMode) editToggle.Draw(new Rect(previousRect.x + 4f, previousRect.y, 40f, 20f));
+        GUILayout.Label(label, labelStyle, GUILayout.ExpandWidth(false));
 
-            Rect labelRect = new Rect(previousRect.width - 45f, previousRect.y, 40f, 20f);
-            GUIStyle slotStyle = new GUIStyle()
-            {
-                alignment = TextAnchor.UpperRight,
-                fontSize = 13
-            };
-            slotStyle.padding.right = 5;
-            slotStyle.normal.textColor = Color.white;
+        GUILayout.FlexibleSpace();
 
-            if (meidoManager.HasActiveMeido) GUI.Label(labelRect, $"{meidoManager.ActiveMeido.Slot + 1}", slotStyle);
-        }
+        nextButton.Draw(buttonStyle, buttonOptions);
 
-        public override void UpdatePane()
-        {
-            if (meidoManager.HasActiveMeido)
-            {
-                this.updating = true;
-                editToggle.Value = meidoManager.ActiveMeido.IsEditMaid;
-                this.updating = false;
-            }
-        }
+        GUILayout.EndHorizontal();
+
+        var previousRect = GUILayoutUtility.GetLastRect();
 
-        private void ChangeMaid(int dir)
+        if (MeidoPhotoStudio.EditMode)
+            editToggle.Draw(new Rect(previousRect.x + 4f, previousRect.y, 40f, 20f));
+
+        var labelRect = new Rect(previousRect.width - 45f, previousRect.y, 40f, 20f);
+
+        var slotStyle = new GUIStyle()
         {
-            int selected = Utility.Wrap(
-                meidoManager.SelectedMeido + (int)Mathf.Sign(dir), 0, meidoManager.ActiveMeidoList.Count
-            );
+            alignment = TextAnchor.UpperRight,
+            fontSize = 13,
+        };
 
-            meidoManager.ChangeMaid(selected);
-        }
+        slotStyle.padding.right = 5;
+        slotStyle.normal.textColor = Color.white;
+
+        if (meidoManager.HasActiveMeido)
+            GUI.Label(labelRect, $"{meidoManager.ActiveMeido.Slot + 1}", slotStyle);
+    }
+
+    public override void UpdatePane()
+    {
+        if (!meidoManager.HasActiveMeido)
+            return;
+
+        updating = true;
+        editToggle.Value = meidoManager.ActiveMeido.IsEditMaid;
+        updating = false;
+    }
 
-        private void SetEditMaid()
+    private void ChangeMaid(int dir)
+    {
+        var selected =
+            Utility.Wrap(meidoManager.SelectedMeido + (int)Mathf.Sign(dir), 0, meidoManager.ActiveMeidoList.Count);
+
+        meidoManager.ChangeMaid(selected);
+    }
+
+    private void SetEditMaid()
+    {
+        if (updating)
+            return;
+
+        if (!editToggle.Value)
         {
-            if (updating) return;
-
-            if (!editToggle.Value)
-            {
-                updating = true;
-                editToggle.Value = true;
-                updating = false;
-                return;
-            }
-
-            if (meidoManager.HasActiveMeido)
-            {
-                meidoManager.SetEditMaid(meidoManager.ActiveMeido);
-            }
+            updating = true;
+            editToggle.Value = true;
+            updating = false;
+
+            return;
         }
+
+        if (meidoManager.HasActiveMeido)
+            meidoManager.SetEditMaid(meidoManager.ActiveMeido);
     }
 }

+ 47 - 38
src/MeidoPhotoStudio.Plugin/GUI/Panes/OtherPanes/TabsPane.cs

@@ -1,45 +1,54 @@
 using System;
+
 using UnityEngine;
 
-namespace MeidoPhotoStudio.Plugin
+namespace MeidoPhotoStudio.Plugin;
+
+public class TabsPane : BasePane
 {
-    public class TabsPane : BasePane
+    private static readonly string[] TabNames = { "call", "pose", "face", "bg", "bg2" };
+
+    private readonly SelectionGrid tabs;
+
+    private Constants.Window selectedTab;
+
+    public TabsPane()
+    {
+        Translation.ReloadTranslationEvent += (_, _) =>
+            ReloadTranslation();
+
+        tabs = new(Translation.GetArray("tabs", TabNames));
+        tabs.ControlEvent += (_, _) =>
+            OnChangeTab();
+    }
+
+    public event EventHandler TabChange;
+
+    public Constants.Window SelectedTab
     {
-        private static readonly string[] tabNames = { "call", "pose", "face", "bg", "bg2" };
-        private readonly SelectionGrid Tabs;
-        private Constants.Window selectedTab;
-        public Constants.Window SelectedTab
-        {
-            get => selectedTab;
-            set => Tabs.SelectedItemIndex = (int)value;
-        }
-        public event EventHandler TabChange;
-
-        public TabsPane()
-        {
-            Translation.ReloadTranslationEvent += (s, a) => ReloadTranslation();
-            Tabs = new SelectionGrid(Translation.GetArray("tabs", tabNames));
-            Tabs.ControlEvent += (s, a) => OnChangeTab();
-        }
-
-        protected override void ReloadTranslation()
-        {
-            updating = true;
-            Tabs.SetItems(Translation.GetArray("tabs", tabNames), Tabs.SelectedItemIndex);
-            updating = false;
-        }
-
-        private void OnChangeTab()
-        {
-            if (updating) return;
-            selectedTab = (Constants.Window)Tabs.SelectedItemIndex;
-            TabChange?.Invoke(null, EventArgs.Empty);
-        }
-
-        public override void Draw()
-        {
-            Tabs.Draw(GUILayout.ExpandWidth(false));
-            MpsGui.BlackLine();
-        }
+        get => selectedTab;
+        set => tabs.SelectedItemIndex = (int)value;
+    }
+
+    public override void Draw()
+    {
+        tabs.Draw(GUILayout.ExpandWidth(false));
+        MpsGui.BlackLine();
+    }
+
+    protected override void ReloadTranslation()
+    {
+        updating = true;
+        tabs.SetItems(Translation.GetArray("tabs", TabNames), tabs.SelectedItemIndex);
+        updating = false;
+    }
+
+    private void OnChangeTab()
+    {
+        if (updating)
+            return;
+
+        selectedTab = (Constants.Window)tabs.SelectedItemIndex;
+        TabChange?.Invoke(null, EventArgs.Empty);
     }
 }

+ 65 - 56
src/MeidoPhotoStudio.Plugin/GUI/Panes/PoseWindowPanes/CopyPosePane.cs

@@ -1,80 +1,89 @@
 using System.Linq;
-using System.Collections.Generic;
+
 using UnityEngine;
 
-namespace MeidoPhotoStudio.Plugin
+namespace MeidoPhotoStudio.Plugin;
+
+public class CopyPosePane : BasePane
 {
-    public class CopyPosePane : BasePane
+    private readonly MeidoManager meidoManager;
+    private readonly Button copyButton;
+    private readonly Dropdown meidoDropdown;
+
+    private int[] copyMeidoSlot;
+    private string copyIKHeader;
+
+    public CopyPosePane(MeidoManager meidoManager)
     {
-        private readonly MeidoManager meidoManager;
-        private readonly Button copyButton;
-        private readonly Dropdown meidoDropdown;
-        private int[] copyMeidoSlot;
-        private bool PlentyOfMaids => meidoManager.ActiveMeidoList.Count >= 2;
-        private Meido FromMeido => meidoManager.HasActiveMeido
-            ? meidoManager.ActiveMeidoList[copyMeidoSlot[meidoDropdown.SelectedItemIndex]]
-            : null;
-        private string copyIKHeader;
+        this.meidoManager = meidoManager;
 
-        public CopyPosePane(MeidoManager meidoManager)
-        {
-            this.meidoManager = meidoManager;
+        meidoDropdown = new(new[] { Translation.Get("systemMessage", "noMaids") });
 
-            meidoDropdown = new Dropdown(new[] { Translation.Get("systemMessage", "noMaids") });
+        copyButton = new(Translation.Get("copyPosePane", "copyButton"));
+        copyButton.ControlEvent += (_, _) =>
+            CopyPose();
 
-            copyButton = new Button(Translation.Get("copyPosePane", "copyButton"));
-            copyButton.ControlEvent += (s, a) => CopyPose();
+        copyIKHeader = Translation.Get("copyPosePane", "header");
+    }
 
-            copyIKHeader = Translation.Get("copyPosePane", "header");
-        }
+    private bool PlentyOfMaids =>
+        meidoManager.ActiveMeidoList.Count >= 2;
 
-        protected override void ReloadTranslation()
-        {
-            if (!PlentyOfMaids)
-            {
-                meidoDropdown.SetDropdownItem(0, Translation.Get("systemMessage", "noMaids"));
-            }
-            copyButton.Label = Translation.Get("copyPosePane", "copyButton");
-            copyIKHeader = Translation.Get("copyPosePane", "header");
-        }
+    private Meido FromMeido =>
+        meidoManager.HasActiveMeido
+            ? meidoManager.ActiveMeidoList[copyMeidoSlot[meidoDropdown.SelectedItemIndex]]
+            : null;
 
-        public override void Draw()
-        {
-            GUI.enabled = PlentyOfMaids;
+    public override void Draw()
+    {
+        GUI.enabled = PlentyOfMaids;
 
-            MpsGui.Header(copyIKHeader);
-            MpsGui.WhiteLine();
+        MpsGui.Header(copyIKHeader);
+        MpsGui.WhiteLine();
 
-            GUILayout.BeginHorizontal();
-            meidoDropdown.Draw(GUILayout.Width(160f));
-            copyButton.Draw(GUILayout.ExpandWidth(false));
-            GUILayout.EndHorizontal();
+        GUILayout.BeginHorizontal();
+        meidoDropdown.Draw(GUILayout.Width(160f));
+        copyButton.Draw(GUILayout.ExpandWidth(false));
+        GUILayout.EndHorizontal();
 
-            GUI.enabled = true;
-        }
+        GUI.enabled = true;
+    }
 
-        public override void UpdatePane() => SetMeidoDropdown();
+    public override void UpdatePane() =>
+        SetMeidoDropdown();
 
-        private void CopyPose()
-        {
-            if (meidoManager.ActiveMeidoList.Count >= 2) meidoManager.ActiveMeido.CopyPose(FromMeido);
-        }
+    protected override void ReloadTranslation()
+    {
+        if (!PlentyOfMaids)
+            meidoDropdown.SetDropdownItem(0, Translation.Get("systemMessage", "noMaids"));
+
+        copyButton.Label = Translation.Get("copyPosePane", "copyButton");
+        copyIKHeader = Translation.Get("copyPosePane", "header");
+    }
+
+    private void CopyPose()
+    {
+        if (meidoManager.ActiveMeidoList.Count >= 2)
+            meidoManager.ActiveMeido.CopyPose(FromMeido);
+    }
 
-        private void SetMeidoDropdown()
+    private void SetMeidoDropdown()
+    {
+        if (meidoManager.ActiveMeidoList.Count >= 2)
         {
-            if (meidoManager.ActiveMeidoList.Count >= 2)
-            {
-                IEnumerable<Meido> copyMeidoList = meidoManager.ActiveMeidoList
-                    .Where(meido => meido.Slot != meidoManager.ActiveMeido.Slot);
+            var copyMeidoList = meidoManager.ActiveMeidoList
+                .Where(meido => meido.Slot != meidoManager.ActiveMeido.Slot);
 
-                copyMeidoSlot = copyMeidoList.Select(meido => meido.Slot).ToArray();
+            copyMeidoSlot = copyMeidoList.Select(meido => meido.Slot).ToArray();
 
-                string[] dropdownList = copyMeidoList
-                    .Select((meido, i) => $"{copyMeidoSlot[i] + 1}: {meido.LastName} {meido.FirstName}").ToArray();
+            var dropdownList = copyMeidoList
+                .Select((meido, i) => $"{copyMeidoSlot[i] + 1}: {meido.LastName} {meido.FirstName}").ToArray();
 
-                meidoDropdown.SetDropdownItems(dropdownList, 0);
-            }
-            else meidoDropdown.SetDropdownItems(new[] { Translation.Get("systemMessage", "noMaids") });
+            meidoDropdown.SetDropdownItems(dropdownList, 0);
+        }
+        else
+        {
+            meidoDropdown.SetDropdownItems(new[] { Translation.Get("systemMessage", "noMaids") });
         }
     }
 }

+ 78 - 66
src/MeidoPhotoStudio.Plugin/GUI/Panes/PoseWindowPanes/GravityControlPane.cs

@@ -1,97 +1,109 @@
 using UnityEngine;
 
-namespace MeidoPhotoStudio.Plugin
+namespace MeidoPhotoStudio.Plugin;
+
+public class GravityControlPane : BasePane
 {
-    public class GravityControlPane : BasePane
+    private readonly MeidoManager meidoManager;
+    private readonly Toggle hairToggle;
+    private readonly Toggle skirtToggle;
+    private readonly Toggle globalToggle;
+
+    private string header;
+
+    public GravityControlPane(MeidoManager meidoManager)
     {
-        private readonly MeidoManager meidoManager;
-        private readonly Toggle hairToggle;
-        private readonly Toggle skirtToggle;
-        private readonly Toggle globalToggle;
-        private string header;
+        this.meidoManager = meidoManager;
 
-        public GravityControlPane(MeidoManager meidoManager)
-        {
-            this.meidoManager = meidoManager;
+        hairToggle = new(Translation.Get("gravityControlPane", "hairToggle"));
+        hairToggle.ControlEvent += (_, _) =>
+            ToggleGravity(hairToggle.Value, skirt: false);
 
-            hairToggle = new Toggle(Translation.Get("gravityControlPane", "hairToggle"));
-            hairToggle.ControlEvent += (s, a) => ToggleGravity(hairToggle.Value, skirt: false);
+        skirtToggle = new(Translation.Get("gravityControlPane", "skirtToggle"));
+        skirtToggle.ControlEvent += (_, _) =>
+            ToggleGravity(skirtToggle.Value, skirt: true);
 
-            skirtToggle = new Toggle(Translation.Get("gravityControlPane", "skirtToggle"));
-            skirtToggle.ControlEvent += (s, a) => ToggleGravity(skirtToggle.Value, skirt: true);
+        globalToggle = new(Translation.Get("gravityControlPane", "globalToggle"));
+        globalToggle.ControlEvent += (_, _) =>
+            SetGlobalGravity(globalToggle.Value);
 
-            globalToggle = new Toggle(Translation.Get("gravityControlPane", "globalToggle"));
-            globalToggle.ControlEvent += (s, a) => SetGlobalGravity(globalToggle.Value);
+        header = Translation.Get("gravityControlPane", "gravityHeader");
+    }
 
-            header = Translation.Get("gravityControlPane", "gravityHeader");
-        }
+    public override void Draw()
+    {
+        var enabled = meidoManager.HasActiveMeido;
 
-        protected override void ReloadTranslation()
-        {
-            hairToggle.Label = Translation.Get("gravityControlPane", "hairToggle");
-            skirtToggle.Label = Translation.Get("gravityControlPane", "skirtToggle");
-            globalToggle.Label = Translation.Get("gravityControlPane", "globalToggle");
-            header = Translation.Get("gravityControlPane", "gravityHeader");
-        }
+        GUI.enabled = enabled;
 
-        public override void Draw()
-        {
-            bool enabled = meidoManager.HasActiveMeido;
-            GUI.enabled = enabled;
+        MpsGui.Header(header);
+        MpsGui.WhiteLine();
 
-            MpsGui.Header(header);
-            MpsGui.WhiteLine();
+        var meido = meidoManager.ActiveMeido;
 
-            Meido meido = meidoManager.ActiveMeido;
-            GUILayout.BeginHorizontal();
+        GUILayout.BeginHorizontal();
 
-            GUI.enabled = enabled && meido.HairGravityControl.Valid;
-            hairToggle.Draw();
+        GUI.enabled = enabled && meido.HairGravityControl.Valid;
+        hairToggle.Draw();
 
-            GUI.enabled = enabled && meido.SkirtGravityControl.Valid;
-            skirtToggle.Draw();
+        GUI.enabled = enabled && meido.SkirtGravityControl.Valid;
+        skirtToggle.Draw();
 
-            GUILayout.EndHorizontal();
+        GUILayout.EndHorizontal();
 
-            GUI.enabled = enabled;
-            globalToggle.Draw();
+        GUI.enabled = enabled;
+        globalToggle.Draw();
 
-            GUI.enabled = true;
-        }
+        GUI.enabled = true;
+    }
 
-        public override void UpdatePane()
-        {
-            if (!meidoManager.HasActiveMeido) return;
+    public override void UpdatePane()
+    {
+        if (!meidoManager.HasActiveMeido)
+            return;
 
-            Meido meido = meidoManager.ActiveMeido;
+        var meido = meidoManager.ActiveMeido;
 
-            updating = true;
+        updating = true;
 
-            hairToggle.Value = meido.HairGravityActive;
-            skirtToggle.Value = meido.SkirtGravityActive;
+        hairToggle.Value = meido.HairGravityActive;
+        skirtToggle.Value = meido.SkirtGravityActive;
 
-            updating = false;
-        }
+        updating = false;
+    }
 
-        private void ToggleGravity(bool value, bool skirt = false)
-        {
-            if (updating) return;
+    protected override void ReloadTranslation()
+    {
+        hairToggle.Label = Translation.Get("gravityControlPane", "hairToggle");
+        skirtToggle.Label = Translation.Get("gravityControlPane", "skirtToggle");
+        globalToggle.Label = Translation.Get("gravityControlPane", "globalToggle");
+        header = Translation.Get("gravityControlPane", "gravityHeader");
+    }
+
+    private void ToggleGravity(bool value, bool skirt = false)
+    {
+        if (updating)
+            return;
 
-            if (meidoManager.GlobalGravity)
+        if (meidoManager.GlobalGravity)
+        {
+            foreach (var meido in meidoManager.ActiveMeidoList)
             {
-                foreach (Meido meido in meidoManager.ActiveMeidoList)
-                {
-                    if (skirt) meido.SkirtGravityActive = value;
-                    else meido.HairGravityActive = value;
-                }
+                if (skirt)
+                    meido.SkirtGravityActive = value;
+                else
+                    meido.HairGravityActive = value;
             }
+        }
+        else
+        {
+            if (skirt)
+                meidoManager.ActiveMeido.SkirtGravityActive = value;
             else
-            {
-                if (skirt) meidoManager.ActiveMeido.SkirtGravityActive = value;
-                else meidoManager.ActiveMeido.HairGravityActive = value;
-            }
+                meidoManager.ActiveMeido.HairGravityActive = value;
         }
-
-        private void SetGlobalGravity(bool value) => meidoManager.GlobalGravity = value;
     }
+
+    private void SetGlobalGravity(bool value) =>
+        meidoManager.GlobalGravity = value;
 }

+ 119 - 99
src/MeidoPhotoStudio.Plugin/GUI/Panes/PoseWindowPanes/HandPresetPane.cs

@@ -1,134 +1,154 @@
-using System.IO;
 using System.Collections.Generic;
+using System.IO;
 using System.Linq;
+
 using UnityEngine;
 
-namespace MeidoPhotoStudio.Plugin
+namespace MeidoPhotoStudio.Plugin;
+
+public class HandPresetPane : BasePane
 {
-    public class HandPresetPane : BasePane
+    private readonly MeidoManager meidoManager;
+    private readonly Dropdown presetCategoryDropdown;
+    private readonly Button nextCategoryButton;
+    private readonly Button previousCategoryButton;
+    private readonly Dropdown presetDropdown;
+    private readonly Button nextPresetButton;
+    private readonly Button previousPresetButton;
+    private readonly Button leftHandButton;
+    private readonly Button rightHandButton;
+
+    private string previousCategory;
+    private bool presetListEnabled = true;
+
+    public HandPresetPane(MeidoManager meidoManager)
     {
-        private readonly MeidoManager meidoManager;
-        private readonly Dropdown presetCategoryDropdown;
-        private readonly Button nextCategoryButton;
-        private readonly Button previousCategoryButton;
-        private readonly Dropdown presetDropdown;
-        private readonly Button nextPresetButton;
-        private readonly Button previousPresetButton;
-        private readonly Button leftHandButton;
-        private readonly Button rightHandButton;
-        private string SelectedCategory => Constants.CustomHandGroupList[presetCategoryDropdown.SelectedItemIndex];
-        private List<string> CurrentPresetList => Constants.CustomHandDict[SelectedCategory];
-        private string CurrentPreset => CurrentPresetList[presetDropdown.SelectedItemIndex];
-        private string previousCategory;
-        private bool presetListEnabled = true;
-
-        public HandPresetPane(MeidoManager meidoManager)
-        {
-            Constants.CustomHandChange += OnPresetChange;
-            this.meidoManager = meidoManager;
+        Constants.CustomHandChange += OnPresetChange;
 
-            presetCategoryDropdown = new Dropdown(Constants.CustomHandGroupList.ToArray());
-            presetCategoryDropdown.SelectionChange += (s, a) => ChangePresetCategory();
+        this.meidoManager = meidoManager;
 
-            nextCategoryButton = new Button(">");
-            nextCategoryButton.ControlEvent += (s, a) => presetCategoryDropdown.Step(1);
+        presetCategoryDropdown = new(Constants.CustomHandGroupList.ToArray());
+        presetCategoryDropdown.SelectionChange += (_, _) =>
+            ChangePresetCategory();
 
-            previousCategoryButton = new Button("<");
-            previousCategoryButton.ControlEvent += (s, a) => presetCategoryDropdown.Step(-1);
+        nextCategoryButton = new(">");
+        nextCategoryButton.ControlEvent += (_, _) =>
+            presetCategoryDropdown.Step(1);
 
-            presetDropdown = new Dropdown(UIPresetList());
+        previousCategoryButton = new("<");
+        previousCategoryButton.ControlEvent += (_, _) =>
+            presetCategoryDropdown.Step(-1);
 
-            nextPresetButton = new Button(">");
-            nextPresetButton.ControlEvent += (s, a) => presetDropdown.Step(1);
+        presetDropdown = new(UIPresetList());
 
-            previousPresetButton = new Button("<");
-            previousPresetButton.ControlEvent += (s, a) => presetDropdown.Step(-1);
+        nextPresetButton = new(">");
+        nextPresetButton.ControlEvent += (_, _) =>
+            presetDropdown.Step(1);
 
-            leftHandButton = new Button(Translation.Get("handPane", "leftHand"));
-            leftHandButton.ControlEvent += (s, a) => SetHandPreset(right: false);
+        previousPresetButton = new("<");
+        previousPresetButton.ControlEvent += (_, _) =>
+            presetDropdown.Step(-1);
 
-            rightHandButton = new Button(Translation.Get("handPane", "rightHand"));
-            rightHandButton.ControlEvent += (s, a) => SetHandPreset(right: true);
+        leftHandButton = new(Translation.Get("handPane", "leftHand"));
+        leftHandButton.ControlEvent += (_, _) =>
+            SetHandPreset(right: false);
 
-            previousCategory = SelectedCategory;
-            presetListEnabled = CurrentPresetList.Count > 0;
-        }
+        rightHandButton = new(Translation.Get("handPane", "rightHand"));
+        rightHandButton.ControlEvent += (_, _) =>
+            SetHandPreset(right: true);
 
-        protected override void ReloadTranslation()
-        {
-            leftHandButton.Label = Translation.Get("handPane", "leftHand");
-            rightHandButton.Label = Translation.Get("handPane", "rightHand");
-            if (CurrentPresetList.Count == 0) presetDropdown.SetDropdownItems(UIPresetList());
-        }
+        previousCategory = SelectedCategory;
+        presetListEnabled = CurrentPresetList.Count > 0;
+    }
 
-        public override void Draw()
-        {
-            GUILayoutOption dropdownWidth = GUILayout.Width(156f);
-            GUILayoutOption noExpandWidth = GUILayout.ExpandWidth(false);
+    private string SelectedCategory =>
+        Constants.CustomHandGroupList[presetCategoryDropdown.SelectedItemIndex];
 
-            GUI.enabled = meidoManager.HasActiveMeido;
+    private List<string> CurrentPresetList =>
+        Constants.CustomHandDict[SelectedCategory];
 
-            GUILayout.BeginHorizontal();
-            presetCategoryDropdown.Draw(dropdownWidth);
-            previousCategoryButton.Draw(noExpandWidth);
-            nextCategoryButton.Draw(noExpandWidth);
-            GUILayout.EndHorizontal();
+    private string CurrentPreset =>
+        CurrentPresetList[presetDropdown.SelectedItemIndex];
 
-            GUI.enabled = GUI.enabled && presetListEnabled;
+    public override void Draw()
+    {
+        var dropdownWidth = GUILayout.Width(156f);
+        var noExpandWidth = GUILayout.ExpandWidth(false);
 
-            GUILayout.BeginHorizontal();
-            presetDropdown.Draw(dropdownWidth);
-            previousPresetButton.Draw(noExpandWidth);
-            nextPresetButton.Draw(noExpandWidth);
-            GUILayout.EndHorizontal();
+        GUI.enabled = meidoManager.HasActiveMeido;
 
-            GUILayout.BeginHorizontal();
-            rightHandButton.Draw();
-            leftHandButton.Draw();
-            GUILayout.EndHorizontal();
+        GUILayout.BeginHorizontal();
+        presetCategoryDropdown.Draw(dropdownWidth);
+        previousCategoryButton.Draw(noExpandWidth);
+        nextCategoryButton.Draw(noExpandWidth);
+        GUILayout.EndHorizontal();
 
-            GUI.enabled = true;
-        }
+        GUI.enabled = GUI.enabled && presetListEnabled;
+
+        GUILayout.BeginHorizontal();
+        presetDropdown.Draw(dropdownWidth);
+        previousPresetButton.Draw(noExpandWidth);
+        nextPresetButton.Draw(noExpandWidth);
+        GUILayout.EndHorizontal();
 
-        private void ChangePresetCategory()
+        GUILayout.BeginHorizontal();
+        rightHandButton.Draw();
+        leftHandButton.Draw();
+        GUILayout.EndHorizontal();
+
+        GUI.enabled = true;
+    }
+
+    protected override void ReloadTranslation()
+    {
+        leftHandButton.Label = Translation.Get("handPane", "leftHand");
+        rightHandButton.Label = Translation.Get("handPane", "rightHand");
+
+        if (CurrentPresetList.Count is 0)
+            presetDropdown.SetDropdownItems(UIPresetList());
+    }
+
+    private void ChangePresetCategory()
+    {
+        presetListEnabled = CurrentPresetList.Count > 0;
+
+        if (previousCategory == SelectedCategory)
         {
-            presetListEnabled = CurrentPresetList.Count > 0;
-            if (previousCategory == SelectedCategory) presetDropdown.SelectedItemIndex = 0;
-            else
-            {
-                previousCategory = SelectedCategory;
-                presetDropdown.SetDropdownItems(UIPresetList(), 0);
-            }
+            presetDropdown.SelectedItemIndex = 0;
         }
-
-        private void SetHandPreset(bool right = false)
+        else
         {
-            if (!meidoManager.HasActiveMeido) return;
-
-            meidoManager.ActiveMeido.SetHandPreset(CurrentPreset, right);
+            previousCategory = SelectedCategory;
+            presetDropdown.SetDropdownItems(UIPresetList(), 0);
         }
+    }
 
-        private void OnPresetChange(object sender, PresetChangeEventArgs args)
+    private void SetHandPreset(bool right = false)
+    {
+        if (!meidoManager.HasActiveMeido)
+            return;
+
+        meidoManager.ActiveMeido.SetHandPreset(CurrentPreset, right);
+    }
+
+    private void OnPresetChange(object sender, PresetChangeEventArgs args)
+    {
+        if (args == PresetChangeEventArgs.Empty)
         {
-            if (args == PresetChangeEventArgs.Empty)
-            {
-                presetCategoryDropdown.SetDropdownItems(Constants.CustomHandGroupList.ToArray(), 0);
-                presetDropdown.SetDropdownItems(UIPresetList(), 0);
-            }
-            else
-            {
-                presetCategoryDropdown.SetDropdownItems(
-                    Constants.CustomHandGroupList.ToArray(), Constants.CustomHandGroupList.IndexOf(args.Category)
-                );
-                presetDropdown.SetDropdownItems(UIPresetList(), CurrentPresetList.IndexOf(args.Path));
-            }
+            presetCategoryDropdown.SetDropdownItems(Constants.CustomHandGroupList.ToArray(), 0);
+            presetDropdown.SetDropdownItems(UIPresetList(), 0);
         }
-
-        private string[] UIPresetList()
+        else
         {
-            return CurrentPresetList.Count == 0
-                ? new[] { Translation.Get("handPane", "noPresetsMessage") }
-                : CurrentPresetList.Select(Path.GetFileNameWithoutExtension).ToArray();
+            presetCategoryDropdown.SetDropdownItems(
+                Constants.CustomHandGroupList.ToArray(), Constants.CustomHandGroupList.IndexOf(args.Category));
+
+            presetDropdown.SetDropdownItems(UIPresetList(), CurrentPresetList.IndexOf(args.Path));
         }
     }
+
+    private string[] UIPresetList() =>
+        CurrentPresetList.Count is 0
+            ? new[] { Translation.Get("handPane", "noPresetsMessage") }
+            : CurrentPresetList.Select(Path.GetFileNameWithoutExtension).ToArray();
 }

+ 264 - 217
src/MeidoPhotoStudio.Plugin/GUI/Panes/PoseWindowPanes/MaidDressingPane.cs

@@ -1,289 +1,336 @@
-using System;
 using System.Collections.Generic;
+
 using UnityEngine;
+
+using static MeidoPhotoStudio.Plugin.Meido;
 using static TBody;
 
-namespace MeidoPhotoStudio.Plugin
+namespace MeidoPhotoStudio.Plugin;
+
+public class MaidDressingPane : BasePane
 {
-    using static Meido;
-    public class MaidDressingPane : BasePane
+    public static readonly SlotID[] ClothingSlots =
     {
-        public static readonly SlotID[] ClothingSlots =
-        {
-            // main slots
-            SlotID.wear, SlotID.skirt, SlotID.bra, SlotID.panz, SlotID.headset, SlotID.megane, SlotID.accUde,
-            SlotID.glove, SlotID.accSenaka, SlotID.stkg, SlotID.shoes, SlotID.body,
-            // detailed slots
-            SlotID.accAshi, SlotID.accHana, SlotID.accHat, SlotID.accHeso, SlotID.accKamiSubL, SlotID.accKamiSubR,
-            SlotID.accKami_1_, SlotID.accKami_2_, SlotID.accKami_3_, SlotID.accKubi, SlotID.accKubiwa, SlotID.accMiMiL,
-            SlotID.accMiMiR, SlotID.accNipL, SlotID.accNipR, SlotID.accShippo, SlotID.accXXX
-            // unused slots
-            // SlotID.mizugi, SlotID.onepiece, SlotID.accHead,
-        };
-
-        public static readonly SlotID[] BodySlots =
-        {
-            SlotID.body, SlotID.head, SlotID.eye, SlotID.hairF, SlotID.hairR, SlotID.hairS, SlotID.hairT,
-            SlotID.hairAho, SlotID.chikubi, SlotID.underhair, SlotID.moza, SlotID.accHa
-        };
+        // main slots
+        SlotID.wear, SlotID.skirt, SlotID.bra, SlotID.panz, SlotID.headset, SlotID.megane, SlotID.accUde,
+        SlotID.glove, SlotID.accSenaka, SlotID.stkg, SlotID.shoes, SlotID.body,
 
-        public static readonly SlotID[] WearSlots = { SlotID.wear, SlotID.mizugi, SlotID.onepiece };
+        // detailed slots
+        SlotID.accAshi, SlotID.accHana, SlotID.accHat, SlotID.accHeso, SlotID.accKamiSubL, SlotID.accKamiSubR,
+        SlotID.accKami_1_, SlotID.accKami_2_, SlotID.accKami_3_, SlotID.accKubi, SlotID.accKubiwa, SlotID.accMiMiL,
+        SlotID.accMiMiR, SlotID.accNipL, SlotID.accNipR, SlotID.accShippo, SlotID.accXXX,
 
-        public static readonly SlotID[] HeadwearSlots =
-        {
-            SlotID.headset, SlotID.accHat, SlotID.accKamiSubL, SlotID.accKamiSubR, SlotID.accKami_1_,
-            SlotID.accKami_2_, SlotID.accKami_3_
-        };
-
-        private readonly MeidoManager meidoManager;
-        private readonly Dictionary<SlotID, Toggle> clothingToggles;
-        private readonly Dictionary<SlotID, bool> loadedSlots;
-        private readonly Toggle detailedClothingToggle;
-        private readonly SelectionGrid maskModeGrid;
-        private readonly Toggle curlingFrontToggle;
-        private readonly Toggle curlingBackToggle;
-        private readonly Toggle pantsuShiftToggle;
-        private bool detailedClothing;
-        private static readonly string[] maskLabels = { "all", "underwear", "nude" };
-
-        public MaidDressingPane(MeidoManager meidoManager)
-        {
-            this.meidoManager = meidoManager;
+        // unused slots
+        // SlotID.mizugi, SlotID.onepiece, SlotID.accHead,
+    };
 
-            clothingToggles = new Dictionary<SlotID, Toggle>(ClothingSlots.Length);
-            loadedSlots = new Dictionary<SlotID, bool>(ClothingSlots.Length);
-            foreach (SlotID slot in ClothingSlots)
-            {
-                var slotToggle = new Toggle(Translation.Get("clothing", slot.ToString()));
-                slotToggle.ControlEvent += (s, a) => ToggleClothing(slot, slotToggle.Value);
-                clothingToggles.Add(slot, slotToggle);
-                loadedSlots[slot] = true;
-            }
+    public static readonly SlotID[] BodySlots =
+    {
+        SlotID.body, SlotID.head, SlotID.eye, SlotID.hairF, SlotID.hairR, SlotID.hairS, SlotID.hairT, SlotID.hairAho,
+        SlotID.chikubi, SlotID.underhair, SlotID.moza, SlotID.accHa,
+    };
 
-            detailedClothingToggle = new Toggle(Translation.Get("clothing", "detail"));
-            detailedClothingToggle.ControlEvent += (s, a) => UpdateDetailedClothing();
+    public static readonly SlotID[] WearSlots = { SlotID.wear, SlotID.mizugi, SlotID.onepiece };
 
-            curlingFrontToggle = new Toggle(Translation.Get("clothing", "curlingFront"));
-            curlingFrontToggle.ControlEvent += (s, a) => ToggleCurling(Curl.Front, curlingFrontToggle.Value);
-            curlingBackToggle = new Toggle(Translation.Get("clothing", "curlingBack"));
-            curlingBackToggle.ControlEvent += (s, a) => ToggleCurling(Curl.Back, curlingBackToggle.Value);
-            pantsuShiftToggle = new Toggle(Translation.Get("clothing", "shiftPanties"));
-            pantsuShiftToggle.ControlEvent += (s, a) => ToggleCurling(Curl.Shift, pantsuShiftToggle.Value);
+    public static readonly SlotID[] HeadwearSlots =
+    {
+        SlotID.headset, SlotID.accHat, SlotID.accKamiSubL, SlotID.accKamiSubR, SlotID.accKami_1_, SlotID.accKami_2_,
+        SlotID.accKami_3_,
+    };
 
-            maskModeGrid = new SelectionGrid(Translation.GetArray("clothing", maskLabels));
-            maskModeGrid.ControlEvent += (s, a) => SetMaskMode((Mask)maskModeGrid.SelectedItemIndex);
+    private static readonly string[] MaskLabels = { "all", "underwear", "nude" };
 
-            UpdateDetailedClothing();
-        }
+    private readonly MeidoManager meidoManager;
+    private readonly Dictionary<SlotID, Toggle> clothingToggles;
+    private readonly Dictionary<SlotID, bool> loadedSlots;
+    private readonly Toggle detailedClothingToggle;
+    private readonly SelectionGrid maskModeGrid;
+    private readonly Toggle curlingFrontToggle;
+    private readonly Toggle curlingBackToggle;
+    private readonly Toggle pantsuShiftToggle;
+
+    private bool detailedClothing;
+
+    public MaidDressingPane(MeidoManager meidoManager)
+    {
+        this.meidoManager = meidoManager;
+
+        clothingToggles = new(ClothingSlots.Length);
+        loadedSlots = new(ClothingSlots.Length);
 
-        protected override void ReloadTranslation()
+        foreach (var slot in ClothingSlots)
         {
-            foreach (SlotID slot in ClothingSlots)
-            {
-                Toggle clothingToggle = clothingToggles[slot];
-                if (slot == SlotID.headset)
-                {
-                    clothingToggle.Label = detailedClothing
-                        ? Translation.Get("clothing", "headset")
-                        : Translation.Get("clothing", "headwear");
-                }
-                else clothingToggle.Label = Translation.Get("clothing", slot.ToString());
-            }
+            var slotToggle = new Toggle(Translation.Get("clothing", slot.ToString()));
 
-            updating = true;
-            maskModeGrid.SetItems(Translation.GetArray("clothing", maskLabels));
-            updating = false;
+            slotToggle.ControlEvent += (_, _) =>
+                ToggleClothing(slot, slotToggle.Value);
 
-            detailedClothingToggle.Label = Translation.Get("clothing", "detail");
-            curlingFrontToggle.Label = Translation.Get("clothing", "curlingFront");
-            curlingBackToggle.Label = Translation.Get("clothing", "curlingBack");
-            pantsuShiftToggle.Label = Translation.Get("clothing", "shiftPanties");
+            clothingToggles.Add(slot, slotToggle);
+            loadedSlots[slot] = true;
         }
 
-        private void ToggleClothing(SlotID slot, bool enabled)
+        detailedClothingToggle = new(Translation.Get("clothing", "detail"));
+        detailedClothingToggle.ControlEvent += (_, _) =>
+            UpdateDetailedClothing();
+
+        curlingFrontToggle = new(Translation.Get("clothing", "curlingFront"));
+        curlingFrontToggle.ControlEvent += (_, _) =>
+            ToggleCurling(Curl.Front, curlingFrontToggle.Value);
+
+        curlingBackToggle = new(Translation.Get("clothing", "curlingBack"));
+        curlingBackToggle.ControlEvent += (_, _) =>
+            ToggleCurling(Curl.Back, curlingBackToggle.Value);
+
+        pantsuShiftToggle = new(Translation.Get("clothing", "shiftPanties"));
+        pantsuShiftToggle.ControlEvent += (_, _) =>
+            ToggleCurling(Curl.Shift, pantsuShiftToggle.Value);
+
+        maskModeGrid = new(Translation.GetArray("clothing", MaskLabels));
+        maskModeGrid.ControlEvent += (_, _) =>
+            SetMaskMode((Mask)maskModeGrid.SelectedItemIndex);
+
+        UpdateDetailedClothing();
+    }
+
+    public override void UpdatePane()
+    {
+        if (!meidoManager.HasActiveMeido)
+            return;
+
+        updating = true;
+
+        var meido = meidoManager.ActiveMeido;
+        var body = meido.Maid.body0;
+
+        foreach (var clothingSlot in ClothingSlots)
         {
-            if (updating) return;
+            var toggleValue = false;
+            var hasSlot = false;
 
-            if (slot == SlotID.body)
+            if (clothingSlot is SlotID.wear)
             {
-                meidoManager.ActiveMeido.SetBodyMask(enabled);
-                return;
-            }
+                foreach (var wearSlot in WearSlots)
+                {
+                    if (body.GetMask(wearSlot))
+                        toggleValue = true;
 
-            TBody body = meidoManager.ActiveMeido.Maid.body0;
+                    if (body.GetSlotLoaded(wearSlot))
+                        hasSlot = true;
 
-            if (!detailedClothing && slot == SlotID.headset)
+                    if (hasSlot && toggleValue)
+                        break;
+                }
+            }
+            else if (clothingSlot is SlotID.megane)
+            {
+                toggleValue = body.GetMask(SlotID.megane) || body.GetMask(SlotID.accHead);
+                hasSlot = body.GetSlotLoaded(SlotID.megane) || body.GetSlotLoaded(SlotID.accHead);
+            }
+            else if (!detailedClothing && clothingSlot is SlotID.headset)
             {
-                updating = true;
-                foreach (SlotID wearSlot in HeadwearSlots)
+                foreach (var headwearSlot in HeadwearSlots)
                 {
-                    body.SetMask(wearSlot, enabled);
-                    clothingToggles[wearSlot].Value = enabled;
+                    if (body.GetMask(headwearSlot))
+                        toggleValue = true;
+
+                    if (body.GetSlotLoaded(headwearSlot))
+                        hasSlot = true;
+
+                    if (hasSlot && toggleValue)
+                        break;
                 }
-                updating = false;
             }
             else
             {
-                if (slot == SlotID.wear)
-                {
-                    foreach (SlotID wearSlot in WearSlots) body.SetMask(wearSlot, enabled);
-                }
-                else if (slot == SlotID.megane)
-                {
-                    body.SetMask(SlotID.megane, enabled);
-                    body.SetMask(SlotID.accHead, enabled);
-                }
-                else body.SetMask(slot, enabled);
+                toggleValue = body.GetMask(clothingSlot);
+                hasSlot = body.GetSlotLoaded(clothingSlot);
             }
+
+            clothingToggles[clothingSlot].Value = hasSlot && toggleValue;
+            loadedSlots[clothingSlot] = hasSlot;
         }
 
-        private void ToggleCurling(Curl curl, bool enabled)
-        {
-            if (updating) return;
+        curlingFrontToggle.Value = meido.CurlingFront;
+        curlingBackToggle.Value = meido.CurlingBack;
+        pantsuShiftToggle.Value = meido.PantsuShift;
 
-            meidoManager.ActiveMeido.SetCurling(curl, enabled);
+        var maskMode = meido.CurrentMaskMode;
 
-            if (!enabled) return;
+        maskModeGrid.SelectedItemIndex = maskMode is MaskMode.Nude ? (int)Mask.Nude : (int)maskMode;
 
-            updating = true;
-            if (curl == Curl.Front && curlingBackToggle.Value) curlingBackToggle.Value = false;
-            else if (curl == Curl.Back && curlingFrontToggle.Value) curlingFrontToggle.Value = false;
+        updating = false;
+    }
 
-            updating = false;
-        }
+    public override void Draw()
+    {
+        GUI.enabled = Enabled = meidoManager.HasActiveMeido;
 
-        private void SetMaskMode(Mask mask)
-        {
-            if (updating) return;
+        detailedClothingToggle.Draw();
 
-            meidoManager.ActiveMeido.SetMaskMode(mask);
+        MpsGui.BlackLine();
 
-            UpdatePane();
-        }
+        maskModeGrid.Draw();
+
+        MpsGui.BlackLine();
+
+        DrawSlotGroup(SlotID.wear, SlotID.skirt);
+        DrawSlotGroup(SlotID.bra, SlotID.panz);
+        DrawSlotGroup(SlotID.headset, SlotID.megane);
+        DrawSlotGroup(SlotID.accUde, SlotID.glove, SlotID.accSenaka);
+        DrawSlotGroup(SlotID.stkg, SlotID.shoes, SlotID.body);
 
-        public override void UpdatePane()
+        if (detailedClothing)
         {
-            if (!meidoManager.HasActiveMeido) return;
+            MpsGui.BlackLine();
+            DrawSlotGroup(SlotID.accShippo, SlotID.accHat);
+            DrawSlotGroup(SlotID.accKami_1_, SlotID.accKami_2_, SlotID.accKami_3_);
+            DrawSlotGroup(SlotID.accKamiSubL, SlotID.accKamiSubR);
+            DrawSlotGroup(SlotID.accMiMiL, SlotID.accMiMiR);
+            DrawSlotGroup(SlotID.accNipL, SlotID.accNipR);
+            DrawSlotGroup(SlotID.accHana, SlotID.accKubi, SlotID.accKubiwa);
+            DrawSlotGroup(SlotID.accHeso, SlotID.accAshi, SlotID.accXXX);
+        }
 
-            updating = true;
+        MpsGui.BlackLine();
 
-            Meido meido = meidoManager.ActiveMeido;
-            TBody body = meido.Maid.body0;
+        GUILayout.BeginHorizontal();
+        curlingFrontToggle.Draw();
+        GUILayout.FlexibleSpace();
+        curlingBackToggle.Draw();
+        GUILayout.FlexibleSpace();
+        pantsuShiftToggle.Draw();
+        GUILayout.FlexibleSpace();
+        GUILayout.EndHorizontal();
 
-            foreach (SlotID clothingSlot in ClothingSlots)
-            {
-                var toggleValue = false;
-                var hasSlot = false;
-                if (clothingSlot == SlotID.wear)
-                {
-                    foreach (SlotID wearSlot in WearSlots)
-                    {
-                        if (body.GetMask(wearSlot)) toggleValue = true;
-                        if (body.GetSlotLoaded(wearSlot)) hasSlot = true;
-                        if (hasSlot && toggleValue) break;
-                    }
-                }
-                else if (clothingSlot == SlotID.megane)
-                {
-                    toggleValue = body.GetMask(SlotID.megane) || body.GetMask(SlotID.accHead);
-                    hasSlot = body.GetSlotLoaded(SlotID.megane) || body.GetSlotLoaded(SlotID.accHead);
-                }
-                else if (!detailedClothing && clothingSlot == SlotID.headset)
-                {
-                    foreach (SlotID headwearSlot in HeadwearSlots)
-                    {
-                        if (body.GetMask(headwearSlot)) toggleValue = true;
-                        if (body.GetSlotLoaded(headwearSlot)) hasSlot = true;
-                        if (hasSlot && toggleValue) break;
-                    }
-                }
-                else
-                {
-                    toggleValue = body.GetMask(clothingSlot);
-                    hasSlot = body.GetSlotLoaded(clothingSlot);
-                }
+        GUI.enabled = true;
+    }
 
-                clothingToggles[clothingSlot].Value = hasSlot && toggleValue;
-                loadedSlots[clothingSlot] = hasSlot;
-            }
+    protected override void ReloadTranslation()
+    {
+        foreach (var slot in ClothingSlots)
+        {
+            var clothingToggle = clothingToggles[slot];
+
+            if (slot is SlotID.headset)
+                clothingToggle.Label = detailedClothing
+                    ? Translation.Get("clothing", "headset")
+                    : Translation.Get("clothing", "headwear");
+            else
+                clothingToggle.Label = Translation.Get("clothing", slot.ToString());
+        }
 
-            curlingFrontToggle.Value = meido.CurlingFront;
-            curlingBackToggle.Value = meido.CurlingBack;
-            pantsuShiftToggle.Value = meido.PantsuShift;
+        updating = true;
+        maskModeGrid.SetItems(Translation.GetArray("clothing", MaskLabels));
+        updating = false;
 
-            MaskMode maskMode = meido.CurrentMaskMode;
+        detailedClothingToggle.Label = Translation.Get("clothing", "detail");
+        curlingFrontToggle.Label = Translation.Get("clothing", "curlingFront");
+        curlingBackToggle.Label = Translation.Get("clothing", "curlingBack");
+        pantsuShiftToggle.Label = Translation.Get("clothing", "shiftPanties");
+    }
 
-            maskModeGrid.SelectedItemIndex = maskMode == MaskMode.Nude ? (int)Mask.Nude : (int)maskMode;
+    private void ToggleClothing(SlotID slot, bool enabled)
+    {
+        if (updating)
+            return;
 
-            updating = false;
+        if (slot is SlotID.body)
+        {
+            meidoManager.ActiveMeido.SetBodyMask(enabled);
+
+            return;
         }
 
-        private void DrawSlotGroup(params SlotID[] slots)
+        var body = meidoManager.ActiveMeido.Maid.body0;
+
+        if (!detailedClothing && slot is SlotID.headset)
         {
-            GUILayout.BeginHorizontal();
-            for (var i = 0; i < slots.Length; i++)
+            updating = true;
+
+            foreach (var wearSlot in HeadwearSlots)
             {
-                SlotID slot = slots[i];
-                GUI.enabled = Enabled && loadedSlots[slot];
-                clothingToggles[slot].Draw();
-                if (i < slots.Length - 1) GUILayout.FlexibleSpace();
+                body.SetMask(wearSlot, enabled);
+                clothingToggles[wearSlot].Value = enabled;
             }
-            GUILayout.EndHorizontal();
 
-            GUI.enabled = Enabled;
+            updating = false;
         }
-
-        private void UpdateDetailedClothing()
+        else
         {
-            detailedClothing = detailedClothingToggle.Value;
-            clothingToggles[SlotID.headset].Label = detailedClothing
-                ? Translation.Get("clothing", "headset")
-                : Translation.Get("clothing", "headwear");
-            UpdatePane();
+            if (slot is SlotID.wear)
+            {
+                foreach (var wearSlot in WearSlots)
+                    body.SetMask(wearSlot, enabled);
+            }
+            else if (slot is SlotID.megane)
+            {
+                body.SetMask(SlotID.megane, enabled);
+                body.SetMask(SlotID.accHead, enabled);
+            }
+            else
+            {
+                body.SetMask(slot, enabled);
+            }
         }
+    }
 
-        public override void Draw()
-        {
-            GUI.enabled = Enabled = meidoManager.HasActiveMeido;
+    private void ToggleCurling(Curl curl, bool enabled)
+    {
+        if (updating)
+            return;
 
-            detailedClothingToggle.Draw();
+        meidoManager.ActiveMeido.SetCurling(curl, enabled);
 
-            MpsGui.BlackLine();
+        if (!enabled)
+            return;
 
-            maskModeGrid.Draw();
+        updating = true;
 
-            MpsGui.BlackLine();
+        if (curl is Curl.Front && curlingBackToggle.Value)
+            curlingBackToggle.Value = false;
+        else if (curl is Curl.Back && curlingFrontToggle.Value)
+            curlingFrontToggle.Value = false;
 
-            DrawSlotGroup(SlotID.wear, SlotID.skirt);
-            DrawSlotGroup(SlotID.bra, SlotID.panz);
-            DrawSlotGroup(SlotID.headset, SlotID.megane);
-            DrawSlotGroup(SlotID.accUde, SlotID.glove, SlotID.accSenaka);
-            DrawSlotGroup(SlotID.stkg, SlotID.shoes, SlotID.body);
+        updating = false;
+    }
 
-            if (detailedClothing)
-            {
-                MpsGui.BlackLine();
-                DrawSlotGroup(SlotID.accShippo, SlotID.accHat);
-                DrawSlotGroup(SlotID.accKami_1_, SlotID.accKami_2_, SlotID.accKami_3_);
-                DrawSlotGroup(SlotID.accKamiSubL, SlotID.accKamiSubR);
-                DrawSlotGroup(SlotID.accMiMiL, SlotID.accMiMiR);
-                DrawSlotGroup(SlotID.accNipL, SlotID.accNipR);
-                DrawSlotGroup(SlotID.accHana, SlotID.accKubi, SlotID.accKubiwa);
-                DrawSlotGroup(SlotID.accHeso, SlotID.accAshi, SlotID.accXXX);
-            }
+    private void SetMaskMode(Mask mask)
+    {
+        if (updating)
+            return;
 
-            MpsGui.BlackLine();
+        meidoManager.ActiveMeido.SetMaskMode(mask);
+
+        UpdatePane();
+    }
+
+    private void DrawSlotGroup(params SlotID[] slots)
+    {
+        GUILayout.BeginHorizontal();
+
+        for (var i = 0; i < slots.Length; i++)
+        {
+            var slot = slots[i];
 
-            GUILayout.BeginHorizontal();
-            curlingFrontToggle.Draw();
-            GUILayout.FlexibleSpace();
-            curlingBackToggle.Draw();
-            GUILayout.FlexibleSpace();
-            pantsuShiftToggle.Draw();
-            GUILayout.FlexibleSpace();
-            GUILayout.EndHorizontal();
+            GUI.enabled = Enabled && loadedSlots[slot];
+            clothingToggles[slot].Draw();
 
-            GUI.enabled = true;
+            if (i < slots.Length - 1)
+                GUILayout.FlexibleSpace();
         }
+
+        GUILayout.EndHorizontal();
+
+        GUI.enabled = Enabled;
+    }
+
+    private void UpdateDetailedClothing()
+    {
+        detailedClothing = detailedClothingToggle.Value;
+        clothingToggles[SlotID.headset].Label = detailedClothing
+            ? Translation.Get("clothing", "headset")
+            : Translation.Get("clothing", "headwear");
+
+        UpdatePane();
     }
 }

+ 92 - 79
src/MeidoPhotoStudio.Plugin/GUI/Panes/PoseWindowPanes/MaidFreeLookPane.cs

@@ -1,102 +1,115 @@
 using UnityEngine;
 
-namespace MeidoPhotoStudio.Plugin
+namespace MeidoPhotoStudio.Plugin;
+
+public class MaidFreeLookPane : BasePane
 {
-    public class MaidFaceLookPane : BasePane
+    private readonly MeidoManager meidoManager;
+    private readonly Slider lookXSlider;
+    private readonly Slider lookYSlider;
+    private readonly Toggle headToCamToggle;
+    private readonly Toggle eyeToCamToggle;
+
+    private string bindLabel;
+
+    public MaidFreeLookPane(MeidoManager meidoManager)
     {
-        private readonly MeidoManager meidoManager;
-        private readonly Slider lookXSlider;
-        private readonly Slider lookYSlider;
-        private readonly Toggle headToCamToggle;
-        private readonly Toggle eyeToCamToggle;
-        private string bindLabel;
-
-        public MaidFaceLookPane(MeidoManager meidoManager)
-        {
-            this.meidoManager = meidoManager;
-            lookXSlider = new Slider(Translation.Get("freeLookPane", "xSlider"), -0.6f, 0.6f);
-            lookXSlider.ControlEvent += (s, a) => SetMaidLook();
+        this.meidoManager = meidoManager;
 
-            lookYSlider = new Slider(Translation.Get("freeLookPane", "ySlider"), 0.5f, -0.55f);
-            lookYSlider.ControlEvent += (s, a) => SetMaidLook();
+        lookXSlider = new(Translation.Get("freeLookPane", "xSlider"), -0.6f, 0.6f);
+        lookXSlider.ControlEvent += (_, _) =>
+            SetMaidLook();
 
-            headToCamToggle = new Toggle(Translation.Get("freeLookPane", "headToCamToggle"));
-            headToCamToggle.ControlEvent += (s, a) => SetHeadToCam(headToCamToggle.Value, eye: false);
+        lookYSlider = new(Translation.Get("freeLookPane", "ySlider"), 0.5f, -0.55f);
+        lookYSlider.ControlEvent += (_, _) =>
+            SetMaidLook();
 
-            eyeToCamToggle = new Toggle(Translation.Get("freeLookPane", "eyeToCamToggle"));
-            eyeToCamToggle.ControlEvent += (s, a) => SetHeadToCam(eyeToCamToggle.Value, eye: true);
+        headToCamToggle = new(Translation.Get("freeLookPane", "headToCamToggle"));
+        headToCamToggle.ControlEvent += (_, _) =>
+            SetHeadToCam(headToCamToggle.Value, eye: false);
 
-            bindLabel = Translation.Get("freeLookPane", "bindLabel");
-        }
+        eyeToCamToggle = new(Translation.Get("freeLookPane", "eyeToCamToggle"));
+        eyeToCamToggle.ControlEvent += (_, _) =>
+            SetHeadToCam(eyeToCamToggle.Value, eye: true);
 
-        protected override void ReloadTranslation()
-        {
-            lookXSlider.Label = Translation.Get("freeLookPane", "xSlider");
-            lookYSlider.Label = Translation.Get("freeLookPane", "ySlider");
-            headToCamToggle.Label = Translation.Get("freeLookPane", "headToCamToggle");
-            eyeToCamToggle.Label = Translation.Get("freeLookPane", "eyeToCamToggle");
-            bindLabel = Translation.Get("freeLookPane", "bindLabel");
-        }
+        bindLabel = Translation.Get("freeLookPane", "bindLabel");
+    }
 
-        public void SetHeadToCam(bool value, bool eye = false)
-        {
-            if (updating) return;
+    public override void Draw()
+    {
+        GUI.enabled = meidoManager.HasActiveMeido && meidoManager.ActiveMeido.FreeLook;
+        GUILayout.BeginHorizontal();
+        lookXSlider.Draw();
+        lookYSlider.Draw();
+        GUILayout.EndHorizontal();
 
-            Meido meido = meidoManager.ActiveMeido;
+        GUI.enabled = meidoManager.HasActiveMeido;
 
-            if (eye) meido.EyeToCam = value;
-            else meido.HeadToCam = value;
-        }
+        GUILayout.BeginHorizontal();
+        GUILayout.Label(bindLabel, GUILayout.ExpandWidth(false));
+        eyeToCamToggle.Draw();
+        headToCamToggle.Draw();
+        GUILayout.EndHorizontal();
 
-        public void SetMaidLook()
-        {
-            if (updating) return;
+        GUI.enabled = true;
+    }
 
-            TBody body = meidoManager.ActiveMeido.Body;
-            body.offsetLookTarget = new Vector3(lookYSlider.Value, 1f, lookXSlider.Value);
-        }
+    public override void UpdatePane()
+    {
+        var meido = meidoManager.ActiveMeido;
+
+        updating = true;
+        SetBounds();
+        lookXSlider.Value = meido.Body.offsetLookTarget.z;
+        lookYSlider.Value = meido.Body.offsetLookTarget.x;
+        eyeToCamToggle.Value = meido.EyeToCam;
+        headToCamToggle.Value = meido.HeadToCam;
+        updating = false;
+    }
 
-        public void SetBounds()
-        {
-            float left = 0.5f;
-            float right = -0.55f;
-            if (meidoManager.ActiveMeido.Stop)
-            {
-                left *= 0.6f;
-                right *= 0.6f;
-            }
-            lookYSlider.SetBounds(left, right);
-        }
+    public void SetHeadToCam(bool value, bool eye = false)
+    {
+        if (updating)
+            return;
 
-        public override void UpdatePane()
-        {
-            Meido meido = meidoManager.ActiveMeido;
-            updating = true;
-            SetBounds();
-            lookXSlider.Value = meido.Body.offsetLookTarget.z;
-            lookYSlider.Value = meido.Body.offsetLookTarget.x;
-            eyeToCamToggle.Value = meido.EyeToCam;
-            headToCamToggle.Value = meido.HeadToCam;
-            updating = false;
-        }
+        var meido = meidoManager.ActiveMeido;
 
-        public override void Draw()
-        {
-            GUI.enabled = meidoManager.HasActiveMeido && meidoManager.ActiveMeido.FreeLook;
-            GUILayout.BeginHorizontal();
-            lookXSlider.Draw();
-            lookYSlider.Draw();
-            GUILayout.EndHorizontal();
+        if (eye)
+            meido.EyeToCam = value;
+        else
+            meido.HeadToCam = value;
+    }
+
+    public void SetMaidLook()
+    {
+        if (updating)
+            return;
 
-            GUI.enabled = meidoManager.HasActiveMeido;
+        var body = meidoManager.ActiveMeido.Body;
 
-            GUILayout.BeginHorizontal();
-            GUILayout.Label(bindLabel, GUILayout.ExpandWidth(false));
-            eyeToCamToggle.Draw();
-            headToCamToggle.Draw();
-            GUILayout.EndHorizontal();
+        body.offsetLookTarget = new(lookYSlider.Value, 1f, lookXSlider.Value);
+    }
+
+    public void SetBounds()
+    {
+        var left = 0.5f;
+        var right = -0.55f;
 
-            GUI.enabled = true;
+        if (meidoManager.ActiveMeido.Stop)
+        {
+            left *= 0.6f;
+            right *= 0.6f;
         }
+
+        lookYSlider.SetBounds(left, right);
+    }
+
+    protected override void ReloadTranslation()
+    {
+        lookXSlider.Label = Translation.Get("freeLookPane", "xSlider");
+        lookYSlider.Label = Translation.Get("freeLookPane", "ySlider");
+        headToCamToggle.Label = Translation.Get("freeLookPane", "headToCamToggle");
+        eyeToCamToggle.Label = Translation.Get("freeLookPane", "eyeToCamToggle");
+        bindLabel = Translation.Get("freeLookPane", "bindLabel");
     }
 }

+ 64 - 54
src/MeidoPhotoStudio.Plugin/GUI/Panes/PoseWindowPanes/MaidIKPane.cs

@@ -1,71 +1,81 @@
 using UnityEngine;
 
-namespace MeidoPhotoStudio.Plugin
+namespace MeidoPhotoStudio.Plugin;
+
+public class MaidIKPane : BasePane
 {
-    public class MaidIKPane : BasePane
+    private readonly MeidoManager meidoManager;
+    private readonly Toggle ikToggle;
+    private readonly Toggle releaseIKToggle;
+    private readonly Toggle boneIKToggle;
+
+    public MaidIKPane(MeidoManager meidoManager)
     {
-        private readonly MeidoManager meidoManager;
-        private readonly Toggle ikToggle;
-        private readonly Toggle releaseIKToggle;
-        private readonly Toggle boneIKToggle;
-        private enum IKToggle
-        {
-            IK, Release, Bone
-        }
+        this.meidoManager = meidoManager;
 
-        public MaidIKPane(MeidoManager meidoManager)
-        {
-            this.meidoManager = meidoManager;
+        ikToggle = new(Translation.Get("maidPoseWindow", "ikToggle"), true);
+        ikToggle.ControlEvent += (_, _) =>
+            SetIK(IKToggle.IK, ikToggle.Value);
 
-            ikToggle = new Toggle(Translation.Get("maidPoseWindow", "ikToggle"), true);
-            ikToggle.ControlEvent += (s, a) => SetIK(IKToggle.IK, ikToggle.Value);
+        releaseIKToggle = new(Translation.Get("maidPoseWindow", "releaseToggle"));
+        releaseIKToggle.ControlEvent += (_, _) =>
+            SetIK(IKToggle.Release, releaseIKToggle.Value);
 
-            releaseIKToggle = new Toggle(Translation.Get("maidPoseWindow", "releaseToggle"));
-            releaseIKToggle.ControlEvent += (s, a) => SetIK(IKToggle.Release, releaseIKToggle.Value);
+        boneIKToggle = new(Translation.Get("maidPoseWindow", "boneToggle"));
+        boneIKToggle.ControlEvent += (_, _) =>
+            SetIK(IKToggle.Bone, boneIKToggle.Value);
+    }
 
-            boneIKToggle = new Toggle(Translation.Get("maidPoseWindow", "boneToggle"));
-            boneIKToggle.ControlEvent += (s, a) => SetIK(IKToggle.Bone, boneIKToggle.Value);
-        }
+    private enum IKToggle
+    {
+        IK,
+        Release,
+        Bone,
+    }
 
-        protected override void ReloadTranslation()
-        {
-            ikToggle.Label = Translation.Get("maidPoseWindow", "ikToggle");
-            releaseIKToggle.Label = Translation.Get("maidPoseWindow", "releaseToggle");
-            boneIKToggle.Label = Translation.Get("maidPoseWindow", "boneToggle");
-        }
+    public override void UpdatePane()
+    {
+        updating = true;
+        ikToggle.Value = meidoManager.ActiveMeido.IK;
+        releaseIKToggle.Value = meidoManager.ActiveMeido.Stop;
+        boneIKToggle.Value = meidoManager.ActiveMeido.Bone;
+        updating = false;
+    }
 
-        private void SetIK(IKToggle toggle, bool value)
-        {
-            if (updating) return;
-            if (toggle == IKToggle.IK) meidoManager.ActiveMeido.IK = value;
-            else if (toggle == IKToggle.Release) meidoManager.ActiveMeido.Stop = false;
-            else if (toggle == IKToggle.Bone) meidoManager.ActiveMeido.Bone = value;
-        }
+    public override void Draw()
+    {
+        var active = meidoManager.HasActiveMeido;
+
+        GUILayout.BeginHorizontal();
+        GUI.enabled = active;
+        ikToggle.Draw();
 
-        public override void UpdatePane()
-        {
-            updating = true;
-            ikToggle.Value = meidoManager.ActiveMeido.IK;
-            releaseIKToggle.Value = meidoManager.ActiveMeido.Stop;
-            boneIKToggle.Value = meidoManager.ActiveMeido.Bone;
-            updating = false;
-        }
+        GUI.enabled = active && meidoManager.ActiveMeido.Stop;
+        releaseIKToggle.Draw();
 
-        public override void Draw()
-        {
-            bool active = meidoManager.HasActiveMeido;
+        GUI.enabled = active && ikToggle.Value;
+        boneIKToggle.Draw();
+        GUILayout.EndHorizontal();
+        GUI.enabled = true;
+    }
 
-            GUILayout.BeginHorizontal();
-            GUI.enabled = active;
-            ikToggle.Draw();
+    protected override void ReloadTranslation()
+    {
+        ikToggle.Label = Translation.Get("maidPoseWindow", "ikToggle");
+        releaseIKToggle.Label = Translation.Get("maidPoseWindow", "releaseToggle");
+        boneIKToggle.Label = Translation.Get("maidPoseWindow", "boneToggle");
+    }
 
-            GUI.enabled = active && meidoManager.ActiveMeido.Stop;
-            releaseIKToggle.Draw();
+    private void SetIK(IKToggle toggle, bool value)
+    {
+        if (updating)
+            return;
 
-            GUI.enabled = active && ikToggle.Value;
-            boneIKToggle.Draw();
-            GUILayout.EndHorizontal();
-            GUI.enabled = true;
-        }
+        if (toggle is IKToggle.IK)
+            meidoManager.ActiveMeido.IK = value;
+        else if (toggle is IKToggle.Release)
+            meidoManager.ActiveMeido.Stop = false;
+        else if (toggle is IKToggle.Bone)
+            meidoManager.ActiveMeido.Bone = value;
     }
 }

+ 205 - 181
src/MeidoPhotoStudio.Plugin/GUI/Panes/PoseWindowPanes/MaidPoseSelectorPane.cs

@@ -1,234 +1,258 @@
-using System.IO;
 using System.Collections.Generic;
+using System.IO;
 using System.Linq;
+
 using UnityEngine;
 
-namespace MeidoPhotoStudio.Plugin
+namespace MeidoPhotoStudio.Plugin;
+
+public class MaidPoseSelectorPane : BasePane
 {
-    public class MaidPoseSelectorPane : BasePane
+    private static readonly string[] TabTranslations = new[] { "baseTab", "customTab" };
+
+    private readonly MeidoManager meidoManager;
+    private readonly Button poseLeftButton;
+    private readonly Button poseRightButton;
+    private readonly Button poseGroupLeftButton;
+    private readonly Button poseGroupRightButton;
+    private readonly Dropdown poseGroupDropdown;
+    private readonly Dropdown poseDropdown;
+    private readonly SelectionGrid poseModeGrid;
+
+    private bool customPoseMode;
+    private bool poseListEnabled;
+    private string previousPoseGroup;
+
+    public MaidPoseSelectorPane(MeidoManager meidoManager)
     {
-        private static readonly string[] tabTranslations = new[] { "baseTab", "customTab" };
-        private readonly MeidoManager meidoManager;
-        private readonly Button poseLeftButton;
-        private readonly Button poseRightButton;
-        private readonly Button poseGroupLeftButton;
-        private readonly Button poseGroupRightButton;
-        private readonly Dropdown poseGroupDropdown;
-        private readonly Dropdown poseDropdown;
-        private readonly SelectionGrid poseModeGrid;
-        private Dictionary<string, List<string>> CurrentPoseDict
-            => customPoseMode ? Constants.CustomPoseDict : Constants.PoseDict;
-        private List<string> CurrentPoseGroupList
-            => customPoseMode ? Constants.CustomPoseGroupList : Constants.PoseGroupList;
-        private string SelectedPoseGroup => CurrentPoseGroupList[poseGroupDropdown.SelectedItemIndex];
-        private List<string> CurrentPoseList => CurrentPoseDict[SelectedPoseGroup];
-        private int SelectedPoseIndex => poseDropdown.SelectedItemIndex;
-        private string SelectedPose => CurrentPoseList[SelectedPoseIndex];
-        private PoseInfo CurrentPoseInfo => new PoseInfo(SelectedPoseGroup, SelectedPose, customPoseMode);
-        private bool customPoseMode;
-        private bool poseListEnabled;
-        private string previousPoseGroup;
-
-        public MaidPoseSelectorPane(MeidoManager meidoManager)
-        {
-            Constants.CustomPoseChange += OnPresetChange;
-            this.meidoManager = meidoManager;
+        Constants.CustomPoseChange += OnPresetChange;
+        this.meidoManager = meidoManager;
 
-            poseModeGrid = new SelectionGrid(Translation.GetArray("posePane", tabTranslations));
-            poseModeGrid.ControlEvent += (s, a) => SetPoseMode();
+        poseModeGrid = new(Translation.GetArray("posePane", TabTranslations));
+        poseModeGrid.ControlEvent += (_, _) =>
+            SetPoseMode();
 
-            poseGroupDropdown = new Dropdown(Translation.GetArray("poseGroupDropdown", Constants.PoseGroupList));
-            poseGroupDropdown.SelectionChange += (s, a) => ChangePoseGroup();
+        poseGroupDropdown = new(Translation.GetArray("poseGroupDropdown", Constants.PoseGroupList));
+        poseGroupDropdown.SelectionChange += (_, _) =>
+            ChangePoseGroup();
 
-            poseDropdown = new Dropdown(UIPoseList());
-            poseDropdown.SelectionChange += (s, a) => ChangePose();
+        poseDropdown = new(UIPoseList());
+        poseDropdown.SelectionChange += (_, _) =>
+            ChangePose();
 
-            poseGroupLeftButton = new Button("<");
-            poseGroupLeftButton.ControlEvent += (s, a) => poseGroupDropdown.Step(-1);
+        poseGroupLeftButton = new("<");
+        poseGroupLeftButton.ControlEvent += (_, _) =>
+            poseGroupDropdown.Step(-1);
 
-            poseGroupRightButton = new Button(">");
-            poseGroupRightButton.ControlEvent += (s, a) => poseGroupDropdown.Step(1);
+        poseGroupRightButton = new(">");
+        poseGroupRightButton.ControlEvent += (_, _) =>
+            poseGroupDropdown.Step(1);
 
-            poseLeftButton = new Button("<");
-            poseLeftButton.ControlEvent += (s, a) => poseDropdown.Step(-1);
+        poseLeftButton = new("<");
+        poseLeftButton.ControlEvent += (_, _) =>
+            poseDropdown.Step(-1);
 
-            poseRightButton = new Button(">");
-            poseRightButton.ControlEvent += (s, a) => poseDropdown.Step(1);
+        poseRightButton = new(">");
+        poseRightButton.ControlEvent += (_, _) =>
+            poseDropdown.Step(1);
 
-            customPoseMode = poseModeGrid.SelectedItemIndex == 1;
-            previousPoseGroup = SelectedPoseGroup;
-            poseListEnabled = CurrentPoseList.Count > 0;
-        }
+        customPoseMode = poseModeGrid.SelectedItemIndex is 1;
+        previousPoseGroup = SelectedPoseGroup;
+        poseListEnabled = CurrentPoseList.Count > 0;
+    }
 
-        protected override void ReloadTranslation()
-        {
-            updating = true;
-            poseModeGrid.SetItems(Translation.GetArray("posePane", tabTranslations));
-            if (!customPoseMode)
-            {
-                poseGroupDropdown.SetDropdownItems(
-                    Translation.GetArray("poseGroupDropdown", Constants.PoseGroupList)
-                );
-            }
-            updating = false;
-        }
+    private Dictionary<string, List<string>> CurrentPoseDict =>
+        customPoseMode ? Constants.CustomPoseDict : Constants.PoseDict;
+
+    private List<string> CurrentPoseGroupList =>
+        customPoseMode ? Constants.CustomPoseGroupList : Constants.PoseGroupList;
+
+    private string SelectedPoseGroup =>
+        CurrentPoseGroupList[poseGroupDropdown.SelectedItemIndex];
+
+    private List<string> CurrentPoseList =>
+        CurrentPoseDict[SelectedPoseGroup];
+
+    private int SelectedPoseIndex =>
+        poseDropdown.SelectedItemIndex;
+
+    private string SelectedPose =>
+        CurrentPoseList[SelectedPoseIndex];
+
+    private PoseInfo CurrentPoseInfo =>
+        new(SelectedPoseGroup, SelectedPose, customPoseMode);
 
-        public override void Draw()
+    public override void Draw()
+    {
+        const float buttonHeight = 30f;
+
+        var arrowLayoutOptions = new[]
         {
-            const float buttonHeight = 30f;
-            GUILayoutOption[] arrowLayoutOptions = {
-                GUILayout.Width(buttonHeight),
-                GUILayout.Height(buttonHeight)
-            };
-
-            const float dropdownButtonWidth = 153f;
-            GUILayoutOption[] dropdownLayoutOptions = new GUILayoutOption[] {
-                GUILayout.Height(buttonHeight),
-                GUILayout.Width(dropdownButtonWidth)
-            };
-
-            GUI.enabled = meidoManager.HasActiveMeido && !meidoManager.ActiveMeido.Stop;
-
-            poseModeGrid.Draw();
-            MpsGui.WhiteLine();
-
-            GUILayout.BeginHorizontal();
-            poseGroupLeftButton.Draw(arrowLayoutOptions);
-            poseGroupDropdown.Draw(dropdownLayoutOptions);
-            poseGroupRightButton.Draw(arrowLayoutOptions);
-            GUILayout.EndHorizontal();
-
-            GUILayout.BeginHorizontal();
-            GUI.enabled = GUI.enabled && poseListEnabled;
-            poseLeftButton.Draw(arrowLayoutOptions);
-            poseDropdown.Draw(dropdownLayoutOptions);
-            poseRightButton.Draw(arrowLayoutOptions);
-            GUILayout.EndHorizontal();
-
-            GUI.enabled = true;
-        }
+            GUILayout.Width(buttonHeight),
+            GUILayout.Height(buttonHeight),
+        };
 
-        public override void UpdatePane()
+        const float dropdownButtonWidth = 153f;
+
+        var dropdownLayoutOptions = new GUILayoutOption[]
         {
-            updating = true;
+            GUILayout.Height(buttonHeight),
+            GUILayout.Width(dropdownButtonWidth),
+        };
 
-            try
-            {
-                var cachedPose = meidoManager.ActiveMeido.CachedPose;
+        GUI.enabled = meidoManager.HasActiveMeido && !meidoManager.ActiveMeido.Stop;
 
-                poseModeGrid.SelectedItemIndex = cachedPose.CustomPose ? 1 : 0;
+        poseModeGrid.Draw();
+        MpsGui.WhiteLine();
 
-                var oldCustomPoseMode = customPoseMode;
-                customPoseMode = cachedPose.CustomPose;
+        GUILayout.BeginHorizontal();
+        poseGroupLeftButton.Draw(arrowLayoutOptions);
+        poseGroupDropdown.Draw(dropdownLayoutOptions);
+        poseGroupRightButton.Draw(arrowLayoutOptions);
+        GUILayout.EndHorizontal();
 
-                if (oldCustomPoseMode != customPoseMode)
-                    poseGroupDropdown.SetDropdownItems(
-                        customPoseMode ? CurrentPoseGroupList.ToArray() : Translation.GetArray(
-                            "poseGroupDropdown", CurrentPoseGroupList
-                        )
-                    );
+        GUILayout.BeginHorizontal();
+        GUI.enabled = GUI.enabled && poseListEnabled;
+        poseLeftButton.Draw(arrowLayoutOptions);
+        poseDropdown.Draw(dropdownLayoutOptions);
+        poseRightButton.Draw(arrowLayoutOptions);
+        GUILayout.EndHorizontal();
 
-                var newPoseGroupIndex = CurrentPoseGroupList.IndexOf(cachedPose.PoseGroup);
+        GUI.enabled = true;
+    }
 
-                if (newPoseGroupIndex < 0)
-                    poseGroupDropdown.SelectedItemIndex = 0;
-                else if (oldCustomPoseMode != customPoseMode
-                    || poseGroupDropdown.SelectedItemIndex != newPoseGroupIndex)
-                {
-                    poseGroupDropdown.SelectedItemIndex = newPoseGroupIndex;
-                    poseDropdown.SetDropdownItems(UIPoseList());
-                }
+    public override void UpdatePane()
+    {
+        updating = true;
 
-                var newPoseIndex = CurrentPoseDict.TryGetValue(cachedPose.PoseGroup, out var poseList)
-                    ? poseList.IndexOf(cachedPose.Pose)
-                    : 0;
+        try
+        {
+            var cachedPose = meidoManager.ActiveMeido.CachedPose;
 
-                if (newPoseIndex < 0)
-                    newPoseIndex = 0;
+            poseModeGrid.SelectedItemIndex = cachedPose.CustomPose ? 1 : 0;
 
-                poseDropdown.SelectedItemIndex = newPoseIndex;
-                poseListEnabled = CurrentPoseList.Count > 0;
-            }
-            catch
+            var oldCustomPoseMode = customPoseMode;
+            customPoseMode = cachedPose.CustomPose;
+
+            if (oldCustomPoseMode != customPoseMode)
+                poseGroupDropdown.SetDropdownItems(
+                    customPoseMode
+                        ? CurrentPoseGroupList.ToArray()
+                        : Translation.GetArray("poseGroupDropdown", CurrentPoseGroupList));
+
+            var newPoseGroupIndex = CurrentPoseGroupList.IndexOf(cachedPose.PoseGroup);
+
+            if (newPoseGroupIndex < 0)
             {
-                // Do nothing
+                poseGroupDropdown.SelectedItemIndex = 0;
             }
-            finally
+            else if (oldCustomPoseMode != customPoseMode || poseGroupDropdown.SelectedItemIndex != newPoseGroupIndex)
             {
-                updating = false;
+                poseGroupDropdown.SelectedItemIndex = newPoseGroupIndex;
+                poseDropdown.SetDropdownItems(UIPoseList());
             }
+
+            var newPoseIndex = CurrentPoseDict.TryGetValue(cachedPose.PoseGroup, out var poseList)
+                ? poseList.IndexOf(cachedPose.Pose)
+                : 0;
+
+            if (newPoseIndex < 0)
+                newPoseIndex = 0;
+
+            poseDropdown.SelectedItemIndex = newPoseIndex;
+            poseListEnabled = CurrentPoseList.Count > 0;
+        }
+        catch
+        {
+            // Ignored
         }
+        finally
+        {
+            updating = false;
+        }
+    }
+
+    protected override void ReloadTranslation()
+    {
+        updating = true;
+        poseModeGrid.SetItems(Translation.GetArray("posePane", TabTranslations));
 
-        private void OnPresetChange(object sender, PresetChangeEventArgs args)
+        if (!customPoseMode)
+            poseGroupDropdown.SetDropdownItems(Translation.GetArray("poseGroupDropdown", Constants.PoseGroupList));
+
+        updating = false;
+    }
+
+    private void OnPresetChange(object sender, PresetChangeEventArgs args)
+    {
+        if (args == PresetChangeEventArgs.Empty)
         {
-            if (args == PresetChangeEventArgs.Empty)
-            {
-                if (poseModeGrid.SelectedItemIndex == 1)
-                {
-                    updating = true;
-                    poseGroupDropdown.SetDropdownItems(CurrentPoseGroupList.ToArray(), 0);
-                    poseDropdown.SetDropdownItems(UIPoseList(), 0);
-                    updating = false;
-                }
-            }
-            else
+            if (poseModeGrid.SelectedItemIndex is 1)
             {
                 updating = true;
-                poseModeGrid.SelectedItemIndex = 1;
-                poseGroupDropdown.SetDropdownItems(
-                    CurrentPoseGroupList.ToArray(), CurrentPoseGroupList.IndexOf(args.Category)
-                );
+                poseGroupDropdown.SetDropdownItems(CurrentPoseGroupList.ToArray(), 0);
+                poseDropdown.SetDropdownItems(UIPoseList(), 0);
                 updating = false;
-
-                poseDropdown.SetDropdownItems(UIPoseList(), CurrentPoseList.IndexOf(args.Path));
-                poseListEnabled = true;
             }
         }
-
-        private void SetPoseMode()
+        else
         {
-            if (updating)
-                return;
-
-            customPoseMode = poseModeGrid.SelectedItemIndex == 1;
+            updating = true;
+            poseModeGrid.SelectedItemIndex = 1;
+            poseGroupDropdown.SetDropdownItems(
+                CurrentPoseGroupList.ToArray(), CurrentPoseGroupList.IndexOf(args.Category));
 
-            string[] list = customPoseMode
-                ? CurrentPoseGroupList.ToArray()
-                : Translation.GetArray("poseGroupDropdown", CurrentPoseGroupList);
+            updating = false;
 
-            poseGroupDropdown.SetDropdownItems(list, 0);
+            poseDropdown.SetDropdownItems(UIPoseList(), CurrentPoseList.IndexOf(args.Path));
+            poseListEnabled = true;
         }
+    }
 
-        private void ChangePoseGroup()
-        {
-            if (updating)
-                return;
+    private void SetPoseMode()
+    {
+        if (updating)
+            return;
 
-            poseListEnabled = CurrentPoseList.Count > 0;
-            if (previousPoseGroup == SelectedPoseGroup)
-            {
-                poseDropdown.SelectedItemIndex = 0;
-            }
-            else
-            {
-                previousPoseGroup = SelectedPoseGroup;
-                poseDropdown.SetDropdownItems(UIPoseList(), 0);
-            }
-        }
+        customPoseMode = poseModeGrid.SelectedItemIndex is 1;
+
+        var list = customPoseMode
+            ? CurrentPoseGroupList.ToArray()
+            : Translation.GetArray("poseGroupDropdown", CurrentPoseGroupList);
+
+        poseGroupDropdown.SetDropdownItems(list, 0);
+    }
 
-        private void ChangePose()
+    private void ChangePoseGroup()
+    {
+        if (updating)
+            return;
+
+        poseListEnabled = CurrentPoseList.Count > 0;
+
+        if (previousPoseGroup == SelectedPoseGroup)
         {
-            if (!poseListEnabled || updating) return;
-            meidoManager.ActiveMeido.SetPose(CurrentPoseInfo);
+            poseDropdown.SelectedItemIndex = 0;
         }
-
-        private string[] UIPoseList()
+        else
         {
-            return CurrentPoseList.Count == 0
-                ? new[] { "No Poses" }
-                : CurrentPoseList
-                    .Select((pose, i) => $"{i + 1}:{(customPoseMode ? Path.GetFileNameWithoutExtension(pose) : pose)}")
-                    .ToArray();
+            previousPoseGroup = SelectedPoseGroup;
+            poseDropdown.SetDropdownItems(UIPoseList(), 0);
         }
     }
+
+    private void ChangePose()
+    {
+        if (!poseListEnabled || updating)
+            return;
+
+        meidoManager.ActiveMeido.SetPose(CurrentPoseInfo);
+    }
+
+    private string[] UIPoseList() =>
+        CurrentPoseList.Count is 0
+            ? new[] { "No Poses" }
+            : CurrentPoseList
+                .Select((pose, i) => $"{i + 1}:{(customPoseMode ? Path.GetFileNameWithoutExtension(pose) : pose)}")
+                .ToArray();
 }

+ 89 - 79
src/MeidoPhotoStudio.Plugin/GUI/Panes/PoseWindowPanes/MpnAttachPropPane.cs

@@ -1,111 +1,121 @@
-using System.Linq;
 using System.Collections.Generic;
+using System.Linq;
+
 using UnityEngine;
 
-namespace MeidoPhotoStudio.Plugin
+namespace MeidoPhotoStudio.Plugin;
+
+public class MpnAttachPropPane : BasePane
 {
-    public class MpnAttachPropPane : BasePane
+    private readonly MeidoManager meidoManager;
+    private readonly Dropdown mpnAttachDropdown;
+    private readonly Button previousPropButton;
+    private readonly Button nextPropButton;
+    private readonly Button attachPropButton;
+    private readonly Button detachPropButton;
+    private readonly Button detachAllButton;
+
+    private string header;
+
+    public MpnAttachPropPane(MeidoManager meidoManager)
     {
-        private readonly MeidoManager meidoManager;
-        private readonly Dropdown mpnAttachDropdown;
-        private readonly Button previousPropButton;
-        private readonly Button nextPropButton;
-        private readonly Button attachPropButton;
-        private readonly Button detachPropButton;
-        private readonly Button detachAllButton;
-        private string header;
+        this.meidoManager = meidoManager;
 
-        public MpnAttachPropPane(MeidoManager meidoManager)
-        {
-            this.meidoManager = meidoManager;
+        mpnAttachDropdown = new(new[] { string.Empty });
 
-            mpnAttachDropdown = new Dropdown(new[] { string.Empty });
+        if (!Constants.MpnAttachInitialized)
+            Constants.MenuFilesChange += InitializeMpnAttach;
 
-            if (!Constants.MpnAttachInitialized) Constants.MenuFilesChange += InitializeMpnAttach;
+        SetDropdownList();
 
-            SetDropdownList();
+        previousPropButton = new("<");
+        previousPropButton.ControlEvent += (_, _) =>
+            mpnAttachDropdown.Step(-1);
 
-            previousPropButton = new Button("<");
-            previousPropButton.ControlEvent += (s, a) => mpnAttachDropdown.Step(-1);
+        nextPropButton = new(">");
+        nextPropButton.ControlEvent += (_, _) =>
+            mpnAttachDropdown.Step(1);
 
-            nextPropButton = new Button(">");
-            nextPropButton.ControlEvent += (s, a) => mpnAttachDropdown.Step(1);
+        attachPropButton = new(Translation.Get("attachMpnPropPane", "attachButton"));
+        attachPropButton.ControlEvent += (_, _) =>
+            AttachProp();
 
-            attachPropButton = new Button(Translation.Get("attachMpnPropPane", "attachButton"));
-            attachPropButton.ControlEvent += (s, a) => AttachProp();
+        detachPropButton = new(Translation.Get("attachMpnPropPane", "detachButton"));
+        detachPropButton.ControlEvent += (_, _) =>
+            AttachProp(detach: true);
 
-            detachPropButton = new Button(Translation.Get("attachMpnPropPane", "detachButton"));
-            detachPropButton.ControlEvent += (s, a) => AttachProp(detach: true);
+        detachAllButton = new(Translation.Get("attachMpnPropPane", "detachAllButton"));
+        detachAllButton.ControlEvent += (_, _) =>
+            DetachAll();
 
-            detachAllButton = new Button(Translation.Get("attachMpnPropPane", "detachAllButton"));
-            detachAllButton.ControlEvent += (s, a) => DetachAll();
+        header = Translation.Get("attachMpnPropPane", "header");
+    }
 
-            header = Translation.Get("attachMpnPropPane", "header");
-        }
+    public override void Draw()
+    {
+        GUI.enabled = meidoManager.HasActiveMeido && Constants.MpnAttachInitialized;
 
-        protected override void ReloadTranslation()
-        {
-            attachPropButton.Label = Translation.Get("attachMpnPropPane", "attachButton");
-            detachPropButton.Label = Translation.Get("attachMpnPropPane", "detachButton");
-            detachAllButton.Label = Translation.Get("attachMpnPropPane", "detachAllButton");
-            header = Translation.Get("attachMpnPropPane", "header");
-            SetDropdownList();
-        }
+        var noExpand = GUILayout.ExpandWidth(false);
 
-        public override void Draw()
-        {
-            GUI.enabled = meidoManager.HasActiveMeido && Constants.MpnAttachInitialized;
+        MpsGui.Header(header);
+        MpsGui.WhiteLine();
 
-            GUILayoutOption noExpand = GUILayout.ExpandWidth(false);
+        GUILayout.BeginHorizontal();
+        previousPropButton.Draw(noExpand);
+        mpnAttachDropdown.Draw(GUILayout.Width(153f));
+        nextPropButton.Draw(noExpand);
+        GUILayout.EndHorizontal();
 
-            MpsGui.Header(header);
-            MpsGui.WhiteLine();
+        GUILayout.BeginHorizontal();
+        attachPropButton.Draw();
+        detachPropButton.Draw();
+        GUILayout.EndHorizontal();
+        detachAllButton.Draw();
 
-            GUILayout.BeginHorizontal();
-            previousPropButton.Draw(noExpand);
-            mpnAttachDropdown.Draw(GUILayout.Width(153f));
-            nextPropButton.Draw(noExpand);
-            GUILayout.EndHorizontal();
+        GUI.enabled = true;
+    }
 
-            GUILayout.BeginHorizontal();
-            attachPropButton.Draw();
-            detachPropButton.Draw();
-            GUILayout.EndHorizontal();
-            detachAllButton.Draw();
+    protected override void ReloadTranslation()
+    {
+        attachPropButton.Label = Translation.Get("attachMpnPropPane", "attachButton");
+        detachPropButton.Label = Translation.Get("attachMpnPropPane", "detachButton");
+        detachAllButton.Label = Translation.Get("attachMpnPropPane", "detachAllButton");
+        header = Translation.Get("attachMpnPropPane", "header");
 
-            GUI.enabled = true;
-        }
+        SetDropdownList();
+    }
 
-        private void InitializeMpnAttach(object sender, MenuFilesEventArgs args)
-        {
-            if (args.Type == MenuFilesEventArgs.EventType.MpnAttach) SetDropdownList();
-        }
+    private void InitializeMpnAttach(object sender, MenuFilesEventArgs args)
+    {
+        if (args.Type is MenuFilesEventArgs.EventType.MpnAttach)
+            SetDropdownList();
+    }
 
-        private void SetDropdownList()
-        {
-            IEnumerable<string> dropdownList = !Constants.MpnAttachInitialized
-                ? new[] { Translation.Get("systemMessage", "initializing") }
-                : Translation.GetArray(
-                    "mpnAttachPropNames", Constants.MpnAttachPropList.Select(mpnProp => mpnProp.MenuFile)
-                );
+    private void SetDropdownList()
+    {
+        IEnumerable<string> dropdownList = !Constants.MpnAttachInitialized
+            ? new[] { Translation.Get("systemMessage", "initializing") }
+            : Translation.GetArray(
+                "mpnAttachPropNames", Constants.MpnAttachPropList.Select(mpnProp => mpnProp.MenuFile));
 
-            mpnAttachDropdown.SetDropdownItems(dropdownList.ToArray());
-        }
+        mpnAttachDropdown.SetDropdownItems(dropdownList.ToArray());
+    }
 
-        private void AttachProp(bool detach = false)
-        {
-            if (!meidoManager.HasActiveMeido) return;
+    private void AttachProp(bool detach = false)
+    {
+        if (!meidoManager.HasActiveMeido)
+            return;
 
-            MpnAttachProp prop = Constants.MpnAttachPropList[mpnAttachDropdown.SelectedItemIndex];
+        var prop = Constants.MpnAttachPropList[mpnAttachDropdown.SelectedItemIndex];
 
-            meidoManager.ActiveMeido.SetMpnProp(prop, detach);
-        }
+        meidoManager.ActiveMeido.SetMpnProp(prop, detach);
+    }
 
-        private void DetachAll()
-        {
-            if (!meidoManager.HasActiveMeido) return;
+    private void DetachAll()
+    {
+        if (!meidoManager.HasActiveMeido)
+            return;
 
-            meidoManager.ActiveMeido.DetachAllMpnAttach();
-        }
+        meidoManager.ActiveMeido.DetachAllMpnAttach();
     }
 }

+ 52 - 49
src/MeidoPhotoStudio.Plugin/GUI/Panes/PoseWindowPanes/SaveHandPane.cs

@@ -1,70 +1,73 @@
 using UnityEngine;
 
-namespace MeidoPhotoStudio.Plugin
+namespace MeidoPhotoStudio.Plugin;
+
+public class SaveHandPane : BasePane
 {
-    public class SaveHandPane : BasePane
+    private readonly MeidoManager meidoManager;
+    private readonly ComboBox categoryComboBox;
+    private readonly TextField handNameTextField;
+    private readonly Button saveLeftHandButton;
+    private readonly Button saveRightHandButton;
+
+    private string categoryHeader;
+    private string nameHeader;
+
+    public SaveHandPane(MeidoManager meidoManager)
     {
-        private readonly MeidoManager meidoManager;
-        private readonly ComboBox categoryComboBox;
-        private readonly TextField handNameTextField;
-        private readonly Button saveLeftHandButton;
-        private readonly Button saveRightHandButton;
-        private string categoryHeader;
-        private string nameHeader;
+        Constants.CustomHandChange += (_, _) =>
+            categoryComboBox.SetDropdownItems(Constants.CustomHandGroupList.ToArray());
 
-        public SaveHandPane(MeidoManager meidoManager)
-        {
-            Constants.CustomHandChange += (s, a)
-                => categoryComboBox.SetDropdownItems(Constants.CustomHandGroupList.ToArray());
+        this.meidoManager = meidoManager;
 
-            this.meidoManager = meidoManager;
+        categoryHeader = Translation.Get("handPane", "categoryHeader");
 
-            categoryHeader = Translation.Get("handPane", "categoryHeader");
+        nameHeader = Translation.Get("handPane", "nameHeader");
 
-            nameHeader = Translation.Get("handPane", "nameHeader");
+        saveLeftHandButton = new(Translation.Get("handPane", "saveLeftButton"));
+        saveLeftHandButton.ControlEvent += (_, _) =>
+            SaveHand(right: false);
 
-            saveLeftHandButton = new Button(Translation.Get("handPane", "saveLeftButton"));
-            saveLeftHandButton.ControlEvent += (s, a) => SaveHand(right: false);
+        saveRightHandButton = new(Translation.Get("handPane", "saveRightButton"));
+        saveRightHandButton.ControlEvent += (_, _) =>
+            SaveHand(right: true);
 
-            saveRightHandButton = new Button(Translation.Get("handPane", "saveRightButton"));
-            saveRightHandButton.ControlEvent += (s, a) => SaveHand(right: true);
+        categoryComboBox = new(Constants.CustomHandGroupList.ToArray());
 
-            categoryComboBox = new ComboBox(Constants.CustomHandGroupList.ToArray());
+        handNameTextField = new();
+    }
 
-            handNameTextField = new TextField();
-        }
+    public override void Draw()
+    {
+        GUI.enabled = meidoManager.HasActiveMeido;
 
-        protected override void ReloadTranslation()
-        {
-            categoryHeader = Translation.Get("handPane", "categoryHeader");
-            nameHeader = Translation.Get("handPane", "nameHeader");
-            saveLeftHandButton.Label = Translation.Get("handPane", "saveLeftButton");
-            saveRightHandButton.Label = Translation.Get("handPane", "saveRightButton");
-        }
+        MpsGui.Header(categoryHeader);
+        categoryComboBox.Draw(GUILayout.Width(165f));
 
-        public override void Draw()
-        {
-            GUI.enabled = meidoManager.HasActiveMeido;
+        MpsGui.Header(nameHeader);
+        handNameTextField.Draw(GUILayout.Width(165f));
 
-            MpsGui.Header(categoryHeader);
-            categoryComboBox.Draw(GUILayout.Width(165f));
+        GUILayout.BeginHorizontal();
+        saveRightHandButton.Draw();
+        saveLeftHandButton.Draw();
+        GUILayout.EndHorizontal();
 
-            MpsGui.Header(nameHeader);
-            handNameTextField.Draw(GUILayout.Width(165f));
+        GUI.enabled = true;
+    }
 
-            GUILayout.BeginHorizontal();
-            saveRightHandButton.Draw();
-            saveLeftHandButton.Draw();
-            GUILayout.EndHorizontal();
+    protected override void ReloadTranslation()
+    {
+        categoryHeader = Translation.Get("handPane", "categoryHeader");
+        nameHeader = Translation.Get("handPane", "nameHeader");
+        saveLeftHandButton.Label = Translation.Get("handPane", "saveLeftButton");
+        saveRightHandButton.Label = Translation.Get("handPane", "saveRightButton");
+    }
 
-            GUI.enabled = true;
-        }
+    private void SaveHand(bool right)
+    {
+        var handBinary = meidoManager.ActiveMeido.IKManager.SerializeHand(right);
 
-        private void SaveHand(bool right)
-        {
-            byte[] handBinary = meidoManager.ActiveMeido.IKManager.SerializeHand(right);
-            Constants.AddHand(handBinary, right, handNameTextField.Value, categoryComboBox.Value);
-            handNameTextField.Value = string.Empty;
-        }
+        Constants.AddHand(handBinary, right, handNameTextField.Value, categoryComboBox.Value);
+        handNameTextField.Value = string.Empty;
     }
 }

+ 59 - 56
src/MeidoPhotoStudio.Plugin/GUI/Panes/PoseWindowPanes/SavePosePane.cs

@@ -1,63 +1,66 @@
 using System;
+
 using UnityEngine;
 
-namespace MeidoPhotoStudio.Plugin
+namespace MeidoPhotoStudio.Plugin;
+
+public class SavePosePane : BasePane
 {
-    public class SavePosePane : BasePane
+    private readonly MeidoManager meidoManager;
+    private readonly Button savePoseButton;
+    private readonly TextField poseNameTextField;
+    private readonly ComboBox categoryComboBox;
+
+    private string categoryHeader;
+    private string nameHeader;
+
+    public SavePosePane(MeidoManager meidoManager)
+    {
+        Constants.CustomPoseChange += (_, _) =>
+            categoryComboBox.SetDropdownItems(Constants.CustomPoseGroupList.ToArray());
+
+        this.meidoManager = meidoManager;
+
+        categoryHeader = Translation.Get("posePane", "categoryHeader");
+        nameHeader = Translation.Get("posePane", "nameHeader");
+
+        savePoseButton = new(Translation.Get("posePane", "saveButton"));
+        savePoseButton.ControlEvent += OnSavePose;
+
+        categoryComboBox = new(Constants.CustomPoseGroupList.ToArray());
+
+        poseNameTextField = new();
+        poseNameTextField.ControlEvent += OnSavePose;
+    }
+
+    public override void Draw()
     {
-        private readonly MeidoManager meidoManager;
-        private readonly Button savePoseButton;
-        private readonly TextField poseNameTextField;
-        private readonly ComboBox categoryComboBox;
-        private string categoryHeader;
-        private string nameHeader;
-
-        public SavePosePane(MeidoManager meidoManager)
-        {
-            Constants.CustomPoseChange += (s, a)
-                => categoryComboBox.SetDropdownItems(Constants.CustomPoseGroupList.ToArray());
-
-            this.meidoManager = meidoManager;
-
-            categoryHeader = Translation.Get("posePane", "categoryHeader");
-            nameHeader = Translation.Get("posePane", "nameHeader");
-
-            savePoseButton = new Button(Translation.Get("posePane", "saveButton"));
-            savePoseButton.ControlEvent += OnSavePose;
-
-            categoryComboBox = new ComboBox(Constants.CustomPoseGroupList.ToArray());
-            poseNameTextField = new TextField();
-            poseNameTextField.ControlEvent += OnSavePose;
-        }
-
-        protected override void ReloadTranslation()
-        {
-            categoryHeader = Translation.Get("posePane", "categoryHeader");
-            nameHeader = Translation.Get("posePane", "nameHeader");
-            savePoseButton.Label = Translation.Get("posePane", "saveButton");
-        }
-
-        public override void Draw()
-        {
-            GUI.enabled = meidoManager.HasActiveMeido;
-
-            MpsGui.Header(categoryHeader);
-            categoryComboBox.Draw(GUILayout.Width(160f));
-
-            MpsGui.Header(nameHeader);
-            GUILayout.BeginHorizontal();
-            poseNameTextField.Draw(GUILayout.Width(160f));
-            savePoseButton.Draw(GUILayout.ExpandWidth(false));
-            GUILayout.EndHorizontal();
-
-            GUI.enabled = true;
-        }
-
-        private void OnSavePose(object sender, EventArgs args)
-        {
-            byte[] anmBinary = meidoManager.ActiveMeido.SerializePose();
-            Constants.AddPose(anmBinary, poseNameTextField.Value, categoryComboBox.Value);
-            poseNameTextField.Value = string.Empty;
-        }
+        GUI.enabled = meidoManager.HasActiveMeido;
+
+        MpsGui.Header(categoryHeader);
+        categoryComboBox.Draw(GUILayout.Width(160f));
+
+        MpsGui.Header(nameHeader);
+        GUILayout.BeginHorizontal();
+        poseNameTextField.Draw(GUILayout.Width(160f));
+        savePoseButton.Draw(GUILayout.ExpandWidth(false));
+        GUILayout.EndHorizontal();
+
+        GUI.enabled = true;
+    }
+
+    protected override void ReloadTranslation()
+    {
+        categoryHeader = Translation.Get("posePane", "categoryHeader");
+        nameHeader = Translation.Get("posePane", "nameHeader");
+        savePoseButton.Label = Translation.Get("posePane", "saveButton");
+    }
+
+    private void OnSavePose(object sender, EventArgs args)
+    {
+        var anmBinary = meidoManager.ActiveMeido.SerializePose();
+
+        Constants.AddPose(anmBinary, poseNameTextField.Value, categoryComboBox.Value);
+        poseNameTextField.Value = string.Empty;
     }
 }

+ 91 - 83
src/MeidoPhotoStudio.Plugin/GUI/Panes/SceneManagerPanes/SceneManagerDirectoryPane.cs

@@ -1,100 +1,108 @@
 using UnityEngine;
 
-namespace MeidoPhotoStudio.Plugin
+namespace MeidoPhotoStudio.Plugin;
+
+public class SceneManagerDirectoryPane : BasePane
 {
-    public class SceneManagerDirectoryPane : BasePane
+    public static readonly int ListWidth = 200;
+
+    private readonly SceneManager sceneManager;
+    private readonly SceneModalWindow sceneModalWindow;
+    private readonly Button createDirectoryButton;
+    private readonly Button deleteDirectoryButton;
+    private readonly TextField directoryTextField;
+    private readonly Button cancelButton;
+    private readonly Texture2D selectedTexture = Utility.MakeTex(2, 2, new(0.5f, 0.5f, 0.5f, 0.4f));
+
+    private Vector2 listScrollPos;
+    private bool createDirectoryMode;
+
+    public SceneManagerDirectoryPane(SceneManager sceneManager, SceneModalWindow sceneModalWindow)
     {
-        public static readonly int listWidth = 200;
-        private readonly SceneManager sceneManager;
-        private readonly SceneModalWindow sceneModalWindow;
-        private readonly Button createDirectoryButton;
-        private readonly Button deleteDirectoryButton;
-        private readonly TextField directoryTextField;
-        private readonly Button cancelButton;
-        private readonly Texture2D selectedTexture = Utility.MakeTex(2, 2, new Color(0.5f, 0.5f, 0.5f, 0.4f));
-        private Vector2 listScrollPos;
-        private bool createDirectoryMode;
-
-        public SceneManagerDirectoryPane(SceneManager sceneManager, SceneModalWindow sceneModalWindow)
+        this.sceneManager = sceneManager;
+        this.sceneModalWindow = sceneModalWindow;
+
+        createDirectoryButton = new(Translation.Get("sceneManager", "createDirectoryButton"));
+        createDirectoryButton.ControlEvent += (_, _) =>
+            createDirectoryMode = true;
+
+        deleteDirectoryButton = new(Translation.Get("sceneManager", "deleteDirectoryButton"));
+        deleteDirectoryButton.ControlEvent += (_, _) =>
+            this.sceneModalWindow.ShowDirectoryDialogue();
+
+        directoryTextField = new();
+        directoryTextField.ControlEvent += (_, _) =>
         {
-            this.sceneManager = sceneManager;
-            this.sceneModalWindow = sceneModalWindow;
+            sceneManager.AddDirectory(directoryTextField.Value);
+            createDirectoryMode = false;
+            directoryTextField.Value = string.Empty;
+        };
+
+        cancelButton = new("X");
+        cancelButton.ControlEvent += (_, _) =>
+            createDirectoryMode = false;
+    }
+
+    public override void Draw()
+    {
+        var directoryStyle = new GUIStyle(GUI.skin.button)
+        {
+            fontSize = Utility.GetPix(12),
+            alignment = TextAnchor.MiddleLeft,
+            margin = new(0, 0, 0, 0),
+        };
 
-            createDirectoryButton = new Button(Translation.Get("sceneManager", "createDirectoryButton"));
-            createDirectoryButton.ControlEvent += (s, a) => createDirectoryMode = true;
+        var directorySelectedStyle = new GUIStyle(directoryStyle);
 
-            deleteDirectoryButton = new Button(Translation.Get("sceneManager", "deleteDirectoryButton"));
-            deleteDirectoryButton.ControlEvent += (s, a) => this.sceneModalWindow.ShowDirectoryDialogue();
+        directorySelectedStyle.normal.textColor = Color.white;
+        directorySelectedStyle.normal.background = selectedTexture;
+        directorySelectedStyle.hover.background = selectedTexture;
 
-            directoryTextField = new TextField();
-            directoryTextField.ControlEvent += (s, a) =>
-            {
-                sceneManager.AddDirectory(directoryTextField.Value);
-                createDirectoryMode = false;
-                directoryTextField.Value = string.Empty;
-            };
+        GUILayout.BeginVertical(GUILayout.Width(Utility.GetPix(ListWidth)));
 
-            cancelButton = new Button("X");
-            cancelButton.ControlEvent += (s, a) => createDirectoryMode = false;
-        }
+        listScrollPos = GUILayout.BeginScrollView(listScrollPos);
 
-        protected override void ReloadTranslation()
+        for (var i = 0; i < sceneManager.CurrentDirectoryList.Count; i++)
         {
-            createDirectoryButton.Label = Translation.Get("sceneManager", "createDirectoryButton");
-            deleteDirectoryButton.Label = Translation.Get("sceneManager", "deleteDirectoryButton");
+            var style = i == sceneManager.CurrentDirectoryIndex ? directorySelectedStyle : directoryStyle;
+            var directoryName = sceneManager.CurrentDirectoryList[i];
+
+            if (GUILayout.Button(directoryName, style, GUILayout.Height(Utility.GetPix(20))))
+                sceneManager.SelectDirectory(i);
         }
 
-        public override void Draw()
+        GUILayout.EndScrollView();
+
+        GUILayout.BeginHorizontal();
+
+        var buttonStyle = new GUIStyle(GUI.skin.button)
+        {
+            fontSize = Utility.GetPix(12),
+        };
+
+        var buttonHeight = GUILayout.Height(Utility.GetPix(20));
+
+        if (createDirectoryMode)
+        {
+            directoryTextField.Draw(buttonHeight, GUILayout.Width(Utility.GetPix(ListWidth - 30)));
+            cancelButton.Draw(buttonStyle, buttonHeight, GUILayout.ExpandWidth(false));
+        }
+        else
         {
-            GUIStyle directoryStyle = new GUIStyle(GUI.skin.button)
-            {
-                fontSize = Utility.GetPix(12),
-                alignment = TextAnchor.MiddleLeft,
-                margin = new RectOffset(0, 0, 0, 0)
-            };
-
-            GUIStyle directorySelectedStyle = new GUIStyle(directoryStyle);
-            directorySelectedStyle.normal.textColor = Color.white;
-            directorySelectedStyle.normal.background = selectedTexture;
-            directorySelectedStyle.hover.background = selectedTexture;
-
-            GUILayout.BeginVertical(GUILayout.Width(Utility.GetPix(listWidth)));
-
-            listScrollPos = GUILayout.BeginScrollView(listScrollPos);
-
-            for (int i = 0; i < sceneManager.CurrentDirectoryList.Count; i++)
-            {
-                GUIStyle style = i == sceneManager.CurrentDirectoryIndex ? directorySelectedStyle : directoryStyle;
-                string directoryName = sceneManager.CurrentDirectoryList[i];
-                if (GUILayout.Button(directoryName, style, GUILayout.Height(Utility.GetPix(20))))
-                {
-                    sceneManager.SelectDirectory(i);
-                }
-            }
-
-            GUILayout.EndScrollView();
-
-            GUILayout.BeginHorizontal();
-
-            GUIStyle buttonStyle = new GUIStyle(GUI.skin.button) { fontSize = Utility.GetPix(12) };
-
-            GUILayoutOption buttonHeight = GUILayout.Height(Utility.GetPix(20));
-
-            if (createDirectoryMode)
-            {
-                directoryTextField.Draw(buttonHeight, GUILayout.Width(Utility.GetPix(listWidth - 30)));
-                cancelButton.Draw(buttonStyle, buttonHeight, GUILayout.ExpandWidth(false));
-            }
-            else
-            {
-                createDirectoryButton.Draw(buttonStyle, buttonHeight);
-                GUI.enabled = sceneManager.CurrentDirectoryIndex > 0;
-                deleteDirectoryButton.Draw(buttonStyle, buttonHeight, GUILayout.ExpandWidth(false));
-                GUI.enabled = true;
-            }
-            GUILayout.EndHorizontal();
-
-            GUILayout.EndVertical();
+            createDirectoryButton.Draw(buttonStyle, buttonHeight);
+            GUI.enabled = sceneManager.CurrentDirectoryIndex > 0;
+            deleteDirectoryButton.Draw(buttonStyle, buttonHeight, GUILayout.ExpandWidth(false));
+            GUI.enabled = true;
         }
+
+        GUILayout.EndHorizontal();
+
+        GUILayout.EndVertical();
+    }
+
+    protected override void ReloadTranslation()
+    {
+        createDirectoryButton.Label = Translation.Get("sceneManager", "createDirectoryButton");
+        deleteDirectoryButton.Label = Translation.Get("sceneManager", "deleteDirectoryButton");
     }
 }

+ 64 - 56
src/MeidoPhotoStudio.Plugin/GUI/Panes/SceneManagerPanes/SceneManagerScenePane.cs

@@ -1,85 +1,93 @@
 using UnityEngine;
 
-namespace MeidoPhotoStudio.Plugin
+namespace MeidoPhotoStudio.Plugin;
+
+public class SceneManagerScenePane : BasePane
 {
-    public class SceneManagerScenePane : BasePane
+    public static readonly float ThumbnailScale = 0.55f;
+    private readonly SceneManager sceneManager;
+    private readonly SceneModalWindow sceneModalWindow;
+    private readonly Button addSceneButton;
+    private Vector2 sceneScrollPos;
+
+    public SceneManagerScenePane(SceneManager sceneManager, SceneModalWindow sceneModalWindow)
     {
-        public static readonly float thumbnailScale = 0.55f;
-        private readonly SceneManager sceneManager;
-        private readonly SceneModalWindow sceneModalWindow;
-        private readonly Button addSceneButton;
-        private Vector2 sceneScrollPos;
+        this.sceneManager = sceneManager;
+        this.sceneModalWindow = sceneModalWindow;
 
-        public SceneManagerScenePane(SceneManager sceneManager, SceneModalWindow sceneModalWindow)
-        {
-            this.sceneManager = sceneManager;
-            this.sceneModalWindow = sceneModalWindow;
+        addSceneButton = new("+");
+        addSceneButton.ControlEvent += (_, _) =>
+            sceneManager.SaveScene(overwrite: false);
+    }
 
-            addSceneButton = new Button("+");
-            addSceneButton.ControlEvent += (s, a) => sceneManager.SaveScene(overwrite: false);
-        }
+    public override void Draw()
+    {
+        var sceneImageStyle = new GUIStyle(GUI.skin.label)
+        {
+            alignment = TextAnchor.MiddleCenter,
+            padding = new RectOffset(0, 0, 0, 0),
+        };
 
-        public override void Draw()
+        var addSceneStyle = new GUIStyle(GUI.skin.button)
         {
-            GUIStyle sceneImageStyle = new GUIStyle(GUI.skin.label)
-            {
-                alignment = TextAnchor.MiddleCenter,
-                padding = new RectOffset(0, 0, 0, 0)
-            };
+            alignment = TextAnchor.MiddleCenter,
+            fontSize = 60,
+        };
 
-            GUIStyle addSceneStyle = new GUIStyle(GUI.skin.button)
-            {
-                alignment = TextAnchor.MiddleCenter,
-                fontSize = 60
-            };
+        GUILayout.BeginVertical();
+
+        var sceneWidth = SceneManager.SceneDimensions.x * ThumbnailScale;
+        var sceneHeight = SceneManager.SceneDimensions.y * ThumbnailScale;
+        var sceneGridWidth = parent.WindowRect.width - SceneManagerDirectoryPane.ListWidth;
 
-            GUILayout.BeginVertical();
+        var sceneLayoutOptions = new[]
+        {
+            GUILayout.Height(sceneHeight),
+            GUILayout.Width(sceneWidth),
+        };
 
-            float sceneWidth = SceneManager.sceneDimensions.x * thumbnailScale;
-            float sceneHeight = SceneManager.sceneDimensions.y * thumbnailScale;
-            float sceneGridWidth = parent.WindowRect.width - SceneManagerDirectoryPane.listWidth;
+        var columns = Mathf.Max(1, (int)(sceneGridWidth / sceneWidth));
+        var rows = (int)Mathf.Ceil(sceneManager.SceneList.Count + 1 / (float)columns);
 
-            GUILayoutOption[] sceneLayoutOptions = new[] { GUILayout.Height(sceneHeight), GUILayout.Width(sceneWidth) };
+        sceneScrollPos = GUILayout.BeginScrollView(sceneScrollPos);
 
-            int columns = Mathf.Max(1, (int)(sceneGridWidth / sceneWidth));
-            int rows = (int)Mathf.Ceil(sceneManager.SceneList.Count + (1 / (float)columns));
+        GUILayout.BeginHorizontal();
+        GUILayout.FlexibleSpace();
+        GUILayout.BeginVertical();
 
-            sceneScrollPos = GUILayout.BeginScrollView(sceneScrollPos);
+        var currentScene = -1;
 
+        for (var i = 0; i < rows; i++)
+        {
             GUILayout.BeginHorizontal();
-            GUILayout.FlexibleSpace();
-            GUILayout.BeginVertical();
 
-            int currentScene = -1;
-            for (int i = 0; i < rows; i++)
+            for (var j = 0; j < columns; j++, currentScene++)
             {
-                GUILayout.BeginHorizontal();
-                for (int j = 0; j < columns; j++, currentScene++)
+                if (currentScene is -1)
                 {
-                    if (currentScene == -1)
-                    {
-                        addSceneButton.Draw(addSceneStyle, sceneLayoutOptions);
-                    }
-                    else if (currentScene < sceneManager.SceneList.Count)
+                    addSceneButton.Draw(addSceneStyle, sceneLayoutOptions);
+                }
+                else if (currentScene < sceneManager.SceneList.Count)
+                {
+                    var scene = sceneManager.SceneList[currentScene];
+
+                    if (GUILayout.Button(scene.Thumbnail, sceneImageStyle, sceneLayoutOptions))
                     {
-                        var scene = sceneManager.SceneList[currentScene];
-                        if (GUILayout.Button(scene.Thumbnail, sceneImageStyle, sceneLayoutOptions))
-                        {
-                            sceneManager.SelectScene(currentScene);
-                            sceneModalWindow.ShowSceneDialogue();
-                        }
+                        sceneManager.SelectScene(currentScene);
+                        sceneModalWindow.ShowSceneDialogue();
                     }
                 }
-                GUILayout.EndHorizontal();
             }
 
-            GUILayout.EndVertical();
-            GUILayout.FlexibleSpace();
             GUILayout.EndHorizontal();
+        }
 
-            GUILayout.EndScrollView();
+        GUILayout.EndVertical();
+        GUILayout.FlexibleSpace();
+        GUILayout.EndHorizontal();
 
-            GUILayout.EndVertical();
-        }
+        GUILayout.EndScrollView();
+
+        GUILayout.EndVertical();
     }
 }

+ 0 - 108
src/MeidoPhotoStudio.Plugin/GUI/Panes/SceneManagerPanes/SceneManagerTitleBar.cs

@@ -1,108 +0,0 @@
-using UnityEngine;
-
-namespace MeidoPhotoStudio.Plugin
-{
-    public class SceneManagerTitleBarPane : BasePane
-    {
-        private static readonly string[] sortModes = new[] { "sortName", "sortCreated", "sortModified" };
-        private readonly SceneManager sceneManager;
-        private readonly Button kankyoToggle;
-        private readonly Button refreshButton;
-        private readonly Dropdown sortDropdown;
-        private readonly Toggle descendingToggle;
-        private readonly Button closeButton;
-        private string sortLabel;
-        public event System.EventHandler CloseChange;
-
-        public SceneManagerTitleBarPane(SceneManager sceneManager)
-        {
-            this.sceneManager = sceneManager;
-            kankyoToggle = new Button(Translation.Get("sceneManager", "kankyoToggle"));
-            kankyoToggle.ControlEvent += (s, a) => sceneManager.ToggleKankyoMode();
-
-            refreshButton = new Button(Translation.Get("sceneManager", "refreshButton"));
-            refreshButton.ControlEvent += (s, a) => sceneManager.Refresh();
-
-            sortDropdown = new Dropdown(
-                Translation.GetArray("sceneManager", sortModes), (int)sceneManager.CurrentSortMode
-            );
-            sortDropdown.SelectionChange += (s, a) =>
-            {
-                SceneManager.SortMode sortMode = (SceneManager.SortMode)sortDropdown.SelectedItemIndex;
-                if (sceneManager.CurrentSortMode == sortMode) return;
-                sceneManager.SortScenes(sortMode);
-            };
-
-            descendingToggle = new Toggle(
-                Translation.Get("sceneManager", "descendingToggle"), sceneManager.SortDescending
-            );
-            descendingToggle.ControlEvent += (s, a) =>
-            {
-                sceneManager.SortDescending = descendingToggle.Value;
-                sceneManager.SortScenes(sceneManager.CurrentSortMode);
-            };
-
-            closeButton = new Button("X");
-            closeButton.ControlEvent += (s, a) => CloseChange?.Invoke(this, System.EventArgs.Empty);
-
-            sortLabel = Translation.Get("sceneManager", "sortLabel");
-        }
-
-        protected override void ReloadTranslation()
-        {
-            kankyoToggle.Label = Translation.Get("sceneManager", "kankyoToggle");
-            refreshButton.Label = Translation.Get("sceneManager", "refreshButton");
-            sortDropdown.SetDropdownItems(Translation.GetArray("sceneManager", sortModes));
-            descendingToggle.Label = Translation.Get("sceneManager", "descendingToggle");
-            sortLabel = Translation.Get("sceneManager", "sortLabel");
-        }
-
-        public override void Draw()
-        {
-            GUIStyle buttonStyle = new GUIStyle(GUI.skin.button) { fontSize = Utility.GetPix(12) };
-
-            GUILayoutOption buttonHeight = GUILayout.Height(Utility.GetPix(20));
-            GUILayout.BeginHorizontal();
-
-            GUILayout.BeginHorizontal(GUILayout.Width(Utility.GetPix(SceneManagerDirectoryPane.listWidth)));
-
-            Color originalColour = GUI.backgroundColor;
-            if (sceneManager.KankyoMode) GUI.backgroundColor = Color.green;
-            kankyoToggle.Draw(buttonStyle, buttonHeight);
-            GUI.backgroundColor = originalColour;
-
-            GUILayout.FlexibleSpace();
-
-            refreshButton.Draw(buttonStyle, buttonHeight);
-
-            GUILayout.EndHorizontal();
-
-            GUILayout.BeginHorizontal();
-
-            GUILayout.Space(Utility.GetPix(15));
-
-            GUIStyle labelStyle = new GUIStyle(GUI.skin.label) { fontSize = buttonStyle.fontSize };
-
-            GUILayout.Label(sortLabel, labelStyle);
-
-            GUIStyle dropdownStyle = new GUIStyle(DropdownHelper.DefaultDropdownStyle)
-            {
-                fontSize = buttonStyle.fontSize
-            };
-
-            sortDropdown.Draw(buttonStyle, dropdownStyle, buttonHeight, GUILayout.Width(Utility.GetPix(100)));
-
-            GUIStyle toggleStyle = new GUIStyle(GUI.skin.toggle) { fontSize = buttonStyle.fontSize };
-
-            descendingToggle.Draw(toggleStyle);
-
-            GUILayout.FlexibleSpace();
-
-            closeButton.Draw();
-
-            GUILayout.EndHorizontal();
-
-            GUILayout.EndHorizontal();
-        }
-    }
-}

+ 125 - 0
src/MeidoPhotoStudio.Plugin/GUI/Panes/SceneManagerPanes/SceneManagerTitleBarPane.cs

@@ -0,0 +1,125 @@
+using UnityEngine;
+
+namespace MeidoPhotoStudio.Plugin;
+
+public class SceneManagerTitleBarPane : BasePane
+{
+    private static readonly string[] SortModes = new[] { "sortName", "sortCreated", "sortModified" };
+
+    private readonly SceneManager sceneManager;
+    private readonly Button kankyoToggle;
+    private readonly Button refreshButton;
+    private readonly Dropdown sortDropdown;
+    private readonly Toggle descendingToggle;
+    private readonly Button closeButton;
+
+    private string sortLabel;
+
+    public SceneManagerTitleBarPane(SceneManager sceneManager)
+    {
+        this.sceneManager = sceneManager;
+        kankyoToggle = new(Translation.Get("sceneManager", "kankyoToggle"));
+        kankyoToggle.ControlEvent += (_, _) =>
+            sceneManager.ToggleKankyoMode();
+
+        refreshButton = new(Translation.Get("sceneManager", "refreshButton"));
+        refreshButton.ControlEvent += (_, _) =>
+            sceneManager.Refresh();
+
+        sortDropdown = new(Translation.GetArray("sceneManager", SortModes), (int)sceneManager.CurrentSortMode);
+        sortDropdown.SelectionChange += (_, _) =>
+        {
+            var sortMode = (SceneManager.SortMode)sortDropdown.SelectedItemIndex;
+
+            if (sceneManager.CurrentSortMode == sortMode)
+                return;
+
+            sceneManager.SortScenes(sortMode);
+        };
+
+        descendingToggle = new(Translation.Get("sceneManager", "descendingToggle"), sceneManager.SortDescending);
+        descendingToggle.ControlEvent += (_, _) =>
+        {
+            sceneManager.SortDescending = descendingToggle.Value;
+            sceneManager.SortScenes(sceneManager.CurrentSortMode);
+        };
+
+        closeButton = new("X");
+        closeButton.ControlEvent += (_, _) =>
+            CloseChange?.Invoke(this, System.EventArgs.Empty);
+
+        sortLabel = Translation.Get("sceneManager", "sortLabel");
+    }
+
+    public event System.EventHandler CloseChange;
+
+    public override void Draw()
+    {
+        var buttonStyle = new GUIStyle(GUI.skin.button)
+        {
+            fontSize = Utility.GetPix(12),
+        };
+
+        var buttonHeight = GUILayout.Height(Utility.GetPix(20));
+
+        GUILayout.BeginHorizontal();
+
+        GUILayout.BeginHorizontal(GUILayout.Width(Utility.GetPix(SceneManagerDirectoryPane.ListWidth)));
+
+        var originalColour = GUI.backgroundColor;
+
+        if (sceneManager.KankyoMode)
+            GUI.backgroundColor = Color.green;
+
+        kankyoToggle.Draw(buttonStyle, buttonHeight);
+        GUI.backgroundColor = originalColour;
+
+        GUILayout.FlexibleSpace();
+
+        refreshButton.Draw(buttonStyle, buttonHeight);
+
+        GUILayout.EndHorizontal();
+
+        GUILayout.BeginHorizontal();
+
+        GUILayout.Space(Utility.GetPix(15));
+
+        var labelStyle = new GUIStyle(GUI.skin.label)
+        {
+            fontSize = buttonStyle.fontSize,
+        };
+
+        GUILayout.Label(sortLabel, labelStyle);
+
+        var dropdownStyle = new GUIStyle(DropdownHelper.DefaultDropdownStyle)
+        {
+            fontSize = buttonStyle.fontSize,
+        };
+
+        sortDropdown.Draw(buttonStyle, dropdownStyle, buttonHeight, GUILayout.Width(Utility.GetPix(100)));
+
+        var toggleStyle = new GUIStyle(GUI.skin.toggle)
+        {
+            fontSize = buttonStyle.fontSize,
+        };
+
+        descendingToggle.Draw(toggleStyle);
+
+        GUILayout.FlexibleSpace();
+
+        closeButton.Draw();
+
+        GUILayout.EndHorizontal();
+
+        GUILayout.EndHorizontal();
+    }
+
+    protected override void ReloadTranslation()
+    {
+        kankyoToggle.Label = Translation.Get("sceneManager", "kankyoToggle");
+        refreshButton.Label = Translation.Get("sceneManager", "refreshButton");
+        sortDropdown.SetDropdownItems(Translation.GetArray("sceneManager", SortModes));
+        descendingToggle.Label = Translation.Get("sceneManager", "descendingToggle");
+        sortLabel = Translation.Get("sceneManager", "sortLabel");
+    }
+}

+ 76 - 68
src/MeidoPhotoStudio.Plugin/GUI/Windows/BaseWindow.cs

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

+ 153 - 126
src/MeidoPhotoStudio.Plugin/GUI/Windows/MainWindow.cs

@@ -1,162 +1,189 @@
 using System.Collections.Generic;
+
 using UnityEngine;
 
-namespace MeidoPhotoStudio.Plugin
+namespace MeidoPhotoStudio.Plugin;
+
+public class MainWindow : BaseWindow
 {
-    public class MainWindow : BaseWindow
+    private readonly MeidoManager meidoManager;
+    private readonly Dictionary<Constants.Window, BaseMainWindowPane> windowPanes;
+    private readonly PropManager propManager;
+    private readonly LightManager lightManager;
+    private readonly TabsPane tabsPane;
+    private readonly Button settingsButton;
+
+    private BaseMainWindowPane currentWindowPane;
+    private string settingsButtonLabel;
+    private string closeButtonLabel;
+    private Constants.Window selectedWindow;
+
+    // TODO: Find a better way of doing this
+    public MainWindow(MeidoManager meidoManager, PropManager propManager, LightManager lightManager)
     {
-        private readonly MeidoManager meidoManager;
-        private readonly Dictionary<Constants.Window, BaseMainWindowPane> windowPanes;
-        private readonly PropManager propManager;
-        private readonly LightManager lightManager;
-        private readonly TabsPane tabsPane;
-        private readonly Button settingsButton;
-        private BaseMainWindowPane currentWindowPane;
-        private string settingsButtonLabel;
-        private string closeButtonLabel;
-
-        public override Rect WindowRect
+        this.meidoManager = meidoManager;
+        this.meidoManager.UpdateMeido += UpdateMeido;
+
+        this.propManager = propManager;
+        this.propManager.FromPropSelect += (_, _) =>
+            ChangeWindow(Constants.Window.BG2);
+
+        this.lightManager = lightManager;
+        this.lightManager.Select += (_, _) =>
+            ChangeWindow(Constants.Window.BG);
+
+        windowPanes = new();
+        WindowRect = new(Screen.width, Screen.height * 0.08f, 240f, Screen.height * 0.9f);
+
+        tabsPane = new();
+        tabsPane.TabChange += (_, _) =>
+            ChangeTab();
+
+        settingsButtonLabel = Translation.Get("settingsLabels", "settingsButton");
+        closeButtonLabel = Translation.Get("settingsLabels", "closeSettingsButton");
+
+        settingsButton = new(settingsButtonLabel);
+        settingsButton.ControlEvent += (_, _) =>
         {
-            set
+            if (selectedWindow is Constants.Window.Settings)
             {
-                value.width = 240f;
-                value.height = Screen.height * 0.9f;
-                if (MeidoPhotoStudio.EditMode) value.height *= 0.85f;
-                value.x = Mathf.Clamp(value.x, 0, Screen.width - value.width);
-                value.y = Mathf.Clamp(value.y, -value.height + 30, Screen.height - 50);
-                windowRect = value;
+                ChangeTab();
             }
-        }
-        private Constants.Window selectedWindow;
+            else
+            {
+                settingsButton.Label = closeButtonLabel;
+                SetCurrentWindow(Constants.Window.Settings);
+            }
+        };
+    }
 
-        public BaseMainWindowPane this[Constants.Window id]
+    public override Rect WindowRect
+    {
+        set
         {
-            get => windowPanes[id];
-            set => AddWindow(id, value);
+            value.width = 240f;
+            value.height = Screen.height * 0.9f;
+
+            if (MeidoPhotoStudio.EditMode)
+                value.height *= 0.85f;
+
+            value.x = Mathf.Clamp(value.x, 0, Screen.width - value.width);
+            value.y = Mathf.Clamp(value.y, -value.height + 30, Screen.height - 50);
+
+            windowRect = value;
         }
+    }
 
-        // TODO: Find a better way of doing this
-        public MainWindow(MeidoManager meidoManager, PropManager propManager, LightManager lightManager)
-        {
-            this.meidoManager = meidoManager;
-            this.meidoManager.UpdateMeido += UpdateMeido;
+    public BaseMainWindowPane this[Constants.Window id]
+    {
+        get => windowPanes[id];
+        set => AddWindow(id, value);
+    }
 
-            this.propManager = propManager;
-            this.propManager.FromPropSelect += (s, a) => ChangeWindow(Constants.Window.BG2);
+    public void AddWindow(Constants.Window id, BaseMainWindowPane window)
+    {
+        if (windowPanes.ContainsKey(id))
+            Panes.Remove(windowPanes[id]);
 
-            this.lightManager = lightManager;
-            this.lightManager.Select += (s, a) => ChangeWindow(Constants.Window.BG);
+        windowPanes[id] = window;
+        windowPanes[id].SetTabsPane(tabsPane);
+        windowPanes[id].SetParent(this);
 
-            windowPanes = new Dictionary<Constants.Window, BaseMainWindowPane>();
-            WindowRect = new Rect(Screen.width, Screen.height * 0.08f, 240f, Screen.height * 0.9f);
+        Panes.Add(windowPanes[id]);
+    }
 
-            tabsPane = new TabsPane();
-            tabsPane.TabChange += (s, a) => ChangeTab();
+    public override void Activate()
+    {
+        base.Activate();
 
-            settingsButtonLabel = Translation.Get("settingsLabels", "settingsButton");
-            closeButtonLabel = Translation.Get("settingsLabels", "closeSettingsButton");
+        updating = true;
+        tabsPane.SelectedTab = Constants.Window.Call;
+        updating = false;
+        Visible = true;
+    }
 
-            settingsButton = new(settingsButtonLabel);
-            settingsButton.ControlEvent += (s, a) =>
-            {
-                if (selectedWindow == Constants.Window.Settings) ChangeTab();
-                else
-                {
-                    settingsButton.Label = closeButtonLabel;
-                    SetCurrentWindow(Constants.Window.Settings);
-                }
-            };
-        }
+    public override void Update()
+    {
+        base.Update();
 
-        protected override void ReloadTranslation()
-        {
-            settingsButtonLabel = Translation.Get("settingsLabels", "settingsButton");
-            closeButtonLabel = Translation.Get("settingsLabels", "closeSettingsButton");
-            settingsButton.Label = selectedWindow == Constants.Window.Settings ? closeButtonLabel : settingsButtonLabel;
-        }
+        if (InputManager.GetKeyDown(MpsKey.ToggleUI))
+            Visible = !Visible;
+    }
 
-        public override void Activate()
-        {
-            base.Activate();
-            updating = true;
-            tabsPane.SelectedTab = Constants.Window.Call;
-            updating = false;
-            Visible = true;
-        }
+    public override void Draw()
+    {
+        currentWindowPane?.Draw();
 
-        public void AddWindow(Constants.Window id, BaseMainWindowPane window)
-        {
-            if (windowPanes.ContainsKey(id))
-            {
-                Panes.Remove(windowPanes[id]);
-            }
-            windowPanes[id] = window;
-            windowPanes[id].SetTabsPane(tabsPane);
-            windowPanes[id].SetParent(this);
-            Panes.Add(windowPanes[id]);
-        }
+        GUI.enabled = true;
 
-        private void ChangeTab()
-        {
-            settingsButton.Label = Translation.Get("settingsLabels", "settingsButton");
-            SetCurrentWindow(tabsPane.SelectedTab);
-        }
+        GUILayout.FlexibleSpace();
 
-        private void SetCurrentWindow(Constants.Window window)
+        var labelStyle = new GUIStyle(GUI.skin.label)
         {
-            if (currentWindowPane != null) currentWindowPane.ActiveWindow = false;
-            selectedWindow = window;
-            currentWindowPane = windowPanes[selectedWindow];
-            currentWindowPane.ActiveWindow = true;
-            currentWindowPane.UpdatePanes();
-        }
+            fontSize = 10,
+            alignment = TextAnchor.LowerLeft,
+        };
 
-        public override void Update()
-        {
-            base.Update();
-            if (InputManager.GetKeyDown(MpsKey.ToggleUI)) Visible = !Visible;
-        }
+        GUILayout.BeginHorizontal();
+        GUILayout.Label(MeidoPhotoStudio.PluginString, labelStyle);
+        GUILayout.FlexibleSpace();
 
-        public override void Draw()
-        {
-            currentWindowPane?.Draw();
+        GUI.enabled = !InputManager.Listening;
 
-            GUI.enabled = true;
+        settingsButton.Draw(GUILayout.ExpandWidth(false));
 
-            GUILayout.FlexibleSpace();
+        GUI.enabled = true;
 
-            GUIStyle labelStyle = new GUIStyle(GUI.skin.label)
-            {
-                fontSize = 10,
-                alignment = TextAnchor.LowerLeft
-            };
-
-            GUILayout.BeginHorizontal();
-            GUILayout.Label(MeidoPhotoStudio.pluginString, labelStyle);
-            GUILayout.FlexibleSpace();
-            GUI.enabled = !InputManager.Listening;
-            settingsButton.Draw(GUILayout.ExpandWidth(false));
-            GUI.enabled = true;
-            GUILayout.EndHorizontal();
-
-            GUI.DragWindow();
-        }
+        GUILayout.EndHorizontal();
+
+        GUI.DragWindow();
+    }
+
+    protected override void ReloadTranslation()
+    {
+        settingsButtonLabel = Translation.Get("settingsLabels", "settingsButton");
+        closeButtonLabel = Translation.Get("settingsLabels", "closeSettingsButton");
+        settingsButton.Label = selectedWindow == Constants.Window.Settings ? closeButtonLabel : settingsButtonLabel;
+    }
+
+    private void ChangeTab()
+    {
+        settingsButton.Label = Translation.Get("settingsLabels", "settingsButton");
+        SetCurrentWindow(tabsPane.SelectedTab);
+    }
 
-        private void UpdateMeido(object sender, MeidoUpdateEventArgs args)
+    private void SetCurrentWindow(Constants.Window window)
+    {
+        if (currentWindowPane is not null)
+            currentWindowPane.ActiveWindow = false;
+
+        selectedWindow = window;
+        currentWindowPane = windowPanes[selectedWindow];
+        currentWindowPane.ActiveWindow = true;
+        currentWindowPane.UpdatePanes();
+    }
+
+    private void UpdateMeido(object sender, MeidoUpdateEventArgs args)
+    {
+        if (args.FromMeido)
         {
-            if (args.FromMeido)
-            {
-                Constants.Window newWindow = args.IsBody ? Constants.Window.Pose : Constants.Window.Face;
-                ChangeWindow(newWindow);
-            }
-            else currentWindowPane.UpdatePanes();
-        }
+            var newWindow = args.IsBody ? Constants.Window.Pose : Constants.Window.Face;
 
-        private void ChangeWindow(Constants.Window window)
+            ChangeWindow(newWindow);
+        }
+        else
         {
-            if (selectedWindow == window) currentWindowPane.UpdatePanes();
-            else tabsPane.SelectedTab = window;
-            Visible = true;
+            currentWindowPane.UpdatePanes();
         }
     }
+
+    private void ChangeWindow(Constants.Window window)
+    {
+        if (selectedWindow == window)
+            currentWindowPane.UpdatePanes();
+        else
+            tabsPane.SelectedTab = window;
+
+        Visible = true;
+    }
 }

+ 87 - 79
src/MeidoPhotoStudio.Plugin/GUI/Windows/MessageWindow.cs

@@ -1,107 +1,115 @@
 using System;
+
 using UnityEngine;
 
-namespace MeidoPhotoStudio.Plugin
+namespace MeidoPhotoStudio.Plugin;
+
+public class MessageWindow : BaseWindow
 {
-    public class MessageWindow : BaseWindow
+    private readonly MessageWindowManager messageWindowManager;
+    private readonly TextField nameTextField;
+    private readonly Slider fontSizeSlider;
+    private readonly TextArea messageTextArea;
+    private readonly Button okButton;
+
+    private int fontSize = 25;
+
+    public MessageWindow(MessageWindowManager messageWindowManager)
     {
-        private readonly MessageWindowManager messageWindowManager;
-        private readonly TextField nameTextField;
-        private readonly Slider fontSizeSlider;
-        private readonly TextArea messageTextArea;
-        private readonly Button okButton;
-        public override Rect WindowRect
-        {
-            set
-            {
-                value.width = Mathf.Clamp(Screen.width * 0.4f, 440, Mathf.Infinity);
-                value.height = Mathf.Clamp(Screen.height * 0.15f, 150, Mathf.Infinity);
-                base.WindowRect = value;
-            }
-        }
-        private int fontSize = 25;
+        this.messageWindowManager = messageWindowManager;
 
-        public MessageWindow(MessageWindowManager messageWindowManager)
-        {
-            WindowRect = WindowRect;
-            windowRect.x = MiddlePosition.x;
-            windowRect.y = Screen.height - WindowRect.height;
-            this.messageWindowManager = messageWindowManager;
-            nameTextField = new TextField();
+        WindowRect = WindowRect;
+        windowRect.x = MiddlePosition.x;
+        windowRect.y = Screen.height - WindowRect.height;
 
-            fontSizeSlider = new Slider(MessageWindowManager.FontBounds);
-            fontSizeSlider.ControlEvent += ChangeFontSize;
+        nameTextField = new();
 
-            messageTextArea = new TextArea();
+        fontSizeSlider = new(MessageWindowManager.FontBounds);
+        fontSizeSlider.ControlEvent += ChangeFontSize;
 
-            okButton = new Button("OK");
-            okButton.ControlEvent += ShowMessage;
-        }
+        messageTextArea = new();
 
-        private void ToggleVisibility()
+        okButton = new("OK");
+        okButton.ControlEvent += ShowMessage;
+    }
+
+    public override Rect WindowRect
+    {
+        set
         {
-            if (messageWindowManager.ShowingMessage) messageWindowManager.CloseMessagePanel();
-            else Visible = !Visible;
+            value.width = Mathf.Clamp(Screen.width * 0.4f, 440, Mathf.Infinity);
+            value.height = Mathf.Clamp(Screen.height * 0.15f, 150, Mathf.Infinity);
+            base.WindowRect = value;
         }
+    }
 
-        private void ChangeFontSize(object sender, EventArgs args)
-        {
-            fontSize = (int)fontSizeSlider.Value;
+    public override void Update()
+    {
+        base.Update();
 
-            if (updating)
-                return;
+        if (InputManager.GetKeyDown(MpsKey.ToggleMessage))
+            ToggleVisibility();
+    }
 
-            messageWindowManager.FontSize = fontSize;
-        }
+    public override void Draw()
+    {
+        GUILayout.BeginHorizontal();
+        GUILayout.Label("Name", GUILayout.ExpandWidth(false));
+        nameTextField.Draw(GUILayout.Width(120));
 
-        private void ShowMessage(object sender, EventArgs args)
-        {
-            Visible = false;
-            messageWindowManager.ShowMessage(nameTextField.Value, messageTextArea.Value);
-        }
+        GUILayout.Space(30);
 
-        private void ResetUI()
-        {
-            updating = true;
+        GUILayout.Label("Font Size", GUILayout.ExpandWidth(false));
+        fontSizeSlider.Draw(GUILayout.Width(120), GUILayout.ExpandWidth(false));
+        GUILayout.Label($"{fontSize}pt");
+        GUILayout.EndHorizontal();
 
-            fontSizeSlider.Value = MessageWindowManager.FontBounds.Left;
-            nameTextField.Value = string.Empty;
-            messageTextArea.Value = string.Empty;
+        messageTextArea.Draw(GUILayout.MinHeight(90));
+        okButton.Draw(GUILayout.ExpandWidth(false), GUILayout.Width(30));
+    }
 
-            updating = false;
-        }
+    public override void Deactivate()
+    {
+        messageWindowManager.CloseMessagePanel();
+        Visible = false;
+        ResetUI();
+    }
 
-        public override void Update()
-        {
-            base.Update();
-            if (InputManager.GetKeyDown(MpsKey.ToggleMessage)) ToggleVisibility();
-        }
+    public override void Activate() =>
+        ResetUI();
 
-        public override void Draw()
-        {
-            GUILayout.BeginHorizontal();
-            GUILayout.Label("Name", GUILayout.ExpandWidth(false));
-            nameTextField.Draw(GUILayout.Width(120));
+    private void ToggleVisibility()
+    {
+        if (messageWindowManager.ShowingMessage)
+            messageWindowManager.CloseMessagePanel();
+        else
+            Visible = !Visible;
+    }
+
+    private void ChangeFontSize(object sender, EventArgs args)
+    {
+        fontSize = (int)fontSizeSlider.Value;
 
-            GUILayout.Space(30);
+        if (updating)
+            return;
 
-            GUILayout.Label("Font Size", GUILayout.ExpandWidth(false));
-            fontSizeSlider.Draw(GUILayout.Width(120), GUILayout.ExpandWidth(false));
-            GUILayout.Label($"{fontSize}pt");
-            GUILayout.EndHorizontal();
+        messageWindowManager.FontSize = fontSize;
+    }
 
-            messageTextArea.Draw(GUILayout.MinHeight(90));
-            okButton.Draw(GUILayout.ExpandWidth(false), GUILayout.Width(30));
-        }
+    private void ShowMessage(object sender, EventArgs args)
+    {
+        Visible = false;
+        messageWindowManager.ShowMessage(nameTextField.Value, messageTextArea.Value);
+    }
 
-        public override void Deactivate()
-        {
-            messageWindowManager.CloseMessagePanel();
-            Visible = false;
-            ResetUI();
-        }
+    private void ResetUI()
+    {
+        updating = true;
+
+        fontSizeSlider.Value = MessageWindowManager.FontBounds.Left;
+        nameTextField.Value = string.Empty;
+        messageTextArea.Value = string.Empty;
 
-        public override void Activate() =>
-            ResetUI();
+        updating = false;
     }
 }

+ 197 - 185
src/MeidoPhotoStudio.Plugin/GUI/Windows/SceneModalWindow.cs

@@ -1,250 +1,262 @@
 using UnityEngine;
 
-namespace MeidoPhotoStudio.Plugin
+namespace MeidoPhotoStudio.Plugin;
+
+public class SceneModalWindow : BaseWindow
 {
-    public class SceneModalWindow : BaseWindow
+    private static readonly Texture2D InfoHighlight = Utility.MakeTex(2, 2, new(0f, 0f, 0f, 0.8f));
+
+    private readonly SceneManager sceneManager;
+    private readonly Button okButton;
+    private readonly Button cancelButton;
+    private readonly Button deleteButton;
+    private readonly Button overwriteButton;
+
+    private bool visible;
+    private string deleteDirectoryMessage;
+    private string deleteSceneMessage;
+    private string directoryDeleteCommit;
+    private string sceneDeleteCommit;
+    private string sceneLoadCommit;
+    private string infoKankyo;
+    private string infoMaidSingular;
+    private string infoMaidPlural;
+    private bool directoryMode;
+    private bool deleteScene;
+
+    public SceneModalWindow(SceneManager sceneManager)
     {
-        private static readonly Texture2D infoHighlight = Utility.MakeTex(2, 2, new Color(0f, 0f, 0f, 0.8f));
-        private readonly SceneManager sceneManager;
-        public override Rect WindowRect
+        ReloadTranslation();
+
+        this.sceneManager = sceneManager;
+
+        windowRect.x = MiddlePosition.x;
+        windowRect.y = MiddlePosition.y;
+
+        okButton = new(sceneLoadCommit);
+        okButton.ControlEvent += (_, _) =>
+            Commit();
+
+        cancelButton = new("Cancel");
+        cancelButton.ControlEvent += (_, _) =>
+            Cancel();
+
+        deleteButton = new("Delete");
+        deleteButton.ControlEvent += (_, _) =>
         {
-            set
-            {
-                value.width = Mathf.Clamp(Screen.width * 0.3f, 360f, 500f);
-                value.height = directoryMode ? 150f : Mathf.Clamp(Screen.height * 0.4f, 240f, 380f);
-                base.WindowRect = value;
-            }
-        }
-        private bool visible;
-        public override bool Visible
+            okButton.Label = sceneDeleteCommit;
+            deleteScene = true;
+        };
+
+        overwriteButton = new("Overwrite");
+        overwriteButton.ControlEvent += (_, _) =>
         {
-            get => visible;
-            set
-            {
-                visible = value;
-                if (value)
-                {
-                    WindowRect = WindowRect;
-                    windowRect.x = MiddlePosition.x;
-                    windowRect.y = MiddlePosition.y;
-                }
-            }
+            sceneManager.OverwriteScene();
+            Visible = false;
+        };
+    }
+
+    public override Rect WindowRect
+    {
+        set
+        {
+            value.width = Mathf.Clamp(Screen.width * 0.3f, 360f, 500f);
+            value.height = directoryMode ? 150f : Mathf.Clamp(Screen.height * 0.4f, 240f, 380f);
+
+            base.WindowRect = value;
         }
+    }
 
-        private readonly Button okButton;
-        private readonly Button cancelButton;
-        private readonly Button deleteButton;
-        private readonly Button overwriteButton;
-        private string deleteDirectoryMessage;
-        private string deleteSceneMessage;
-        private string directoryDeleteCommit;
-        private string sceneDeleteCommit;
-        private string sceneLoadCommit;
-        private string infoKankyo;
-        private string infoMaidSingular;
-        private string infoMaidPlural;
-        private bool directoryMode;
-        private bool deleteScene;
-
-        public SceneModalWindow(SceneManager sceneManager)
+    public override bool Visible
+    {
+        get => visible;
+        set
         {
-            ReloadTranslation();
+            visible = value;
 
-            this.sceneManager = sceneManager;
+            if (!value)
+                return;
 
+            WindowRect = WindowRect;
             windowRect.x = MiddlePosition.x;
             windowRect.y = MiddlePosition.y;
-            okButton = new Button(sceneLoadCommit);
-            okButton.ControlEvent += (s, a) => Commit();
-
-            cancelButton = new Button("Cancel");
-            cancelButton.ControlEvent += (s, a) => Cancel();
-
-            deleteButton = new Button("Delete");
-            deleteButton.ControlEvent += (s, a) =>
-            {
-                okButton.Label = sceneDeleteCommit;
-                deleteScene = true;
-            };
-
-            overwriteButton = new Button("Overwrite");
-            overwriteButton.ControlEvent += (s, a) =>
-            {
-                sceneManager.OverwriteScene();
-                Visible = false;
-            };
         }
+    }
 
-        protected override void ReloadTranslation()
-        {
-            deleteDirectoryMessage = Translation.Get("sceneManagerModal", "deleteDirectoryConfirm");
-            deleteSceneMessage = Translation.Get("sceneManagerModal", "deleteFileConfirm");
-            directoryDeleteCommit = Translation.Get("sceneManagerModal", "deleteDirectoryButton");
-            sceneDeleteCommit = Translation.Get("sceneManagerModal", "deleteFileCommit");
-            sceneLoadCommit = Translation.Get("sceneManagerModal", "fileLoadCommit");
-            infoKankyo = Translation.Get("sceneManagerModal", "infoKankyo");
-            infoMaidSingular = Translation.Get("sceneManagerModal", "infoMaidSingular");
-            infoMaidPlural = Translation.Get("sceneManagerModal", "infoMaidPlural");
-        }
+    public override void Draw()
+    {
+        GUILayout.BeginArea(new(10f, 10f, WindowRect.width - 20f, WindowRect.height - 20f));
 
-        public override void Draw()
+        // thumbnail
+        if (!directoryMode)
         {
-            GUILayout.BeginArea(new Rect(10f, 10f, WindowRect.width - 20f, WindowRect.height - 20f));
+            var scene = sceneManager.CurrentScene;
+            var thumb = scene.Thumbnail;
 
-            // thumbnail
-            if (!directoryMode)
-            {
-                MPSScene scene = sceneManager.CurrentScene;
-                Texture2D thumb = scene.Thumbnail;
+            var scale = Mathf.Min((WindowRect.width - 20f) / thumb.width, (WindowRect.height - 110f) / thumb.height);
+            var width = Mathf.Min(thumb.width, thumb.width * scale);
+            var height = Mathf.Min(thumb.height, thumb.height * scale);
 
-                float scale = Mathf.Min(
-                    (WindowRect.width - 20f) / thumb.width, (WindowRect.height - 110f) / thumb.height
-                );
-                float width = Mathf.Min(thumb.width, thumb.width * scale);
-                float height = Mathf.Min(thumb.height, thumb.height * scale);
+            GUILayout.BeginHorizontal();
+            GUILayout.FlexibleSpace();
 
-                GUILayout.BeginHorizontal();
-                GUILayout.FlexibleSpace();
+            MpsGui.DrawTexture(thumb, GUILayout.Width(width), GUILayout.Height(height));
 
-                MpsGui.DrawTexture(thumb, GUILayout.Width(width), GUILayout.Height(height));
+            var labelStyle = new GUIStyle(GUI.skin.label)
+            {
+                fontSize = Utility.GetPix(12),
+                alignment = TextAnchor.MiddleCenter,
+            };
 
-                GUIStyle labelStyle = new GUIStyle(GUI.skin.label)
-                {
-                    fontSize = Utility.GetPix(12),
-                    alignment = TextAnchor.MiddleCenter
-                };
-                labelStyle.normal.background = infoHighlight;
+            labelStyle.normal.background = InfoHighlight;
 
-                Rect labelBox = GUILayoutUtility.GetLastRect();
+            var labelBox = GUILayoutUtility.GetLastRect();
 
-                var infoString = scene.Environment
-                    ? infoKankyo
-                    : string.Format(scene.NumberOfMaids == 1 ? infoMaidSingular : infoMaidPlural, scene.NumberOfMaids);
+            var infoString = scene.Environment
+                ? infoKankyo
+                : string.Format(scene.NumberOfMaids is 1 ? infoMaidSingular : infoMaidPlural, scene.NumberOfMaids);
 
-                Vector2 labelSize = labelStyle.CalcSize(new GUIContent(infoString));
+            var labelSize = labelStyle.CalcSize(new GUIContent(infoString));
 
-                labelBox = new Rect(
-                    labelBox.x + 10, labelBox.y + labelBox.height - (labelSize.y + 10),
-                    labelSize.x + 10, labelSize.y + 2
-                );
+            labelBox = new(
+                labelBox.x + 10, labelBox.y + labelBox.height - (labelSize.y + 10), labelSize.x + 10, labelSize.y + 2);
 
-                GUI.Label(labelBox, infoString, labelStyle);
+            GUI.Label(labelBox, infoString, labelStyle);
 
-                GUILayout.FlexibleSpace();
-                GUILayout.EndHorizontal();
-            }
+            GUILayout.FlexibleSpace();
+            GUILayout.EndHorizontal();
+        }
 
-            // message
-            string currentMessage;
-            string context;
+        // message
+        string currentMessage;
+        string context;
 
-            if (directoryMode)
+        if (directoryMode)
+        {
+            currentMessage = deleteDirectoryMessage;
+            context = sceneManager.CurrentDirectoryName;
+        }
+        else
+        {
+            if (deleteScene)
             {
-                currentMessage = deleteDirectoryMessage;
-                context = sceneManager.CurrentDirectoryName;
+                currentMessage = deleteSceneMessage;
+                context = sceneManager.CurrentScene.FileInfo.Name;
             }
             else
             {
-                if (deleteScene)
-                {
-                    currentMessage = deleteSceneMessage;
-                    context = sceneManager.CurrentScene.FileInfo.Name;
-                }
-                else
-                {
-                    currentMessage = sceneManager.CurrentScene.FileInfo.Name;
-                    context = currentMessage;
-                }
+                currentMessage = sceneManager.CurrentScene.FileInfo.Name;
+                context = currentMessage;
             }
+        }
 
-            GUIStyle messageStyle = new GUIStyle(GUI.skin.label)
-            {
-                alignment = TextAnchor.MiddleCenter,
-                fontSize = Utility.GetPix(12)
-            };
+        var messageStyle = new GUIStyle(GUI.skin.label)
+        {
+            alignment = TextAnchor.MiddleCenter,
+            fontSize = Utility.GetPix(12),
+        };
 
-            GUILayout.FlexibleSpace();
+        GUILayout.FlexibleSpace();
 
-            GUILayout.Label(string.Format(currentMessage, context), messageStyle);
+        GUILayout.Label(string.Format(currentMessage, context), messageStyle);
 
-            GUILayout.FlexibleSpace();
+        GUILayout.FlexibleSpace();
 
-            // Buttons
+        // Buttons
+        var buttonStyle = new GUIStyle(GUI.skin.button)
+        {
+            fontSize = Utility.GetPix(12),
+        };
 
-            GUIStyle buttonStyle = new GUIStyle(GUI.skin.button)
-            {
-                fontSize = Utility.GetPix(12)
-            };
+        var buttonHeight = GUILayout.Height(Utility.GetPix(20));
 
-            GUILayoutOption buttonHeight = GUILayout.Height(Utility.GetPix(20));
+        GUILayout.BeginHorizontal();
 
-            GUILayout.BeginHorizontal();
+        if (directoryMode || deleteScene)
+        {
+            GUILayout.FlexibleSpace();
+            okButton.Draw(buttonStyle, buttonHeight, GUILayout.ExpandWidth(false));
+            cancelButton.Draw(buttonStyle, buttonHeight, GUILayout.Width(100));
+        }
+        else
+        {
+            deleteButton.Draw(buttonStyle, buttonHeight, GUILayout.ExpandWidth(false));
+            overwriteButton.Draw(buttonStyle, buttonHeight, GUILayout.ExpandWidth(false));
 
-            if (directoryMode || deleteScene)
-            {
-                GUILayout.FlexibleSpace();
-                okButton.Draw(buttonStyle, buttonHeight, GUILayout.ExpandWidth(false));
-                cancelButton.Draw(buttonStyle, buttonHeight, GUILayout.Width(100));
-            }
-            else
-            {
-                deleteButton.Draw(buttonStyle, buttonHeight, GUILayout.ExpandWidth(false));
-                overwriteButton.Draw(buttonStyle, buttonHeight, GUILayout.ExpandWidth(false));
+            GUILayout.FlexibleSpace();
 
-                GUILayout.FlexibleSpace();
+            okButton.Draw(buttonStyle, buttonHeight, GUILayout.ExpandWidth(false));
+            cancelButton.Draw(buttonStyle, buttonHeight, GUILayout.Width(100));
+        }
 
-                okButton.Draw(buttonStyle, buttonHeight, GUILayout.ExpandWidth(false));
-                cancelButton.Draw(buttonStyle, buttonHeight, GUILayout.Width(100));
-            }
+        GUILayout.EndHorizontal();
 
-            GUILayout.EndHorizontal();
+        GUILayout.EndArea();
+    }
 
-            GUILayout.EndArea();
-        }
+    public void ShowDirectoryDialogue()
+    {
+        okButton.Label = directoryDeleteCommit;
+        directoryMode = true;
+        Modal.Show(this);
+    }
 
-        public void ShowDirectoryDialogue()
-        {
-            okButton.Label = directoryDeleteCommit;
-            directoryMode = true;
-            Modal.Show(this);
-        }
+    public void ShowSceneDialogue()
+    {
+        directoryMode = false;
+        okButton.Label = sceneLoadCommit;
+        Modal.Show(this);
+    }
 
-        public void ShowSceneDialogue()
+    protected override void ReloadTranslation()
+    {
+        deleteDirectoryMessage = Translation.Get("sceneManagerModal", "deleteDirectoryConfirm");
+        deleteSceneMessage = Translation.Get("sceneManagerModal", "deleteFileConfirm");
+        directoryDeleteCommit = Translation.Get("sceneManagerModal", "deleteDirectoryButton");
+        sceneDeleteCommit = Translation.Get("sceneManagerModal", "deleteFileCommit");
+        sceneLoadCommit = Translation.Get("sceneManagerModal", "fileLoadCommit");
+        infoKankyo = Translation.Get("sceneManagerModal", "infoKankyo");
+        infoMaidSingular = Translation.Get("sceneManagerModal", "infoMaidSingular");
+        infoMaidPlural = Translation.Get("sceneManagerModal", "infoMaidPlural");
+    }
+
+    private void Commit()
+    {
+        if (directoryMode)
         {
-            directoryMode = false;
-            okButton.Label = sceneLoadCommit;
-            Modal.Show(this);
+            sceneManager.DeleteDirectory();
+            Modal.Close();
         }
-
-        private void Commit()
+        else
         {
-            if (directoryMode)
+            if (deleteScene)
             {
-                sceneManager.DeleteDirectory();
-                Modal.Close();
+                sceneManager.DeleteScene();
+                deleteScene = false;
             }
             else
             {
-                if (deleteScene)
-                {
-                    sceneManager.DeleteScene();
-                    deleteScene = false;
-                }
-                else sceneManager.LoadScene(sceneManager.CurrentScene);
-
-                Modal.Close();
+                sceneManager.LoadScene(sceneManager.CurrentScene);
             }
+
+            Modal.Close();
         }
+    }
 
-        private void Cancel()
+    private void Cancel()
+    {
+        if (directoryMode)
         {
-            if (directoryMode) Modal.Close();
+            Modal.Close();
+        }
+        else
+        {
+            if (deleteScene)
+                deleteScene = false;
             else
-            {
-                if (deleteScene) deleteScene = false;
-                else Modal.Close();
-            }
-            okButton.Label = sceneLoadCommit;
+                Modal.Close();
         }
+
+        okButton.Label = sceneLoadCommit;
     }
 }

+ 84 - 79
src/MeidoPhotoStudio.Plugin/GUI/Windows/SceneWindow.cs

@@ -1,108 +1,113 @@
 using UnityEngine;
 
-namespace MeidoPhotoStudio.Plugin
+namespace MeidoPhotoStudio.Plugin;
+
+public class SceneWindow : BaseWindow
 {
-    public class SceneWindow : BaseWindow
-    {
-        private const float resizeHandleSize = 15f;
-        private readonly SceneManager sceneManager;
-        private readonly SceneManagerTitleBarPane titleBar;
-        private readonly SceneManagerDirectoryPane directoryList;
-        private readonly SceneManagerScenePane sceneGrid;
-        private Rect resizeHandleRect;
-        private bool resizing;
-        private bool visible;
-        public override bool Visible
-        {
-            get => visible;
-            set
-            {
-                visible = value;
-                if (visible && !sceneManager.Initialized) sceneManager.Initialize();
-            }
-        }
+    private const float ResizeHandleSize = 15f;
 
-        public SceneWindow(SceneManager sceneManager)
-        {
-            windowRect.width = Screen.width * 0.65f;
-            windowRect.height = Screen.height * 0.75f;
-            windowRect.x = (Screen.width * 0.5f) - (windowRect.width * 0.5f);
-            windowRect.y = (Screen.height * 0.5f) - (windowRect.height * 0.5f);
+    private readonly SceneManager sceneManager;
+    private readonly SceneManagerTitleBarPane titleBar;
+    private readonly SceneManagerDirectoryPane directoryList;
+    private readonly SceneManagerScenePane sceneGrid;
 
-            resizeHandleRect = new Rect(0f, 0f, resizeHandleSize, resizeHandleSize);
+    private Rect resizeHandleRect;
+    private bool resizing;
+    private bool visible;
 
-            this.sceneManager = sceneManager;
-            SceneModalWindow sceneModalWindow = new SceneModalWindow(this.sceneManager);
+    public SceneWindow(SceneManager sceneManager)
+    {
+        windowRect.width = Screen.width * 0.65f;
+        windowRect.height = Screen.height * 0.75f;
+        windowRect.x = Screen.width * 0.5f - windowRect.width * 0.5f;
+        windowRect.y = Screen.height * 0.5f - windowRect.height * 0.5f;
 
-            titleBar = AddPane(new SceneManagerTitleBarPane(sceneManager));
-            titleBar.CloseChange += (s, a) => Visible = false;
+        resizeHandleRect = new(0f, 0f, ResizeHandleSize, ResizeHandleSize);
 
-            directoryList = AddPane(new SceneManagerDirectoryPane(sceneManager, sceneModalWindow));
+        this.sceneManager = sceneManager;
+        var sceneModalWindow = new SceneModalWindow(this.sceneManager);
 
-            sceneGrid = AddPane(new SceneManagerScenePane(sceneManager, sceneModalWindow));
-        }
+        titleBar = AddPane(new SceneManagerTitleBarPane(sceneManager));
+        titleBar.CloseChange += (_, _) =>
+            Visible = false;
 
-        public override void GUIFunc(int id)
-        {
-            HandleResize();
-            Draw();
-            if (!resizing) GUI.DragWindow();
-        }
+        directoryList = AddPane(new SceneManagerDirectoryPane(sceneManager, sceneModalWindow));
 
-        public override void Update()
-        {
-            base.Update();
-            if (InputManager.GetKeyDown(MpsKey.OpenSceneManager)) Visible = !Visible;
-        }
+        sceneGrid = AddPane(new SceneManagerScenePane(sceneManager, sceneModalWindow));
+    }
 
-        public override void Deactivate()
+    public override bool Visible
+    {
+        get => visible;
+        set
         {
-            Visible = false;
+            visible = value;
+
+            if (visible && !sceneManager.Initialized)
+                sceneManager.Initialize();
         }
+    }
 
-        public override void Draw()
-        {
-            GUI.enabled = !SceneManager.Busy && !Modal.Visible;
+    public override void GUIFunc(int id)
+    {
+        HandleResize();
+        Draw();
 
-            GUILayout.BeginArea(new Rect(10f, 10f, windowRect.width - 20f, windowRect.height - 20f));
+        if (!resizing)
+            GUI.DragWindow();
+    }
 
-            titleBar.Draw();
+    public override void Update()
+    {
+        base.Update();
 
-            GUILayout.BeginHorizontal();
-            directoryList.Draw();
-            sceneGrid.Draw();
-            GUILayout.EndHorizontal();
+        if (InputManager.GetKeyDown(MpsKey.OpenSceneManager))
+            Visible = !Visible;
+    }
 
-            GUILayout.EndArea();
+    public override void Deactivate() =>
+        Visible = false;
 
-            GUI.Box(resizeHandleRect, GUIContent.none);
-        }
+    public override void Draw()
+    {
+        GUI.enabled = !SceneManager.Busy && !Modal.Visible;
 
-        private void HandleResize()
-        {
-            resizeHandleRect.x = windowRect.width - resizeHandleSize;
-            resizeHandleRect.y = windowRect.height - resizeHandleSize;
+        GUILayout.BeginArea(new(10f, 10f, windowRect.width - 20f, windowRect.height - 20f));
 
-            if (!resizing && Input.GetMouseButton(0) && resizeHandleRect.Contains(Event.current.mousePosition))
-            {
-                resizing = true;
-            }
+        titleBar.Draw();
 
-            if (resizing)
-            {
-                float rectWidth = Event.current.mousePosition.x;
-                float rectHeight = Event.current.mousePosition.y;
+        GUILayout.BeginHorizontal();
+        directoryList.Draw();
+        sceneGrid.Draw();
+        GUILayout.EndHorizontal();
+
+        GUILayout.EndArea();
+
+        GUI.Box(resizeHandleRect, GUIContent.none);
+    }
+
+    private void HandleResize()
+    {
+        resizeHandleRect.x = windowRect.width - ResizeHandleSize;
+        resizeHandleRect.y = windowRect.height - ResizeHandleSize;
+
+        if (!resizing && Input.GetMouseButton(0) && resizeHandleRect.Contains(Event.current.mousePosition))
+            resizing = true;
+
+        if (resizing)
+        {
+            var rectWidth = Event.current.mousePosition.x;
+            var rectHeight = Event.current.mousePosition.y;
 
-                float minWidth = Utility.GetPix(
-                    SceneManagerDirectoryPane.listWidth
-                    + (int)(SceneManager.sceneDimensions.x * SceneManagerScenePane.thumbnailScale)
-                );
+            var minWidth = Utility.GetPix(
+                SceneManagerDirectoryPane.ListWidth + (int)(SceneManager.SceneDimensions.x
+                * SceneManagerScenePane.ThumbnailScale));
 
-                windowRect.width = Mathf.Max(minWidth, rectWidth);
-                windowRect.height = Mathf.Max(300, rectHeight);
+            windowRect.width = Mathf.Max(minWidth, rectWidth);
+            windowRect.height = Mathf.Max(300, rectHeight);
 
-                if (!Input.GetMouseButton(0)) resizing = false;
-            }
+            if (!Input.GetMouseButton(0))
+                resizing = false;
         }
     }
 }

+ 12 - 0
src/MeidoPhotoStudio.Plugin/KeyValuePairExtensions.cs

@@ -0,0 +1,12 @@
+using System.Collections.Generic;
+
+namespace MeidoPhotoStudio.Plugin;
+
+public static class KeyValuePairExtensions
+{
+    public static void Deconstruct<TKey, TValue>(this KeyValuePair<TKey, TValue> kvp, out TKey key, out TValue value)
+    {
+        key = kvp.Key;
+        value = kvp.Value;
+    }
+}

+ 55 - 45
src/MeidoPhotoStudio.Plugin/MPSScene.cs

@@ -1,70 +1,80 @@
-using System.IO;
+using System.IO;
 using System.Text;
+
 using UnityEngine;
 
-namespace MeidoPhotoStudio.Plugin
+namespace MeidoPhotoStudio.Plugin;
+
+public class MPSScene
 {
-    public class MPSScene
-    {
-        public Texture2D Thumbnail { get; }
-        public FileInfo FileInfo { get; }
-        public bool Environment { get; private set; }
-        public int NumberOfMaids { get; private set; }
+    private byte[] data;
 
-        private byte[] data;
+    public MPSScene(string path, Texture2D thumbnail = null)
+    {
+        FileInfo = new(path);
 
-        public byte[] Data
+        if (!thumbnail)
         {
-            get
-            {
-                if (data == null) Preload();
-                return data;
-            }
-            private set => data = value;
+            thumbnail = new(1, 1, TextureFormat.ARGB32, false);
+            thumbnail.LoadImage(File.ReadAllBytes(FileInfo.FullName));
         }
 
-        public MPSScene(string path, Texture2D thumbnail = null)
-        {
-            FileInfo = new FileInfo(path);
+        Thumbnail = thumbnail;
+    }
 
-            if (!thumbnail)
-            {
-                thumbnail = new Texture2D(1, 1, TextureFormat.ARGB32, false);
-                thumbnail.LoadImage(File.ReadAllBytes(FileInfo.FullName));
-            }
+    public Texture2D Thumbnail { get; }
 
-            Thumbnail = thumbnail;
-        }
+    public FileInfo FileInfo { get; }
+
+    public bool Environment { get; private set; }
+
+    public int NumberOfMaids { get; private set; }
 
-        public void Preload()
+    public byte[] Data
+    {
+        get
         {
-            if (data != null) return;
+            if (data is null)
+                Preload();
 
-            using var fileStream = FileInfo.OpenRead();
-            Utility.SeekPngEnd(fileStream);
+            return data;
+        }
+        private set => data = value;
+    }
 
-            using var memoryStream = new MemoryStream();
+    public void Preload()
+    {
+        if (data is not null)
+            return;
 
-            fileStream.CopyTo(memoryStream);
-            memoryStream.Position = 0L;
+        using var fileStream = FileInfo.OpenRead();
 
-            using var binaryReader = new BinaryReader(memoryStream, Encoding.UTF8);
+        Utility.SeekPngEnd(fileStream);
 
-            var sceneHeader = MeidoPhotoStudio.SceneHeader;
-            if (!Utility.BytesEqual(binaryReader.ReadBytes(sceneHeader.Length), sceneHeader))
-            {
-                Utility.LogWarning($"'{FileInfo.FullName}' is not a MPS Scene");
-                return;
-            }
+        using var memoryStream = new MemoryStream();
 
-            (_, Environment, NumberOfMaids, _) = SceneMetadata.ReadMetadata(binaryReader);
+        fileStream.CopyTo(memoryStream);
+        memoryStream.Position = 0L;
 
-            Data = memoryStream.ToArray();
-        }
+        using var binaryReader = new BinaryReader(memoryStream, Encoding.UTF8);
 
-        public void Destroy()
+        var sceneHeader = MeidoPhotoStudio.SceneHeader;
+
+        if (!Utility.BytesEqual(binaryReader.ReadBytes(sceneHeader.Length), sceneHeader))
         {
-            if (Thumbnail) Object.DestroyImmediate(Thumbnail);
+            Utility.LogWarning($"'{FileInfo.FullName}' is not a MPS Scene");
+
+            return;
         }
+
+        (_, Environment, NumberOfMaids, _) = SceneMetadata.ReadMetadata(binaryReader);
+
+        Data = memoryStream.ToArray();
+    }
+
+    public void Destroy()
+    {
+        if (Thumbnail)
+            Object.DestroyImmediate(Thumbnail);
     }
 }

+ 135 - 99
src/MeidoPhotoStudio.Plugin/MaidPlacementUtility.cs

@@ -1,145 +1,181 @@
 using System.Collections.Generic;
+
 using UnityEngine;
 
-namespace MeidoPhotoStudio.Plugin
+namespace MeidoPhotoStudio.Plugin;
+
+public static class MaidPlacementUtility
 {
-    public static class MaidPlacementUtility
+    public static readonly string[] PlacementTypes =
     {
-        private const float pi = Mathf.PI;
-        private const float tau = Mathf.PI * 2f;
-        public static readonly string[] placementTypes = {
-            "horizontalRow", "verticalRow", "diagonalRow", "diagonalRowInverse", "wave", "waveInverse",
-            "v", "vInverse", "circleInner", "circleOuter", "fanInner", "fanOuter"
-        };
+        "horizontalRow", "verticalRow", "diagonalRow", "diagonalRowInverse", "wave", "waveInverse", "v", "vInverse",
+        "circleInner", "circleOuter", "fanInner", "fanOuter",
+    };
 
-        public static int AlternatingSequence(int x) => (int)((x % 2 == 0 ? 1 : -1) * Mathf.Ceil(x / 2f));
+    private const float Pi = Mathf.PI;
+    private const float Tau = Mathf.PI * 2f;
 
-        public static void ApplyPlacement(string placementType, IList<Meido> list)
-        {
-            switch (placementType)
-            {
-                case "horizontalRow": PlacementRow(list, false); break;
-                case "verticalRow": PlacementRow(list, true); break;
-                case "diagonalRow": PlacementDiagonal(list, false); break;
-                case "diagonalRowInverse": PlacementDiagonal(list, true); break;
-                case "wave": PlacementWave(list, false); break;
-                case "waveInverse": PlacementWave(list, true); break;
-                case "v": PlacementV(list, false); break;
-                case "vInverse": PlacementV(list, true); break;
-                case "circleOuter": PlacementCircle(list, false); break;
-                case "circleInner": PlacementCircle(list, true); break;
-                case "fanInner": PlacementFan(list, false); break;
-                case "fanOuter": PlacementFan(list, true); break;
-                default: return;
-            }
-        }
+    public static int AlternatingSequence(int x) =>
+        (int)((x % 2 == 0 ? 1 : -1) * Mathf.Ceil(x / 2f));
 
-        public static void PlacementRow(IList<Meido> list, bool vertical = false)
+    public static void ApplyPlacement(string placementType, IList<Meido> list)
+    {
+        switch (placementType)
         {
-            for (int i = 0; i < list.Count; i++)
-            {
-                Vector3 position = Vector3.zero;
+            case "horizontalRow":
+                PlacementRow(list, false);
 
-                Maid maid = list[i].Maid;
+                break;
+            case "verticalRow":
+                PlacementRow(list, true);
 
-                float a = AlternatingSequence(i) * 0.6f;
+                break;
+            case "diagonalRow":
+                PlacementDiagonal(list, false);
 
-                if (vertical) position.z = a;
-                else position.x = a;
+                break;
+            case "diagonalRowInverse":
+                PlacementDiagonal(list, true);
 
-                maid.SetPos(position);
-                maid.SetRot(Vector3.zero);
-            }
-        }
+                break;
+            case "wave":
+                PlacementWave(list, false);
 
-        public static void PlacementDiagonal(IList<Meido> list, bool inverse = false)
-        {
-            for (int i = 0; i < list.Count; i++)
-            {
-                Maid maid = list[i].Maid;
+                break;
+            case "waveInverse":
+                PlacementWave(list, true);
 
-                float z = AlternatingSequence(i) * 0.5f;
+                break;
+            case "v":
+                PlacementV(list, false);
 
-                maid.SetPos(inverse ? new Vector3(z, 0, -z) : new Vector3(z, 0, z));
-                maid.SetRot(Vector3.zero);
-            }
-        }
+                break;
+            case "vInverse":
+                PlacementV(list, true);
 
-        public static void PlacementWave(IList<Meido> list, bool inverse = false)
-        {
-            for (int i = 0; i < list.Count; i++)
-            {
-                Maid maid = list[i].Maid;
+                break;
+            case "circleOuter":
+                PlacementCircle(list, false);
+
+                break;
+            case "circleInner":
+                PlacementCircle(list, true);
 
-                float x = AlternatingSequence(i) * 0.4f;
-                float z = (inverse ? -1 : 1) * Mathf.Cos(AlternatingSequence(i) * pi) * 0.35f;
+                break;
+            case "fanInner":
+                PlacementFan(list, false);
 
-                maid.SetPos(new Vector3(x, 0, z));
-                maid.SetRot(Vector3.zero);
-            }
+                break;
+            case "fanOuter":
+                PlacementFan(list, true);
+
+                break;
+            default:
+                return;
         }
+    }
 
-        public static void PlacementV(IList<Meido> list, bool inverse = false)
+    public static void PlacementRow(IList<Meido> list, bool vertical = false)
+    {
+        for (var i = 0; i < list.Count; i++)
         {
-            for (int i = 0; i < list.Count; i++)
-            {
-                Maid maid = list[i].Maid;
+            var position = Vector3.zero;
+            var maid = list[i].Maid;
+            var a = AlternatingSequence(i) * 0.6f;
 
-                float x = AlternatingSequence(i) * 0.4f;
-                float z = (inverse ? 1 : -1) * Mathf.Abs(AlternatingSequence(i)) * 0.4f;
+            if (vertical)
+                position.z = a;
+            else
+                position.x = a;
 
-                maid.SetPos(new Vector3(x, 0, z));
-                maid.SetRot(Vector3.zero);
-            }
+            maid.SetPos(position);
+            maid.SetRot(Vector3.zero);
         }
+    }
 
-        public static void PlacementCircle(IList<Meido> list, bool inner = false)
+    public static void PlacementDiagonal(IList<Meido> list, bool inverse = false)
+    {
+        for (var i = 0; i < list.Count; i++)
         {
-            int maidCount = list.Count;
+            var maid = list[i].Maid;
 
-            float radius = 0.3f + (0.1f * maidCount);
+            var z = AlternatingSequence(i) * 0.5f;
 
-            for (int i = 0; i < maidCount; i++)
-            {
-                Maid maid = list[i].Maid;
+            maid.SetPos(inverse ? new(z, 0, -z) : new(z, 0, z));
+            maid.SetRot(Vector3.zero);
+        }
+    }
 
-                float angle = (pi / 2f) + (tau * AlternatingSequence(i) / maidCount);
+    public static void PlacementWave(IList<Meido> list, bool inverse = false)
+    {
+        for (var i = 0; i < list.Count; i++)
+        {
+            var maid = list[i].Maid;
+            var x = AlternatingSequence(i) * 0.4f;
+            var z = (inverse ? -1 : 1) * Mathf.Cos(AlternatingSequence(i) * Pi) * 0.35f;
 
-                float x = Mathf.Cos(angle) * radius;
-                float z = Mathf.Sin(angle) * radius;
+            maid.SetPos(new(x, 0, z));
+            maid.SetRot(Vector3.zero);
+        }
+    }
 
-                float rotation = Mathf.Atan2(x, z);
-                if (inner) rotation += pi;
+    public static void PlacementV(IList<Meido> list, bool inverse = false)
+    {
+        for (var i = 0; i < list.Count; i++)
+        {
+            var maid = list[i].Maid;
+            var x = AlternatingSequence(i) * 0.4f;
+            var z = (inverse ? 1 : -1) * Mathf.Abs(AlternatingSequence(i)) * 0.4f;
 
-                maid.SetPos(new Vector3(x, 0, z));
-                maid.SetRot(new Vector3(0, rotation * Mathf.Rad2Deg, 0));
-            }
+            maid.SetPos(new(x, 0, z));
+            maid.SetRot(Vector3.zero);
         }
+    }
 
-        public static void PlacementFan(IList<Meido> list, bool outer = false)
+    public static void PlacementCircle(IList<Meido> list, bool inner = false)
+    {
+        var maidCount = list.Count;
+
+        var radius = 0.3f + 0.1f * maidCount;
+
+        for (var i = 0; i < maidCount; i++)
         {
-            int maidCount = list.Count;
+            var maid = list[i].Maid;
+            var angle = Pi / 2f + Tau * AlternatingSequence(i) / maidCount;
+            var x = Mathf.Cos(angle) * radius;
+            var z = Mathf.Sin(angle) * radius;
+
+            var rotation = Mathf.Atan2(x, z);
 
-            float radius = 0.2f + (0.2f * maidCount);
+            if (inner)
+                rotation += Pi;
 
-            list[0].Maid.SetPos(Vector3.zero);
-            list[0].Maid.SetRot(Vector3.zero);
+            maid.SetPos(new(x, 0, z));
+            maid.SetRot(new(0, rotation * Mathf.Rad2Deg, 0));
+        }
+    }
 
-            for (int i = 1; i < maidCount; i++)
-            {
-                Maid maid = list[i].Maid;
+    public static void PlacementFan(IList<Meido> list, bool outer = false)
+    {
+        var maidCount = list.Count;
+        var radius = 0.2f + 0.2f * maidCount;
 
-                float angle = pi * AlternatingSequence(i - 1) / maidCount;
+        list[0].Maid.SetPos(Vector3.zero);
+        list[0].Maid.SetRot(Vector3.zero);
 
-                float x = Mathf.Sin(angle) * radius;
-                float z = Mathf.Cos(angle) * radius;
+        for (var i = 1; i < maidCount; i++)
+        {
+            var maid = list[i].Maid;
+            var angle = Pi * AlternatingSequence(i - 1) / maidCount;
+            var x = Mathf.Sin(angle) * radius;
+            var z = Mathf.Cos(angle) * radius;
+            var rotation = Mathf.Atan2(x, z);
 
-                float rotation = Mathf.Atan2(x, z);
-                if (outer) rotation += pi;
+            if (outer)
+                rotation += Pi;
 
-                maid.SetPos(new Vector3(-x, 0, -z));
-                maid.SetRot(new Vector3(0, rotation * Mathf.Rad2Deg, 0));
-            }
+            maid.SetPos(new(-x, 0, -z));
+            maid.SetRot(new(0, rotation * Mathf.Rad2Deg, 0));
         }
     }
 }

+ 50 - 0
src/MeidoPhotoStudio.Plugin/Managers/CameraInfo.cs

@@ -0,0 +1,50 @@
+using UnityEngine;
+
+namespace MeidoPhotoStudio.Plugin;
+
+public class CameraInfo
+{
+    public CameraInfo() =>
+        Reset();
+
+    public Vector3 TargetPos { get; set; }
+
+    public Quaternion Angle { get; set; }
+
+    public float Distance { get; set; }
+
+    public float FOV { get; set; }
+
+    public static CameraInfo FromCamera(CameraMain mainCamera)
+    {
+        var info = new CameraInfo();
+
+        info.UpdateInfo(mainCamera);
+
+        return info;
+    }
+
+    public void Reset()
+    {
+        TargetPos = new(0f, 0.9f, 0f);
+        Angle = Quaternion.Euler(10f, 180f, 0f);
+        Distance = 3f;
+        FOV = 35f;
+    }
+
+    public void UpdateInfo(CameraMain camera)
+    {
+        TargetPos = camera.GetTargetPos();
+        Angle = camera.transform.rotation;
+        Distance = camera.GetDistance();
+        FOV = camera.camera.fieldOfView;
+    }
+
+    public void Apply(CameraMain camera)
+    {
+        camera.SetTargetPos(TargetPos);
+        camera.SetDistance(Distance);
+        camera.transform.rotation = Angle;
+        camera.camera.fieldOfView = FOV;
+    }
+}

+ 128 - 152
src/MeidoPhotoStudio.Plugin/Managers/CameraManager.cs

@@ -1,192 +1,168 @@
 using System;
+
 using UnityEngine;
 
-namespace MeidoPhotoStudio.Plugin
+using Input = MeidoPhotoStudio.Plugin.InputManager;
+using UInput = UnityEngine.Input;
+
+namespace MeidoPhotoStudio.Plugin;
+
+public class CameraManager : IManager
 {
-    using Input = InputManager;
-    using UInput = Input;
-    public class CameraManager : IManager
-    {
-        public const string header = "CAMERA";
-        private static readonly CameraMain mainCamera = CameraUtility.MainCamera;
-        private static readonly UltimateOrbitCamera ultimateOrbitCamera = CameraUtility.UOCamera;
-        private float defaultCameraMoveSpeed;
-        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;
-        public int CameraCount => cameraInfos.Length;
-        public EventHandler CameraChange;
-
-        private int currentCameraIndex;
-        public int CurrentCameraIndex
-        {
-            get => currentCameraIndex;
-            set
-            {
-                cameraInfos[currentCameraIndex].UpdateInfo(mainCamera);
-                currentCameraIndex = value;
-                LoadCameraInfo(cameraInfos[currentCameraIndex]);
-            }
-        }
-        private CameraInfo[] cameraInfos;
+    public const string Header = "CAMERA";
 
-        static CameraManager()
-        {
-            Input.Register(MpsKey.CameraLayer, KeyCode.Q, "Camera control layer");
-            Input.Register(MpsKey.CameraSave, KeyCode.S, "Save camera transform");
-            Input.Register(MpsKey.CameraLoad, KeyCode.A, "Load camera transform");
-            Input.Register(MpsKey.CameraReset, KeyCode.R, "Reset camera transform");
-        }
+    private const float CameraFastMoveSpeed = 0.1f;
+    private const float CameraFastZoomSpeed = 3f;
+    private const float CameraSlowMoveSpeed = 0.004f;
+    private const float CameraSlowZoomSpeed = 0.1f;
+    private const KeyCode AlphaOne = KeyCode.Alpha1;
 
-        public CameraManager()
-        {
-            cameraInfos = new CameraInfo[5];
-            for (var i = 0; i < cameraInfos.Length; i++) cameraInfos[i] = new CameraInfo();
-            Activate();
-        }
+    private static readonly CameraMain MainCamera = CameraUtility.MainCamera;
+    private static readonly UltimateOrbitCamera UltimateOrbitCamera = CameraUtility.UOCamera;
 
-        public void Activate()
-        {
-            ultimateOrbitCamera.enabled = true;
+    private readonly CameraInfo tempCameraInfo = new();
+    private readonly CameraInfo[] cameraInfos;
 
-            defaultCameraMoveSpeed = ultimateOrbitCamera.moveSpeed;
-            defaultCameraZoomSpeed = ultimateOrbitCamera.zoomSpeed;
+    private float defaultCameraMoveSpeed;
+    private float defaultCameraZoomSpeed;
+    private Camera subCamera;
+    private int currentCameraIndex;
 
-            if (!MeidoPhotoStudio.EditMode) ResetCamera();
+    static CameraManager()
+    {
+        Input.Register(MpsKey.CameraLayer, KeyCode.Q, "Camera control layer");
+        Input.Register(MpsKey.CameraSave, KeyCode.S, "Save camera transform");
+        Input.Register(MpsKey.CameraLoad, KeyCode.A, "Load camera transform");
+        Input.Register(MpsKey.CameraReset, KeyCode.R, "Reset camera transform");
+    }
 
-            currentCameraIndex = 0;
+    public CameraManager()
+    {
+        cameraInfos = new CameraInfo[5];
 
-            tempCameraInfo.Reset();
+        for (var i = 0; i < cameraInfos.Length; i++)
+            cameraInfos[i] = new();
 
-            for (var i = 0; i < CameraCount; i++) cameraInfos[i].Reset();
+        Activate();
+    }
 
-            mainCamera.ForceCalcNearClip();
+    public event EventHandler CameraChange;
 
-            var subCamGo = new GameObject("subcam");
-            subCamera = subCamGo.AddComponent<Camera>();
-            subCamera.CopyFrom(mainCamera.camera);
-            subCamera.clearFlags = CameraClearFlags.Depth;
-            subCamera.cullingMask = 1 << 8;
-            subCamera.depth = 1f;
-            subCamera.transform.parent = mainCamera.transform;
-        }
+    public int CameraCount =>
+        cameraInfos.Length;
 
-        public void Deactivate()
+    public int CurrentCameraIndex
+    {
+        get => currentCameraIndex;
+        set
         {
-            UnityEngine.Object.Destroy(subCamera.gameObject);
-            mainCamera.camera.backgroundColor = Color.black;
+            cameraInfos[currentCameraIndex].UpdateInfo(MainCamera);
+            currentCameraIndex = value;
+            LoadCameraInfo(cameraInfos[currentCameraIndex]);
+        }
+    }
 
-            ultimateOrbitCamera.moveSpeed = defaultCameraMoveSpeed;
-            ultimateOrbitCamera.zoomSpeed = defaultCameraZoomSpeed;
+    public void Activate()
+    {
+        UltimateOrbitCamera.enabled = true;
 
-            if (MeidoPhotoStudio.EditMode) return;
+        defaultCameraMoveSpeed = UltimateOrbitCamera.moveSpeed;
+        defaultCameraZoomSpeed = UltimateOrbitCamera.zoomSpeed;
 
-            mainCamera.Reset(CameraMain.CameraType.Target, true);
-            mainCamera.SetTargetPos(new Vector3(0.5609447f, 1.380762f, -1.382336f));
-            mainCamera.SetDistance(1.6f);
-            mainCamera.SetAroundAngle(new Vector2(245.5691f, 6.273283f));
+        if (!MeidoPhotoStudio.EditMode)
+            ResetCamera();
 
-            mainCamera.ResetCalcNearClip();
-        }
+        currentCameraIndex = 0;
 
-        public void Update()
-        {
-            if (Input.GetKey(MpsKey.CameraLayer))
-            {
-                if (Input.GetKeyDown(MpsKey.CameraSave)) SaveTempCamera();
-                else if (Input.GetKeyDown(MpsKey.CameraLoad)) LoadCameraInfo(tempCameraInfo);
-                else if (Input.GetKeyDown(MpsKey.CameraReset)) ResetCamera();
-
-                for (var i = 0; i < CameraCount; i++)
-                {
-                    if (i != CurrentCameraIndex && UInput.GetKeyDown(AlphaOne + i)) CurrentCameraIndex = i;
-                }
-            }
-
-            subCamera.fieldOfView = mainCamera.camera.fieldOfView;
-
-            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;
-            }
-        }
+        tempCameraInfo.Reset();
 
-        private void SaveTempCamera()
-        {
-            tempCameraInfo.UpdateInfo(mainCamera);
-            CameraUtility.StopAll();
-        }
+        for (var i = 0; i < CameraCount; i++)
+            cameraInfos[i].Reset();
 
-        public void LoadCameraInfo(CameraInfo info)
-        {
-            info.Apply(mainCamera);
-            CameraUtility.StopAll();
-            CameraChange?.Invoke(this, EventArgs.Empty);
-        }
+        MainCamera.ForceCalcNearClip();
 
-        private void ResetCamera()
-        {
-            mainCamera.Reset(CameraMain.CameraType.Target, true);
-            mainCamera.SetTargetPos(new Vector3(0f, 0.9f, 0f));
-            mainCamera.SetDistance(3f);
-            CameraChange?.Invoke(this, EventArgs.Empty);
-        }
+        var subCamGo = new GameObject("subcam");
+
+        subCamera = subCamGo.AddComponent<Camera>();
+        subCamera.CopyFrom(MainCamera.camera);
+        subCamera.clearFlags = CameraClearFlags.Depth;
+        subCamera.cullingMask = 1 << 8;
+        subCamera.depth = 1f;
+        subCamera.transform.parent = MainCamera.transform;
     }
 
-    public class CameraInfo
+    public void Deactivate()
     {
-        public Vector3 TargetPos { get; set; }
-        public Quaternion Angle { get; set; }
-        public float Distance { get; set; }
-        public float FOV { get; set; }
+        UnityEngine.Object.Destroy(subCamera.gameObject);
+        MainCamera.camera.backgroundColor = Color.black;
 
-        public CameraInfo() => Reset();
+        UltimateOrbitCamera.moveSpeed = defaultCameraMoveSpeed;
+        UltimateOrbitCamera.zoomSpeed = defaultCameraZoomSpeed;
 
-        public static CameraInfo FromCamera(CameraMain mainCamera)
+        if (MeidoPhotoStudio.EditMode)
+            return;
+
+        MainCamera.Reset(CameraMain.CameraType.Target, true);
+        MainCamera.SetTargetPos(new(0.5609447f, 1.380762f, -1.382336f));
+        MainCamera.SetDistance(1.6f);
+        MainCamera.SetAroundAngle(new(245.5691f, 6.273283f));
+
+        MainCamera.ResetCalcNearClip();
+    }
+
+    public void Update()
+    {
+        if (Input.GetKey(MpsKey.CameraLayer))
         {
-            var info = new CameraInfo();
-            info.UpdateInfo(mainCamera);
-            return info;
+            if (Input.GetKeyDown(MpsKey.CameraSave))
+                SaveTempCamera();
+            else if (Input.GetKeyDown(MpsKey.CameraLoad))
+                LoadCameraInfo(tempCameraInfo);
+            else if (Input.GetKeyDown(MpsKey.CameraReset))
+                ResetCamera();
+
+            for (var i = 0; i < CameraCount; i++)
+                if (i != CurrentCameraIndex && UInput.GetKeyDown(AlphaOne + i))
+                    CurrentCameraIndex = i;
         }
 
-        public void Reset()
+        subCamera.fieldOfView = MainCamera.camera.fieldOfView;
+
+        if (Input.Shift)
         {
-            TargetPos = new Vector3(0f, 0.9f, 0f);
-            Angle = Quaternion.Euler(10f, 180f, 0f);
-            Distance = 3f;
-            FOV = 35f;
+            UltimateOrbitCamera.moveSpeed = CameraFastMoveSpeed;
+            UltimateOrbitCamera.zoomSpeed = CameraFastZoomSpeed;
         }
-
-        public void UpdateInfo(CameraMain camera)
+        else if (Input.Control)
         {
-            TargetPos = camera.GetTargetPos();
-            Angle = camera.transform.rotation;
-            Distance = camera.GetDistance();
-            FOV = camera.camera.fieldOfView;
+            UltimateOrbitCamera.moveSpeed = CameraSlowMoveSpeed;
+            UltimateOrbitCamera.zoomSpeed = CameraSlowZoomSpeed;
         }
-
-        public void Apply(CameraMain camera)
+        else
         {
-            camera.SetTargetPos(TargetPos);
-            camera.SetDistance(Distance);
-            camera.transform.rotation = Angle;
-            camera.camera.fieldOfView = FOV;
+            UltimateOrbitCamera.moveSpeed = defaultCameraMoveSpeed;
+            UltimateOrbitCamera.zoomSpeed = defaultCameraZoomSpeed;
         }
     }
+
+    public void LoadCameraInfo(CameraInfo info)
+    {
+        info.Apply(MainCamera);
+        CameraUtility.StopAll();
+        CameraChange?.Invoke(this, EventArgs.Empty);
+    }
+
+    private void SaveTempCamera()
+    {
+        tempCameraInfo.UpdateInfo(MainCamera);
+        CameraUtility.StopAll();
+    }
+
+    private void ResetCamera()
+    {
+        MainCamera.Reset(CameraMain.CameraType.Target, true);
+        MainCamera.SetTargetPos(new(0f, 0.9f, 0f));
+        MainCamera.SetDistance(3f);
+        CameraChange?.Invoke(this, EventArgs.Empty);
+    }
 }

+ 36 - 28
src/MeidoPhotoStudio.Plugin/Managers/EffectManager.cs

@@ -1,35 +1,43 @@
 using System;
 using System.Collections.Generic;
 
-namespace MeidoPhotoStudio.Plugin
+namespace MeidoPhotoStudio.Plugin;
+
+public class EffectManager : IManager
 {
-    public class EffectManager : IManager
+    public const string Header = "EFFECT";
+    public const string Footer = "END_EFFECT";
+
+    private readonly Dictionary<Type, IEffectManager> effectManagers = new();
+
+    public T Get<T>()
+        where T : IEffectManager =>
+        effectManagers.ContainsKey(typeof(T)) ? (T)effectManagers[typeof(T)] : default;
+
+    public T AddManager<T>()
+        where T : IEffectManager, new()
+    {
+        var manager = new T();
+
+        effectManagers[typeof(T)] = manager;
+        manager.Activate();
+
+        return manager;
+    }
+
+    public void Activate()
+    {
+        foreach (var effectManager in effectManagers.Values)
+            effectManager.Activate();
+    }
+
+    public void Deactivate()
+    {
+        foreach (var effectManager in effectManagers.Values)
+            effectManager.Deactivate();
+    }
+
+    public void Update()
     {
-        public const string header = "EFFECT";
-        public const string footer = "END_EFFECT";
-        private readonly Dictionary<Type, IEffectManager> EffectManagers = new Dictionary<Type, IEffectManager>();
-
-        public T Get<T>() where T : IEffectManager
-            => EffectManagers.ContainsKey(typeof(T)) ? (T)EffectManagers[typeof(T)] : default;
-
-        public T AddManager<T>() where T : IEffectManager, new()
-        {
-            T manager = new T();
-            EffectManagers[typeof(T)] = manager;
-            manager.Activate();
-            return manager;
-        }
-
-        public void Activate()
-        {
-            foreach (IEffectManager effectManager in EffectManagers.Values) effectManager.Activate();
-        }
-
-        public void Deactivate()
-        {
-            foreach (IEffectManager effectManager in EffectManagers.Values) effectManager.Deactivate();
-        }
-
-        public void Update() { }
     }
 }

+ 146 - 116
src/MeidoPhotoStudio.Plugin/Managers/EffectManagers/BloomEffectManager.cs

@@ -1,143 +1,173 @@
 using System.Reflection;
+
 using UnityEngine;
 
-namespace MeidoPhotoStudio.Plugin
+namespace MeidoPhotoStudio.Plugin;
+
+public class BloomEffectManager : IEffectManager
 {
-    public class BloomEffectManager : IEffectManager
+    public const string Header = "EFFECT_BLOOM";
+
+    private const float DefaultBloomDefIntensity = 5.7f;
+
+    private static readonly CameraMain Camera = GameMain.Instance.MainCamera;
+
+#pragma warning disable SA1308, SA1310, SA1311
+
+    // TODO: Refactor reflection to using private members directly
+    private static readonly float backup_m_fBloomDefIntensity;
+    private static readonly FieldInfo m_fBloomDefIntensity = Utility.GetFieldInfo<CameraMain>("m_fBloomDefIntensity");
+#pragma warning restore SA1308, SA1310, SA1311
+
+    // CMSystem's bloomValue;
+    private static int backupBloomValue;
+
+    private float initialIntensity;
+    private int initialBlurIterations;
+    private Color initialThresholdColour;
+    private Bloom.HDRBloomMode initialHDRBloomMode;
+    private int blurIterations;
+    private Color bloomThresholdColour;
+    private bool bloomHdr;
+
+    private float bloomValue;
+
+    static BloomEffectManager() =>
+        backup_m_fBloomDefIntensity = BloomDefIntensity;
+
+    public bool Ready { get; private set; }
+
+    public bool Active { get; private set; }
+
+    public float BloomValue
     {
-        public const string header = "EFFECT_BLOOM";
-        private const float bloomDefIntensity = 5.7f;
-        private static readonly CameraMain camera = GameMain.Instance.MainCamera;
-        private Bloom Bloom { get; set; }
-        // CMSystem's bloomValue;
-        private static int backupBloomValue;
-        private static readonly float backup_m_fBloomDefIntensity;
-        private static readonly FieldInfo m_fBloomDefIntensity
-            = Utility.GetFieldInfo<CameraMain>("m_fBloomDefIntensity");
-        private static float BloomDefIntensity
-        {
-            set => m_fBloomDefIntensity.SetValue(camera, value);
-            get => (float)m_fBloomDefIntensity.GetValue(camera);
-        }
-        private float initialIntensity;
-        private int initialBlurIterations;
-        private Color initialThresholdColour;
-        private Bloom.HDRBloomMode initialHDRBloomMode;
-        public bool Ready { get; private set; }
-        public bool Active { get; private set; }
-        private float bloomValue;
-        public float BloomValue
-        {
-            get => bloomValue;
-            set => GameMain.Instance.CMSystem.BloomValue = (int)(bloomValue = value);
-        }
-        private int blurIterations;
-        public int BlurIterations
-        {
-            get => blurIterations;
-            set => blurIterations = Bloom.bloomBlurIterations = value;
-        }
-        public float BloomThresholdColorRed
-        {
-            get => BloomThresholdColour.r;
-            set
-            {
-                Color colour = Bloom.bloomThreshholdColor;
-                BloomThresholdColour = new Color(value, colour.g, colour.b);
-            }
-        }
-        public float BloomThresholdColorGreen
-        {
-            get => BloomThresholdColour.g;
-            set
-            {
-                Color colour = Bloom.bloomThreshholdColor;
-                BloomThresholdColour = new Color(colour.r, value, colour.b);
-            }
-        }
-        public float BloomThresholdColorBlue
+        get => bloomValue;
+        set => GameMain.Instance.CMSystem.BloomValue = (int)(bloomValue = value);
+    }
+
+    public int BlurIterations
+    {
+        get => blurIterations;
+        set => blurIterations = Bloom.bloomBlurIterations = value;
+    }
+
+    public float BloomThresholdColorRed
+    {
+        get => BloomThresholdColour.r;
+        set
         {
-            get => BloomThresholdColour.b;
-            set
-            {
-                Color colour = Bloom.bloomThreshholdColor;
-                BloomThresholdColour = new Color(colour.r, colour.g, value);
-            }
+            var colour = Bloom.bloomThreshholdColor;
+
+            BloomThresholdColour = new(value, colour.g, colour.b);
         }
-        private Color bloomThresholdColour;
-        public Color BloomThresholdColour
+    }
+
+    public float BloomThresholdColorGreen
+    {
+        get => BloomThresholdColour.g;
+        set
         {
-            get => bloomThresholdColour;
-            set => bloomThresholdColour = Bloom.bloomThreshholdColor = value;
+            var colour = Bloom.bloomThreshholdColor;
+
+            BloomThresholdColour = new(colour.r, value, colour.b);
         }
-        private bool bloomHdr;
-        public bool BloomHDR
+    }
+
+    public float BloomThresholdColorBlue
+    {
+        get => BloomThresholdColour.b;
+        set
         {
-            get => bloomHdr;
-            set
-            {
-                Bloom.hdr = value ? Bloom.HDRBloomMode.On : Bloom.HDRBloomMode.Auto;
-                bloomHdr = value;
-            }
+            var colour = Bloom.bloomThreshholdColor;
+
+            BloomThresholdColour = new(colour.r, colour.g, value);
         }
+    }
 
-        static BloomEffectManager() => backup_m_fBloomDefIntensity = BloomDefIntensity;
+    public Color BloomThresholdColour
+    {
+        get => bloomThresholdColour;
+        set => bloomThresholdColour = Bloom.bloomThreshholdColor = value;
+    }
 
-        public void Activate()
+    public bool BloomHDR
+    {
+        get => bloomHdr;
+        set
         {
-            if (Bloom == null)
-            {
-                Ready = true;
-                Bloom = GameMain.Instance.MainCamera.GetComponent<Bloom>();
-                initialIntensity = bloomValue = 50f;
-                initialBlurIterations = blurIterations = Bloom.bloomBlurIterations;
-                initialThresholdColour = bloomThresholdColour = Bloom.bloomThreshholdColor;
-                initialHDRBloomMode = Bloom.hdr;
-                bloomHdr = Bloom.hdr == Bloom.HDRBloomMode.On;
-
-                backupBloomValue = GameMain.Instance.CMSystem.BloomValue;
-            }
-            SetEffectActive(false);
+            Bloom.hdr = value ? Bloom.HDRBloomMode.On : Bloom.HDRBloomMode.Auto;
+            bloomHdr = value;
         }
+    }
+
+    private static float BloomDefIntensity
+    {
+        get => (float)m_fBloomDefIntensity.GetValue(Camera);
+        set => m_fBloomDefIntensity.SetValue(Camera, value);
+    }
 
-        public void Deactivate()
+    private Bloom Bloom { get; set; }
+
+    public void Activate()
+    {
+        if (!Bloom)
         {
-            BloomValue = initialIntensity;
-            BlurIterations = initialBlurIterations;
-            BloomThresholdColour = initialThresholdColour;
-            BloomHDR = initialHDRBloomMode == Bloom.HDRBloomMode.On;
-            BloomHDR = false;
-            Active = false;
-
-            BloomDefIntensity = backup_m_fBloomDefIntensity;
-            GameMain.Instance.CMSystem.BloomValue = backupBloomValue;
+            Ready = true;
+            Bloom = GameMain.Instance.MainCamera.GetComponent<Bloom>();
+            initialIntensity = bloomValue = 50f;
+            initialBlurIterations = blurIterations = Bloom.bloomBlurIterations;
+            initialThresholdColour = bloomThresholdColour = Bloom.bloomThreshholdColor;
+            initialHDRBloomMode = Bloom.hdr;
+            bloomHdr = Bloom.hdr is Bloom.HDRBloomMode.On;
+
+            backupBloomValue = GameMain.Instance.CMSystem.BloomValue;
         }
 
-        public void Reset()
+        SetEffectActive(false);
+    }
+
+    public void Deactivate()
+    {
+        BloomValue = initialIntensity;
+        BlurIterations = initialBlurIterations;
+        BloomThresholdColour = initialThresholdColour;
+        BloomHDR = initialHDRBloomMode is Bloom.HDRBloomMode.On;
+        BloomHDR = false;
+        Active = false;
+
+        BloomDefIntensity = backup_m_fBloomDefIntensity;
+        GameMain.Instance.CMSystem.BloomValue = backupBloomValue;
+    }
+
+    public void Reset()
+    {
+        GameMain.Instance.CMSystem.BloomValue = backupBloomValue;
+        Bloom.bloomBlurIterations = initialBlurIterations;
+        Bloom.bloomThreshholdColor = initialThresholdColour;
+        Bloom.hdr = initialHDRBloomMode;
+
+        BloomDefIntensity = backup_m_fBloomDefIntensity;
+    }
+
+    public void SetEffectActive(bool active)
+    {
+        if (Active = active)
         {
-            GameMain.Instance.CMSystem.BloomValue = backupBloomValue;
-            Bloom.bloomBlurIterations = initialBlurIterations;
-            Bloom.bloomThreshholdColor = initialThresholdColour;
-            Bloom.hdr = initialHDRBloomMode;
+            backupBloomValue = GameMain.Instance.CMSystem.BloomValue;
+            GameMain.Instance.CMSystem.BloomValue = (int)BloomValue;
+            Bloom.bloomBlurIterations = BlurIterations;
+            Bloom.bloomThreshholdColor = BloomThresholdColour;
+            Bloom.hdr = BloomHDR ? Bloom.HDRBloomMode.On : Bloom.HDRBloomMode.Auto;
 
-            BloomDefIntensity = backup_m_fBloomDefIntensity;
+            BloomDefIntensity = DefaultBloomDefIntensity;
         }
-
-        public void SetEffectActive(bool active)
+        else
         {
-            if (Active = active)
-            {
-                backupBloomValue = GameMain.Instance.CMSystem.BloomValue;
-                GameMain.Instance.CMSystem.BloomValue = (int)BloomValue;
-                Bloom.bloomBlurIterations = BlurIterations;
-                Bloom.bloomThreshholdColor = BloomThresholdColour;
-                Bloom.hdr = BloomHDR ? Bloom.HDRBloomMode.On : Bloom.HDRBloomMode.Auto;
-
-                BloomDefIntensity = bloomDefIntensity;
-            }
-            else Reset();
+            Reset();
         }
+    }
 
-        public void Update() { }
+    public void Update()
+    {
     }
 }

+ 64 - 54
src/MeidoPhotoStudio.Plugin/Managers/EffectManagers/BlurEffectManager.cs

@@ -1,70 +1,80 @@
-namespace MeidoPhotoStudio.Plugin
+namespace MeidoPhotoStudio.Plugin;
+
+public class BlurEffectManager : IEffectManager
 {
-    public class BlurEffectManager : IEffectManager
+    public const string Header = "EFFECT_BLUR";
+
+    private float initialBlurSize;
+    private int initialBlurIterations;
+    private int initialDownsample;
+    private float blurSize;
+
+    public bool Ready { get; private set; }
+
+    public bool Active { get; private set; }
+
+    public float BlurSize
     {
-        public const string header = "EFFECT_BLUR";
-        private Blur Blur { get; set; }
-        public bool Ready { get; private set; }
-        public bool Active { get; private set; }
-        private float initialBlurSize;
-        private int initialBlurIterations;
-        private int initialDownsample;
-        private float blurSize;
-        public float BlurSize
+        get => blurSize;
+        set
         {
-            get => blurSize;
-            set
+            blurSize = value;
+            Blur.blurSize = blurSize / 10f;
+
+            if (blurSize >= 3f)
             {
-                blurSize = value;
-                Blur.blurSize = blurSize / 10f;
-                if (blurSize >= 3f)
-                {
-                    Blur.blurSize -= 0.3f;
-                    Blur.blurIterations = 1;
-                    Blur.downsample = 1;
-                }
-                else
-                {
-                    Blur.blurIterations = 0;
-                    Blur.downsample = 0;
-                }
+                Blur.blurSize -= 0.3f;
+                Blur.blurIterations = 1;
+                Blur.downsample = 1;
             }
-        }
-
-        public void Activate()
-        {
-            if (Blur == null)
+            else
             {
-                Ready = true;
-                Blur = GameMain.Instance.MainCamera.GetComponent<Blur>();
-                initialBlurSize = Blur.blurSize;
-                initialBlurIterations = Blur.blurIterations;
-                initialDownsample = Blur.downsample;
+                Blur.blurIterations = 0;
+                Blur.downsample = 0;
             }
-            SetEffectActive(false);
         }
+    }
 
-        public void Deactivate()
-        {
-            BlurSize = 0f;
-            Reset();
-            Blur.enabled = false;
-            Active = false;
-        }
+    private Blur Blur { get; set; }
 
-        public void SetEffectActive(bool active)
+    public void Activate()
+    {
+        if (!Blur)
         {
-            if (Blur.enabled = Active = active) BlurSize = BlurSize;
-            else Reset();
+            Ready = true;
+            Blur = GameMain.Instance.MainCamera.GetComponent<Blur>();
+            initialBlurSize = Blur.blurSize;
+            initialBlurIterations = Blur.blurIterations;
+            initialDownsample = Blur.downsample;
         }
 
-        public void Reset()
-        {
-            Blur.blurSize = initialBlurSize;
-            Blur.blurIterations = initialBlurIterations;
-            Blur.downsample = initialDownsample;
-        }
+        SetEffectActive(false);
+    }
+
+    public void Deactivate()
+    {
+        BlurSize = 0f;
+        Reset();
+        Blur.enabled = false;
+        Active = false;
+    }
+
+    public void SetEffectActive(bool active)
+    {
+        if (Blur.enabled = Active = active)
+            BlurSize = BlurSize;
+        else
+            Reset();
+    }
 
-        public void Update() { }
+    public void Reset()
+    {
+        Blur.blurSize = initialBlurSize;
+        Blur.blurIterations = initialBlurIterations;
+        Blur.downsample = initialDownsample;
+    }
+
+    public void Update()
+    {
     }
 }

+ 112 - 0
src/MeidoPhotoStudio.Plugin/Managers/EffectManagers/DepthOfFieldEffectManager.cs

@@ -0,0 +1,112 @@
+using UnityEngine;
+
+namespace MeidoPhotoStudio.Plugin;
+
+public class DepthOfFieldEffectManager : IEffectManager
+{
+    public const string Header = "EFFECT_DOF";
+
+    private readonly float initialValue = 0f;
+
+    private float focalLength;
+    private float focalSize;
+    private float aperture;
+    private float maxBlurSize;
+    private bool visualizeFocus;
+
+    public bool Ready { get; private set; }
+
+    public bool Active { get; private set; }
+
+    public float FocalLength
+    {
+        get => focalLength;
+        set => focalLength = DepthOfField.focalLength = value;
+    }
+
+    public float FocalSize
+    {
+        get => focalSize;
+        set => focalSize = DepthOfField.focalSize = value;
+    }
+
+    public float Aperture
+    {
+        get => aperture;
+        set => aperture = DepthOfField.aperture = value;
+    }
+
+    public float MaxBlurSize
+    {
+        get => maxBlurSize;
+        set => maxBlurSize = DepthOfField.maxBlurSize = value;
+    }
+
+    public bool VisualizeFocus
+    {
+        get => visualizeFocus;
+        set => visualizeFocus = DepthOfField.visualizeFocus = value;
+    }
+
+    private DepthOfFieldScatter DepthOfField { get; set; }
+
+    public void Activate()
+    {
+        if (!DepthOfField)
+        {
+            Ready = true;
+            DepthOfField = GameMain.Instance.MainCamera.GetOrAddComponent<DepthOfFieldScatter>();
+
+            if (!DepthOfField.dofHdrShader)
+                DepthOfField.dofHdrShader = Shader.Find("Hidden/Dof/DepthOfFieldHdr");
+
+            if (!DepthOfField.dx11BokehShader)
+                DepthOfField.dx11BokehShader = Shader.Find("Hidden/Dof/DX11Dof");
+
+            if (!DepthOfField.dx11BokehTexture)
+                DepthOfField.dx11BokehTexture = Resources.Load("Textures/hexShape") as Texture2D;
+        }
+
+        SetEffectActive(false);
+    }
+
+    public void Deactivate()
+    {
+        FocalLength = initialValue;
+        FocalSize = initialValue;
+        Aperture = initialValue;
+        MaxBlurSize = initialValue;
+        VisualizeFocus = false;
+        DepthOfField.enabled = false;
+        Active = false;
+    }
+
+    public void Reset()
+    {
+        DepthOfField.focalLength = initialValue;
+        DepthOfField.focalSize = initialValue;
+        DepthOfField.aperture = initialValue;
+        DepthOfField.maxBlurSize = initialValue;
+    }
+
+    public void SetEffectActive(bool active)
+    {
+        DepthOfField.enabled = active;
+
+        if (Active = active)
+        {
+            DepthOfField.focalLength = FocalLength;
+            DepthOfField.focalSize = FocalSize;
+            DepthOfField.aperture = Aperture;
+            DepthOfField.maxBlurSize = MaxBlurSize;
+        }
+        else
+        {
+            Reset();
+        }
+    }
+
+    public void Update()
+    {
+    }
+}

+ 0 - 100
src/MeidoPhotoStudio.Plugin/Managers/EffectManagers/DepthOfFieldManager.cs

@@ -1,100 +0,0 @@
-using UnityEngine;
-
-namespace MeidoPhotoStudio.Plugin
-{
-    public class DepthOfFieldEffectManager : IEffectManager
-    {
-        public const string header = "EFFECT_DOF";
-        private DepthOfFieldScatter DepthOfField { get; set; }
-        public bool Ready { get; private set; }
-        public bool Active { get; private set; }
-        private readonly float initialValue = 0f;
-        private float focalLength;
-        public float FocalLength
-        {
-            get => focalLength;
-            set => focalLength = DepthOfField.focalLength = value;
-        }
-
-        private float focalSize;
-        public float FocalSize
-        {
-            get => focalSize;
-            set => focalSize = DepthOfField.focalSize = value;
-        }
-        private float aperture;
-        public float Aperture
-        {
-            get => aperture;
-            set => aperture = DepthOfField.aperture = value;
-        }
-        private float maxBlurSize;
-        public float MaxBlurSize
-        {
-            get => maxBlurSize;
-            set => maxBlurSize = DepthOfField.maxBlurSize = value;
-        }
-        private bool visualizeFocus;
-        public bool VisualizeFocus
-        {
-            get => visualizeFocus;
-            set => visualizeFocus = DepthOfField.visualizeFocus = value;
-        }
-
-        public void Activate()
-        {
-            if (DepthOfField == null)
-            {
-                Ready = true;
-                DepthOfField = GameMain.Instance.MainCamera.GetOrAddComponent<DepthOfFieldScatter>();
-                if (DepthOfField.dofHdrShader == null)
-                {
-                    DepthOfField.dofHdrShader = Shader.Find("Hidden/Dof/DepthOfFieldHdr");
-                }
-                if (DepthOfField.dx11BokehShader == null)
-                {
-                    DepthOfField.dx11BokehShader = Shader.Find("Hidden/Dof/DX11Dof");
-                }
-                if (DepthOfField.dx11BokehTexture == null)
-                {
-                    DepthOfField.dx11BokehTexture = Resources.Load("Textures/hexShape") as Texture2D;
-                }
-            }
-            SetEffectActive(false);
-        }
-
-        public void Deactivate()
-        {
-            FocalLength = initialValue;
-            FocalSize = initialValue;
-            Aperture = initialValue;
-            MaxBlurSize = initialValue;
-            VisualizeFocus = false;
-            DepthOfField.enabled = false;
-            Active = false;
-        }
-
-        public void Reset()
-        {
-            DepthOfField.focalLength = initialValue;
-            DepthOfField.focalSize = initialValue;
-            DepthOfField.aperture = initialValue;
-            DepthOfField.maxBlurSize = initialValue;
-        }
-
-        public void SetEffectActive(bool active)
-        {
-            DepthOfField.enabled = active;
-            if (Active = active)
-            {
-                DepthOfField.focalLength = FocalLength;
-                DepthOfField.focalSize = FocalSize;
-                DepthOfField.aperture = Aperture;
-                DepthOfField.maxBlurSize = MaxBlurSize;
-            }
-            else Reset();
-        }
-
-        public void Update() { }
-    }
-}

+ 124 - 99
src/MeidoPhotoStudio.Plugin/Managers/EffectManagers/FogEffectManager.cs

@@ -1,126 +1,151 @@
 using UnityEngine;
 
-namespace MeidoPhotoStudio.Plugin
+namespace MeidoPhotoStudio.Plugin;
+
+public class FogEffectManager : IEffectManager
 {
-    public class FogEffectManager : IEffectManager
+    public const string Header = "EFFECT_FOG";
+
+    private readonly float initialDistance = 4f;
+    private readonly float initialDensity = 1f;
+    private readonly float initialHeightScale = 1f;
+    private readonly float initialHeight = 0f;
+    private readonly Color initialColour = Color.white;
+
+    private float distance;
+    private float density;
+    private float heightScale;
+    private float height;
+    private Color fogColour;
+
+    public bool Ready { get; private set; }
+
+    public bool Active { get; private set; }
+
+    public float Distance
     {
-        public const string header = "EFFECT_FOG";
-        private GlobalFog Fog { get; set; }
-        public bool Ready { get; private set; }
-        public bool Active { get; private set; }
-        private readonly float initialDistance = 4f;
-        private readonly float initialDensity = 1f;
-        private readonly float initialHeightScale = 1f;
-        private readonly float initialHeight = 0f;
-        private readonly Color initialColour = Color.white;
-        private float distance;
-        public float Distance
-        {
-            get => distance;
-            set => distance = Fog.startDistance = value;
-        }
-        private float density;
-        public float Density
-        {
-            get => density;
-            set => density = Fog.globalDensity = value;
-        }
-        private float heightScale;
-        public float HeightScale
-        {
-            get => heightScale;
-            set => heightScale = Fog.heightScale = value;
-        }
-        private float height;
-        public float Height
-        {
-            get => height;
-            set => height = Fog.height = value;
-        }
-        public float FogColourRed
-        {
-            get => FogColour.r;
-            set
-            {
-                Color fogColour = FogColour;
-                FogColour = new Color(value, fogColour.g, fogColour.b);
-            }
-        }
-        public float FogColourGreen
-        {
-            get => FogColour.g;
-            set
-            {
-                Color fogColour = FogColour;
-                FogColour = new Color(fogColour.r, value, fogColour.b);
-            }
-        }
-        public float FogColourBlue
+        get => distance;
+        set => distance = Fog.startDistance = value;
+    }
+
+    public float Density
+    {
+        get => density;
+        set => density = Fog.globalDensity = value;
+    }
+
+    public float HeightScale
+    {
+        get => heightScale;
+        set => heightScale = Fog.heightScale = value;
+    }
+
+    public float Height
+    {
+        get => height;
+        set => height = Fog.height = value;
+    }
+
+    public float FogColourRed
+    {
+        get => FogColour.r;
+        set
         {
-            get => FogColour.b;
-            set
-            {
-                Color fogColour = FogColour;
-                FogColour = new Color(fogColour.r, fogColour.g, value);
-            }
+            var fogColour = FogColour;
+
+            FogColour = new(value, fogColour.g, fogColour.b);
         }
-        private Color fogColour;
-        public Color FogColour
+    }
+
+    public float FogColourGreen
+    {
+        get => FogColour.g;
+        set
         {
-            get => fogColour;
-            set => fogColour = Fog.globalFogColor = value;
+            var fogColour = FogColour;
+
+            FogColour = new(fogColour.r, value, fogColour.b);
         }
+    }
 
-        public void Activate()
+    public float FogColourBlue
+    {
+        get => FogColour.b;
+        set
         {
-            if (Fog == null)
-            {
-                Ready = true;
-                Fog = GameMain.Instance.MainCamera.GetOrAddComponent<GlobalFog>();
-                if (Fog.fogShader == null) Fog.fogShader = Shader.Find("Hidden/GlobalFog");
-                Distance = initialDistance;
-                Density = initialDensity;
-                HeightScale = initialHeightScale;
-                Height = initialHeight;
-                FogColour = initialColour;
-            }
-            SetEffectActive(false);
+            var fogColour = FogColour;
+
+            FogColour = new(fogColour.r, fogColour.g, value);
         }
+    }
+
+    public Color FogColour
+    {
+        get => fogColour;
+        set => fogColour = Fog.globalFogColor = value;
+    }
 
-        public void Deactivate()
+    private GlobalFog Fog { get; set; }
+
+    public void Activate()
+    {
+        if (!Fog)
         {
+            Ready = true;
+            Fog = GameMain.Instance.MainCamera.GetOrAddComponent<GlobalFog>();
+
+            if (!Fog.fogShader)
+                Fog.fogShader = Shader.Find("Hidden/GlobalFog");
+
             Distance = initialDistance;
             Density = initialDensity;
             HeightScale = initialHeightScale;
             Height = initialHeight;
             FogColour = initialColour;
-            Fog.enabled = false;
-            Active = false;
         }
 
-        public void Reset()
+        SetEffectActive(false);
+    }
+
+    public void Deactivate()
+    {
+        Distance = initialDistance;
+        Density = initialDensity;
+        HeightScale = initialHeightScale;
+        Height = initialHeight;
+        FogColour = initialColour;
+        Fog.enabled = false;
+        Active = false;
+    }
+
+    public void Reset()
+    {
+        Fog.startDistance = initialDistance;
+        Fog.globalDensity = initialDensity;
+        Fog.heightScale = initialHeightScale;
+        Fog.height = initialHeight;
+        Fog.globalFogColor = initialColour;
+    }
+
+    public void SetEffectActive(bool active)
+    {
+        Fog.enabled = active;
+
+        if (Active = active)
         {
-            Fog.startDistance = initialDistance;
-            Fog.globalDensity = initialDensity;
-            Fog.heightScale = initialHeightScale;
-            Fog.height = initialHeight;
-            Fog.globalFogColor = initialColour;
+            Fog.startDistance = Distance;
+            Fog.globalDensity = Density;
+            Fog.heightScale = HeightScale;
+            Fog.height = Height;
+            Fog.globalFogColor = FogColour;
         }
-
-        public void SetEffectActive(bool active)
+        else
         {
-            Fog.enabled = active;
-            if (Active = active)
-            {
-                Fog.startDistance = Distance;
-                Fog.globalDensity = Density;
-                Fog.heightScale = HeightScale;
-                Fog.height = Height;
-                Fog.globalFogColor = FogColour;
-            }
-            else Reset();
+            Reset();
         }
+    }
 
-        public void Update() { }
+    public void Update()
+    {
     }
 }

+ 10 - 8
src/MeidoPhotoStudio.Plugin/Managers/EffectManagers/IEffectManager.cs

@@ -1,10 +1,12 @@
-namespace MeidoPhotoStudio.Plugin
+namespace MeidoPhotoStudio.Plugin;
+
+public interface IEffectManager : IManager
 {
-    public interface IEffectManager : IManager
-    {
-        bool Ready { get; }
-        bool Active { get; }
-        void SetEffectActive(bool active);
-        void Reset();
-    }
+    bool Ready { get; }
+
+    bool Active { get; }
+
+    void SetEffectActive(bool active);
+
+    void Reset();
 }

+ 30 - 20
src/MeidoPhotoStudio.Plugin/Managers/EffectManagers/SepiaToneEffectManager.cs

@@ -1,32 +1,42 @@
 using UnityEngine;
 
-namespace MeidoPhotoStudio.Plugin
+namespace MeidoPhotoStudio.Plugin;
+
+public class SepiaToneEffectManager : IEffectManager
 {
-    public class SepiaToneEffectManger : IEffectManager
-    {
-        public const string header = "EFFECT_SEPIA";
-        private SepiaToneEffect SepiaTone { get; set; }
-        public bool Ready { get; private set; }
-        public bool Active { get; private set; }
+    public const string Header = "EFFECT_SEPIA";
+
+    public bool Ready { get; private set; }
 
-        public void Activate()
+    public bool Active { get; private set; }
+
+    private SepiaToneEffect SepiaTone { get; set; }
+
+    public void Activate()
+    {
+        if (!SepiaTone)
         {
-            if (SepiaTone == null)
-            {
-                Ready = true;
-                SepiaTone = GameMain.Instance.MainCamera.GetOrAddComponent<SepiaToneEffect>();
-
-                if (SepiaTone.shader == null) SepiaTone.shader = Shader.Find("Hidden/Sepiatone Effect");
-            }
-            SetEffectActive(false);
+            Ready = true;
+            SepiaTone = GameMain.Instance.MainCamera.GetOrAddComponent<SepiaToneEffect>();
+
+            if (!SepiaTone.shader)
+                SepiaTone.shader = Shader.Find("Hidden/Sepiatone Effect");
         }
 
-        public void Deactivate() => SetEffectActive(false);
+        SetEffectActive(false);
+    }
+
+    public void Deactivate() =>
+        SetEffectActive(false);
 
-        public void SetEffectActive(bool active) => SepiaTone.enabled = Active = active;
+    public void SetEffectActive(bool active) =>
+        SepiaTone.enabled = Active = active;
 
-        public void Reset() { }
+    public void Reset()
+    {
+    }
 
-        public void Update() { }
+    public void Update()
+    {
     }
 }

+ 87 - 73
src/MeidoPhotoStudio.Plugin/Managers/EffectManagers/VignetteEffectManager.cs

@@ -1,87 +1,101 @@
-namespace MeidoPhotoStudio.Plugin
+namespace MeidoPhotoStudio.Plugin;
+
+public class VignetteEffectManager : IEffectManager
 {
-    public class VignetteEffectManager : IEffectManager
+    public const string Header = "EFFECT_VIGNETTE";
+
+    private float initialIntensity;
+    private float initialBlur;
+    private float initialBlurSpread;
+    private float initialChromaticAberration;
+    private float blur;
+    private float blurSpread;
+    private float chromaticAberration;
+    private float intensity;
+
+    public bool Ready { get; private set; }
+
+    public bool Active { get; private set; }
+
+    public float Intensity
     {
-        public const string header = "EFFECT_VIGNETTE";
-        private Vignetting Vignette { get; set; }
-        private float initialIntensity;
-        private float initialBlur;
-        private float initialBlurSpread;
-        private float initialChromaticAberration;
-        public bool Ready { get; private set; }
-        public bool Active { get; private set; }
-        private float intensity;
-        public float Intensity
-        {
-            get => intensity;
-            set => intensity = Vignette.intensity = value;
-        }
-        private float blur;
-        public float Blur
-        {
-            get => blur;
-            set => blur = Vignette.blur = value;
-        }
-        private float blurSpread;
-        public float BlurSpread
-        {
-            get => blurSpread;
-            set => blurSpread = Vignette.blurSpread = value;
-        }
-        private float chromaticAberration;
-        public float ChromaticAberration
-        {
-            get => chromaticAberration;
-            set => chromaticAberration = Vignette.chromaticAberration = value;
-        }
+        get => intensity;
+        set => intensity = Vignette.intensity = value;
+    }
 
-        public void Activate()
-        {
-            if (Vignette == null)
-            {
-                Ready = true;
-                Vignette = GameMain.Instance.MainCamera.GetOrAddComponent<Vignetting>();
-                Vignette.mode = Vignetting.AberrationMode.Simple;
-
-                initialIntensity = Vignette.intensity;
-                initialBlur = Vignette.blur;
-                initialBlurSpread = Vignette.blurSpread;
-                initialChromaticAberration = Vignette.chromaticAberration;
-            }
-            SetEffectActive(false);
-        }
+    public float Blur
+    {
+        get => blur;
+        set => blur = Vignette.blur = value;
+    }
+
+    public float BlurSpread
+    {
+        get => blurSpread;
+        set => blurSpread = Vignette.blurSpread = value;
+    }
+
+    public float ChromaticAberration
+    {
+        get => chromaticAberration;
+        set => chromaticAberration = Vignette.chromaticAberration = value;
+    }
+
+    private Vignetting Vignette { get; set; }
 
-        public void Deactivate()
+    public void Activate()
+    {
+        if (!Vignette)
         {
-            Intensity = initialIntensity;
-            Blur = initialBlur;
-            BlurSpread = initialBlurSpread;
-            ChromaticAberration = initialChromaticAberration;
-            Vignette.enabled = false;
-            Active = false;
+            Ready = true;
+            Vignette = GameMain.Instance.MainCamera.GetOrAddComponent<Vignetting>();
+            Vignette.mode = Vignetting.AberrationMode.Simple;
+
+            initialIntensity = Vignette.intensity;
+            initialBlur = Vignette.blur;
+            initialBlurSpread = Vignette.blurSpread;
+            initialChromaticAberration = Vignette.chromaticAberration;
         }
 
-        public void Reset()
+        SetEffectActive(false);
+    }
+
+    public void Deactivate()
+    {
+        Intensity = initialIntensity;
+        Blur = initialBlur;
+        BlurSpread = initialBlurSpread;
+        ChromaticAberration = initialChromaticAberration;
+        Vignette.enabled = false;
+        Active = false;
+    }
+
+    public void Reset()
+    {
+        Vignette.intensity = initialIntensity;
+        Vignette.blur = initialBlur;
+        Vignette.blurSpread = initialBlurSpread;
+        Vignette.chromaticAberration = initialChromaticAberration;
+    }
+
+    public void SetEffectActive(bool active)
+    {
+        Vignette.enabled = active;
+
+        if (Active = active)
         {
-            Vignette.intensity = initialIntensity;
-            Vignette.blur = initialBlur;
-            Vignette.blurSpread = initialBlurSpread;
-            Vignette.chromaticAberration = initialChromaticAberration;
+            Vignette.intensity = Intensity;
+            Vignette.blur = Blur;
+            Vignette.blurSpread = BlurSpread;
+            Vignette.chromaticAberration = ChromaticAberration;
         }
-
-        public void SetEffectActive(bool active)
+        else
         {
-            Vignette.enabled = active;
-            if (Active = active)
-            {
-                Vignette.intensity = Intensity;
-                Vignette.blur = Blur;
-                Vignette.blurSpread = BlurSpread;
-                Vignette.chromaticAberration = ChromaticAberration;
-            }
-            else Reset();
+            Reset();
         }
+    }
 
-        public void Update() { }
+    public void Update()
+    {
     }
 }

+ 137 - 109
src/MeidoPhotoStudio.Plugin/Managers/EnvironmentManager.cs

@@ -1,144 +1,172 @@
-using System;
+using System;
+
 using UnityEngine;
+
 using Object = UnityEngine.Object;
 
-namespace MeidoPhotoStudio.Plugin
+namespace MeidoPhotoStudio.Plugin;
+
+public class EnvironmentManager : IManager
 {
-    using Input = InputManager;
-    public class EnvironmentManager : IManager
+    public const string Header = "ENVIRONMENT";
+    public const string DefaultBg = "Theater";
+
+    private const string MyRoomPrefix = "マイルーム:";
+
+    private static readonly BgMgr BgMgr = GameMain.Instance.BgMgr;
+
+    private static bool cubeActive;
+    private static bool cubeSmall;
+
+    private Transform bg;
+    private GameObject bgObject;
+    private DragPointBG bgDragPoint;
+    private bool bgVisible = true;
+
+    public EnvironmentManager()
     {
-        private static readonly BgMgr bgMgr = GameMain.Instance.BgMgr;
-        public const string header = "ENVIRONMENT";
-        public const string defaultBg = "Theater";
-        private const string myRoomPrefix = "マイルーム:";
-        private static bool cubeActive;
-        public static bool CubeActive
-        {
-            get => cubeActive;
-            set
-            {
-                if (value == cubeActive) return;
-                cubeActive = value;
-                CubeActiveChange?.Invoke(null, EventArgs.Empty);
-            }
-        }
-        private static bool cubeSmall;
-        public static bool CubeSmall
-        {
-            get => cubeSmall;
-            set
-            {
-                if (value == cubeSmall) return;
-                cubeSmall = value;
-                CubeSmallChange?.Invoke(null, EventArgs.Empty);
-            }
-        }
-        private static event EventHandler CubeActiveChange;
-        private static event EventHandler CubeSmallChange;
-        private Transform bg;
-        private GameObject bgObject;
-        private DragPointBG bgDragPoint;
-        public string CurrentBgAsset { get; private set; } = defaultBg;
-        private bool bgVisible = true;
-        public bool BGVisible
+        DragPointLight.EnvironmentManager = this;
+        Activate();
+    }
+
+    private static event EventHandler CubeActiveChange;
+
+    private static event EventHandler CubeSmallChange;
+
+    public static bool CubeActive
+    {
+        get => cubeActive;
+        set
         {
-            get => bgVisible;
-            set
-            {
-                bgVisible = value;
-                bgObject.SetActive(bgVisible);
-            }
+            if (value == cubeActive)
+                return;
+
+            cubeActive = value;
+            CubeActiveChange?.Invoke(null, EventArgs.Empty);
         }
+    }
 
-        public EnvironmentManager()
+    public static bool CubeSmall
+    {
+        get => cubeSmall;
+        set
         {
-            DragPointLight.EnvironmentManager = this;
-            Activate();
+            if (value == cubeSmall)
+                return;
+
+            cubeSmall = value;
+            CubeSmallChange?.Invoke(null, EventArgs.Empty);
         }
+    }
 
-        public void Update() { }
+    public string CurrentBgAsset { get; private set; } = DefaultBg;
 
-        public void Activate()
+    public bool BGVisible
+    {
+        get => bgVisible;
+        set
         {
-            BgMgrPatcher.ChangeBgBegin += OnChangeBegin;
-            BgMgrPatcher.ChangeBgEnd += OnChangeEnd;
+            bgVisible = value;
+            bgObject.SetActive(bgVisible);
+        }
+    }
 
-            bgObject = bgMgr.Parent;
+    public void Update()
+    {
+    }
 
-            bgObject.SetActive(true);
-            
-            if (MeidoPhotoStudio.EditMode) UpdateBG();
-            else ChangeBackground(defaultBg);
+    public void Activate()
+    {
+        BgMgrPatcher.ChangeBgBegin += OnChangeBegin;
+        BgMgrPatcher.ChangeBgEnd += OnChangeEnd;
 
-            CubeSmallChange += OnCubeSmall;
-            CubeActiveChange += OnCubeActive;
-        }
+        bgObject = BgMgr.Parent;
 
-        public void Deactivate()
-        {
-            BgMgrPatcher.ChangeBgBegin -= OnChangeBegin;
-            BgMgrPatcher.ChangeBgEnd -= OnChangeEnd;
+        bgObject.SetActive(true);
 
-            DestroyDragPoint();
-            BGVisible = true;
+        if (MeidoPhotoStudio.EditMode)
+            UpdateBG();
+        else
+            ChangeBackground(DefaultBg);
 
-            if (MeidoPhotoStudio.EditMode) bgMgr.ChangeBg(defaultBg);
-            else
-            {
-                var isNight = GameMain.Instance.CharacterMgr.status.GetFlag("時間帯") == 3;
-                bgMgr.ChangeBg(isNight ? "ShinShitsumu_ChairRot_Night" : "ShinShitsumu_ChairRot");
-            }
+        CubeSmallChange += OnCubeSmall;
+        CubeActiveChange += OnCubeActive;
+    }
 
-            if (bgMgr.BgObject) bgMgr.BgObject.transform.localScale = Vector3.one;
+    public void Deactivate()
+    {
+        BgMgrPatcher.ChangeBgBegin -= OnChangeBegin;
+        BgMgrPatcher.ChangeBgEnd -= OnChangeEnd;
 
-            CubeSmallChange -= OnCubeSmall;
-            CubeActiveChange -= OnCubeActive;
-        }
+        DestroyDragPoint();
+        BGVisible = true;
 
-        public void ChangeBackground(string assetName, bool creative = false)
+        if (MeidoPhotoStudio.EditMode)
         {
-            if (creative) bgMgr.ChangeBgMyRoom(assetName);
-            else bgMgr.ChangeBg(assetName);
+            BgMgr.ChangeBg(DefaultBg);
         }
-
-        private void AttachDragPoint(Transform bgTransform)
+        else
         {
-            bgDragPoint = DragPoint.Make<DragPointBG>(PrimitiveType.Cube, Vector3.one * 0.12f);
-            bgDragPoint.Initialize(() => bgTransform.position, () => Vector3.zero);
-            bgDragPoint.Set(bgTransform);
-            bgDragPoint.AddGizmo();
-            bgDragPoint.ConstantScale = true;
-            bgDragPoint.gameObject.SetActive(CubeActive);
+            var isNight = GameMain.Instance.CharacterMgr.status.GetFlag("時間帯") is 3;
+
+            BgMgr.ChangeBg(isNight ? "ShinShitsumu_ChairRot_Night" : "ShinShitsumu_ChairRot");
         }
 
-        private void OnChangeBegin(object sender, EventArgs args) => DestroyDragPoint();
+        if (BgMgr.BgObject)
+            BgMgr.BgObject.transform.localScale = Vector3.one;
 
-        private void OnChangeEnd(object sender, EventArgs args) => UpdateBG();
+        CubeSmallChange -= OnCubeSmall;
+        CubeActiveChange -= OnCubeActive;
+    }
 
-        private void UpdateBG()
-        {
-            if (!bgMgr.BgObject) return;
+    public void ChangeBackground(string assetName, bool creative = false)
+    {
+        if (creative)
+            BgMgr.ChangeBgMyRoom(assetName);
+        else
+            BgMgr.ChangeBg(assetName);
+    }
 
-            CurrentBgAsset = bgMgr.GetBGName();
-            if (CurrentBgAsset.StartsWith(myRoomPrefix))
-                CurrentBgAsset = CurrentBgAsset.Replace(myRoomPrefix, string.Empty);
-            bg = bgMgr.BgObject.transform;
-            AttachDragPoint(bg);
-        }
+    private void AttachDragPoint(Transform bgTransform)
+    {
+        bgDragPoint = DragPoint.Make<DragPointBG>(PrimitiveType.Cube, Vector3.one * 0.12f);
+        bgDragPoint.Initialize(() => bgTransform.position, () => Vector3.zero);
+        bgDragPoint.Set(bgTransform);
+        bgDragPoint.AddGizmo();
+        bgDragPoint.ConstantScale = true;
+        bgDragPoint.gameObject.SetActive(CubeActive);
+    }
 
-        private void DestroyDragPoint()
-        {
-            if (bgDragPoint) Object.Destroy(bgDragPoint.gameObject);
-        }
+    private void OnChangeBegin(object sender, EventArgs args) =>
+        DestroyDragPoint();
 
-        private void OnCubeSmall(object sender, EventArgs args)
-        {
-            bgDragPoint.DragPointScale = CubeSmall ? DragPointGeneral.smallCube : 1f;
-        }
+    private void OnChangeEnd(object sender, EventArgs args) =>
+        UpdateBG();
 
-        private void OnCubeActive(object sender, EventArgs args)
-        {
-            bgDragPoint.gameObject.SetActive(CubeActive);
-        }
+    private void UpdateBG()
+    {
+        if (!BgMgr.BgObject)
+            return;
+
+        CurrentBgAsset = BgMgr.GetBGName();
+
+        if (CurrentBgAsset.StartsWith(MyRoomPrefix))
+            CurrentBgAsset = CurrentBgAsset.Replace(MyRoomPrefix, string.Empty);
+
+        bg = BgMgr.BgObject.transform;
+
+        AttachDragPoint(bg);
     }
+
+    private void DestroyDragPoint()
+    {
+        if (bgDragPoint)
+            Object.Destroy(bgDragPoint.gameObject);
+    }
+
+    private void OnCubeSmall(object sender, EventArgs args) =>
+        bgDragPoint.DragPointScale = CubeSmall ? DragPointGeneral.SmallCube : 1f;
+
+    private void OnCubeActive(object sender, EventArgs args) =>
+        bgDragPoint.gameObject.SetActive(CubeActive);
 }

+ 8 - 7
src/MeidoPhotoStudio.Plugin/Managers/IManager.cs

@@ -1,9 +1,10 @@
-namespace MeidoPhotoStudio.Plugin
+namespace MeidoPhotoStudio.Plugin;
+
+public interface IManager
 {
-    public interface IManager
-    {
-        void Update();
-        void Activate();
-        void Deactivate();
-    }
+    void Update();
+
+    void Activate();
+
+    void Deactivate();
 }

+ 160 - 114
src/MeidoPhotoStudio.Plugin/Managers/InputManager.cs

@@ -1,148 +1,194 @@
 using System;
 using System.Collections.Generic;
 using System.Linq;
-using UnityEngine;
+
 using BepInEx.Configuration;
+using UnityEngine;
+
+namespace MeidoPhotoStudio.Plugin;
 
-namespace MeidoPhotoStudio.Plugin
+public enum MpsKey
 {
-    public static class InputManager
+    // MeidoPhotoStudio
+    Activate,
+    Screenshot,
+    ToggleUI,
+    ToggleMessage,
+
+    // MeidoManager
+    MeidoUndressing,
+
+    // Camera
+    CameraLayer,
+    CameraReset,
+    CameraSave,
+    CameraLoad,
+
+    // Dragpoint
+    DragSelect,
+    DragDelete,
+    DragMove,
+    DragRotate,
+    DragScale,
+    DragFinger,
+
+    // Scene management
+    SaveScene,
+    LoadScene,
+    OpenSceneManager,
+}
+
+public static class InputManager
+{
+    public const KeyCode UpperKeyCode = KeyCode.F15;
+    public const string ConfigHeader = "Controls";
+
+    public static readonly AcceptableValueBase ControlRange;
+
+    private static readonly Dictionary<MpsKey, KeyCode> ActionKeys = new();
+    private static readonly Dictionary<MpsKey, ConfigEntry<KeyCode>> ConfigEntries = new();
+
+    private static InputListener inputListener;
+
+    static InputManager() =>
+        ControlRange = new AcceptableValueRange<KeyCode>(default, UpperKeyCode);
+
+    public static event EventHandler KeyChange;
+
+    public static KeyCode CurrentKeyCode { get; private set; }
+
+    public static bool Listening { get; private set; }
+
+    public static bool Control =>
+        Input.GetKey(KeyCode.LeftControl) || Input.GetKey(KeyCode.RightControl);
+
+    public static bool Alt =>
+        Input.GetKey(KeyCode.LeftAlt) || Input.GetKey(KeyCode.RightAlt);
+
+    public static bool Shift =>
+        Input.GetKey(KeyCode.LeftShift) || Input.GetKey(KeyCode.RightShift);
+
+    public static void Register(MpsKey action, KeyCode key, string description)
     {
-        private static InputListener inputListener;
-        private static readonly Dictionary<MpsKey, KeyCode> ActionKeys = new Dictionary<MpsKey, KeyCode>();
-        private static readonly Dictionary<MpsKey, ConfigEntry<KeyCode>> ConfigEntries
-            = new Dictionary<MpsKey, ConfigEntry<KeyCode>>();
-        public static KeyCode CurrentKeyCode { get; private set; }
-        public static bool Listening { get; private set; }
-        public static event EventHandler KeyChange;
-        public static bool Control => Input.GetKey(KeyCode.LeftControl) || Input.GetKey(KeyCode.RightControl);
-        public static bool Alt => Input.GetKey(KeyCode.LeftAlt) || Input.GetKey(KeyCode.RightAlt);
-        public static bool Shift => Input.GetKey(KeyCode.LeftShift) || Input.GetKey(KeyCode.RightShift);
-        public static readonly AcceptableValueBase controlRange;
-        public const KeyCode upperKeyCode = KeyCode.F15;
-        public const string configHeader = "Controls";
-
-        static InputManager() => controlRange = new AcceptableValueRange<KeyCode>(default, upperKeyCode);
-
-        public static void Register(MpsKey action, KeyCode key, string description)
+        key = Clamp(key, default, UpperKeyCode);
+
+        if (ConfigEntries.ContainsKey(action))
         {
-            key = Clamp(key, default, upperKeyCode);
-            if (ConfigEntries.ContainsKey(action)) Rebind(action, key);
-            else
-            {
-                ConfigDescription configDescription = new ConfigDescription(description, controlRange);
-                ConfigEntries[action] = Configuration.Config.Bind(
-                    configHeader, action.ToString(), key, configDescription
-                );
-                key = ConfigEntries[action].Value;
-                ActionKeys[action] = key;
-            }
+            Rebind(action, key);
         }
-
-        public static void Rebind(MpsKey action, KeyCode key)
+        else
         {
-            key = Clamp(key, default, upperKeyCode);
-            if (ConfigEntries.ContainsKey(action)) ConfigEntries[action].Value = key;
+            var configDescription = new ConfigDescription(description, ControlRange);
+
+            ConfigEntries[action] =
+                Configuration.Config.Bind(ConfigHeader, action.ToString(), key, configDescription);
+
+            key = ConfigEntries[action].Value;
             ActionKeys[action] = key;
         }
+    }
 
-        public static KeyCode Clamp(KeyCode value, KeyCode min, KeyCode max)
-            => value < min ? min : value > max ? max : value;
+    public static void Rebind(MpsKey action, KeyCode key)
+    {
+        key = Clamp(key, default, UpperKeyCode);
 
-        public static KeyCode GetActionKey(MpsKey action)
-            => ActionKeys.TryGetValue(action, out KeyCode keyCode) ? keyCode : default;
+        if (ConfigEntries.ContainsKey(action))
+            ConfigEntries[action].Value = key;
 
-        public static void StartListening()
-        {
-            if (inputListener == null) inputListener = new GameObject().AddComponent<InputListener>();
-            else if (inputListener.gameObject.activeSelf) StopListening();
+        ActionKeys[action] = key;
+    }
 
-            inputListener.gameObject.SetActive(true);
-            inputListener.KeyChange += OnKeyChange;
-            CurrentKeyCode = KeyCode.None;
-            Listening = true;
-        }
+    public static KeyCode Clamp(KeyCode value, KeyCode min, KeyCode max) =>
+        value < min ? min : value > max ? max : value;
 
-        public static void StopListening()
-        {
-            if (!inputListener || !inputListener.gameObject.activeSelf) return;
-            inputListener.gameObject.SetActive(false);
-            inputListener.KeyChange -= OnKeyChange;
-            CurrentKeyCode = KeyCode.None;
-            Listening = false;
-            Input.ResetInputAxes();
-        }
+    public static KeyCode GetActionKey(MpsKey action) =>
+        ActionKeys.TryGetValue(action, out var keyCode) ? keyCode : default;
 
-        public static bool GetKey(MpsKey action)
-            => !Listening && ActionKeys.ContainsKey(action) && Input.GetKey(ActionKeys[action]);
+    public static void StartListening()
+    {
+        if (!inputListener)
+            inputListener = new GameObject().AddComponent<InputListener>();
+        else if (inputListener.gameObject.activeSelf)
+            StopListening();
 
-        public static bool GetKeyDown(MpsKey action)
-            => !Listening && ActionKeys.ContainsKey(action) && Input.GetKeyDown(ActionKeys[action]);
+        inputListener.gameObject.SetActive(true);
+        inputListener.KeyChange += OnKeyChange;
+        CurrentKeyCode = KeyCode.None;
+        Listening = true;
+    }
 
-        public static void Deactivate()
-        {
-            StopListening();
-            GameObject.Destroy(inputListener?.gameObject);
-            inputListener = null;
-        }
+    public static void StopListening()
+    {
+        if (!inputListener || !inputListener.gameObject.activeSelf)
+            return;
+
+        inputListener.gameObject.SetActive(false);
+        inputListener.KeyChange -= OnKeyChange;
+        CurrentKeyCode = KeyCode.None;
+        Listening = false;
+        Input.ResetInputAxes();
+    }
 
-        private static void OnKeyChange(object sender, KeyChangeEventArgs args)
-        {
-            CurrentKeyCode = args.Key;
-            KeyChange?.Invoke(null, EventArgs.Empty);
-            StopListening();
-        }
+    public static bool GetKey(MpsKey action) =>
+        !Listening && ActionKeys.ContainsKey(action) && Input.GetKey(ActionKeys[action]);
+
+    public static bool GetKeyDown(MpsKey action) =>
+        !Listening && ActionKeys.ContainsKey(action) && Input.GetKeyDown(ActionKeys[action]);
+
+    public static void Deactivate()
+    {
+        StopListening();
+
+        // TODO: Null propagation does not work with UnityEngine.Object
+        UnityEngine.Object.Destroy(inputListener?.gameObject);
+        inputListener = null;
+    }
+
+    private static void OnKeyChange(object sender, KeyChangeEventArgs args)
+    {
+        CurrentKeyCode = args.Key;
+        KeyChange?.Invoke(null, EventArgs.Empty);
+        StopListening();
+    }
+
+    /* Listener taken from https://forum.unity.com/threads/find-out-which-key-was-pressed.385250/ */
+    private class InputListener : MonoBehaviour
+    {
+        private static readonly KeyCode[] KeyCodes;
+
+        static InputListener() =>
+            KeyCodes = Enum.GetValues(typeof(KeyCode))
+            .Cast<KeyCode>()
+            .Where(keyCode => keyCode <= UpperKeyCode)
+            .ToArray();
+
+        public event EventHandler<KeyChangeEventArgs> KeyChange;
 
-        /* Listener taken from https://forum.unity.com/threads/find-out-which-key-was-pressed.385250/ */
-        private class InputListener : MonoBehaviour
+        private void Awake() =>
+            DontDestroyOnLoad(this);
+
+        private void Update()
         {
-            private static readonly KeyCode[] keyCodes;
-            public event EventHandler<KeyChangeEventArgs> KeyChange;
+            if (!Input.anyKeyDown)
+                return;
 
-            static InputListener()
+            foreach (var key in KeyCodes)
             {
-                keyCodes = Enum.GetValues(typeof(KeyCode))
-                    .Cast<KeyCode>()
-                    .Where(keyCode => keyCode <= upperKeyCode)
-                    .ToArray();
-            }
+                if (!Input.GetKeyDown(key))
+                    continue;
 
-            private void Awake() => DontDestroyOnLoad(this);
+                KeyChange?.Invoke(this, new(key));
 
-            private void Update()
-            {
-                if (Input.anyKeyDown)
-                {
-                    foreach (KeyCode key in keyCodes)
-                    {
-                        if (Input.GetKeyDown(key))
-                        {
-                            KeyChange?.Invoke(this, new KeyChangeEventArgs(key));
-                            break;
-                        }
-                    }
-                }
+                break;
             }
         }
-
-        private class KeyChangeEventArgs : EventArgs
-        {
-            public KeyCode Key { get; }
-            public KeyChangeEventArgs(KeyCode key) => Key = key;
-        }
     }
 
-    public enum MpsKey
+    private class KeyChangeEventArgs : EventArgs
     {
-        // MeidoPhotoStudio
-        Activate, Screenshot, ToggleUI, ToggleMessage,
-        // MeidoManager
-        MeidoUndressing,
-        // Camera
-        CameraLayer, CameraReset, CameraSave, CameraLoad,
-        // Dragpoint
-        DragSelect, DragDelete, DragMove, DragRotate, DragScale, DragFinger,
-        // Scene management
-        SaveScene, LoadScene, OpenSceneManager
+        public KeyChangeEventArgs(KeyCode key) =>
+            Key = key;
+
+        public KeyCode Key { get; }
     }
 }

+ 174 - 132
src/MeidoPhotoStudio.Plugin/Managers/LightManager.cs

@@ -1,173 +1,215 @@
 using System;
 using System.Collections.Generic;
 using System.Linq;
+
 using UnityEngine;
 
-namespace MeidoPhotoStudio.Plugin
+namespace MeidoPhotoStudio.Plugin;
+
+public class LightManager : IManager
 {
-    public class LightManager : IManager
+    public const string Header = "LIGHT";
+
+    private static bool cubeActive = true;
+
+    private readonly List<DragPointLight> lightList = new();
+
+    private int selectedLightIndex;
+
+    public LightManager() =>
+        Activate();
+
+    public event EventHandler Rotate;
+
+    public event EventHandler Scale;
+
+    public event EventHandler ListModified;
+
+    public event EventHandler Select;
+
+    private static event EventHandler CubeActiveChange;
+
+    public static bool CubeActive
     {
-        public const string header = "LIGHT";
-        private static bool cubeActive = true;
-        public static bool CubeActive
+        get => cubeActive;
+        set
         {
-            get => cubeActive;
-            set
-            {
-                if (value != cubeActive)
-                {
-                    cubeActive = value;
-                    CubeActiveChange?.Invoke(null, EventArgs.Empty);
-                }
-            }
+            if (value == cubeActive)
+                return;
+
+            cubeActive = value;
+            CubeActiveChange?.Invoke(null, EventArgs.Empty);
         }
-        private static event EventHandler CubeActiveChange;
-        private readonly List<DragPointLight> lightList = new List<DragPointLight>();
-        private int selectedLightIndex;
-        public int SelectedLightIndex
+    }
+
+    public int SelectedLightIndex
+    {
+        get => selectedLightIndex;
+        set
         {
-            get => selectedLightIndex;
-            set
-            {
-                selectedLightIndex = Mathf.Clamp(value, 0, lightList.Count - 1);
-                lightList[SelectedLightIndex].IsActiveLight = true;
-            }
+            selectedLightIndex = Mathf.Clamp(value, 0, lightList.Count - 1);
+            lightList[SelectedLightIndex].IsActiveLight = true;
         }
-        public string[] LightNameList => lightList.Select(light => LightName(light.Name)).ToArray();
-        public string ActiveLightName => LightName(lightList[SelectedLightIndex].Name);
-        public DragPointLight CurrentLight => lightList[SelectedLightIndex];
-        public event EventHandler Rotate;
-        public event EventHandler Scale;
-        public event EventHandler ListModified;
-        public event EventHandler Select;
+    }
 
-        public LightManager() => Activate();
+    public string[] LightNameList =>
+        lightList.Select(light => LightName(light.Name)).ToArray();
 
-        public void Activate()
-        {
-            GameMain.Instance.MainCamera.GetComponent<Camera>().backgroundColor = Color.black;
-            AddLight(GameMain.Instance.MainLight.gameObject, true);
-            CubeActiveChange += OnCubeActive;
-        }
+    public string ActiveLightName =>
+        LightName(lightList[SelectedLightIndex].Name);
 
-        public void Deactivate()
-        {
-            for (int i = 0; i < lightList.Count; i++)
-            {
-                DestroyLight(lightList[i]);
-            }
-            selectedLightIndex = 0;
-            lightList.Clear();
+    public DragPointLight CurrentLight =>
+        lightList[SelectedLightIndex];
 
-            GameMain.Instance.MainLight.Reset();
+    public void Activate()
+    {
+        GameMain.Instance.MainCamera.GetComponent<Camera>().backgroundColor = Color.black;
+        AddLight(GameMain.Instance.MainLight.gameObject, true);
+        CubeActiveChange += OnCubeActive;
+    }
 
-            Light mainLight = GameMain.Instance.MainLight.GetComponent<Light>();
-            mainLight.type = LightType.Directional;
-            DragPointLight.SetLightProperties(mainLight, new LightProperty());
-            CubeActiveChange -= OnCubeActive;
-        }
+    public void Deactivate()
+    {
+        for (var i = 0; i < lightList.Count; i++)
+            DestroyLight(lightList[i]);
 
-        public void Update() { }
+        selectedLightIndex = 0;
+        lightList.Clear();
 
-        public void AddLight(GameObject lightGo = null, bool isMain = false)
-        {
-            GameObject go = lightGo ?? new GameObject("MPS Light");
-            DragPointLight light = DragPoint.Make<DragPointLight>(PrimitiveType.Cube, Vector3.one * 0.12f);
-            light.Initialize(() => go.transform.position, () => go.transform.eulerAngles);
-            light.Set(go.transform);
-            light.IsMain = isMain;
-
-            light.Rotate += OnRotate;
-            light.Scale += OnScale;
-            light.Delete += OnDelete;
-            light.Select += OnSelect;
-
-            lightList.Add(light);
-
-            CurrentLight.IsActiveLight = false;
-            SelectedLightIndex = lightList.Count;
-            OnListModified();
-        }
+        GameMain.Instance.MainLight.Reset();
 
-        public void DeleteActiveLight()
-        {
-            if (selectedLightIndex == 0) return;
+        var mainLight = GameMain.Instance.MainLight.GetComponent<Light>();
 
-            DeleteLight(SelectedLightIndex);
-        }
+        mainLight.type = LightType.Directional;
+        DragPointLight.SetLightProperties(mainLight, new());
+        CubeActiveChange -= OnCubeActive;
+    }
 
-        public void DeleteLight(int lightIndex, bool noUpdate = false)
-        {
-            if (lightIndex == 0) return;
+    public void Update()
+    {
+    }
 
-            DestroyLight(lightList[lightIndex]);
-            lightList.RemoveAt(lightIndex);
+    public void AddLight(GameObject lightGo = null, bool isMain = false)
+    {
+        // TODO: null propagation does not work with UntiyEngine.Object
+        var go = lightGo ?? new GameObject("MPS Light");
+        var light = DragPoint.Make<DragPointLight>(PrimitiveType.Cube, Vector3.one * 0.12f);
 
-            if (lightIndex <= SelectedLightIndex) SelectedLightIndex--;
+        light.Initialize(() => go.transform.position, () => go.transform.eulerAngles);
+        light.Set(go.transform);
+        light.IsMain = isMain;
 
-            if (noUpdate) return;
-            OnListModified();
-        }
+        light.Rotate += OnRotate;
+        light.Scale += OnScale;
+        light.Delete += OnDelete;
+        light.Select += OnSelect;
 
-        public void SetColourModeActive(bool isColourMode) => lightList[0].IsColourMode = isColourMode;
+        lightList.Add(light);
 
-        public void ClearLights()
-        {
-            for (int i = lightList.Count - 1; i > 0; i--) DeleteLight(i);
-            selectedLightIndex = 0;
-        }
+        CurrentLight.IsActiveLight = false;
+        SelectedLightIndex = lightList.Count;
+        OnListModified();
+    }
 
-        private void DestroyLight(DragPointLight light)
-        {
-            if (light == null) return;
-            light.Rotate -= OnRotate;
-            light.Scale -= OnScale;
-            light.Delete -= OnDelete;
-            light.Select -= OnSelect;
-            GameObject.Destroy(light.gameObject);
-        }
+    public void DeleteActiveLight()
+    {
+        if (selectedLightIndex is 0)
+            return;
 
-        private string LightName(string name) => Translation.Get("lightType", name);
+        DeleteLight(SelectedLightIndex);
+    }
 
-        private void OnDelete(object sender, EventArgs args)
-        {
-            DragPointLight theLight = (DragPointLight)sender;
-            for (int i = 1; i < lightList.Count; i++)
-            {
-                DragPointLight light = lightList[i];
-                if (light == theLight)
-                {
-                    DeleteLight(i);
-                    return;
-                }
-            }
-        }
+    public void DeleteLight(int lightIndex, bool noUpdate = false)
+    {
+        if (lightIndex is 0)
+            return;
 
-        private void OnRotate(object sender, EventArgs args) => OnTransformEvent((DragPointLight)sender, Rotate);
+        DestroyLight(lightList[lightIndex]);
+        lightList.RemoveAt(lightIndex);
 
-        private void OnScale(object sender, EventArgs args) => OnTransformEvent((DragPointLight)sender, Scale);
+        if (lightIndex <= SelectedLightIndex)
+            SelectedLightIndex--;
 
-        private void OnTransformEvent(DragPointLight light, EventHandler handler)
-        {
-            if (light.IsActiveLight) handler?.Invoke(this, EventArgs.Empty);
-        }
+        if (noUpdate)
+            return;
+
+        OnListModified();
+    }
+
+    public void SetColourModeActive(bool isColourMode) =>
+        lightList[0].IsColourMode = isColourMode;
+
+    public void ClearLights()
+    {
+        for (var i = lightList.Count - 1; i > 0; i--)
+            DeleteLight(i);
+
+        selectedLightIndex = 0;
+    }
+
+    private void DestroyLight(DragPointLight light)
+    {
+        if (!light)
+            return;
+
+        light.Rotate -= OnRotate;
+        light.Scale -= OnScale;
+        light.Delete -= OnDelete;
+        light.Select -= OnSelect;
 
-        private void OnSelect(object sender, EventArgs args)
+        UnityEngine.Object.Destroy(light.gameObject);
+    }
+
+    private string LightName(string name) =>
+        Translation.Get("lightType", name);
+
+    private void OnDelete(object sender, EventArgs args)
+    {
+        var theLight = (DragPointLight)sender;
+
+        for (var i = 1; i < lightList.Count; i++)
         {
-            DragPointLight theLight = (DragPointLight)sender;
-            int select = lightList.FindIndex(light => light == theLight);
-            if (select >= 0)
+            var light = lightList[i];
+
+            if (light == theLight)
             {
-                SelectedLightIndex = select;
-                Select?.Invoke(this, EventArgs.Empty);
+                DeleteLight(i);
+
+                return;
             }
         }
+    }
 
-        private void OnListModified() => ListModified?.Invoke(this, EventArgs.Empty);
+    private void OnRotate(object sender, EventArgs args) =>
+        OnTransformEvent((DragPointLight)sender, Rotate);
 
-        private void OnCubeActive(object sender, EventArgs args)
-        {
-            foreach (DragPointLight dragPoint in lightList) dragPoint.gameObject.SetActive(CubeActive);
-        }
+    private void OnScale(object sender, EventArgs args) =>
+        OnTransformEvent((DragPointLight)sender, Scale);
+
+    private void OnTransformEvent(DragPointLight light, EventHandler handler)
+    {
+        if (light.IsActiveLight)
+            handler?.Invoke(this, EventArgs.Empty);
+    }
+
+    private void OnSelect(object sender, EventArgs args)
+    {
+        var theLight = (DragPointLight)sender;
+        var select = lightList.FindIndex(light => light == theLight);
+
+        if (select < 0)
+            return;
+
+        SelectedLightIndex = select;
+        Select?.Invoke(this, EventArgs.Empty);
+    }
+
+    private void OnListModified() =>
+        ListModified?.Invoke(this, EventArgs.Empty);
+
+    private void OnCubeActive(object sender, EventArgs args)
+    {
+        foreach (var dragPoint in lightList)
+            dragPoint.gameObject.SetActive(CubeActive);
     }
 }

+ 292 - 273
src/MeidoPhotoStudio.Plugin/Managers/MeidoManager.cs

@@ -1,390 +1,409 @@
 using System;
 using System.Collections.Generic;
 using System.Linq;
+
 using HarmonyLib;
 using UnityEngine;
 
-namespace MeidoPhotoStudio.Plugin
+namespace MeidoPhotoStudio.Plugin;
+
+public class MeidoManager : IManager
 {
-    public class MeidoManager : IManager
-    {
-        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 tempEditMaidIndex = -1;
-        public Meido[] Meidos { get; private set; }
-        public HashSet<int> SelectedMeidoSet { get; } = new HashSet<int>();
-        public List<int> SelectMeidoList { get; } = new List<int>();
-        public List<Meido> ActiveMeidoList { get; } = new List<Meido>();
-        public Meido ActiveMeido => ActiveMeidoList.Count > 0 ? ActiveMeidoList[SelectedMeido] : null;
-        public Meido EditMeido => tempEditMaidIndex >= 0 ? Meidos[tempEditMaidIndex] : Meidos[EditMaidIndex];
-        public bool HasActiveMeido => ActiveMeido != null;
-        public event EventHandler<MeidoUpdateEventArgs> UpdateMeido;
-        public event EventHandler EndCallMeidos;
-        public event EventHandler BeginCallMeidos;
-        private int selectedMeido;
-        public int SelectedMeido
-        {
-            get => selectedMeido;
-            private set => selectedMeido = Utility.Bound(value, 0, ActiveMeidoList.Count - 1);
-        }
-        public bool Busy => ActiveMeidoList.Any(meido => meido.Busy);
-        private bool globalGravity;
-        public bool GlobalGravity
-        {
-            get => globalGravity;
-            set
-            {
-                globalGravity = value;
+    public const string Header = "MEIDO";
 
-                if (!HasActiveMeido) return;
+    private static readonly CharacterMgr CharacterMgr = GameMain.Instance.CharacterMgr;
 
-                Meido activeMeido = ActiveMeido;
-                int activeMeidoSlot = activeMeido.Slot;
+    private static bool active;
 
-                foreach (Meido meido in ActiveMeidoList)
-                {
-                    if (meido.Slot != activeMeidoSlot)
-                    {
-                        meido.HairGravityActive = value && activeMeido.HairGravityActive;
-                        meido.SkirtGravityActive = value && activeMeido.SkirtGravityActive;
-                    }
-                }
-            }
-        }
+    private int selectedMeido;
+    private bool globalGravity;
+    private int undress;
+    private int tempEditMaidIndex = -1;
 
-        static MeidoManager() => InputManager.Register(MpsKey.MeidoUndressing, KeyCode.H, "All maid undressing");
+    static MeidoManager() =>
+        InputManager.Register(MpsKey.MeidoUndressing, KeyCode.H, "All maid undressing");
 
-        public MeidoManager() => Activate();
+    public MeidoManager() =>
+        Activate();
 
-        public void ChangeMaid(int index) => OnUpdateMeido(null, new MeidoUpdateEventArgs(index));
+    public event EventHandler<MeidoUpdateEventArgs> UpdateMeido;
 
-        public void Activate()
-        {
-            characterMgr.ResetCharaPosAll();
+    public event EventHandler EndCallMeidos;
 
-            if (!MeidoPhotoStudio.EditMode)
-                characterMgr.DeactivateMaid(0);
+    public event EventHandler BeginCallMeidos;
 
-            Meidos = characterMgr.GetStockMaidList()
-                .Select((_, stockNo) => new Meido(stockNo)).ToArray();
+    public Meido[] Meidos { get; private set; }
 
-            tempEditMaidIndex = -1;
+    public HashSet<int> SelectedMeidoSet { get; } = new();
 
-            if (MeidoPhotoStudio.EditMode && EditMaidIndex >= 0)
-                Meidos[EditMaidIndex].IsEditMaid = true;
+    public List<int> SelectMeidoList { get; } = new();
 
-            ClearSelectList();
-            active = true;
-        }
+    public List<Meido> ActiveMeidoList { get; } = new();
 
-        public void Deactivate()
-        {
-            foreach (Meido meido in Meidos)
-            {
-                meido.UpdateMeido -= OnUpdateMeido;
-                meido.GravityMove -= OnGravityMove;
-                meido.Deactivate();
-            }
+    public int SelectedMeido
+    {
+        get => selectedMeido;
+        private set => selectedMeido = Utility.Bound(value, 0, ActiveMeidoList.Count - 1);
+    }
 
-            ActiveMeidoList.Clear();
+    public bool Busy =>
+        ActiveMeidoList.Any(meido => meido.Busy);
 
-            if (MeidoPhotoStudio.EditMode && !GameMain.Instance.MainCamera.IsFadeOut())
-            {
-                Meido meido = Meidos[EditMaidIndex];
-                meido.Maid.Visible = true;
-                meido.Stop = false;
-                meido.EyeToCam = true;
+    public Meido ActiveMeido =>
+        ActiveMeidoList.Count > 0 ? ActiveMeidoList[SelectedMeido] : null;
 
-                SetEditorMaid(meido.Maid);
-            }
+    public Meido EditMeido =>
+        tempEditMaidIndex >= 0 ? Meidos[tempEditMaidIndex] : Meidos[EditMaidIndex];
 
-            active = false;
-        }
+    public bool HasActiveMeido =>
+        ActiveMeido is not null;
 
-        public void Update()
+    public bool GlobalGravity
+    {
+        get => globalGravity;
+        set
         {
-            if (InputManager.GetKeyDown(MpsKey.MeidoUndressing)) UndressAll();
-        }
+            globalGravity = value;
 
-        private void UnloadMeidos()
-        {
-            SelectedMeido = 0;
+            if (!HasActiveMeido)
+                return;
 
-            var commonMeidoIDs = new HashSet<int>(
-                ActiveMeidoList.Where(meido => SelectedMeidoSet.Contains(meido.StockNo)).Select(meido => meido.StockNo)
-            );
+            var activeMeido = ActiveMeido;
+            var activeMeidoSlot = activeMeido.Slot;
 
-            foreach (Meido meido in ActiveMeidoList)
+            foreach (var meido in ActiveMeidoList)
             {
-                meido.UpdateMeido -= OnUpdateMeido;
-                meido.GravityMove -= OnGravityMove;
-                
-                if (!commonMeidoIDs.Contains(meido.StockNo))
-                    meido.Unload();
-            }
+                if (meido.Slot == activeMeidoSlot)
+                    continue;
 
-            ActiveMeidoList.Clear();
+                meido.HairGravityActive = value && activeMeido.HairGravityActive;
+                meido.SkirtGravityActive = value && activeMeido.SkirtGravityActive;
+            }
         }
+    }
 
-        public void CallMeidos()
-        {
-            BeginCallMeidos?.Invoke(this, EventArgs.Empty);
+    private static int EditMaidIndex { get; set; }
 
-            bool moreThanEditMaid = ActiveMeidoList.Count > 1;
+    public void ChangeMaid(int index) =>
+        OnUpdateMeido(null, new(index));
 
-            UnloadMeidos();
+    public void Activate()
+    {
+        CharacterMgr.ResetCharaPosAll();
 
-            if (SelectMeidoList.Count == 0)
-            {
-                OnEndCallMeidos(this, EventArgs.Empty);
-                return;
-            }
+        if (!MeidoPhotoStudio.EditMode)
+            CharacterMgr.DeactivateMaid(0);
+
+        Meidos = CharacterMgr.GetStockMaidList()
+            .Select((_, stockNo) => new Meido(stockNo))
+            .ToArray();
 
-            void callMeidos() => GameMain.Instance.StartCoroutine(LoadMeidos());
+        tempEditMaidIndex = -1;
 
-            if (MeidoPhotoStudio.EditMode && !moreThanEditMaid && SelectMeidoList.Count == 1) callMeidos();
-            else GameMain.Instance.MainCamera.FadeOut(0.01f, f_bSkipable: false, f_dg: callMeidos);
+        if (MeidoPhotoStudio.EditMode && EditMaidIndex >= 0)
+            Meidos[EditMaidIndex].IsEditMaid = true;
+
+        ClearSelectList();
+        active = true;
+    }
+
+    public void Deactivate()
+    {
+        foreach (var meido in Meidos)
+        {
+            meido.UpdateMeido -= OnUpdateMeido;
+            meido.GravityMove -= OnGravityMove;
+            meido.Deactivate();
         }
 
-        private System.Collections.IEnumerator LoadMeidos()
+        ActiveMeidoList.Clear();
+
+        if (MeidoPhotoStudio.EditMode && !GameMain.Instance.MainCamera.IsFadeOut())
         {
-            foreach (int slot in SelectMeidoList) ActiveMeidoList.Add(Meidos[slot]);
+            var meido = Meidos[EditMaidIndex];
 
-            for (int i = 0; i < ActiveMeidoList.Count; i++) ActiveMeidoList[i].Load(i);
+            meido.Maid.Visible = true;
+            meido.Stop = false;
+            meido.EyeToCam = true;
 
-            while (Busy) yield return null;
+            SetEditorMaid(meido.Maid);
+        }
 
-            yield return new WaitForEndOfFrame();
+        active = false;
+    }
+
+    public void Update()
+    {
+        if (InputManager.GetKeyDown(MpsKey.MeidoUndressing))
+            UndressAll();
+    }
 
+    public void CallMeidos()
+    {
+        BeginCallMeidos?.Invoke(this, EventArgs.Empty);
+
+        var moreThanEditMaid = ActiveMeidoList.Count > 1;
+
+        UnloadMeidos();
+
+        if (SelectMeidoList.Count is 0)
+        {
             OnEndCallMeidos(this, EventArgs.Empty);
+
+            return;
         }
 
-        public void SelectMeido(int index)
+        void LoadMeido() =>
+            GameMain.Instance.StartCoroutine(LoadMeidos());
+
+        if (MeidoPhotoStudio.EditMode && !moreThanEditMaid && SelectMeidoList.Count is 1)
+            LoadMeido();
+        else
+            GameMain.Instance.MainCamera.FadeOut(0.01f, f_bSkipable: false, f_dg: LoadMeido);
+    }
+
+    public void SelectMeido(int index)
+    {
+        if (SelectedMeidoSet.Contains(index))
         {
-            if (SelectedMeidoSet.Contains(index))
-            {
-                if (!MeidoPhotoStudio.EditMode || index != EditMaidIndex)
-                {
-                    SelectedMeidoSet.Remove(index);
-                    SelectMeidoList.Remove(index);
-                }
-            }
-            else
+            if (!MeidoPhotoStudio.EditMode || index != EditMaidIndex)
             {
-                SelectedMeidoSet.Add(index);
-                SelectMeidoList.Add(index);
+                SelectedMeidoSet.Remove(index);
+                SelectMeidoList.Remove(index);
             }
         }
-
-        public void ClearSelectList()
+        else
         {
-            SelectedMeidoSet.Clear();
-            SelectMeidoList.Clear();
-            if (MeidoPhotoStudio.EditMode)
-            {
-                SelectedMeidoSet.Add(EditMaidIndex);
-                SelectMeidoList.Add(EditMaidIndex);
-            }
+            SelectedMeidoSet.Add(index);
+            SelectMeidoList.Add(index);
         }
+    }
 
-        public void SetEditMaid(Meido meido)
+    public void ClearSelectList()
+    {
+        SelectedMeidoSet.Clear();
+        SelectMeidoList.Clear();
+
+        if (MeidoPhotoStudio.EditMode)
         {
-            if (!MeidoPhotoStudio.EditMode) return;
+            SelectedMeidoSet.Add(EditMaidIndex);
+            SelectMeidoList.Add(EditMaidIndex);
+        }
+    }
 
-            EditMeido.IsEditMaid = false;
+    public void SetEditMaid(Meido meido)
+    {
+        if (!MeidoPhotoStudio.EditMode)
+            return;
 
-            tempEditMaidIndex = meido.StockNo == EditMaidIndex ? -1 : meido.StockNo;
+        EditMeido.IsEditMaid = false;
 
-            EditMeido.IsEditMaid = true;
+        tempEditMaidIndex = meido.StockNo == EditMaidIndex ? -1 : meido.StockNo;
 
-            SetEditorMaid(EditMeido.Maid);
-        }
+        EditMeido.IsEditMaid = true;
 
-        public Meido GetMeido(string guid)
-        {
-            return string.IsNullOrEmpty(guid) ? null : ActiveMeidoList.Find(meido => meido.Maid.status.guid == guid);
-        }
+        SetEditorMaid(EditMeido.Maid);
+    }
+
+    public Meido GetMeido(string guid) =>
+        string.IsNullOrEmpty(guid) ? null : ActiveMeidoList.Find(meido => meido.Maid.status.guid == guid);
+
+    public Meido GetMeido(int activeIndex) =>
+        activeIndex >= 0 && activeIndex < ActiveMeidoList.Count ? ActiveMeidoList[activeIndex] : null;
 
-        public Meido GetMeido(int activeIndex)
+    public void PlaceMeidos(string placementType) =>
+        MaidPlacementUtility.ApplyPlacement(placementType, ActiveMeidoList);
+
+    private static void SetEditorMaid(Maid maid)
+    {
+        if (!maid)
         {
-            return activeIndex >= 0 && activeIndex < ActiveMeidoList.Count ? ActiveMeidoList[activeIndex] : null;
+            Utility.LogWarning("Refusing to change editing maid because the new maid is null!");
+
+            return;
         }
 
-        public void PlaceMeidos(string placementType)
+        if (SceneEdit.Instance.maid.status.guid == maid.status.guid)
         {
-            MaidPlacementUtility.ApplyPlacement(placementType, ActiveMeidoList);
+            Utility.LogDebug("Editing maid is the same as new maid");
+
+            return;
         }
 
-        private void UndressAll()
-        {
-            if (!HasActiveMeido) return;
+        var uiRoot = GameObject.Find("UI Root");
 
-            undress = ++undress % Enum.GetNames(typeof(Meido.Mask)).Length;
+        if (!TryGetUIControl<PresetCtrl>(uiRoot, "PresetPanel", out var presetCtrl))
+            return;
 
-            foreach (Meido activeMeido in ActiveMeidoList) activeMeido.SetMaskMode((Meido.Mask)undress);
+        if (!TryGetUIControl<PresetButtonCtrl>(uiRoot, "PresetButtonPanel", out var presetButtonCtrl))
+            return;
 
-            UpdateMeido?.Invoke(ActiveMeido, new MeidoUpdateEventArgs(SelectedMeido));
-        }
+        if (!TryGetUIControl<ProfileCtrl>(uiRoot, "ProfilePanel", out var profileCtrl))
+            return;
 
-        private void OnUpdateMeido(object sender, MeidoUpdateEventArgs args)
-        {
-            if (!args.IsEmpty) SelectedMeido = args.SelectedMeido;
-            UpdateMeido?.Invoke(ActiveMeido, args);
-        }
+        if (!TryGetUIControl<SceneEditWindow.CustomPartsWindow>(
+            uiRoot, "Window/CustomPartsWindow", out var sceneEditWindow))
+            return;
 
-        private void OnEndCallMeidos(object sender, EventArgs args)
-        {
-            GameMain.Instance.MainCamera.FadeIn(1f);
-            EndCallMeidos?.Invoke(this, EventArgs.Empty);
-            foreach (Meido meido in ActiveMeidoList)
-            {
-                meido.UpdateMeido += OnUpdateMeido;
-                meido.GravityMove += OnGravityMove;
-            }
+        // Preset application
+        presetCtrl.m_maid = maid;
 
-            if (MeidoPhotoStudio.EditMode && tempEditMaidIndex >= 0 && !SelectedMeidoSet.Contains(tempEditMaidIndex))
-            {
-                SetEditMaid(Meidos[EditMaidIndex]);
-            }
-        }
+        // Preset saving
+        presetButtonCtrl.m_maid = maid;
 
-        private void OnGravityMove(object sender, GravityEventArgs args)
-        {
-            if (!GlobalGravity) return;
+        // Maid profile (name, description, experience etc)
+        profileCtrl.m_maidStatus = maid.status;
 
-            foreach (Meido meido in ActiveMeidoList)
-            {
-                meido.ApplyGravity(args.LocalPosition, args.IsSkirt);
-            }
-        }
+        // Accessory/Parts placement
+        sceneEditWindow.maid = maid;
 
-        private static void SetEditorMaid(Maid maid)
-        {
-            if (maid == null)
-            {
-                Utility.LogWarning("Refusing to change editing maid because the new maid is null!");
-                return;
-            }
+        // Stopping maid animation and head movement when customizing parts placement
+        sceneEditWindow.animation = maid.GetAnimation();
 
-            if (SceneEdit.Instance.maid.status.guid == maid.status.guid)
-            {
-                Utility.LogDebug("Editing maid is the same as new maid");
-                return;
-            }
+        // Clothing/body in general and maybe other things
+        SceneEdit.Instance.m_maid = maid;
 
-            var uiRoot = GameObject.Find("UI Root");
+        // Body status, parts colours and maybe more
+        GameMain.Instance.CharacterMgr.m_gcActiveMaid[0] = maid;
 
-            if (!TryGetUIControl<PresetCtrl>(uiRoot, "PresetPanel", out var presetCtrl))
-                return;
+        static bool TryGetUIControl<T>(GameObject root, string hierarchy, out T uiControl)
+            where T : MonoBehaviour
+        {
+            uiControl = null;
 
-            if (!TryGetUIControl<PresetButtonCtrl>(uiRoot, "PresetButtonPanel", out var presetButtonCtrl))
-                return;
+            var uiElement = UTY.GetChildObjectNoError(root, hierarchy);
 
-            if (!TryGetUIControl<ProfileCtrl>(uiRoot, "ProfilePanel", out var profileCtrl))
-                return;
+            if (!uiElement)
+                return false;
 
-            if (!TryGetUIControl<SceneEditWindow.CustomPartsWindow>(
-                uiRoot, "Window/CustomPartsWindow", out var sceneEditWindow
-            ))
-                return;
+            uiControl = uiElement.GetComponent<T>();
 
-            // Preset application
-            presetCtrl.m_maid = maid;
+            return uiControl;
+        }
+    }
 
-            // Preset saving
-            presetButtonCtrl.m_maid = maid;
+    [HarmonyPostfix]
+    [HarmonyPatch(typeof(SceneEdit), nameof(SceneEdit.Start))]
+    private static void SceneEditStartPostfix()
+    {
+        EditMaidIndex = -1;
 
-            // Maid profile (name, description, experience etc)
-            profileCtrl.m_maidStatus = maid.status;
+        if (!SceneEdit.Instance.maid)
+            return;
 
-            // Accessory/Parts placement
-            sceneEditWindow.maid = maid;
+        var originalEditingMaid = SceneEdit.Instance.maid;
 
-            // Stopping maid animation and head movement when customizing parts placement
-            sceneEditWindow.animation = maid.GetAnimation();
+        EditMaidIndex = GameMain.Instance.CharacterMgr.GetStockMaidList()
+            .FindIndex(maid => maid.status.guid == originalEditingMaid.status.guid);
 
-            // Clothing/body in general and maybe other things
-            SceneEdit.Instance.m_maid = maid;
+        try
+        {
+            var editOkCancelButton = UTY.GetChildObject(GameObject.Find("UI Root"), "OkCancel")
+                .GetComponent<EditOkCancel>();
 
-            // Body status, parts colours and maybe more
-            GameMain.Instance.CharacterMgr.m_gcActiveMaid[0] = maid;
+            EditOkCancel.OnClick newEditOkCancelDelegate = RestoreOriginalEditingMaid;
 
-            static bool TryGetUIControl<T>(GameObject root, string hierarchy, out T uiControl) where T : MonoBehaviour
-            {
-                uiControl = null;
+            newEditOkCancelDelegate += editOkCancelButton.m_dgOnClickOk;
 
-                var uiElement = UTY.GetChildObjectNoError(root, hierarchy);
+            editOkCancelButton.m_dgOnClickOk = newEditOkCancelDelegate;
 
-                if (!uiElement)
-                    return false;
+            void RestoreOriginalEditingMaid()
+            {
+                // Only restore original editing maid when active.
+                if (!active)
+                    return;
 
-                uiControl = uiElement.GetComponent<T>();
+                Utility.LogDebug($"Setting Editing maid back to '{originalEditingMaid.status.fullNameJpStyle}'");
 
-                return uiControl;
+                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}");
+        }
+    }
 
-        [HarmonyPostfix]
-        [HarmonyPatch(typeof(SceneEdit), nameof(SceneEdit.Start))]
-        private static void SceneEditStartPostfix()
+    private void UnloadMeidos()
+    {
+        SelectedMeido = 0;
+
+        var commonMeidoIDs = new HashSet<int>(
+            ActiveMeidoList.Where(meido => SelectedMeidoSet.Contains(meido.StockNo)).Select(meido => meido.StockNo));
+
+        foreach (var meido in ActiveMeidoList)
         {
-            EditMaidIndex = -1;
+            meido.UpdateMeido -= OnUpdateMeido;
+            meido.GravityMove -= OnGravityMove;
 
-            if (SceneEdit.Instance.maid == null)
-                return;
+            if (!commonMeidoIDs.Contains(meido.StockNo))
+                meido.Unload();
+        }
 
-            var originalEditingMaid = SceneEdit.Instance.maid;
+        ActiveMeidoList.Clear();
+    }
 
-            EditMaidIndex = GameMain.Instance.CharacterMgr.GetStockMaidList()
-                .FindIndex(maid => maid.status.guid == originalEditingMaid.status.guid);
+    private System.Collections.IEnumerator LoadMeidos()
+    {
+        foreach (var slot in SelectMeidoList)
+            ActiveMeidoList.Add(Meidos[slot]);
 
-            try
-            {
-                var editOkCancelButton = UTY.GetChildObject(GameObject.Find("UI Root"), "OkCancel")
-                    .GetComponent<EditOkCancel>();
+        for (var i = 0; i < ActiveMeidoList.Count; i++)
+            ActiveMeidoList[i].Load(i);
 
-                EditOkCancel.OnClick newEditOkCancelDelegate = RestoreOriginalEditingMaid;
+        while (Busy)
+            yield return null;
 
-                newEditOkCancelDelegate += editOkCancelButton.m_dgOnClickOk;
+        yield return new WaitForEndOfFrame();
 
-                editOkCancelButton.m_dgOnClickOk = newEditOkCancelDelegate;
+        OnEndCallMeidos(this, EventArgs.Empty);
+    }
 
-                void RestoreOriginalEditingMaid()
-                {
-                    // Only restore original editing maid when active.
-                    if (!active)
-                        return;
+    private void UndressAll()
+    {
+        if (!HasActiveMeido)
+            return;
 
-                    Utility.LogDebug($"Setting Editing maid back to '{originalEditingMaid.status.fullNameJpStyle}'");
+        undress = ++undress % Enum.GetNames(typeof(Meido.Mask)).Length;
 
-                    SetEditorMaid(originalEditingMaid);
+        foreach (var activeMeido in ActiveMeidoList)
+            activeMeido.SetMaskMode((Meido.Mask)undress);
 
-                    // 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}");
-            }
-        }
+        UpdateMeido?.Invoke(ActiveMeido, new(SelectedMeido));
+    }
+
+    private void OnUpdateMeido(object sender, MeidoUpdateEventArgs args)
+    {
+        if (!args.IsEmpty)
+            SelectedMeido = args.SelectedMeido;
+
+        UpdateMeido?.Invoke(ActiveMeido, args);
     }
 
-    public class MeidoUpdateEventArgs : EventArgs
+    private void OnEndCallMeidos(object sender, EventArgs args)
     {
-        public static new MeidoUpdateEventArgs Empty { get; } = new MeidoUpdateEventArgs(-1);
-        public bool IsEmpty => (this == Empty) || (SelectedMeido == -1 && !FromMeido && IsBody);
-        public int SelectedMeido { get; }
-        public bool IsBody { get; }
-        public bool FromMeido { get; }
-        public MeidoUpdateEventArgs(int meidoIndex = -1, bool fromMaid = false, bool isBody = true)
+        GameMain.Instance.MainCamera.FadeIn(1f);
+        EndCallMeidos?.Invoke(this, EventArgs.Empty);
+
+        foreach (var meido in ActiveMeidoList)
         {
-            SelectedMeido = meidoIndex;
-            IsBody = isBody;
-            FromMeido = fromMaid;
+            meido.UpdateMeido += OnUpdateMeido;
+            meido.GravityMove += OnGravityMove;
         }
+
+        if (MeidoPhotoStudio.EditMode && tempEditMaidIndex >= 0 && !SelectedMeidoSet.Contains(tempEditMaidIndex))
+            SetEditMaid(Meidos[EditMaidIndex]);
+    }
+
+    private void OnGravityMove(object sender, GravityEventArgs args)
+    {
+        if (!GlobalGravity)
+            return;
+
+        foreach (var meido in ActiveMeidoList)
+            meido.ApplyGravity(args.LocalPosition, args.IsSkirt);
     }
 }

+ 24 - 0
src/MeidoPhotoStudio.Plugin/Managers/MeidoUpdateEventArgs.cs

@@ -0,0 +1,24 @@
+using System;
+
+namespace MeidoPhotoStudio.Plugin;
+
+public class MeidoUpdateEventArgs : EventArgs
+{
+    public MeidoUpdateEventArgs(int meidoIndex = -1, bool fromMaid = false, bool isBody = true)
+    {
+        SelectedMeido = meidoIndex;
+        IsBody = isBody;
+        FromMeido = fromMaid;
+    }
+
+    public static new MeidoUpdateEventArgs Empty { get; } = new(-1);
+
+    public int SelectedMeido { get; }
+
+    public bool IsBody { get; }
+
+    public bool FromMeido { get; }
+
+    public bool IsEmpty =>
+        this == Empty || SelectedMeido is -1 && !FromMeido && IsBody;
+}

+ 105 - 106
src/MeidoPhotoStudio.Plugin/Managers/MessageWindowManager.cs

@@ -1,139 +1,138 @@
-using System.Collections.Generic;
 using UnityEngine;
 
-namespace MeidoPhotoStudio.Plugin
+namespace MeidoPhotoStudio.Plugin;
+
+public class MessageWindowManager : IManager
 {
-    public class MessageWindowManager : IManager
-    {
-        public const string header = "TEXTBOX";
-        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 const string Header = "TEXTBOX";
 
-        public string MessageName
-        {
-            get => speakerLabel.text;
-            private set => speakerLabel.text = value;
-        }
+    public static readonly SliderProp FontBounds = new(25f, 60f);
 
-        public string MessageText
-        {
-            get => messageLabel.text;
-            private set => messageLabel.text = value;
-        }
+    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 int FontSize
-        {
-            get => messageLabel.fontSize;
-            set => messageLabel.fontSize = (int)Mathf.Clamp(value, FontBounds.Left, FontBounds.Right);
-        }
+    static MessageWindowManager() =>
+        InputManager.Register(MpsKey.ToggleMessage, KeyCode.M, "Show/hide message box");
 
-        static MessageWindowManager()
-        {
-            InputManager.Register(MpsKey.ToggleMessage, KeyCode.M, "Show/hide message box");
-        }
+    public MessageWindowManager()
+    {
+        messageWindowMgr = GameMain.Instance.MsgWnd;
 
-        public MessageWindowManager()
-        {
-            messageWindowMgr = GameMain.Instance.MsgWnd;
+        var messageWindowPanel =
+            Utility.GetFieldValue<MessageWindowMgr, GameObject>(messageWindowMgr, "m_goMessageWindowPanel");
 
-            var messageWindowPanel =
-                Utility.GetFieldValue<MessageWindowMgr, GameObject>(messageWindowMgr, "m_goMessageWindowPanel");
+        var msgParent = UTY.GetChildObject(messageWindowPanel, "MessageViewer/MsgParent");
 
-            var msgParent = UTY.GetChildObject(messageWindowPanel, "MessageViewer/MsgParent");
+        messageButtons = UTY.GetChildObject(msgParent, "Buttons");
+        hitRetSprite = UTY.GetChildObject(msgParent, "Hitret");
+        subtitlesDisplayPanel = UTY.GetChildObject(msgParent, "SubtitlesDisplayPanel");
 
-            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>();
+    }
 
-            messageBox = UTY.GetChildObject(msgParent, "MessageBox");
-            speakerLabel = UTY.GetChildObject(msgParent, "SpeakerName/Name").GetComponent<UILabel>();
-            messageLabel = UTY.GetChildObject(msgParent, "Message").GetComponent<UILabel>();
+    public bool ShowingMessage
+    {
+        get => messageWindowMgr.IsVisibleMessageViewer;
+        private set
+        {
+            if (value)
+                messageWindowMgr.OpenMessageWindowPanel();
+            else
+                messageWindowMgr.CloseMessageWindowPanel();
         }
+    }
 
-        public void Update() { }
+    public string MessageName
+    {
+        get => speakerLabel.text;
+        private set => speakerLabel.text = value;
+    }
 
-        public void Activate()
-        {
-            if (Product.supportMultiLanguage)
-                subtitlesDisplayPanel.SetActive(false);
+    public string MessageText
+    {
+        get => messageLabel.text;
+        private set => messageLabel.text = value;
+    }
+
+    public int FontSize
+    {
+        get => messageLabel.fontSize;
+        set => messageLabel.fontSize = (int)Mathf.Clamp(value, FontBounds.Left, FontBounds.Right);
+    }
+
+    public void Update()
+    {
+    }
 
-            ResetMessageBoxProperties();
+    public void Activate()
+    {
+        if (Product.supportMultiLanguage)
+            subtitlesDisplayPanel.SetActive(false);
 
-            SetMessageBoxActive(true);
+        ResetMessageBoxProperties();
 
-            SetMessageBoxExtrasActive(false);
+        SetMessageBoxActive(true);
 
-            CloseMessagePanel();
-        }
+        SetMessageBoxExtrasActive(false);
 
-        public void Deactivate()
+        CloseMessagePanel();
+    }
+
+    public void Deactivate()
+    {
+        if (Product.supportMultiLanguage)
         {
-            if (Product.supportMultiLanguage)
-            {
-                subtitlesDisplayPanel.SetActive(true);
+            subtitlesDisplayPanel.SetActive(true);
 
-                SetMessageBoxActive(false);
-            }
+            SetMessageBoxActive(false);
+        }
 
-            ResetMessageBoxProperties();
+        ResetMessageBoxProperties();
 
-            SetMessageBoxExtrasActive(true);
+        SetMessageBoxExtrasActive(true);
 
-            CloseMessagePanel();
-        }
+        CloseMessagePanel();
+    }
 
-        public void ShowMessage(string name, string message)
-        {
-            MessageName = name;
-            MessageText = message;
-            ShowingMessage = true;
-        }
+    public void ShowMessage(string name, string message)
+    {
+        MessageName = name;
+        MessageText = message;
+        ShowingMessage = true;
+    }
 
-        public void CloseMessagePanel()
-        {
-            if (!ShowingMessage)
-                return;
+    public void CloseMessagePanel()
+    {
+        if (!ShowingMessage)
+            return;
 
-            ShowingMessage = false;
-        }
+        ShowingMessage = false;
+    }
 
-        private void SetMessageBoxActive(bool active)
-        {
-            messageBox.SetActive(active);
-            messageLabel.gameObject.SetActive(active);
-            speakerLabel.gameObject.SetActive(active);
-        }
+    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 SetMessageBoxExtrasActive(bool active)
+    {
+        messageButtons.SetActive(active);
+        hitRetSprite.SetActive(active);
+    }
 
-        private void ResetMessageBoxProperties()
-        {
-            FontSize = 25;
-            MessageName = string.Empty;
-            MessageText = string.Empty;
-        }
+    private void ResetMessageBoxProperties()
+    {
+        FontSize = 25;
+        MessageName = string.Empty;
+        MessageText = string.Empty;
     }
 }

+ 289 - 234
src/MeidoPhotoStudio.Plugin/Managers/PropManager.cs

@@ -1,320 +1,375 @@
-using System;
+using System;
 using System.Collections.Generic;
 using System.IO;
 using System.Linq;
+
 using BepInEx.Configuration;
 using UnityEngine;
+
+using static MeidoPhotoStudio.Plugin.ModelUtility;
+
 using Object = UnityEngine.Object;
 
-namespace MeidoPhotoStudio.Plugin
+namespace MeidoPhotoStudio.Plugin;
+
+public class PropManager : IManager
 {
-    using static ModelUtility;
+    public const string Header = "PROP";
+
+    private static readonly ConfigEntry<bool> ModItemsOnlyValue;
 
-    public class PropManager : IManager
+    private static bool cubeActive = true;
+    private static Dictionary<string, string> modFileToFullPath;
+    private static bool cubeSmall;
+
+    private readonly List<DragPointProp> propList = new();
+    private readonly MeidoManager meidoManager;
+
+    private int currentPropIndex;
+
+    static PropManager() =>
+        ModItemsOnlyValue = Configuration.Config.Bind(
+            "Prop", "ModItemsOnly", false, "Disable waiting for and loading base game clothing");
+
+    public PropManager(MeidoManager meidoManager)
     {
-        public const string header = "PROP";
-        private static readonly ConfigEntry<bool> modItemsOnly;
-        private static bool cubeActive = true;
-        private static Dictionary<string, string> modFileToFullPath;
+        this.meidoManager = meidoManager;
 
-        private static Dictionary<string, string> ModFileToFullPath
-        {
-            get
-            {
-                if (modFileToFullPath != null) return modFileToFullPath;
+        meidoManager.BeginCallMeidos += OnBeginCallMeidos;
+        meidoManager.EndCallMeidos += OnEndCallMeidos;
 
-                string[] modFiles = Menu.GetModFiles();
-                modFileToFullPath = new Dictionary<string, string>(modFiles.Length, StringComparer.OrdinalIgnoreCase);
+        Activate();
+    }
 
-                foreach (var mod in modFiles)
-                {
-                    var key = Path.GetFileName(mod);
-                    if (!modFileToFullPath.ContainsKey(key)) modFileToFullPath[key] = mod;
-                }
+    public event EventHandler PropSelectionChange;
 
-                return modFileToFullPath;
-            }
-        }
+    public event EventHandler FromPropSelect;
+
+    public event EventHandler PropListChange;
+
+    private static event EventHandler CubeActiveChange;
 
-        public static bool CubeActive
+    private static event EventHandler CubeSmallChange;
+
+    public static bool CubeActive
+    {
+        get => cubeActive;
+        set
         {
-            get => cubeActive;
-            set
-            {
-                if (value != cubeActive)
-                {
-                    cubeActive = value;
-                    CubeActiveChange?.Invoke(null, EventArgs.Empty);
-                }
-            }
-        }
+            if (value == cubeActive)
+                return;
 
-        private static bool cubeSmall;
+            cubeActive = value;
+            CubeActiveChange?.Invoke(null, EventArgs.Empty);
+        }
+    }
 
-        public static bool CubeSmall
+    public static bool CubeSmall
+    {
+        get => cubeSmall;
+        set
         {
-            get => cubeSmall;
-            set
-            {
-                if (value != cubeSmall)
-                {
-                    cubeSmall = value;
-                    CubeSmallChange?.Invoke(null, EventArgs.Empty);
-                }
-            }
+            if (value == cubeSmall)
+                return;
+
+            cubeSmall = value;
+            CubeSmallChange?.Invoke(null, EventArgs.Empty);
         }
+    }
+
+    public static bool ModItemsOnly =>
+        ModItemsOnlyValue.Value;
 
-        private static event EventHandler CubeActiveChange;
-        private static event EventHandler CubeSmallChange;
-        public static bool ModItemsOnly => modItemsOnly.Value;
-        private readonly List<DragPointProp> propList = new List<DragPointProp>();
+    public DragPointProp CurrentProp =>
+        PropCount is 0 ? null : propList[CurrentPropIndex];
 
-        public string[] PropNameList => propList.Count == 0
+    public string[] PropNameList =>
+        propList.Count is 0
             ? new[] { Translation.Get("systemMessage", "noProps") }
             : propList.Select(prop => prop.Name).ToArray();
 
-        public int PropCount => propList.Count;
-        private int currentPropIndex;
-        private MeidoManager meidoManager;
+    public int PropCount =>
+        propList.Count;
 
-        public int CurrentPropIndex
+    public int CurrentPropIndex
+    {
+        get => currentPropIndex;
+        set
         {
-            get => currentPropIndex;
-            set
+            if (PropCount is 0)
             {
-                if (PropCount == 0)
-                {
-                    currentPropIndex = 0;
-                    return;
-                }
-
-                if ((uint) value >= (uint) PropCount) throw new ArgumentOutOfRangeException(nameof(value));
-
-                if (currentPropIndex == value) return;
+                currentPropIndex = 0;
 
-                currentPropIndex = value;
-                PropSelectionChange?.Invoke(this, EventArgs.Empty);
+                return;
             }
-        }
 
-        public DragPointProp CurrentProp => PropCount == 0 ? null : propList[CurrentPropIndex];
-        public event EventHandler PropSelectionChange;
-        public event EventHandler FromPropSelect;
-        public event EventHandler PropListChange;
+            if ((uint)value >= (uint)PropCount)
+                throw new ArgumentOutOfRangeException(nameof(value));
 
-        static PropManager() => modItemsOnly = Configuration.Config.Bind(
-            "Prop", "ModItemsOnly",
-            false,
-            "Disable waiting for and loading base game clothing"
-        );
+            if (currentPropIndex == value)
+                return;
 
-        public PropManager(MeidoManager meidoManager)
-        {
-            this.meidoManager = meidoManager;
-            meidoManager.BeginCallMeidos += OnBeginCallMeidos;
-            meidoManager.EndCallMeidos += OnEndCallMeidos;
-            Activate();
+            currentPropIndex = value;
+            PropSelectionChange?.Invoke(this, EventArgs.Empty);
         }
+    }
 
-        public bool AddFromPropInfo(PropInfo propInfo)
+    private static Dictionary<string, string> ModFileToFullPath
+    {
+        get
         {
-            switch (propInfo.Type)
+            if (modFileToFullPath is not null)
+                return modFileToFullPath;
+
+            var modFiles = Menu.GetModFiles();
+
+            modFileToFullPath = new(modFiles.Length, StringComparer.OrdinalIgnoreCase);
+
+            foreach (var mod in modFiles)
             {
-                case PropInfo.PropType.Mod:
-                    ModItem modItem;
-                    if (!string.IsNullOrEmpty(propInfo.SubFilename))
-                    {
-                        modItem = ModItem.OfficialMod(ModFileToFullPath[propInfo.Filename]);
-                        modItem.BaseMenuFile = propInfo.SubFilename;
-                    }
-                    else modItem = ModItem.Mod(propInfo.Filename);
-
-                    return AddModProp(modItem);
-                case PropInfo.PropType.MyRoom:
-                    return AddMyRoomProp(new MyRoomItem { ID = propInfo.MyRoomID, PrefabName = propInfo.Filename });
-                case PropInfo.PropType.Bg:
-                    return AddBgProp(propInfo.Filename);
-                case PropInfo.PropType.Odogu:
-                    return AddGameProp(propInfo.Filename);
-                default: throw new ArgumentOutOfRangeException();
+                var key = Path.GetFileName(mod);
+
+                if (!modFileToFullPath.ContainsKey(key))
+                    modFileToFullPath[key] = mod;
             }
+
+            return modFileToFullPath;
         }
+    }
 
-        public bool AddModProp(ModItem modItem)
+    public bool AddFromPropInfo(PropInfo propInfo)
+    {
+        switch (propInfo.Type)
         {
-            var model = LoadMenuModel(modItem);
-            if (!model) return false;
+            case PropInfo.PropType.Mod:
+                ModItem modItem;
 
-            var name = modItem.MenuFile;
-            if (modItem.IsOfficialMod) name = Path.GetFileName(name) + ".menu"; // add '.menu' for partsedit support
-            model.name = name;
+                if (!string.IsNullOrEmpty(propInfo.SubFilename))
+                {
+                    modItem = ModItem.OfficialMod(ModFileToFullPath[propInfo.Filename]);
+                    modItem.BaseMenuFile = propInfo.SubFilename;
+                }
+                else
+                {
+                    modItem = ModItem.Mod(propInfo.Filename);
+                }
 
-            var dragPoint = AttachDragPoint(model);
-            dragPoint.Info = PropInfo.FromModItem(modItem);
+                return AddModProp(modItem);
+            case PropInfo.PropType.MyRoom:
+                return AddMyRoomProp(new() { ID = propInfo.MyRoomID, PrefabName = propInfo.Filename });
+            case PropInfo.PropType.Bg:
+                return AddBgProp(propInfo.Filename);
+            case PropInfo.PropType.Odogu:
+                return AddGameProp(propInfo.Filename);
+            default:
+                throw new ArgumentOutOfRangeException();
+        }
+    }
 
-            AddProp(dragPoint);
+    public bool AddModProp(ModItem modItem)
+    {
+        var model = LoadMenuModel(modItem);
 
-            return true;
-        }
+        if (!model)
+            return false;
 
-        public bool AddMyRoomProp(MyRoomItem myRoomItem)
-        {
-            var model = LoadMyRoomModel(myRoomItem);
-            if (!model) return false;
+        var name = modItem.MenuFile;
 
-            model.name = Translation.Get("myRoomPropNames", myRoomItem.PrefabName);
+        if (modItem.IsOfficialMod)
+            name = Path.GetFileName(name) + ".menu"; // add '.menu' for partsedit support
 
-            var dragPoint = AttachDragPoint(model);
-            dragPoint.Info = PropInfo.FromMyRoom(myRoomItem);
+        model.name = name;
 
-            AddProp(dragPoint);
+        var dragPoint = AttachDragPoint(model);
 
-            return true;
-        }
+        dragPoint.Info = PropInfo.FromModItem(modItem);
 
-        public bool AddBgProp(string assetName)
-        {
-            var model = LoadBgModel(assetName);
-            if (!model) return false;
+        AddProp(dragPoint);
 
-            model.name = Translation.Get("bgNames", assetName);
+        return true;
+    }
 
-            var dragPoint = AttachDragPoint(model);
-            dragPoint.Info = PropInfo.FromBg(assetName);
+    public bool AddMyRoomProp(MyRoomItem myRoomItem)
+    {
+        var model = LoadMyRoomModel(myRoomItem);
 
-            AddProp(dragPoint);
+        if (!model)
+            return false;
 
-            return true;
-        }
+        model.name = Translation.Get("myRoomPropNames", myRoomItem.PrefabName);
 
-        public bool AddGameProp(string assetName)
-        {
-            var isMenu = assetName.EndsWith(".menu");
-            var model = isMenu ? LoadMenuModel(assetName) : LoadGameModel(assetName);
-            if (!model) return false;
+        var dragPoint = AttachDragPoint(model);
 
-            model.name = Translation.Get("propNames", isMenu ? Utility.HandItemToOdogu(assetName) : assetName, !isMenu);
+        dragPoint.Info = PropInfo.FromMyRoom(myRoomItem);
 
-            var dragPoint = AttachDragPoint(model);
-            dragPoint.Info = PropInfo.FromGameProp(assetName);
+        AddProp(dragPoint);
 
-            AddProp(dragPoint);
+        return true;
+    }
 
-            return true;
-        }
+    public bool AddBgProp(string assetName)
+    {
+        var model = LoadBgModel(assetName);
 
-        public void CopyProp(int propIndex)
-        {
-            if ((uint) propIndex >= (uint) PropCount) throw new ArgumentOutOfRangeException(nameof(propIndex));
+        if (!model)
+            return false;
 
-            AddFromPropInfo(propList[propIndex].Info);
-        }
+        model.name = Translation.Get("bgNames", assetName);
 
-        public void DeleteAllProps()
-        {
-            foreach (var prop in propList) DestroyProp(prop);
-            propList.Clear();
-            CurrentPropIndex = 0;
-            EmitPropListChange();
-        }
+        var dragPoint = AttachDragPoint(model);
 
-        public void RemoveProp(int index)
-        {
-            if ((uint) index >= (uint) PropCount) throw new ArgumentOutOfRangeException(nameof(index));
+        dragPoint.Info = PropInfo.FromBg(assetName);
 
-            DestroyProp(propList[index]);
-            propList.RemoveAt(index);
-            CurrentPropIndex = Utility.Bound(CurrentPropIndex, 0, PropCount - 1);
-            EmitPropListChange();
-        }
+        AddProp(dragPoint);
 
-        public void AttachProp(DragPointProp prop, AttachPoint point, int index)
-        {
-            if ((uint) index >= (uint) meidoManager.ActiveMeidoList.Count) return;
+        return true;
+    }
 
-            var meido = meidoManager.ActiveMeidoList[index];
+    public bool AddGameProp(string assetName)
+    {
+        var isMenu = assetName.EndsWith(".menu");
+        var model = isMenu ? LoadMenuModel(assetName) : LoadGameModel(assetName);
 
-            prop.AttachTo(meido, point);
-        }
+        if (!model)
+            return false;
 
-        private DragPointProp AttachDragPoint(GameObject model)
-        {
-            var dragPoint = DragPoint.Make<DragPointProp>(PrimitiveType.Cube, Vector3.one * 0.12f);
-            dragPoint.Initialize(() => model.transform.position, () => Vector3.zero);
-            dragPoint.Set(model.transform);
-            dragPoint.AddGizmo(0.45f, CustomGizmo.GizmoMode.World);
-            dragPoint.ConstantScale = true;
-            dragPoint.DragPointScale = CubeSmall ? DragPointGeneral.smallCube : 1f;
-            dragPoint.Delete += OnDeleteProp;
-            dragPoint.Select += OnSelectProp;
-            return dragPoint;
-        }
+        model.name = Translation.Get("propNames", isMenu ? Utility.HandItemToOdogu(assetName) : assetName, !isMenu);
 
-        private void AddProp(DragPointProp dragPoint)
-        {
-            propList.Add(dragPoint);
-            EmitPropListChange();
-        }
+        var dragPoint = AttachDragPoint(model);
 
-        private void DestroyProp(DragPointProp prop)
-        {
-            if (!prop) return;
+        dragPoint.Info = PropInfo.FromGameProp(assetName);
 
-            prop.Delete -= OnDeleteProp;
-            prop.Select -= OnSelectProp;
-            Object.Destroy(prop.gameObject);
-        }
+        AddProp(dragPoint);
 
-        private void EmitPropListChange() => PropListChange?.Invoke(this, EventArgs.Empty);
+        return true;
+    }
 
-        private void OnBeginCallMeidos(object sender, EventArgs args)
-        {
-            foreach (var prop in propList.Where(p => p.AttachPointInfo.AttachPoint != AttachPoint.None))
-                prop.DetachTemporary();
-        }
+    public void CopyProp(int propIndex)
+    {
+        if ((uint)propIndex >= (uint)PropCount)
+            throw new ArgumentOutOfRangeException(nameof(propIndex));
 
-        private void OnEndCallMeidos(object sender, EventArgs args)
-        {
-            foreach (var prop in propList.Where(p => p.AttachPointInfo.AttachPoint != AttachPoint.None))
-            {
-                var info = prop.AttachPointInfo;
-                var meido = meidoManager.GetMeido(info.MaidGuid);
-                prop.AttachTo(meido, info.AttachPoint, meido == null);
-            }
-        }
+        AddFromPropInfo(propList[propIndex].Info);
+    }
 
-        private void OnDeleteProp(object sender, EventArgs args)
-            => RemoveProp(propList.IndexOf((DragPointProp) sender));
+    public void DeleteAllProps()
+    {
+        foreach (var prop in propList)
+            DestroyProp(prop);
 
-        private void OnSelectProp(object sender, EventArgs args)
-        {
-            CurrentPropIndex = propList.IndexOf((DragPointProp) sender);
-            FromPropSelect?.Invoke(this, EventArgs.Empty);
-        }
+        propList.Clear();
+        CurrentPropIndex = 0;
 
-        private void OnCubeSmall(object sender, EventArgs args)
-        {
-            foreach (var dragPoint in propList) dragPoint.DragPointScale = CubeSmall ? DragPointGeneral.smallCube : 1f;
-        }
+        EmitPropListChange();
+    }
 
-        private void OnCubeActive(object sender, EventArgs args)
-        {
-            foreach (var dragPoint in propList) dragPoint.gameObject.SetActive(CubeActive);
-        }
+    public void RemoveProp(int index)
+    {
+        if ((uint)index >= (uint)PropCount)
+            throw new ArgumentOutOfRangeException(nameof(index));
 
-        public void Update() { }
+        DestroyProp(propList[index]);
+        propList.RemoveAt(index);
+        CurrentPropIndex = Utility.Bound(CurrentPropIndex, 0, PropCount - 1);
+        EmitPropListChange();
+    }
 
-        public void Activate()
-        {
-            CubeSmallChange += OnCubeSmall;
-            CubeActiveChange += OnCubeActive;
-        }
+    public void AttachProp(DragPointProp prop, AttachPoint point, int index)
+    {
+        if ((uint)index >= (uint)meidoManager.ActiveMeidoList.Count)
+            return;
+
+        var meido = meidoManager.ActiveMeidoList[index];
+
+        prop.AttachTo(meido, point);
+    }
 
-        public void Deactivate()
+    public void Update()
+    {
+    }
+
+    public void Activate()
+    {
+        CubeSmallChange += OnCubeSmall;
+        CubeActiveChange += OnCubeActive;
+    }
+
+    public void Deactivate()
+    {
+        DeleteAllProps();
+        CubeSmallChange -= OnCubeSmall;
+        CubeActiveChange -= OnCubeActive;
+    }
+
+    private DragPointProp AttachDragPoint(GameObject model)
+    {
+        var dragPoint = DragPoint.Make<DragPointProp>(PrimitiveType.Cube, Vector3.one * 0.12f);
+
+        dragPoint.Initialize(() => model.transform.position, () => Vector3.zero);
+        dragPoint.Set(model.transform);
+        dragPoint.AddGizmo(0.45f, CustomGizmo.GizmoMode.World);
+        dragPoint.ConstantScale = true;
+        dragPoint.DragPointScale = CubeSmall ? DragPointGeneral.SmallCube : 1f;
+        dragPoint.Delete += OnDeleteProp;
+        dragPoint.Select += OnSelectProp;
+
+        return dragPoint;
+    }
+
+    private void AddProp(DragPointProp dragPoint)
+    {
+        propList.Add(dragPoint);
+        EmitPropListChange();
+    }
+
+    private void DestroyProp(DragPointProp prop)
+    {
+        if (!prop)
+            return;
+
+        prop.Delete -= OnDeleteProp;
+        prop.Select -= OnSelectProp;
+        Object.Destroy(prop.gameObject);
+    }
+
+    private void EmitPropListChange() =>
+        PropListChange?.Invoke(this, EventArgs.Empty);
+
+    private void OnBeginCallMeidos(object sender, EventArgs args)
+    {
+        foreach (var prop in propList.Where(p => p.AttachPointInfo.AttachPoint is not AttachPoint.None))
+            prop.DetachTemporary();
+    }
+
+    private void OnEndCallMeidos(object sender, EventArgs args)
+    {
+        foreach (var prop in propList.Where(p => p.AttachPointInfo.AttachPoint is not AttachPoint.None))
         {
-            DeleteAllProps();
-            CubeSmallChange -= OnCubeSmall;
-            CubeActiveChange -= OnCubeActive;
+            var info = prop.AttachPointInfo;
+            var meido = meidoManager.GetMeido(info.MaidGuid);
+
+            prop.AttachTo(meido, info.AttachPoint, meido is null);
         }
     }
+
+    private void OnDeleteProp(object sender, EventArgs args) =>
+        RemoveProp(propList.IndexOf((DragPointProp)sender));
+
+    private void OnSelectProp(object sender, EventArgs args)
+    {
+        CurrentPropIndex = propList.IndexOf((DragPointProp)sender);
+        FromPropSelect?.Invoke(this, EventArgs.Empty);
+    }
+
+    private void OnCubeSmall(object sender, EventArgs args)
+    {
+        foreach (var dragPoint in propList)
+            dragPoint.DragPointScale = CubeSmall ? DragPointGeneral.SmallCube : 1f;
+    }
+
+    private void OnCubeActive(object sender, EventArgs args)
+    {
+        foreach (var dragPoint in propList)
+            dragPoint.gameObject.SetActive(CubeActive);
+    }
 }

+ 302 - 254
src/MeidoPhotoStudio.Plugin/Managers/SceneManager.cs

@@ -1,332 +1,380 @@
 using System;
-using System.IO;
 using System.Collections.Generic;
+using System.IO;
 using System.Linq;
-using UnityEngine;
+
 using BepInEx.Configuration;
+using UnityEngine;
+
+using Input = MeidoPhotoStudio.Plugin.InputManager;
 using Object = UnityEngine.Object;
 
-namespace MeidoPhotoStudio.Plugin
+namespace MeidoPhotoStudio.Plugin;
+
+public class SceneManager : IManager
 {
-    using Input = InputManager;
-    public class SceneManager : IManager
+    public static readonly Vector2 SceneDimensions = new(480, 270);
+
+    private static readonly ConfigEntry<bool> SortDescendingConfig =
+        Configuration.Config.Bind("SceneManager", "SortDescending", false, "Sort scenes descending (Z-A)");
+
+    private static readonly ConfigEntry<SortMode> CurrentSortModeConfig =
+        Configuration.Config.Bind("SceneManager", "SortMode", SortMode.Name, "Scene sorting mode");
+
+    private static byte[] tempSceneData;
+
+    private readonly MeidoPhotoStudio meidoPhotoStudio;
+
+    static SceneManager()
     {
+        Input.Register(MpsKey.OpenSceneManager, KeyCode.F8, "Hide/show scene manager");
+        Input.Register(MpsKey.SaveScene, KeyCode.S, "Quick save scene");
+        Input.Register(MpsKey.LoadScene, KeyCode.A, "Load quick saved scene");
+    }
 
-        private static byte[] tempSceneData;
-        private static string TempScenePath => Path.Combine(Constants.configPath, "mpstempscene");
-        public static bool Busy { get; private set; }
-        public static readonly Vector2 sceneDimensions = new Vector2(480, 270);
-        private static readonly ConfigEntry<bool> sortDescending;
-        private static readonly ConfigEntry<SortMode> currentSortMode;
-        private readonly MeidoPhotoStudio meidoPhotoStudio;
-        private int SortDirection => SortDescending ? -1 : 1;
-        public bool Initialized { get; private set; }
-        public bool KankyoMode { get; set; }
-        public bool SortDescending
-        {
-            get => sortDescending.Value;
-            set => sortDescending.Value = value;
-        }
-        public List<MPSScene> SceneList { get; } = new();
-        public int CurrentDirectoryIndex { get; private set; } = -1;
-        public string CurrentDirectoryName => CurrentDirectoryList[CurrentDirectoryIndex];
-        public List<string> CurrentDirectoryList
-            => KankyoMode ? Constants.KankyoDirectoryList : Constants.SceneDirectoryList;
-        public string CurrentBasePath => KankyoMode ? Constants.kankyoPath : Constants.scenesPath;
-        public string CurrentScenesDirectory
-            => CurrentDirectoryIndex == 0 ? CurrentBasePath : Path.Combine(CurrentBasePath, CurrentDirectoryName);
-        public SortMode CurrentSortMode
-        {
-            get => currentSortMode.Value;
-            private set => currentSortMode.Value = value;
-        }
-        public int CurrentSceneIndex { get; private set; } = -1;
-        public MPSScene CurrentScene => SceneList.Count == 0 ? null : SceneList[CurrentSceneIndex];
-        public enum SortMode
-        {
-            Name, DateCreated, DateModified
-        }
+    public SceneManager(MeidoPhotoStudio meidoPhotoStudio)
+    {
+        this.meidoPhotoStudio = meidoPhotoStudio;
 
-        static SceneManager()
-        {
-            sortDescending = Configuration.Config.Bind(
-                "SceneManager", "SortDescending",
-                false,
-                "Sort scenes descending (Z-A)"
-            );
-
-            currentSortMode = Configuration.Config.Bind(
-                "SceneManager", "SortMode",
-                SortMode.Name,
-                "Scene sorting mode"
-            );
-
-            Input.Register(MpsKey.OpenSceneManager, KeyCode.F8, "Hide/show scene manager");
-            Input.Register(MpsKey.SaveScene, KeyCode.S, "Quick save scene");
-            Input.Register(MpsKey.LoadScene, KeyCode.A, "Load quick saved scene");
-        }
+        Activate();
+    }
 
-        public SceneManager(MeidoPhotoStudio meidoPhotoStudio)
-        {
-            this.meidoPhotoStudio = meidoPhotoStudio;
-            Activate();
-        }
+    public enum SortMode
+    {
+        Name,
+        DateCreated,
+        DateModified,
+    }
 
-        public void Activate() { }
+    public static bool Busy { get; private set; }
 
-        public void Initialize()
-        {
-            if (!Initialized)
-            {
-                Initialized = true;
-                SelectDirectory(0);
-            }
-        }
+    public bool Initialized { get; private set; }
 
-        public void Deactivate() => ClearSceneList();
+    public bool KankyoMode { get; set; }
 
-        public void Update()
-        {
-            if (Input.Control)
-            {
-                if (Input.GetKeyDown(MpsKey.SaveScene)) QuickSaveScene();
-                else if (Input.GetKeyDown(MpsKey.LoadScene)) QuickLoadScene();
-            }
-        }
+    public int CurrentSceneIndex { get; private set; } = -1;
 
-        public void DeleteDirectory()
-        {
-            if (Directory.Exists(CurrentScenesDirectory)) Directory.Delete(CurrentScenesDirectory, true);
+    public List<MPSScene> SceneList { get; } = new();
 
-            CurrentDirectoryList.RemoveAt(CurrentDirectoryIndex);
-            CurrentDirectoryIndex = Mathf.Clamp(CurrentDirectoryIndex, 0, CurrentDirectoryList.Count - 1);
-            UpdateSceneList();
-        }
+    public int CurrentDirectoryIndex { get; private set; } = -1;
 
-        public void OverwriteScene() => SaveScene(overwrite: true);
+    public bool SortDescending
+    {
+        get => SortDescendingConfig.Value;
+        set => SortDescendingConfig.Value = value;
+    }
 
-        public void ToggleKankyoMode()
-        {
-            KankyoMode = !KankyoMode;
-            CurrentDirectoryIndex = 0;
-            UpdateSceneList();
-        }
+    public string CurrentDirectoryName =>
+        CurrentDirectoryList[CurrentDirectoryIndex];
 
-        public void SaveScene(bool overwrite = false)
-        {
-            if (Busy) return;
-            Busy = true;
+    public List<string> CurrentDirectoryList =>
+        KankyoMode ? Constants.KankyoDirectoryList : Constants.SceneDirectoryList;
 
-            if (!Directory.Exists(CurrentScenesDirectory)) Directory.CreateDirectory(CurrentScenesDirectory);
+    public string CurrentBasePath =>
+        KankyoMode ? Constants.KankyoPath : Constants.ScenesPath;
 
-            MeidoPhotoStudio.NotifyRawScreenshot += SaveScene;
+    public string CurrentScenesDirectory =>
+        CurrentDirectoryIndex is 0 ? CurrentBasePath : Path.Combine(CurrentBasePath, CurrentDirectoryName);
 
-            MeidoPhotoStudio.TakeScreenshot(new ScreenshotEventArgs() { InMemory = true });
+    public SortMode CurrentSortMode
+    {
+        get => CurrentSortModeConfig.Value;
+        private set => CurrentSortModeConfig.Value = value;
+    }
 
-            void SaveScene(object sender, ScreenshotEventArgs args)
-            {
-                MeidoPhotoStudio.NotifyRawScreenshot -= SaveScene;
-                SaveSceneToFile(args.Screenshot, overwrite);
-            }
-        }
+    public MPSScene CurrentScene =>
+        SceneList.Count is 0 ? null : SceneList[CurrentSceneIndex];
 
-        public void SelectDirectory(int directoryIndex)
-        {
-            directoryIndex = Mathf.Clamp(directoryIndex, 0, CurrentDirectoryList.Count - 1);
+    private static string TempScenePath =>
+        Path.Combine(Constants.ConfigPath, "mpstempscene");
 
-            if (directoryIndex == CurrentDirectoryIndex) return;
+    private int SortDirection =>
+        SortDescending ? -1 : 1;
 
-            CurrentDirectoryIndex = directoryIndex;
+    public void Activate()
+    {
+    }
 
-            UpdateSceneList();
-        }
+    public void Initialize()
+    {
+        if (Initialized)
+            return;
 
-        public void SelectScene(int sceneIndex)
-        {
-            CurrentSceneIndex = Mathf.Clamp(sceneIndex, 0, SceneList.Count - 1);
-            CurrentScene.Preload();
-        }
+        Initialized = true;
+        SelectDirectory(0);
+    }
 
-        public void AddDirectory(string directoryName)
-        {
-            directoryName = Utility.SanitizePathPortion(directoryName);
+    public void Deactivate() =>
+        ClearSceneList();
 
-            if (!CurrentDirectoryList.Contains(directoryName, StringComparer.InvariantCultureIgnoreCase))
-            {
-                string finalPath = Path.Combine(CurrentBasePath, directoryName);
-                string fullPath = Path.GetFullPath(finalPath);
+    public void Update()
+    {
+        if (!Input.Control)
+            return;
 
-                if (!fullPath.StartsWith(CurrentBasePath))
-                {
-                    string baseDirectoryName = KankyoMode ? Constants.kankyoDirectory : Constants.sceneDirectory;
-                    Utility.LogError($"Could not add directory to {baseDirectoryName}. Path is invalid: '{fullPath}'");
-                    return;
-                }
+        if (Input.GetKeyDown(MpsKey.SaveScene))
+            QuickSaveScene();
+        else if (Input.GetKeyDown(MpsKey.LoadScene))
+            QuickLoadScene();
+    }
 
-                CurrentDirectoryList.Add(directoryName);
-                Directory.CreateDirectory(finalPath);
+    public void DeleteDirectory()
+    {
+        if (Directory.Exists(CurrentScenesDirectory))
+            Directory.Delete(CurrentScenesDirectory, true);
 
-                UpdateDirectoryList();
-                CurrentDirectoryIndex = CurrentDirectoryList.IndexOf(directoryName);
+        CurrentDirectoryList.RemoveAt(CurrentDirectoryIndex);
+        CurrentDirectoryIndex = Mathf.Clamp(CurrentDirectoryIndex, 0, CurrentDirectoryList.Count - 1);
 
-                UpdateSceneList();
-            }
-        }
+        UpdateSceneList();
+    }
 
-        public void Refresh()
-        {
-            if (!Directory.Exists(CurrentScenesDirectory)) CurrentDirectoryIndex = 0;
+    public void OverwriteScene() =>
+        SaveScene(overwrite: true);
 
-            if (KankyoMode) Constants.InitializeKankyoDirectories();
-            else Constants.InitializeSceneDirectories();
+    public void ToggleKankyoMode()
+    {
+        KankyoMode = !KankyoMode;
+        CurrentDirectoryIndex = 0;
 
-            UpdateSceneList();
-        }
+        UpdateSceneList();
+    }
 
-        public void SortScenes(SortMode sortMode)
-        {
-            CurrentSortMode = sortMode;
-            Comparison<MPSScene> comparator = CurrentSortMode switch
-            {
-                SortMode.DateModified => SortByDateModified,
-                SortMode.DateCreated => SortByDateCreated,
-                _ => SortByName,
-            };
-            SceneList.Sort(comparator);
-        }
+    public void SaveScene(bool overwrite = false)
+    {
+        if (Busy)
+            return;
 
-        public void DeleteScene()
-        {
-            if (CurrentScene.FileInfo.Exists)
-            {
-                CurrentScene.FileInfo.Delete();
-            }
-            SceneList.RemoveAt(CurrentSceneIndex);
-            CurrentSceneIndex = Mathf.Clamp(CurrentSceneIndex, 0, SceneList.Count - 1);
-        }
+        Busy = true;
 
-        public void LoadScene(MPSScene scene) => meidoPhotoStudio.LoadScene(scene.Data);
+        if (!Directory.Exists(CurrentScenesDirectory))
+            Directory.CreateDirectory(CurrentScenesDirectory);
 
-        private int SortByName(MPSScene a, MPSScene b)
-        {
-            return SortDirection * WindowsLogicalComparer.StrCmpLogicalW(a.FileInfo.Name, b.FileInfo.Name);
-        }
+        MeidoPhotoStudio.NotifyRawScreenshot += SaveScene;
 
-        private int SortByDateCreated(MPSScene a, MPSScene b)
-        {
-            return SortDirection * DateTime.Compare(a.FileInfo.CreationTime, b.FileInfo.CreationTime);
-        }
+        MeidoPhotoStudio.TakeScreenshot(new() { InMemory = true });
 
-        private int SortByDateModified(MPSScene a, MPSScene b)
+        void SaveScene(object sender, ScreenshotEventArgs args)
         {
-            return SortDirection * DateTime.Compare(a.FileInfo.LastWriteTime, b.FileInfo.LastWriteTime);
+            MeidoPhotoStudio.NotifyRawScreenshot -= SaveScene;
+
+            SaveSceneToFile(args.Screenshot, overwrite);
         }
+    }
 
-        private void UpdateSceneList()
-        {
-            ClearSceneList();
+    public void SelectDirectory(int directoryIndex)
+    {
+        directoryIndex = Mathf.Clamp(directoryIndex, 0, CurrentDirectoryList.Count - 1);
 
-            if (!Directory.Exists(CurrentScenesDirectory))
-            {
-                Directory.CreateDirectory(CurrentScenesDirectory);
-            }
+        if (directoryIndex == CurrentDirectoryIndex)
+            return;
 
-            foreach (string filename in Directory.GetFiles(CurrentScenesDirectory))
-            {
-                if (Path.GetExtension(filename) == ".png") SceneList.Add(new MPSScene(filename));
-            }
+        CurrentDirectoryIndex = directoryIndex;
 
-            SortScenes(CurrentSortMode);
+        UpdateSceneList();
+    }
 
-            CurrentSceneIndex = Mathf.Clamp(CurrentSceneIndex, 0, SceneList.Count - 1);
-        }
+    public void SelectScene(int sceneIndex)
+    {
+        CurrentSceneIndex = Mathf.Clamp(sceneIndex, 0, SceneList.Count - 1);
+        CurrentScene.Preload();
+    }
+
+    public void AddDirectory(string directoryName)
+    {
+        directoryName = Utility.SanitizePathPortion(directoryName);
+
+        if (CurrentDirectoryList.Contains(directoryName, StringComparer.InvariantCultureIgnoreCase))
+            return;
 
-        private void UpdateDirectoryList()
+        var finalPath = Path.Combine(CurrentBasePath, directoryName);
+        var fullPath = Path.GetFullPath(finalPath);
+
+        if (!fullPath.StartsWith(CurrentBasePath))
         {
-            string baseDirectoryName = KankyoMode ? Constants.kankyoDirectory : Constants.sceneDirectory;
-            CurrentDirectoryList.Sort((a, b)
-                => a.Equals(baseDirectoryName, StringComparison.InvariantCultureIgnoreCase) 
-                    ? -1 : WindowsLogicalComparer.StrCmpLogicalW(a, b));
+            var baseDirectoryName = KankyoMode ? Constants.KankyoDirectory : Constants.SceneDirectory;
+
+            Utility.LogError($"Could not add directory to {baseDirectoryName}. Path is invalid: '{fullPath}'");
+
+            return;
         }
 
-        private void ClearSceneList()
+        CurrentDirectoryList.Add(directoryName);
+        Directory.CreateDirectory(finalPath);
+
+        UpdateDirectoryList();
+        CurrentDirectoryIndex = CurrentDirectoryList.IndexOf(directoryName);
+
+        UpdateSceneList();
+    }
+
+    public void Refresh()
+    {
+        if (!Directory.Exists(CurrentScenesDirectory))
+            CurrentDirectoryIndex = 0;
+
+        if (KankyoMode)
+            Constants.InitializeKankyoDirectories();
+        else
+            Constants.InitializeSceneDirectories();
+
+        UpdateSceneList();
+    }
+
+    public void SortScenes(SortMode sortMode)
+    {
+        CurrentSortMode = sortMode;
+
+        Comparison<MPSScene> comparator = CurrentSortMode switch
         {
-            foreach (MPSScene scene in SceneList) scene.Destroy();
-            SceneList.Clear();
-        }
+            SortMode.DateModified => SortByDateModified,
+            SortMode.DateCreated => SortByDateCreated,
+            SortMode.Name => SortByName,
+            _ => SortByName,
+        };
+
+        SceneList.Sort(comparator);
+    }
+
+    public void DeleteScene()
+    {
+        if (CurrentScene.FileInfo.Exists)
+            CurrentScene.FileInfo.Delete();
+
+        SceneList.RemoveAt(CurrentSceneIndex);
+        CurrentSceneIndex = Mathf.Clamp(CurrentSceneIndex, 0, SceneList.Count - 1);
+    }
+
+    public void LoadScene(MPSScene scene) =>
+        meidoPhotoStudio.LoadScene(scene.Data);
+
+    private int SortByName(MPSScene a, MPSScene b) =>
+        SortDirection * WindowsLogicalComparer.StrCmpLogicalW(a.FileInfo.Name, b.FileInfo.Name);
+
+    private int SortByDateCreated(MPSScene a, MPSScene b) =>
+        SortDirection * DateTime.Compare(a.FileInfo.CreationTime, b.FileInfo.CreationTime);
 
-        private void QuickSaveScene()
+    private int SortByDateModified(MPSScene a, MPSScene b) =>
+        SortDirection * DateTime.Compare(a.FileInfo.LastWriteTime, b.FileInfo.LastWriteTime);
+
+    private void UpdateSceneList()
+    {
+        ClearSceneList();
+
+        if (!Directory.Exists(CurrentScenesDirectory))
+            Directory.CreateDirectory(CurrentScenesDirectory);
+
+        foreach (var filename in Directory.GetFiles(CurrentScenesDirectory))
+            if (Path.GetExtension(filename) is ".png")
+                SceneList.Add(new(filename));
+
+        SortScenes(CurrentSortMode);
+
+        CurrentSceneIndex = Mathf.Clamp(CurrentSceneIndex, 0, SceneList.Count - 1);
+    }
+
+    private void UpdateDirectoryList()
+    {
+        var baseDirectoryName = KankyoMode ? Constants.KankyoDirectory : Constants.SceneDirectory;
+
+        CurrentDirectoryList.Sort((a, b) =>
+            a.Equals(baseDirectoryName, StringComparison.InvariantCultureIgnoreCase)
+                ? -1
+                : WindowsLogicalComparer.StrCmpLogicalW(a, b));
+    }
+
+    private void ClearSceneList()
+    {
+        foreach (var scene in SceneList)
+            scene.Destroy();
+
+        SceneList.Clear();
+    }
+
+    private void QuickSaveScene()
+    {
+        if (Busy)
+            return;
+
+        var data = meidoPhotoStudio.SaveScene();
+
+        if (data is null)
+            return;
+
+        tempSceneData = data;
+
+        File.WriteAllBytes(TempScenePath, data);
+    }
+
+    private void QuickLoadScene()
+    {
+        if (Busy)
+            return;
+
+        if (tempSceneData is null)
         {
-            if (Busy) return;
-            
-            var data = meidoPhotoStudio.SaveScene();
-            if (data == null) return;
-
-            tempSceneData = data;
-            
-            File.WriteAllBytes(TempScenePath, data);
+            if (File.Exists(TempScenePath))
+                tempSceneData = File.ReadAllBytes(TempScenePath);
+            else
+                return;
         }
 
-        private void QuickLoadScene()
-        {
-            if (Busy) return;
+        meidoPhotoStudio.LoadScene(tempSceneData);
+    }
 
-            if (tempSceneData == null)
-            {
-                if (File.Exists(TempScenePath)) tempSceneData = File.ReadAllBytes(TempScenePath);
-                else return;
-            }
+    private void SaveSceneToFile(Texture2D screenshot, bool overwrite = false)
+    {
+        Busy = true;
 
-            meidoPhotoStudio.LoadScene(tempSceneData);
-        }
+        var sceneData = meidoPhotoStudio.SaveScene(KankyoMode);
 
-        private void SaveSceneToFile(Texture2D screenshot, bool overwrite = false)
+        if (sceneData is not null)
         {
-            Busy = true;
+            var scenePrefix = KankyoMode ? "mpskankyo" : "mpsscene";
+            var fileName = $"{scenePrefix}{Utility.Timestamp}.png";
+            var savePath = Path.Combine(CurrentScenesDirectory, fileName);
 
-            byte[] sceneData = meidoPhotoStudio.SaveScene(KankyoMode);
+            Utility.ResizeToFit(screenshot, (int)SceneDimensions.x, (int)SceneDimensions.y);
 
-            if (sceneData != null)
+            try
             {
-                string scenePrefix = KankyoMode ? "mpskankyo" : "mpsscene";
-                string fileName = $"{scenePrefix}{Utility.Timestamp}.png";
-                string savePath = Path.Combine(CurrentScenesDirectory, fileName);
-
-                Utility.ResizeToFit(screenshot, (int) sceneDimensions.x, (int) sceneDimensions.y);
+                if (overwrite && CurrentScene?.FileInfo is not null)
+                    savePath = CurrentScene.FileInfo.FullName;
+                else
+                    overwrite = false;
 
-                try
+                using (var fileStream = File.Create(savePath))
                 {
-                    if (overwrite && CurrentScene?.FileInfo != null) savePath = CurrentScene.FileInfo.FullName;
-                    else overwrite = false;
-
-                    using (FileStream fileStream = File.Create(savePath))
-                    {
-                        byte[] encodedPng = screenshot.EncodeToPNG();
-                        fileStream.Write(encodedPng, 0, encodedPng.Length);
-                        fileStream.Write(sceneData, 0, sceneData.Length);
-                    }
-
-                    if (overwrite)
-                    {
-                        File.SetCreationTime(savePath, CurrentScene.FileInfo.CreationTime);
-                        CurrentScene.Destroy();
-                        SceneList.RemoveAt(CurrentSceneIndex);
-                    }
+                    var encodedPng = screenshot.EncodeToPNG();
+
+                    fileStream.Write(encodedPng, 0, encodedPng.Length);
+                    fileStream.Write(sceneData, 0, sceneData.Length);
                 }
-                catch (Exception e)
+
+                if (overwrite)
                 {
-                    Utility.LogError($"Failed to save scene to disk because {e.Message}\n{e.StackTrace}");
-                    Object.DestroyImmediate(screenshot);
-                    Busy = false;
-                    return;
+                    File.SetCreationTime(savePath, CurrentScene.FileInfo.CreationTime);
+                    CurrentScene.Destroy();
+                    SceneList.RemoveAt(CurrentSceneIndex);
                 }
+            }
+            catch (Exception e)
+            {
+                Utility.LogError($"Failed to save scene to disk because {e.Message}\n{e.StackTrace}");
+                Object.DestroyImmediate(screenshot);
+                Busy = false;
 
-                SceneList.Add(new MPSScene(savePath, screenshot));
-                SortScenes(CurrentSortMode);
+                return;
             }
-            else Object.DestroyImmediate(screenshot);
 
-            Busy = false;
+            SceneList.Add(new(savePath, screenshot));
+            SortScenes(CurrentSortMode);
         }
+        else
+        {
+            Object.DestroyImmediate(screenshot);
+        }
+
+        Busy = false;
     }
 }

+ 44 - 36
src/MeidoPhotoStudio.Plugin/Managers/WindowManager.cs

@@ -1,51 +1,59 @@
 using System.Collections.Generic;
+
 using UnityEngine;
 
-namespace MeidoPhotoStudio.Plugin
+using static MeidoPhotoStudio.Plugin.Constants;
+
+namespace MeidoPhotoStudio.Plugin;
+
+public class WindowManager : IManager
 {
-    using static Constants;
-    public class WindowManager : IManager
+    private readonly Dictionary<Window, BaseWindow> windows = new();
+
+    public WindowManager() =>
+        InputManager.Register(MpsKey.ToggleUI, KeyCode.Tab, "Show/hide all UI");
+
+    public BaseWindow this[Window id]
     {
-        private readonly Dictionary<Window, BaseWindow> Windows = new Dictionary<Window, BaseWindow>();
-        public BaseWindow this[Window id]
+        get => windows[id];
+        set
         {
-            get => Windows[id];
-            set
-            {
-                Windows[id] = value;
-                Windows[id].Activate();
-            }
+            windows[id] = value;
+            windows[id].Activate();
         }
+    }
 
-        public WindowManager() => InputManager.Register(MpsKey.ToggleUI, KeyCode.Tab, "Show/hide all UI");
+    public void DrawWindow(BaseWindow window)
+    {
+        if (!window.Visible)
+            return;
 
-        public void DrawWindow(BaseWindow window)
-        {
-            if (window.Visible)
-            {
-                GUIStyle windowStyle = new GUIStyle(GUI.skin.box);
-                window.WindowRect = GUI.Window(window.windowID, window.WindowRect, window.GUIFunc, "", windowStyle);
-            }
-        }
+        var windowStyle = new GUIStyle(GUI.skin.box);
 
-        public void DrawWindows()
-        {
-            foreach (BaseWindow window in Windows.Values) DrawWindow(window);
-        }
+        window.WindowRect = GUI.Window(window.WindowID, window.WindowRect, window.GUIFunc, string.Empty, windowStyle);
+    }
 
-        public void Update()
-        {
-            foreach (BaseWindow window in Windows.Values) window.Update();
-        }
+    public void DrawWindows()
+    {
+        foreach (var window in windows.Values)
+            DrawWindow(window);
+    }
 
-        public void Activate()
-        {
-            foreach (BaseWindow window in Windows.Values) window.Activate();
-        }
+    public void Update()
+    {
+        foreach (var window in windows.Values)
+            window.Update();
+    }
 
-        public void Deactivate()
-        {
-            foreach (BaseWindow window in Windows.Values) window.Deactivate();
-        }
+    public void Activate()
+    {
+        foreach (var window in windows.Values)
+            window.Activate();
+    }
+
+    public void Deactivate()
+    {
+        foreach (var window in windows.Values)
+            window.Deactivate();
     }
 }

+ 27 - 0
src/MeidoPhotoStudio.Plugin/Meido/AttachPoint.cs

@@ -0,0 +1,27 @@
+namespace MeidoPhotoStudio.Plugin;
+
+public enum AttachPoint
+{
+    None,
+    Head,
+    Neck,
+    UpperArmL,
+    UpperArmR,
+    ForearmL,
+    ForearmR,
+    MuneL,
+    MuneR,
+    HandL,
+    HandR,
+    Pelvis,
+    ThighL,
+    ThighR,
+    CalfL,
+    CalfR,
+    FootL,
+    FootR,
+    Spine1a,
+    Spine1,
+    Spine0a,
+    Spine0,
+}

+ 29 - 0
src/MeidoPhotoStudio.Plugin/Meido/AttachPointInfo.cs

@@ -0,0 +1,29 @@
+namespace MeidoPhotoStudio.Plugin;
+
+public readonly struct AttachPointInfo
+{
+    private static readonly AttachPointInfo EmptyValue = new(AttachPoint.None, string.Empty, -1);
+
+    public AttachPointInfo(AttachPoint attachPoint, Meido meido)
+    {
+        AttachPoint = attachPoint;
+        MaidGuid = meido.Maid.status.guid;
+        MaidIndex = meido.Slot;
+    }
+
+    public AttachPointInfo(AttachPoint attachPoint, string maidGuid, int maidIndex)
+    {
+        AttachPoint = attachPoint;
+        MaidGuid = maidGuid;
+        MaidIndex = maidIndex;
+    }
+
+    public static ref readonly AttachPointInfo Empty =>
+        ref EmptyValue;
+
+    public AttachPoint AttachPoint { get; }
+
+    public string MaidGuid { get; }
+
+    public int MaidIndex { get; }
+}

+ 18 - 0
src/MeidoPhotoStudio.Plugin/Meido/GravityEventArgs.cs

@@ -0,0 +1,18 @@
+using System;
+
+using UnityEngine;
+
+namespace MeidoPhotoStudio.Plugin;
+
+public class GravityEventArgs : EventArgs
+{
+    public GravityEventArgs(bool isSkirt, Vector3 localPosition)
+    {
+        LocalPosition = localPosition;
+        IsSkirt = isSkirt;
+    }
+
+    public Vector3 LocalPosition { get; }
+
+    public bool IsSkirt { get; }
+}

+ 78 - 69
src/MeidoPhotoStudio.Plugin/Meido/IK/DragPointFinger.cs

@@ -1,87 +1,96 @@
 using UnityEngine;
 
-namespace MeidoPhotoStudio.Plugin
+using Input = MeidoPhotoStudio.Plugin.InputManager;
+
+namespace MeidoPhotoStudio.Plugin;
+
+public class DragPointFinger : DragPointMeido
 {
-    using Input = InputManager;
-    public class DragPointFinger : DragPointMeido
+    private static readonly Color DragpointColour = new(0.1f, 0.4f, 0.95f, DefaultAlpha);
+
+    private readonly TBody.IKCMO ik = new();
+    private readonly Quaternion[] jointRotation = new Quaternion[2];
+
+    private IKCtrlData ikCtrlData;
+    private Transform[] ikChain;
+    private bool baseFinger;
+
+    public override void Set(Transform myObject)
     {
-        private static readonly Color dragpointColour = new Color(0.1f, 0.4f, 0.95f, defaultAlpha);
-        private readonly TBody.IKCMO IK = new TBody.IKCMO();
-        private readonly Quaternion[] jointRotation = new Quaternion[2];
-        private IKCtrlData ikCtrlData;
-        private Transform[] ikChain;
-        private bool baseFinger;
-
-        public override void Set(Transform myObject)
-        {
-            base.Set(myObject);
-            string parentName = myObject.parent.name.Split(' ')[2];
-            // Base finger names have the form 'FingerN' or 'ToeN' where N is a natural number
-            baseFinger = (parentName.Length == 7) || (parentName.Length == 4);
-            ikChain = new Transform[2] {
-                myObject.parent,
-                myObject
-            };
-            ikCtrlData = IkCtrlData;
-        }
+        base.Set(myObject);
 
-        private void SetRotation(int joint)
-        {
-            Vector3 rotation = jointRotation[joint].eulerAngles;
-            rotation.z = ikChain[joint].localEulerAngles.z;
-            ikChain[joint].localRotation = Quaternion.Euler(rotation);
-        }
+        var parentName = myObject.parent.name.Split(' ')[2];
 
-        protected override void ApplyDragType()
-        {
-            if (baseFinger && CurrentDragType == DragType.RotLocalY) ApplyProperties(true, true, false);
-            else if (CurrentDragType == DragType.MoveXZ) ApplyProperties(true, true, false);
-            else ApplyProperties(false, false, false);
-            ApplyColour(dragpointColour);
-        }
+        // Base finger names have the form 'FingerN' or 'ToeN' where N is a natural number
+        baseFinger = parentName.Length is 7 or 4;
+        ikChain = new Transform[2] { myObject.parent, myObject };
 
-        protected override void UpdateDragType()
-        {
-            CurrentDragType = Input.GetKey(MpsKey.DragFinger)
-                ? Input.Shift
-                    ? DragType.RotLocalY
-                    : DragType.MoveXZ
-                : DragType.None;
-        }
+        ikCtrlData = IkCtrlData;
+    }
 
-        protected override void OnMouseDown()
-        {
-            base.OnMouseDown();
-            jointRotation[jointUpper] = ikChain[jointUpper].localRotation;
-            jointRotation[jointMiddle] = ikChain[jointMiddle].localRotation;
-            InitializeIK(IK, ikChain[jointUpper], ikChain[jointUpper], ikChain[jointMiddle]);
-        }
+    protected override void ApplyDragType()
+    {
+        if (baseFinger && CurrentDragType is DragType.RotLocalY)
+            ApplyProperties(true, true, false);
+        else if (CurrentDragType is DragType.MoveXZ)
+            ApplyProperties(true, true, false);
+        else
+            ApplyProperties(false, false, false);
 
-        protected override void Drag()
+        ApplyColour(DragpointColour);
+    }
+
+    protected override void UpdateDragType() =>
+        CurrentDragType = !Input.GetKey(MpsKey.DragFinger)
+            ? DragType.None
+            : Input.Shift
+                ? DragType.RotLocalY
+                : DragType.MoveXZ;
+
+    protected override void OnMouseDown()
+    {
+        base.OnMouseDown();
+
+        jointRotation[JointUpper] = ikChain[JointUpper].localRotation;
+        jointRotation[JointMiddle] = ikChain[JointMiddle].localRotation;
+
+        InitializeIK(ik, ikChain[JointUpper], ikChain[JointUpper], ikChain[JointMiddle]);
+    }
+
+    protected override void Drag()
+    {
+        if (isPlaying)
+            meido.Stop = true;
+
+        if (CurrentDragType is DragType.MoveXZ)
         {
-            if (isPlaying) meido.Stop = true;
+            Porc(ik, ikCtrlData, ikChain[JointUpper], ikChain[JointUpper], ikChain[JointMiddle]);
 
-            if (CurrentDragType == DragType.MoveXZ)
+            if (!baseFinger)
             {
-                Porc(IK, ikCtrlData, ikChain[jointUpper], ikChain[jointUpper], ikChain[jointMiddle]);
-                if (!baseFinger)
-                {
-                    SetRotation(jointUpper);
-                    SetRotation(jointMiddle);
-                }
-                else
-                {
-                    jointRotation[jointUpper] = ikChain[jointUpper].localRotation;
-                    jointRotation[jointMiddle] = ikChain[jointMiddle].localRotation;
-                }
+                SetRotation(JointUpper);
+                SetRotation(JointMiddle);
             }
-            else if (CurrentDragType == DragType.RotLocalY)
+            else
             {
-                Vector3 mouseDelta = MouseDelta();
-
-                ikChain[jointUpper].localRotation = jointRotation[jointUpper];
-                ikChain[jointUpper].Rotate(Vector3.right * (mouseDelta.x / 1.5f));
+                jointRotation[JointUpper] = ikChain[JointUpper].localRotation;
+                jointRotation[JointMiddle] = ikChain[JointMiddle].localRotation;
             }
         }
+        else if (CurrentDragType is DragType.RotLocalY)
+        {
+            var mouseDelta = MouseDelta();
+
+            ikChain[JointUpper].localRotation = jointRotation[JointUpper];
+            ikChain[JointUpper].Rotate(Vector3.right * (mouseDelta.x / 1.5f));
+        }
+    }
+
+    private void SetRotation(int joint)
+    {
+        var rotation = jointRotation[joint].eulerAngles;
+
+        rotation.z = ikChain[joint].localEulerAngles.z;
+        ikChain[joint].localRotation = Quaternion.Euler(rotation);
     }
 }

+ 95 - 87
src/MeidoPhotoStudio.Plugin/Meido/IK/DragPointHead.cs

@@ -1,112 +1,120 @@
 using System;
+
 using UnityEngine;
 
-namespace MeidoPhotoStudio.Plugin
+using Input = MeidoPhotoStudio.Plugin.InputManager;
+
+namespace MeidoPhotoStudio.Plugin;
+
+public class DragPointHead : DragPointMeido
 {
-    using Input = InputManager;
-    public class DragPointHead : DragPointMeido
+    private Quaternion headRotation;
+    private Vector3 eyeRotationL;
+    private Vector3 eyeRotationR;
+
+    public event EventHandler Select;
+
+    public bool IsIK { get; set; }
+
+    protected override void ApplyDragType()
     {
-        private Quaternion headRotation;
-        private Vector3 eyeRotationL;
-        private Vector3 eyeRotationR;
-        public event EventHandler Select;
-        public bool IsIK { get; set; }
+        if (IsBone)
+        {
+            var current = CurrentDragType;
+            var active = current is DragType.MoveY or DragType.MoveXZ or DragType.Select;
 
-        protected override void ApplyDragType()
+            ApplyProperties(active, false, false);
+        }
+        else
         {
-            if (IsBone)
-            {
-                DragType current = CurrentDragType;
-                bool active = current == DragType.MoveY || current == DragType.MoveXZ || current == DragType.Select;
-                ApplyProperties(active, false, false);
-            }
-            else ApplyProperties(CurrentDragType != DragType.None, false, false);
+            ApplyProperties(CurrentDragType is not DragType.None, false, false);
         }
+    }
+
+    protected override void UpdateDragType()
+    {
+        var shift = Input.Shift;
+        var alt = Input.Alt;
 
-        protected override void UpdateDragType()
+        if (alt && Input.Control)
+        {
+            // eyes
+            CurrentDragType = shift
+                ? DragType.MoveY
+                : DragType.MoveXZ;
+        }
+        else if (alt)
+        {
+            // head
+            CurrentDragType = shift
+                ? DragType.RotLocalY
+                : DragType.RotLocalXZ;
+        }
+        else
         {
-            bool shift = Input.Shift;
-            bool alt = Input.Alt;
-            if (alt && Input.Control)
-            {
-                // eyes
-                CurrentDragType = shift ? DragType.MoveY : DragType.MoveXZ;
-            }
-            else if (alt)
-            {
-                // head
-                CurrentDragType = shift ? DragType.RotLocalY : DragType.RotLocalXZ;
-            }
-            else if (Input.GetKey(MpsKey.DragSelect))
-            {
-                CurrentDragType = DragType.Select;
-            }
-            else
-            {
-                CurrentDragType = DragType.None;
-            }
+            CurrentDragType = Input.GetKey(MpsKey.DragSelect)
+                ? DragType.Select
+                : DragType.None;
         }
+    }
 
-        protected override void OnMouseDown()
+    protected override void OnMouseDown()
+    {
+        base.OnMouseDown();
+
+        if (CurrentDragType is DragType.Select)
+            Select?.Invoke(this, EventArgs.Empty);
+
+        headRotation = MyObject.rotation;
+
+        eyeRotationL = meido.Body.quaDefEyeL.eulerAngles;
+        eyeRotationR = meido.Body.quaDefEyeR.eulerAngles;
+    }
+
+    protected override void OnDoubleClick()
+    {
+        if (CurrentDragType is DragType.MoveXZ or DragType.MoveY)
         {
-            base.OnMouseDown();
+            meido.Body.quaDefEyeL = meido.DefaultEyeRotL;
+            meido.Body.quaDefEyeR = meido.DefaultEyeRotR;
+        }
+        else if (CurrentDragType is DragType.RotLocalY or DragType.RotLocalXZ)
+        {
+            meido.FreeLook = !meido.FreeLook;
+        }
+    }
 
-            if (CurrentDragType == DragType.Select) Select?.Invoke(this, EventArgs.Empty);
+    protected override void Drag()
+    {
+        if (IsIK || CurrentDragType is DragType.Select)
+            return;
 
-            headRotation = MyObject.rotation;
+        if (CurrentDragType is not DragType.MoveXZ and not DragType.MoveY && isPlaying)
+            meido.Stop = true;
 
-            eyeRotationL = meido.Body.quaDefEyeL.eulerAngles;
-            eyeRotationR = meido.Body.quaDefEyeR.eulerAngles;
+        var mouseDelta = MouseDelta();
+
+        if (CurrentDragType is DragType.RotLocalXZ)
+        {
+            MyObject.rotation = headRotation;
+            MyObject.Rotate(camera.transform.forward, -mouseDelta.x / 3f, Space.World);
+            MyObject.Rotate(camera.transform.right, mouseDelta.y / 3f, Space.World);
         }
 
-        protected override void OnDoubleClick()
+        if (CurrentDragType is DragType.RotLocalY)
         {
-            if (CurrentDragType == DragType.MoveXZ || CurrentDragType == DragType.MoveY)
-            {
-                meido.Body.quaDefEyeL = meido.DefaultEyeRotL;
-                meido.Body.quaDefEyeR = meido.DefaultEyeRotR;
-            }
-            else if (CurrentDragType == DragType.RotLocalY || CurrentDragType == DragType.RotLocalXZ)
-            {
-                meido.FreeLook = !meido.FreeLook;
-            }
+            MyObject.rotation = headRotation;
+            MyObject.Rotate(Vector3.right * mouseDelta.x / 3f);
         }
 
-        protected override void Drag()
+        if (CurrentDragType is DragType.MoveXZ or DragType.MoveY)
         {
-            if (IsIK || CurrentDragType == DragType.Select) return;
-
-            if (!(CurrentDragType == DragType.MoveXZ || CurrentDragType == DragType.MoveY) && isPlaying)
-            {
-                meido.Stop = true;
-            }
-
-            Vector3 mouseDelta = MouseDelta();
-
-            if (CurrentDragType == DragType.RotLocalXZ)
-            {
-                MyObject.rotation = headRotation;
-                MyObject.Rotate(camera.transform.forward, -mouseDelta.x / 3f, Space.World);
-                MyObject.Rotate(camera.transform.right, mouseDelta.y / 3f, Space.World);
-            }
-
-            if (CurrentDragType == DragType.RotLocalY)
-            {
-                MyObject.rotation = headRotation;
-                MyObject.Rotate(Vector3.right * mouseDelta.x / 3f);
-            }
-
-            if (CurrentDragType == DragType.MoveXZ || CurrentDragType == DragType.MoveY)
-            {
-                int inv = CurrentDragType == DragType.MoveY ? -1 : 1;
-
-                meido.Body.quaDefEyeL.eulerAngles = new Vector3(
-                    eyeRotationL.x, eyeRotationL.y - (mouseDelta.x / 10f), eyeRotationL.z - (mouseDelta.y / 10f)
-                );
-                meido.Body.quaDefEyeR.eulerAngles = new Vector3(
-                    eyeRotationR.x, eyeRotationR.y + (inv * mouseDelta.x / 10f), eyeRotationR.z + (mouseDelta.y / 10f)
-                );
-            }
+            var inv = CurrentDragType is DragType.MoveY ? -1 : 1;
+
+            meido.Body.quaDefEyeL.eulerAngles =
+                new(eyeRotationL.x, eyeRotationL.y - mouseDelta.x / 10f, eyeRotationL.z - mouseDelta.y / 10f);
+            meido.Body.quaDefEyeR.eulerAngles =
+                new(eyeRotationR.x, eyeRotationR.y + inv * mouseDelta.x / 10f, eyeRotationR.z + mouseDelta.y / 10f);
         }
     }
 }

+ 47 - 37
src/MeidoPhotoStudio.Plugin/Meido/IK/DragPointPelvis.cs

@@ -1,52 +1,62 @@
 using UnityEngine;
 
-namespace MeidoPhotoStudio.Plugin
+using Input = MeidoPhotoStudio.Plugin.InputManager;
+
+namespace MeidoPhotoStudio.Plugin;
+
+public class DragPointPelvis : DragPointMeido
 {
-    using Input = InputManager;
-    public class DragPointPelvis : DragPointMeido
+    private Quaternion pelvisRotation;
+
+    protected override void ApplyDragType()
     {
-        private Quaternion pelvisRotation;
+        if (CurrentDragType is DragType.Ignore)
+            ApplyProperties();
+        else if (IsBone)
+            ApplyProperties(false, false, false);
+        else
+            ApplyProperties(CurrentDragType is not DragType.None, false, false);
+    }
 
-        protected override void ApplyDragType()
-        {
-            if (CurrentDragType == DragType.Ignore) ApplyProperties();
-            else if (IsBone) ApplyProperties(false, false, false);
-            else ApplyProperties(CurrentDragType != DragType.None, false, false);
-        }
+    protected override void UpdateDragType() =>
 
-        protected override void UpdateDragType()
-        {
-            CurrentDragType = Input.Alt && !Input.Control
-                ? Input.Shift ? DragType.RotLocalY : DragType.RotLocalXZ
-                : OtherDragType() ? DragType.Ignore : DragType.None;
-        }
+        // TODO: Rethink this formatting
+        CurrentDragType = Input.Alt && !Input.Control
+            ? Input.Shift
+                ? DragType.RotLocalY
+                : DragType.RotLocalXZ
+            : OtherDragType()
+                ? DragType.Ignore
+                : DragType.None;
 
-        protected override void OnMouseDown()
-        {
-            base.OnMouseDown();
-            pelvisRotation = MyObject.rotation;
-        }
+    protected override void OnMouseDown()
+    {
+        base.OnMouseDown();
 
-        protected override void Drag()
-        {
-            if (CurrentDragType == DragType.None) return;
+        pelvisRotation = MyObject.rotation;
+    }
 
-            if (isPlaying) meido.Stop = true;
+    protected override void Drag()
+    {
+        if (CurrentDragType is DragType.None)
+            return;
 
-            Vector3 mouseDelta = MouseDelta();
+        if (isPlaying)
+            meido.Stop = true;
 
-            if (CurrentDragType == DragType.RotLocalXZ)
-            {
-                MyObject.rotation = pelvisRotation;
-                MyObject.Rotate(camera.transform.forward, mouseDelta.x / 6f, Space.World);
-                MyObject.Rotate(camera.transform.right, mouseDelta.y / 4f, Space.World);
-            }
+        var mouseDelta = MouseDelta();
 
-            if (CurrentDragType == DragType.RotLocalY)
-            {
-                MyObject.rotation = pelvisRotation;
-                MyObject.Rotate(Vector3.right * (mouseDelta.x / 2.2f));
-            }
+        if (CurrentDragType is DragType.RotLocalXZ)
+        {
+            MyObject.rotation = pelvisRotation;
+            MyObject.Rotate(camera.transform.forward, mouseDelta.x / 6f, Space.World);
+            MyObject.Rotate(camera.transform.right, mouseDelta.y / 4f, Space.World);
+        }
+
+        if (CurrentDragType is DragType.RotLocalY)
+        {
+            MyObject.rotation = pelvisRotation;
+            MyObject.Rotate(Vector3.right * (mouseDelta.x / 2.2f));
         }
     }
 }

+ 100 - 80
src/MeidoPhotoStudio.Plugin/Meido/IK/DragPointSpine.cs

@@ -1,106 +1,126 @@
 using UnityEngine;
 
-namespace MeidoPhotoStudio.Plugin
+using Input = MeidoPhotoStudio.Plugin.InputManager;
+
+namespace MeidoPhotoStudio.Plugin;
+
+public class DragPointSpine : DragPointMeido
 {
-    using Input = InputManager;
-    public class DragPointSpine : DragPointMeido
+    private Quaternion spineRotation;
+    private bool isHip;
+    private bool isThigh;
+    private bool isHead;
+
+    public override void AddGizmo(float scale = 0.25f, CustomGizmo.GizmoMode mode = CustomGizmo.GizmoMode.Local)
+    {
+        base.AddGizmo(scale, mode);
+
+        if (isHead)
+            Gizmo.GizmoDrag += (_, _) =>
+                meido.HeadToCam = false;
+    }
+
+    public override void Set(Transform myObject)
     {
-        private Quaternion spineRotation;
-        private bool isHip;
-        private bool isThigh;
-        private bool isHead;
+        base.Set(myObject);
 
-        public override void AddGizmo(float scale = 0.25f, CustomGizmo.GizmoMode mode = CustomGizmo.GizmoMode.Local)
+        isHip = myObject.name is "Bip01";
+        isThigh = myObject.name.EndsWith("Thigh");
+        isHead = myObject.name.EndsWith("Head");
+    }
+
+    protected override void ApplyDragType()
+    {
+        var current = CurrentDragType;
+
+        if (IsBone && current is not DragType.Ignore)
         {
-            base.AddGizmo(scale, mode);
-            if (isHead) Gizmo.GizmoDrag += (s, a) => meido.HeadToCam = false;
+            if (!isHead && current is DragType.RotLocalXZ)
+                ApplyProperties(false, false, isThigh);
+            else if (!isThigh && current is DragType.MoveY)
+                ApplyProperties(isHip, isHip, !isHip);
+            else if (!isThigh && !isHead && current is DragType.RotLocalY)
+                ApplyProperties(!isHip, !isHip, isHip);
+            else
+                ApplyProperties(!isThigh, !isThigh, false);
         }
-
-        public override void Set(Transform myObject)
+        else
         {
-            base.Set(myObject);
-            isHip = myObject.name == "Bip01";
-            isThigh = myObject.name.EndsWith("Thigh");
-            isHead = myObject.name.EndsWith("Head");
+            ApplyProperties(false, false, false);
         }
+    }
 
-        protected override void ApplyDragType()
+    protected override void UpdateDragType()
+    {
+        var shift = Input.Shift;
+        var alt = Input.Alt;
+
+        if (OtherDragType())
         {
-            DragType current = CurrentDragType;
-            if (IsBone && current != DragType.Ignore)
-            {
-                if (!isHead && current == DragType.RotLocalXZ) ApplyProperties(false, false, isThigh);
-                else if (!isThigh && (current == DragType.MoveY)) ApplyProperties(isHip, isHip, !isHip);
-                else if (!isThigh && !isHead && (current == DragType.RotLocalY)) ApplyProperties(!isHip, !isHip, isHip);
-                else ApplyProperties(!isThigh, !isThigh, false);
-            }
-            else ApplyProperties(false, false, false);
+            CurrentDragType = DragType.Ignore;
         }
-
-        protected override void UpdateDragType()
+        else if (isThigh && !Input.Control && alt && shift)
         {
-            bool shift = Input.Shift;
-            bool alt = Input.Alt;
-
-            if (OtherDragType()) CurrentDragType = DragType.Ignore;
-            else if (isThigh && !Input.Control && alt && shift)
-            {
-                // gizmo thigh rotation
-                CurrentDragType = DragType.RotLocalXZ;
-            }
-            else if (alt)
-            {
-                CurrentDragType = DragType.Ignore;
-            }
-            else if (shift)
-            {
-                CurrentDragType = DragType.RotLocalY;
-            }
-            else if (Input.Control)
-            {
-                // hip y transform and spine gizmo rotation
-                CurrentDragType = DragType.MoveY;
-            }
-            else
-            {
-                CurrentDragType = DragType.None;
-            }
+            // gizmo thigh rotation
+            CurrentDragType = DragType.RotLocalXZ;
         }
-
-        protected override void OnMouseDown()
+        else if (alt)
         {
-            base.OnMouseDown();
-            spineRotation = MyObject.rotation;
+            CurrentDragType = DragType.Ignore;
         }
-
-        protected override void Drag()
+        else if (shift)
+        {
+            CurrentDragType = DragType.RotLocalY;
+        }
+        else if (Input.Control)
+        {
+            // hip y transform and spine gizmo rotation
+            CurrentDragType = DragType.MoveY;
+        }
+        else
         {
-            if (isPlaying) meido.Stop = true;
+            CurrentDragType = DragType.None;
+        }
+    }
 
-            Vector3 mouseDelta = MouseDelta();
+    protected override void OnMouseDown()
+    {
+        base.OnMouseDown();
 
-            if (CurrentDragType == DragType.None)
-            {
-                if (isHead) meido.HeadToCam = false;
+        spineRotation = MyObject.rotation;
+    }
 
-                MyObject.rotation = spineRotation;
-                MyObject.Rotate(camera.transform.forward, -mouseDelta.x / 4.5f, Space.World);
-                MyObject.Rotate(camera.transform.right, mouseDelta.y / 3f, Space.World);
-            }
+    protected override void Drag()
+    {
+        if (isPlaying)
+            meido.Stop = true;
 
-            if (CurrentDragType == DragType.RotLocalY)
-            {
-                if (isHead) meido.HeadToCam = false;
+        var mouseDelta = MouseDelta();
 
-                MyObject.rotation = spineRotation;
-                MyObject.Rotate(Vector3.right * mouseDelta.x / 4f);
-            }
+        if (CurrentDragType is DragType.None)
+        {
+            if (isHead)
+                meido.HeadToCam = false;
+
+            MyObject.rotation = spineRotation;
+            MyObject.Rotate(camera.transform.forward, -mouseDelta.x / 4.5f, Space.World);
+            MyObject.Rotate(camera.transform.right, mouseDelta.y / 3f, Space.World);
+        }
+
+        if (CurrentDragType is DragType.RotLocalY)
+        {
+            if (isHead)
+                meido.HeadToCam = false;
+
+            MyObject.rotation = spineRotation;
+            MyObject.Rotate(Vector3.right * mouseDelta.x / 4f);
+        }
+
+        if (CurrentDragType is DragType.MoveY)
+        {
+            var cursorPosition = CursorPosition();
 
-            if (CurrentDragType == DragType.MoveY)
-            {
-                Vector3 cursorPosition = CursorPosition();
-                MyObject.position = new Vector3(MyObject.position.x, cursorPosition.y, MyObject.position.z);
-            }
+            MyObject.position = new(MyObject.position.x, cursorPosition.y, MyObject.position.z);
         }
     }
 }

+ 63 - 56
src/MeidoPhotoStudio.Plugin/Meido/IK/DragPointTorso.cs

@@ -1,77 +1,84 @@
 using UnityEngine;
 
-namespace MeidoPhotoStudio.Plugin
+using Input = MeidoPhotoStudio.Plugin.InputManager;
+
+namespace MeidoPhotoStudio.Plugin;
+
+public class DragPointTorso : DragPointMeido
 {
-    using Input = InputManager;
-    public class DragPointTorso : DragPointMeido
+    // TODO: Rename these to something more descriptive
+    private static readonly float[] Blah = new[] { 0.03f, 0.1f, 0.09f, 0.07f };
+    private static readonly float[] Something = new[] { 0.08f, 0.15f };
+
+    private readonly Quaternion[] spineRotation = new Quaternion[4];
+    private readonly Transform[] spine = new Transform[4];
+
+    public override void Set(Transform myObject)
     {
-        private static readonly float[] blah = new[] { 0.03f, 0.1f, 0.09f, 0.07f };
-        private static readonly float[] something = new[] { 0.08f, 0.15f };
-        private readonly Quaternion[] spineRotation = new Quaternion[4];
-        private readonly Transform[] spine = new Transform[4];
+        base.Set(myObject);
 
-        public override void Set(Transform myObject)
-        {
-            base.Set(myObject);
-            Transform spine = myObject;
-            for (int i = 0; i < this.spine.Length; i++)
-            {
-                this.spine[i] = spine;
-                spine = spine.parent;
-            }
-        }
+        var spine = myObject;
 
-        protected override void ApplyDragType()
+        for (var i = 0; i < this.spine.Length; i++)
         {
-            if (CurrentDragType == DragType.Ignore) ApplyProperties();
-            else if (IsBone) ApplyProperties(false, false, false);
-            else ApplyProperties(CurrentDragType != DragType.None, false, false);
-        }
+            this.spine[i] = spine;
 
-        protected override void UpdateDragType()
-        {
-            CurrentDragType = Input.Alt && !Input.Control
-                ? Input.Shift ? DragType.RotLocalY : DragType.RotLocalXZ
-                : OtherDragType() ? DragType.Ignore : DragType.None;
+            spine = spine.parent;
         }
+    }
 
-        protected override void OnMouseDown()
-        {
-            base.OnMouseDown();
-            for (int i = 0; i < spine.Length; i++)
-            {
-                spineRotation[i] = spine[i].localRotation;
-            }
-        }
+    protected override void ApplyDragType()
+    {
+        if (CurrentDragType is DragType.Ignore)
+            ApplyProperties();
+        else if (IsBone)
+            ApplyProperties(false, false, false);
+        else
+            ApplyProperties(CurrentDragType is not DragType.None, false, false);
+    }
 
-        protected override void Drag()
-        {
-            if (CurrentDragType == DragType.None) return;
+    protected override void UpdateDragType() =>
 
-            if (isPlaying) meido.Stop = true;
+        // TODO: Rethink this formatting
+        CurrentDragType = Input.Alt && !Input.Control
+            ? Input.Shift
+                ? DragType.RotLocalY
+                : DragType.RotLocalXZ
+            : OtherDragType()
+                ? DragType.Ignore
+                : DragType.None;
 
-            Vector3 mouseDelta = MouseDelta();
+    protected override void OnMouseDown()
+    {
+        base.OnMouseDown();
 
-            if (CurrentDragType == DragType.RotLocalXZ)
+        for (var i = 0; i < spine.Length; i++)
+            spineRotation[i] = spine[i].localRotation;
+    }
+
+    protected override void Drag()
+    {
+        if (CurrentDragType is DragType.None)
+            return;
+
+        if (isPlaying)
+            meido.Stop = true;
+
+        var mouseDelta = MouseDelta();
+
+        if (CurrentDragType is DragType.RotLocalXZ)
+            for (var i = 0; i < spine.Length; i++)
             {
-                for (int i = 0; i < spine.Length; i++)
-                {
-                    spine[i].localRotation = spineRotation[i];
-                    spine[i].Rotate(
-                        camera.transform.forward, -mouseDelta.x / 1.5f * blah[i], Space.World
-                    );
-                    spine[i].Rotate(camera.transform.right, mouseDelta.y * blah[i], Space.World);
-                }
+                spine[i].localRotation = spineRotation[i];
+                spine[i].Rotate(camera.transform.forward, -mouseDelta.x / 1.5f * Blah[i], Space.World);
+                spine[i].Rotate(camera.transform.right, mouseDelta.y * Blah[i], Space.World);
             }
 
-            if (CurrentDragType == DragType.RotLocalY)
+        if (CurrentDragType is DragType.RotLocalY)
+            for (var i = 0; i < spine.Length; i++)
             {
-                for (int i = 0; i < spine.Length; i++)
-                {
-                    spine[i].localRotation = spineRotation[i];
-                    spine[i].Rotate(Vector3.right * (mouseDelta.x / 1.5f * something[i / 2]));
-                }
+                spine[i].localRotation = spineRotation[i];
+                spine[i].Rotate(Vector3.right * (mouseDelta.x / 1.5f * Something[i / 2]));
             }
-        }
     }
 }

+ 28 - 26
src/MeidoPhotoStudio.Plugin/Meido/IK/IK Chain/DragPointChain.cs

@@ -1,39 +1,41 @@
 using UnityEngine;
 
-namespace MeidoPhotoStudio.Plugin
+namespace MeidoPhotoStudio.Plugin;
+
+public abstract class DragPointChain : DragPointMeido
 {
-    public abstract class DragPointChain : DragPointMeido
+    protected readonly TBody.IKCMO IK = new();
+    protected readonly Quaternion[] jointRotation = new Quaternion[3];
+
+    protected IKCtrlData ikCtrlData;
+    protected Transform[] ikChain;
+
+    public override void Set(Transform myObject)
     {
-        protected readonly TBody.IKCMO IK = new TBody.IKCMO();
-        protected readonly Quaternion[] jointRotation = new Quaternion[3];
-        protected IKCtrlData ikCtrlData;
-        protected Transform[] ikChain;
+        base.Set(myObject);
 
-        public override void Set(Transform myObject)
+        ikChain = new Transform[]
         {
-            base.Set(myObject);
+            myObject.parent,
+            myObject.parent,
+            myObject,
+        };
 
-            ikChain = new Transform[] {
-                myObject.parent,
-                myObject.parent,
-                myObject
-            };
-
-            ikCtrlData = IkCtrlData;
-        }
+        ikCtrlData = IkCtrlData;
+    }
 
-        protected void InitializeRotation()
-        {
-            for (int i = 0; i < jointRotation.Length; i++) jointRotation[i] = ikChain[i].localRotation;
-        }
+    protected override void OnMouseDown()
+    {
+        base.OnMouseDown();
 
-        protected override void OnMouseDown()
-        {
-            base.OnMouseDown();
+        InitializeRotation();
 
-            InitializeRotation();
+        InitializeIK(IK, ikChain[JointUpper], ikChain[JointMiddle], ikChain[JointLower]);
+    }
 
-            InitializeIK(IK, ikChain[jointUpper], ikChain[jointMiddle], ikChain[jointLower]);
-        }
+    protected void InitializeRotation()
+    {
+        for (var i = 0; i < jointRotation.Length; i++)
+            jointRotation[i] = ikChain[i].localRotation;
     }
 }

+ 122 - 84
src/MeidoPhotoStudio.Plugin/Meido/IK/IK Chain/DragPointLimb.cs

@@ -1,111 +1,149 @@
 using UnityEngine;
 
-namespace MeidoPhotoStudio.Plugin
+using Input = MeidoPhotoStudio.Plugin.InputManager;
+
+namespace MeidoPhotoStudio.Plugin;
+
+public class DragPointLimb : DragPointChain
 {
-    using Input = InputManager;
-    public class DragPointLimb : DragPointChain
+    private int foot = 1;
+    private bool isLower;
+    private bool isMiddle;
+    private bool isUpper;
+
+    public override bool IsBone
     {
-        private int foot = 1;
-        private bool isLower;
-        private bool isMiddle;
-        private bool isUpper;
-        public override bool IsBone
+        set
         {
-            set
-            {
-                base.IsBone = value;
-                BaseScale = isBone ? boneScale : OriginalScale;
-            }
+            base.IsBone = value;
+
+            BaseScale = isBone ? BoneScale : OriginalScale;
         }
+    }
 
-        public override void Set(Transform myObject)
-        {
-            base.Set(myObject);
+    public override void Set(Transform myObject)
+    {
+        base.Set(myObject);
 
-            string name = myObject.name;
+        var name = myObject.name;
 
-            foot = name.EndsWith("Foot") ? -1 : 1;
-            isLower = name.EndsWith("Hand") || foot == -1;
-            isMiddle = name.EndsWith("Calf") || name.EndsWith("Forearm");
-            isUpper = !isMiddle && !isLower;
+        foot = name.EndsWith("Foot") ? -1 : 1;
+        isLower = name.EndsWith("Hand") || foot is -1;
+        isMiddle = name.EndsWith("Calf") || name.EndsWith("Forearm");
+        isUpper = !isMiddle && !isLower;
 
-            if (isLower) ikChain[0] = ikChain[0].parent;
-        }
+        if (isLower)
+            ikChain[0] = ikChain[0].parent;
+    }
+
+    protected override void ApplyDragType()
+    {
+        var current = CurrentDragType;
+        var isBone = IsBone;
 
-        protected override void ApplyDragType()
+        if (CurrentDragType is DragType.Ignore)
         {
-            DragType current = CurrentDragType;
-            bool isBone = IsBone;
-            if (CurrentDragType == DragType.Ignore) ApplyProperties();
-            else if (current == DragType.RotLocalXZ)
-            {
-                if (isLower) ApplyProperties(!isBone, false, isBone);
-                else ApplyProperties();
-            }
-            else if (current == DragType.RotLocalY)
-            {
-                if (isLower || isMiddle) ApplyProperties(!isBone, false, false);
-                else if (isUpper) ApplyProperties(false, false, isBone);
-                else ApplyProperties();
-            }
-            else if (current == DragType.RotY)
-            {
-                if (isMiddle) ApplyProperties(false, false, isBone);
-                else ApplyProperties();
-            }
-            else if (current == DragType.MoveXZ)
-            {
-                if (isLower) ApplyProperties(true, isBone, false);
-                else ApplyProperties();
-            }
-            else ApplyProperties(true, isBone, false);
+            ApplyProperties();
         }
-
-        protected override void UpdateDragType()
+        else if (current is DragType.RotLocalXZ)
+        {
+            if (isLower)
+                ApplyProperties(!isBone, false, isBone);
+            else
+                ApplyProperties();
+        }
+        else if (current is DragType.RotLocalY)
+        {
+            if (isLower || isMiddle)
+                ApplyProperties(!isBone, false, false);
+            else if (isUpper)
+                ApplyProperties(false, false, isBone);
+            else
+                ApplyProperties();
+        }
+        else if (current is DragType.RotY)
+        {
+            if (isMiddle)
+                ApplyProperties(false, false, isBone);
+            else
+                ApplyProperties();
+        }
+        else if (current is DragType.MoveXZ)
+        {
+            if (isLower)
+                ApplyProperties(true, isBone, false);
+            else
+                ApplyProperties();
+        }
+        else
         {
-            bool control = Input.Control;
-            bool alt = Input.Alt;
-            // Check for DragMove so that hand dragpoint is not in the way
-            if (OtherDragType()) CurrentDragType = DragType.Ignore;
-            else if (control && !Input.GetKey(MpsKey.DragMove))
-            {
-                if (alt) CurrentDragType = DragType.RotY;
-                else CurrentDragType = DragType.MoveXZ;
-            }
-            else if (alt) CurrentDragType = Input.Shift ? DragType.RotLocalY : DragType.RotLocalXZ;
-            else CurrentDragType = Input.Shift ? DragType.Ignore : DragType.None;
+            ApplyProperties(true, isBone, false);
         }
+    }
+
+    protected override void UpdateDragType()
+    {
+        var control = Input.Control;
+        var alt = Input.Alt;
 
-        protected override void Drag()
+        // Check for DragMove so that hand dragpoint is not in the way
+        if (OtherDragType())
+        {
+            CurrentDragType = DragType.Ignore;
+        }
+        else if (control && !Input.GetKey(MpsKey.DragMove))
         {
-            if (isPlaying) meido.Stop = true;
+            if (alt)
+                CurrentDragType = DragType.RotY;
+            else
+                CurrentDragType = DragType.MoveXZ;
+        }
+        else if (alt)
+        {
+            // TODO: Rethink this formatting
+            CurrentDragType = Input.Shift
+                ? DragType.RotLocalY
+                : DragType.RotLocalXZ;
+        }
+        else
+        {
+            CurrentDragType = Input.Shift
+                ? DragType.Ignore
+                : DragType.None;
+        }
+    }
 
-            bool altRotation = CurrentDragType == DragType.MoveXZ || CurrentDragType == DragType.RotY;
+    protected override void Drag()
+    {
+        if (isPlaying)
+            meido.Stop = true;
 
-            if (CurrentDragType == DragType.None || altRotation)
-            {
-                int upperJoint = altRotation ? jointMiddle : jointUpper;
+        var altRotation = CurrentDragType is DragType.MoveXZ or DragType.RotY;
 
-                Porc(IK, ikCtrlData, ikChain[upperJoint], ikChain[jointMiddle], ikChain[jointLower]);
+        if (CurrentDragType is DragType.None || altRotation)
+        {
+            var upperJoint = altRotation ? JointMiddle : JointUpper;
 
-                InitializeRotation();
-            }
+            Porc(IK, ikCtrlData, ikChain[upperJoint], ikChain[JointMiddle], ikChain[JointLower]);
 
-            Vector3 mouseDelta = MouseDelta();
+            InitializeRotation();
+        }
 
-            if (CurrentDragType == DragType.RotLocalY)
-            {
-                int joint = isMiddle ? jointUpper : jointLower;
-                ikChain[joint].localRotation = jointRotation[joint];
-                ikChain[joint].Rotate(Vector3.right * (-mouseDelta.x / 1.5f));
-            }
+        var mouseDelta = MouseDelta();
 
-            if (CurrentDragType == DragType.RotLocalXZ)
-            {
-                ikChain[jointLower].localRotation = jointRotation[jointLower];
-                ikChain[jointLower].Rotate(Vector3.up * (foot * mouseDelta.x / 1.5f));
-                ikChain[jointLower].Rotate(Vector3.forward * (foot * mouseDelta.y / 1.5f));
-            }
+        if (CurrentDragType is DragType.RotLocalY)
+        {
+            var joint = isMiddle ? JointUpper : JointLower;
+
+            ikChain[joint].localRotation = jointRotation[joint];
+            ikChain[joint].Rotate(Vector3.right * (-mouseDelta.x / 1.5f));
+        }
+
+        if (CurrentDragType is DragType.RotLocalXZ)
+        {
+            ikChain[JointLower].localRotation = jointRotation[JointLower];
+            ikChain[JointLower].Rotate(Vector3.up * (foot * mouseDelta.x / 1.5f));
+            ikChain[JointLower].Rotate(Vector3.forward * (foot * mouseDelta.y / 1.5f));
         }
     }
 }

+ 52 - 40
src/MeidoPhotoStudio.Plugin/Meido/IK/IK Chain/DragPointMune.cs

@@ -1,57 +1,69 @@
 using UnityEngine;
 
-namespace MeidoPhotoStudio.Plugin
+using Input = MeidoPhotoStudio.Plugin.InputManager;
+
+namespace MeidoPhotoStudio.Plugin;
+
+public class DragPointMune : DragPointChain
 {
-    using Input = InputManager;
-    public class DragPointMune : DragPointChain
+    private bool isMuneL;
+    private int inv = 1;
+
+    public override void Set(Transform myObject)
     {
-        private bool isMuneL;
-        private int inv = 1;
+        base.Set(myObject);
 
-        public override void Set(Transform myObject)
-        {
-            base.Set(myObject);
-            isMuneL = myObject.name[5] == 'L'; // Mune_L_Sub
-            if (isMuneL) inv *= -1;
-        }
+        isMuneL = myObject.name[5] is 'L'; // Mune_L_Sub
 
-        protected override void ApplyDragType() => ApplyProperties(CurrentDragType != DragType.None, false, false);
+        if (isMuneL)
+            inv *= -1;
+    }
 
-        protected override void OnMouseDown()
-        {
-            base.OnMouseDown();
+    protected override void ApplyDragType() =>
+        ApplyProperties(CurrentDragType is not DragType.None, false, false);
 
-            meido.SetMune(false, isMuneL);
-        }
+    protected override void OnMouseDown()
+    {
+        base.OnMouseDown();
 
-        protected override void OnDoubleClick()
-        {
-            if (CurrentDragType != DragType.None) meido.SetMune(true, isMuneL);
-        }
+        meido.SetMune(false, isMuneL);
+    }
+
+    protected override void OnDoubleClick()
+    {
+        if (CurrentDragType is not DragType.None)
+            meido.SetMune(true, isMuneL);
+    }
+
+    protected override void UpdateDragType() =>
 
-        protected override void UpdateDragType()
+        // TODO: Rethink this formatting
+        CurrentDragType = Input.Control && Input.Alt
+            ? Input.Shift
+            ? DragType.RotLocalY
+            : DragType.RotLocalXZ
+            : DragType.None;
+
+    protected override void Drag()
+    {
+        if (isPlaying)
+            meido.Stop = true;
+
+        if (CurrentDragType is DragType.RotLocalXZ)
         {
-            if (Input.Control && Input.Alt) CurrentDragType = Input.Shift ? DragType.RotLocalY : DragType.RotLocalXZ;
-            else CurrentDragType = DragType.None;
+            Porc(IK, ikCtrlData, ikChain[JointUpper], ikChain[JointMiddle], ikChain[JointLower]);
+            InitializeRotation();
         }
 
-        protected override void Drag()
+        if (CurrentDragType is DragType.RotLocalY)
         {
-            if (isPlaying) meido.Stop = true;
-
-            if (CurrentDragType == DragType.RotLocalXZ)
-            {
-                Porc(IK, ikCtrlData, ikChain[jointUpper], ikChain[jointMiddle], ikChain[jointLower]);
-                InitializeRotation();
-            }
-
-            if (CurrentDragType == DragType.RotLocalY)
-            {
-                Vector3 mouseDelta = MouseDelta();
-                ikChain[jointLower].localRotation = jointRotation[jointLower];
-                ikChain[jointLower].Rotate(Vector3.up * (-mouseDelta.x / 1.5f) * inv);
-                ikChain[jointLower].Rotate(Vector3.forward * (mouseDelta.y / 1.5f) * inv);
-            }
+            var mouseDelta = MouseDelta();
+
+            ikChain[JointLower].localRotation = jointRotation[JointLower];
+
+            // TODO: Reorder operands for better performance
+            ikChain[JointLower].Rotate(Vector3.up * (-mouseDelta.x / 1.5f) * inv);
+            ikChain[JointLower].Rotate(Vector3.forward * (mouseDelta.y / 1.5f) * inv);
         }
     }
 }

File diff suppressed because it is too large
+ 732 - 584
src/MeidoPhotoStudio.Plugin/Meido/Meido.cs


File diff suppressed because it is too large
+ 755 - 684
src/MeidoPhotoStudio.Plugin/Meido/MeidoDragPointManager.cs


+ 23 - 0
src/MeidoPhotoStudio.Plugin/Meido/PoseInfo.cs

@@ -0,0 +1,23 @@
+namespace MeidoPhotoStudio.Plugin;
+
+public readonly struct PoseInfo
+{
+    private static readonly PoseInfo DefaultPoseValue =
+        new(Constants.PoseGroupList[0], Constants.PoseDict[Constants.PoseGroupList[0]][0]);
+
+    public PoseInfo(string poseGroup, string pose, bool customPose = false)
+    {
+        PoseGroup = poseGroup;
+        Pose = pose;
+        CustomPose = customPose;
+    }
+
+    public static ref readonly PoseInfo DefaultPose =>
+        ref DefaultPoseValue;
+
+    public string PoseGroup { get; }
+
+    public string Pose { get; }
+
+    public bool CustomPose { get; }
+}

+ 453 - 401
src/MeidoPhotoStudio.Plugin/MeidoPhotoStudio.cs

@@ -1,504 +1,556 @@
 using System;
-using System.IO;
 using System.Collections;
-using System.Collections.Generic;
+using System.IO;
 using System.Text;
+
+using BepInEx;
 using UnityEngine;
 using UnityEngine.SceneManagement;
-using BepInEx;
 
-namespace MeidoPhotoStudio.Plugin
+using Input = MeidoPhotoStudio.Plugin.InputManager;
+
+namespace MeidoPhotoStudio.Plugin;
+
+[BepInPlugin(PluginGuid, PluginName, PluginVersion)]
+[BepInDependency("org.bepinex.plugins.unityinjectorloader", BepInDependency.DependencyFlags.SoftDependency)]
+public class MeidoPhotoStudio : BaseUnityPlugin
 {
-    using Input = InputManager;
-    [BepInPlugin(pluginGuid, pluginName, pluginVersion)]
-    [BepInDependency("org.bepinex.plugins.unityinjectorloader", BepInDependency.DependencyFlags.SoftDependency)]
-    public class MeidoPhotoStudio : BaseUnityPlugin
+    public const string PluginName = "MeidoPhotoStudio";
+    public const string PluginVersion = "1.0.0";
+    public const string PluginSubVersion = "beta.4.1";
+    public const short SceneVersion = 2;
+    public const int KankyoMagic = -765;
+
+    public static readonly byte[] SceneHeader = Encoding.UTF8.GetBytes("MPSSCENE");
+    public static readonly string PluginString = $"{PluginName} {PluginVersion}";
+
+    private const string PluginGuid = "com.habeebweeb.com3d2.meidophotostudio";
+
+    private static Constants.Scene currentScene;
+
+    private HarmonyLib.Harmony harmony;
+    private WindowManager windowManager;
+    private SceneManager sceneManager;
+    private MeidoManager meidoManager;
+    private EnvironmentManager environmentManager;
+    private MessageWindowManager messageWindowManager;
+    private LightManager lightManager;
+    private PropManager propManager;
+    private EffectManager effectManager;
+    private CameraManager cameraManager;
+    private bool initialized;
+    private bool active;
+    private bool uiActive;
+
+    static MeidoPhotoStudio()
     {
-        public static readonly byte[] SceneHeader = Encoding.UTF8.GetBytes("MPSSCENE");
-        private static event EventHandler<ScreenshotEventArgs> ScreenshotEvent;
-        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.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;
-        private EnvironmentManager environmentManager;
-        private MessageWindowManager messageWindowManager;
-        private LightManager lightManager;
-        private PropManager propManager;
-        private EffectManager effectManager;
-        private CameraManager cameraManager;
-        private static Constants.Scene currentScene;
-        private bool initialized;
-        private bool active;
-        private bool uiActive;
-
-        static MeidoPhotoStudio()
-        {
-            Input.Register(MpsKey.Screenshot, KeyCode.S, "Take screenshot");
-            Input.Register(MpsKey.Activate, KeyCode.F6, "Activate/deactivate MeidoPhotoStudio");
+        Input.Register(MpsKey.Screenshot, KeyCode.S, "Take screenshot");
+        Input.Register(MpsKey.Activate, KeyCode.F6, "Activate/deactivate MeidoPhotoStudio");
 
-            if (!string.IsNullOrEmpty(pluginSubVersion)) pluginString += $"-{pluginSubVersion}";
-        }
+        if (!string.IsNullOrEmpty(PluginSubVersion))
+            PluginString += $"-{PluginSubVersion}";
+    }
 
-        private void Awake()
-        {
-            harmony = HarmonyLib.Harmony.CreateAndPatchAll(typeof(AllProcPropSeqStartPatcher));
-            harmony.PatchAll(typeof(BgMgrPatcher));
-            harmony.PatchAll(typeof(MeidoManager));
-            ScreenshotEvent += OnScreenshotEvent;
-            DontDestroyOnLoad(this);
-            UnityEngine.SceneManagement.SceneManager.sceneLoaded += OnSceneLoaded;
-            UnityEngine.SceneManagement.SceneManager.activeSceneChanged += OnSceneChanged;
-        }
+    public static event EventHandler<ScreenshotEventArgs> NotifyRawScreenshot;
 
-        private void Start()
-        {
-            Constants.Initialize();
-            Translation.Initialize(Translation.CurrentLanguage);
-        }
+    private static event EventHandler<ScreenshotEventArgs> ScreenshotEvent;
 
-        private void OnSceneLoaded(Scene scene, LoadSceneMode sceneMode)
-        {
-            currentScene = (Constants.Scene)scene.buildIndex;
-        }
+    public static bool EditMode =>
+        currentScene is Constants.Scene.Edit;
 
-        private void OnSceneChanged(Scene current, Scene next)
-        {
-            if (active) Deactivate(true);
-            CameraUtility.MainCamera.ResetCalcNearClip();
-        }
+    public static void TakeScreenshot(ScreenshotEventArgs args) =>
+        ScreenshotEvent?.Invoke(null, args);
+
+    public static void TakeScreenshot(string path = "", int superSize = -1, bool hideMaids = false) =>
+        TakeScreenshot(new ScreenshotEventArgs() { Path = path, SuperSize = superSize, HideMaids = hideMaids });
 
-        public byte[] SaveScene(bool environment = false)
+    public byte[] SaveScene(bool environment = false)
+    {
+        if (meidoManager.Busy)
+            return null;
+
+        try
         {
-            if (meidoManager.Busy) return null;
+            using var memoryStream = new MemoryStream();
+            using var headerWriter = new BinaryWriter(memoryStream, Encoding.UTF8);
+
+            headerWriter.Write(SceneHeader);
 
-            try
+            new SceneMetadata
             {
-                using var memoryStream = new MemoryStream();
-                using var headerWriter = new BinaryWriter(memoryStream, Encoding.UTF8);
+                Version = SceneVersion,
+                Environment = environment,
+                MaidCount = environment ? KankyoMagic : meidoManager.ActiveMeidoList.Count,
+                MMConverted = false,
+            }.WriteMetadata(headerWriter);
 
-                headerWriter.Write(SceneHeader);
+            using var compressionStream = memoryStream.GetCompressionStream();
+            using var dataWriter = new BinaryWriter(compressionStream, Encoding.UTF8);
 
-                new SceneMetadata
-                {
-                    Version = sceneVersion,
-                    Environment = environment,
-                    MaidCount = environment ? kankyoMagic : meidoManager.ActiveMeidoList.Count
-                }.WriteMetadata(headerWriter);
+            if (!environment)
+            {
+                Serialization.Get<MeidoManager>().Serialize(meidoManager, dataWriter);
+                Serialization.Get<MessageWindowManager>().Serialize(messageWindowManager, dataWriter);
+                Serialization.Get<CameraManager>().Serialize(cameraManager, dataWriter);
+            }
 
-                using var compressionStream = memoryStream.GetCompressionStream();
-                using var dataWriter = new BinaryWriter(compressionStream, Encoding.UTF8);
+            Serialization.Get<LightManager>().Serialize(lightManager, dataWriter);
+            Serialization.Get<EffectManager>().Serialize(effectManager, dataWriter);
+            Serialization.Get<EnvironmentManager>().Serialize(environmentManager, dataWriter);
+            Serialization.Get<PropManager>().Serialize(propManager, dataWriter);
 
-                if (!environment)
-                {
-                    Serialization.Get<MeidoManager>().Serialize(meidoManager, dataWriter);
-                    Serialization.Get<MessageWindowManager>().Serialize(messageWindowManager, dataWriter);
-                    Serialization.Get<CameraManager>().Serialize(cameraManager, dataWriter);
-                }
+            dataWriter.Write("END");
 
-                Serialization.Get<LightManager>().Serialize(lightManager, dataWriter);
-                Serialization.Get<EffectManager>().Serialize(effectManager, dataWriter);
-                Serialization.Get<EnvironmentManager>().Serialize(environmentManager, dataWriter);
-                Serialization.Get<PropManager>().Serialize(propManager, dataWriter);
+            compressionStream.Close();
 
-                dataWriter.Write("END");
+            var data = memoryStream.ToArray();
 
-                compressionStream.Close();
+            return data;
+        }
+        catch (Exception e)
+        {
+            Utility.LogError($"Failed to save scene because {e.Message}\n{e.StackTrace}");
 
-                var data = memoryStream.ToArray();
-                return data;
-            }
-            catch (Exception e)
-            {
-                Utility.LogError($"Failed to save scene because {e.Message}\n{e.StackTrace}");
-                return null;
-            }
+            return null;
         }
+    }
 
-        public void LoadScene(byte[] buffer)
+    public void LoadScene(byte[] buffer)
+    {
+        if (meidoManager.Busy)
         {
-            if (meidoManager.Busy)
-            {
-                Utility.LogMessage("Could not apply scene. Meidos are Busy");
-                return;
-            }
+            Utility.LogMessage("Could not apply scene. Meidos are Busy");
 
-            using var memoryStream = new MemoryStream(buffer);
-            using var headerReader = new BinaryReader(memoryStream, Encoding.UTF8);
-            
-            if (!Utility.BytesEqual(headerReader.ReadBytes(SceneHeader.Length), SceneHeader))
-            {
-                Utility.LogError("Not a MPS scene!");
-                return;
-            }
+            return;
+        }
 
-            var metadata = SceneMetadata.ReadMetadata(headerReader);
+        using var memoryStream = new MemoryStream(buffer);
+        using var headerReader = new BinaryReader(memoryStream, Encoding.UTF8);
 
-            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);
+        if (!Utility.BytesEqual(headerReader.ReadBytes(SceneHeader.Length), SceneHeader))
+        {
+            Utility.LogError("Not a MPS scene!");
 
-            var header = string.Empty;
-            var previousHeader = string.Empty;
-            try
-            {
-                while ((header = dataReader.ReadString()) != "END")
-                {
-                    switch (header)
-                    {
-                        case MeidoManager.header: 
-                            Serialization.Get<MeidoManager>().Deserialize(meidoManager, dataReader, metadata);
-                            break;
-                        case MessageWindowManager.header:
-                            Serialization.Get<MessageWindowManager>().Deserialize(messageWindowManager, dataReader, metadata);
-                            break;
-                        case CameraManager.header:
-                            Serialization.Get<CameraManager>().Deserialize(cameraManager, dataReader, metadata);
-                            break;
-                        case LightManager.header:
-                            Serialization.Get<LightManager>().Deserialize(lightManager, dataReader, metadata);
-                            break;
-                        case EffectManager.header:
-                            Serialization.Get<EffectManager>().Deserialize(effectManager, dataReader, metadata);
-                            break;
-                        case EnvironmentManager.header:
-                            Serialization.Get<EnvironmentManager>().Deserialize(environmentManager, dataReader, metadata);
-                            break;
-                        case PropManager.header:
-                            Serialization.Get<PropManager>().Deserialize(propManager, dataReader, metadata);
-                            break;
-                        default: throw new Exception($"Unknown header '{header}'");
-                    }
-
-                    previousHeader = header;
-                }
-            }
-            catch (Exception e)
-            {
-                Utility.LogError(
-                    $"Failed to deserialize scene because {e.Message}"
-                    + $"\nCurrent header: '{header}'. Last header: '{previousHeader}'"
-                );
-                Utility.LogError(e.StackTrace);
-            }
+            return;
         }
 
-        public static void TakeScreenshot(ScreenshotEventArgs args) => ScreenshotEvent?.Invoke(null, args);
+        var metadata = SceneMetadata.ReadMetadata(headerReader);
 
-        public static void TakeScreenshot(string path = "", int superSize = -1, bool hideMaids = false)
+        if (metadata.Version > SceneVersion)
         {
-            TakeScreenshot(new ScreenshotEventArgs() { Path = path, SuperSize = superSize, HideMaids = hideMaids });
-        }
+            Utility.LogWarning("Cannot load scene. Scene is too new.");
+            Utility.LogWarning($"Your version: {SceneVersion}, Scene version: {metadata.Version}");
 
-        private void OnScreenshotEvent(object sender, ScreenshotEventArgs args)
-        {
-            StartCoroutine(Screenshot(args));
+            return;
         }
 
-        private void Update()
+        using var uncompressed = memoryStream.Decompress();
+        using var dataReader = new BinaryReader(uncompressed, Encoding.UTF8);
+
+        var header = string.Empty;
+        var previousHeader = string.Empty;
+
+        try
         {
-            if (currentScene == Constants.Scene.Daily || currentScene == Constants.Scene.Edit)
+            while ((header = dataReader.ReadString()) is not "END")
             {
-                if (Input.GetKeyDown(MpsKey.Activate))
+                switch (header)
                 {
-                    if (active) Deactivate();
-                    else Activate();
-                }
+                    case MeidoManager.Header:
+                        Serialization.Get<MeidoManager>().Deserialize(meidoManager, dataReader, metadata);
 
-                if (active)
-                {
-                    if (!Input.Control && !Input.GetKey(MpsKey.CameraLayer) && Input.GetKeyDown(MpsKey.Screenshot))
-                    {
-                        TakeScreenshot();
-                    }
-
-                    meidoManager.Update();
-                    cameraManager.Update();
-                    windowManager.Update();
-                    effectManager.Update();
-                    sceneManager.Update();
+                        break;
+                    case MessageWindowManager.Header:
+                        Serialization.Get<MessageWindowManager>()
+                            .Deserialize(messageWindowManager, dataReader, metadata);
+
+                        break;
+                    case CameraManager.Header:
+                        Serialization.Get<CameraManager>().Deserialize(cameraManager, dataReader, metadata);
+
+                        break;
+                    case LightManager.Header:
+                        Serialization.Get<LightManager>().Deserialize(lightManager, dataReader, metadata);
+
+                        break;
+                    case EffectManager.Header:
+                        Serialization.Get<EffectManager>().Deserialize(effectManager, dataReader, metadata);
+
+                        break;
+                    case EnvironmentManager.Header:
+                        Serialization.Get<EnvironmentManager>().Deserialize(environmentManager, dataReader, metadata);
+
+                        break;
+                    case PropManager.Header:
+                        Serialization.Get<PropManager>().Deserialize(propManager, dataReader, metadata);
+
+                        break;
+                    default:
+                        throw new Exception($"Unknown header '{header}'");
                 }
+
+                previousHeader = header;
             }
         }
-
-        private IEnumerator Screenshot(ScreenshotEventArgs args)
+        catch (Exception e)
         {
-            // Hide UI and dragpoints
-            GameObject gameMain = GameMain.Instance.gameObject;
-            GameObject editUI = UTY.GetChildObject(GameObject.Find("UI Root"), "Camera");
-            GameObject fpsViewer = UTY.GetChildObject(gameMain, "SystemUI Root/FpsCounter");
-            GameObject sysDialog = UTY.GetChildObject(gameMain, "SystemUI Root/SystemDialog");
-            GameObject sysShortcut = UTY.GetChildObject(gameMain, "SystemUI Root/SystemShortcut");
+            Utility.LogError(
+                $"Failed to deserialize scene because {e.Message}\nCurrent header: '{header}'. " +
+                $"Last header: '{previousHeader}'");
 
-            // CameraUtility can hide the edit UI so keep its state for later
-            bool editUIWasActive = editUI.activeSelf;
+            Utility.LogError(e.StackTrace);
+        }
+    }
 
-            uiActive = false;
-            editUI.SetActive(false);
-            fpsViewer.SetActive(false);
-            sysDialog.SetActive(false);
-            sysShortcut.SetActive(false);
-
-            // Hide maid dragpoints and maids
-            List<Meido> activeMeidoList = meidoManager.ActiveMeidoList;
-            bool[] isIK = new bool[activeMeidoList.Count];
-            bool[] isVisible = new bool[activeMeidoList.Count];
-            for (int i = 0; i < activeMeidoList.Count; i++)
-            {
-                Meido meido = activeMeidoList[i];
-                isIK[i] = meido.IK;
-                if (meido.IK) meido.IK = false;
+    private void OnSceneLoaded(Scene scene, LoadSceneMode sceneMode) =>
+        currentScene = (Constants.Scene)scene.buildIndex;
 
-                // Hide the maid if needed
-                if (args.HideMaids)
-                {
-                    isVisible[i] = meido.Maid.Visible;
-                    meido.Maid.Visible = false;
-                }
-            }
+    private void OnSceneChanged(Scene current, Scene next)
+    {
+        if (active)
+            Deactivate(true);
 
-            // Hide other drag points
-            bool[] isCubeActive = {
-                MeidoDragPointManager.CubeActive,
-                PropManager.CubeActive,
-                LightManager.CubeActive,
-                EnvironmentManager.CubeActive
-            };
+        CameraUtility.MainCamera.ResetCalcNearClip();
+    }
 
-            MeidoDragPointManager.CubeActive = false;
-            PropManager.CubeActive = false;
-            LightManager.CubeActive = false;
-            EnvironmentManager.CubeActive = false;
+    private void OnScreenshotEvent(object sender, ScreenshotEventArgs args) =>
+        StartCoroutine(Screenshot(args));
 
-            // hide gizmos
-            GizmoRender.UIVisible = false;
+    private void Awake()
+    {
+        harmony = HarmonyLib.Harmony.CreateAndPatchAll(typeof(AllProcPropSeqStartPatcher));
+        harmony.PatchAll(typeof(BgMgrPatcher));
+        harmony.PatchAll(typeof(MeidoManager));
 
-            yield return new WaitForEndOfFrame();
+        ScreenshotEvent += OnScreenshotEvent;
 
-            Texture2D rawScreenshot = null;
+        DontDestroyOnLoad(this);
 
-            if (args.InMemory)
-            {
-                // Take a screenshot directly to a Texture2D for immediate processing
-                RenderTexture renderTexture = new RenderTexture(Screen.width, Screen.height, 24);
-                RenderTexture.active = renderTexture;
-                CameraUtility.MainCamera.camera.targetTexture = renderTexture;
-                CameraUtility.MainCamera.camera.Render();
-
-                rawScreenshot = new Texture2D(Screen.width, Screen.height, TextureFormat.RGB24, false);
-                rawScreenshot.ReadPixels(new Rect(0, 0, renderTexture.width, renderTexture.height), 0, 0, false);
-                rawScreenshot.Apply();
-
-                CameraUtility.MainCamera.camera.targetTexture = null;
-                RenderTexture.active = null;
-                DestroyImmediate(renderTexture);
-            }
+        UnityEngine.SceneManagement.SceneManager.sceneLoaded += OnSceneLoaded;
+        UnityEngine.SceneManagement.SceneManager.activeSceneChanged += OnSceneChanged;
+    }
+
+    private void Start()
+    {
+        Constants.Initialize();
+        Translation.Initialize(Translation.CurrentLanguage);
+    }
+
+    private void Update()
+    {
+        if (currentScene is not Constants.Scene.Daily and not Constants.Scene.Edit)
+            return;
+
+        if (Input.GetKeyDown(MpsKey.Activate))
+        {
+            if (active)
+                Deactivate();
             else
-            {
-                // Take Screenshot
-                int[] defaultSuperSize = new[] { 1, 2, 4 };
-                int selectedSuperSize = args.SuperSize < 1
-                    ? defaultSuperSize[(int)GameMain.Instance.CMSystem.ScreenShotSuperSize]
-                    : args.SuperSize;
+                Activate();
+        }
 
-                string path = string.IsNullOrEmpty(args.Path)
-                    ? Utility.ScreenshotFilename()
-                    : args.Path;
+        if (!active)
+            return;
 
-                Application.CaptureScreenshot(path, selectedSuperSize);
-            }
-            GameMain.Instance.SoundMgr.PlaySe("se022.ogg", false);
+        if (!Input.Control && !Input.GetKey(MpsKey.CameraLayer) && Input.GetKeyDown(MpsKey.Screenshot))
+            TakeScreenshot();
 
-            yield return new WaitForEndOfFrame();
+        meidoManager.Update();
+        cameraManager.Update();
+        windowManager.Update();
+        effectManager.Update();
+        sceneManager.Update();
+    }
 
-            // Show UI and dragpoints
-            uiActive = true;
-            editUI.SetActive(editUIWasActive);
-            fpsViewer.SetActive(GameMain.Instance.CMSystem.ViewFps);
-            sysDialog.SetActive(true);
-            sysShortcut.SetActive(true);
+    private IEnumerator Screenshot(ScreenshotEventArgs args)
+    {
+        // Hide UI and dragpoints
+        var gameMain = GameMain.Instance.gameObject;
+        var editUI = UTY.GetChildObject(GameObject.Find("UI Root"), "Camera");
+        var fpsViewer = UTY.GetChildObject(gameMain, "SystemUI Root/FpsCounter");
+        var sysDialog = UTY.GetChildObject(gameMain, "SystemUI Root/SystemDialog");
+        var sysShortcut = UTY.GetChildObject(gameMain, "SystemUI Root/SystemShortcut");
+
+        // CameraUtility can hide the edit UI so keep its state for later
+        var editUIWasActive = editUI.activeSelf;
+
+        uiActive = false;
+        editUI.SetActive(false);
+        fpsViewer.SetActive(false);
+        sysDialog.SetActive(false);
+        sysShortcut.SetActive(false);
+
+        // Hide maid dragpoints and maids
+        var activeMeidoList = meidoManager.ActiveMeidoList;
+        var isIK = new bool[activeMeidoList.Count];
+        var isVisible = new bool[activeMeidoList.Count];
+
+        for (var i = 0; i < activeMeidoList.Count; i++)
+        {
+            var meido = activeMeidoList[i];
+
+            isIK[i] = meido.IK;
 
-            for (int i = 0; i < activeMeidoList.Count; i++)
+            if (meido.IK)
+                meido.IK = false;
+
+            // Hide the maid if needed
+            if (args.HideMaids)
             {
-                Meido meido = activeMeidoList[i];
-                if (isIK[i]) meido.IK = true;
-                if (args.HideMaids && isVisible[i]) meido.Maid.Visible = true;
+                isVisible[i] = meido.Maid.Visible;
+                meido.Maid.Visible = false;
             }
+        }
 
-            MeidoDragPointManager.CubeActive = isCubeActive[0];
-            PropManager.CubeActive = isCubeActive[1];
-            LightManager.CubeActive = isCubeActive[2];
-            EnvironmentManager.CubeActive = isCubeActive[3];
+        // Hide other drag points
+        var isCubeActive = new[]
+        {
+            MeidoDragPointManager.CubeActive,
+            PropManager.CubeActive,
+            LightManager.CubeActive,
+            EnvironmentManager.CubeActive,
+        };
 
-            GizmoRender.UIVisible = true;
+        MeidoDragPointManager.CubeActive = false;
+        PropManager.CubeActive = false;
+        LightManager.CubeActive = false;
+        EnvironmentManager.CubeActive = false;
 
-            if (args.InMemory && rawScreenshot)
-                NotifyRawScreenshot?.Invoke(null, new ScreenshotEventArgs() { Screenshot = rawScreenshot });
-        }
+        // hide gizmos
+        GizmoRender.UIVisible = false;
 
-        private void OnGUI()
-        {
-            if (uiActive)
-            {
-                windowManager.DrawWindows();
+        yield return new WaitForEndOfFrame();
 
-                if (DropdownHelper.Visible) DropdownHelper.HandleDropdown();
-                if (Modal.Visible) Modal.Draw();
-            }
-        }
+        Texture2D rawScreenshot = null;
 
-        private void Initialize()
+        if (args.InMemory)
         {
-            if (initialized) return;
-            initialized = true;
+            // Take a screenshot directly to a Texture2D for immediate processing
+            var renderTexture = new RenderTexture(Screen.width, Screen.height, 24);
 
-            meidoManager = new MeidoManager();
-            environmentManager = new EnvironmentManager();
-            messageWindowManager = new MessageWindowManager();
-            messageWindowManager.Activate();
-            lightManager = new LightManager();
-            propManager = new PropManager(meidoManager);
-            sceneManager = new SceneManager(this);
-            cameraManager = new CameraManager();
+            RenderTexture.active = renderTexture;
+            CameraUtility.MainCamera.camera.targetTexture = renderTexture;
+            CameraUtility.MainCamera.camera.Render();
 
-            effectManager = new EffectManager();
-            effectManager.AddManager<BloomEffectManager>();
-            effectManager.AddManager<DepthOfFieldEffectManager>();
-            effectManager.AddManager<FogEffectManager>();
-            effectManager.AddManager<VignetteEffectManager>();
-            effectManager.AddManager<SepiaToneEffectManger>();
-            effectManager.AddManager<BlurEffectManager>();
+            rawScreenshot = new(Screen.width, Screen.height, TextureFormat.RGB24, false);
+            rawScreenshot.ReadPixels(new Rect(0, 0, renderTexture.width, renderTexture.height), 0, 0, false);
+            rawScreenshot.Apply();
 
-            meidoManager.BeginCallMeidos += (s, a) => uiActive = false;
-            meidoManager.EndCallMeidos += (s, a) => uiActive = true;
+            CameraUtility.MainCamera.camera.targetTexture = null;
+            RenderTexture.active = null;
 
-            MaidSwitcherPane maidSwitcherPane = new MaidSwitcherPane(meidoManager);
+            DestroyImmediate(renderTexture);
+        }
+        else
+        {
+            // Take Screenshot
+            var defaultSuperSize = new[] { 1, 2, 4 };
+            var selectedSuperSize = args.SuperSize < 1
+                ? defaultSuperSize[(int)GameMain.Instance.CMSystem.ScreenShotSuperSize]
+                : args.SuperSize;
 
-            SceneWindow sceneWindow = new SceneWindow(sceneManager);
+            var path = string.IsNullOrEmpty(args.Path)
+                ? Utility.ScreenshotFilename()
+                : args.Path;
 
-            windowManager = new WindowManager()
-            {
-                [Constants.Window.Main] = new MainWindow(meidoManager, propManager, lightManager)
-                {
-                    [Constants.Window.Call] = new CallWindowPane(meidoManager),
-                    [Constants.Window.Pose] = new PoseWindowPane(meidoManager, maidSwitcherPane),
-                    [Constants.Window.Face] = new FaceWindowPane(meidoManager, maidSwitcherPane),
-                    [Constants.Window.BG] = new BGWindowPane(
-                        environmentManager, lightManager, effectManager, sceneWindow, cameraManager
-                    ),
-                    [Constants.Window.BG2] = new BG2WindowPane(meidoManager, propManager),
-                    [Constants.Window.Settings] = new SettingsWindowPane()
-                },
-                [Constants.Window.Message] = new MessageWindow(messageWindowManager),
-                [Constants.Window.Save] = sceneWindow
-            };
+            Application.CaptureScreenshot(path, selectedSuperSize);
         }
 
-        private void Activate()
-        {
-            if (!GameMain.Instance.SysDlg.IsDecided) return;
+        GameMain.Instance.SoundMgr.PlaySe("se022.ogg", false);
 
-            if (!initialized) Initialize();
-            else
-            {
-                meidoManager.Activate();
-                environmentManager.Activate();
-                cameraManager.Activate();
-                propManager.Activate();
-                lightManager.Activate();
-                effectManager.Activate();
-                messageWindowManager.Activate();
-                windowManager.Activate();
-            }
+        yield return new WaitForEndOfFrame();
 
-            uiActive = true;
-            active = true;
+        // Show UI and dragpoints
+        uiActive = true;
+        editUI.SetActive(editUIWasActive);
+        fpsViewer.SetActive(GameMain.Instance.CMSystem.ViewFps);
+        sysDialog.SetActive(true);
+        sysShortcut.SetActive(true);
 
-            if (!EditMode)
-            {
-                GameObject dailyPanel = GameObject.Find("UI Root")?.transform.Find("DailyPanel")?.gameObject;
-                if (dailyPanel) dailyPanel.SetActive(false);
-            }
-            else meidoManager.CallMeidos();
+        for (var i = 0; i < activeMeidoList.Count; i++)
+        {
+            var meido = activeMeidoList[i];
+
+            if (isIK[i])
+                meido.IK = true;
+
+            if (args.HideMaids && isVisible[i])
+                meido.Maid.Visible = true;
         }
 
-        private void Deactivate(bool force = false)
-        {
-            if (meidoManager.Busy || SceneManager.Busy) return;
+        MeidoDragPointManager.CubeActive = isCubeActive[0];
+        PropManager.CubeActive = isCubeActive[1];
+        LightManager.CubeActive = isCubeActive[2];
+        EnvironmentManager.CubeActive = isCubeActive[3];
+
+        GizmoRender.UIVisible = true;
+
+        if (args.InMemory && rawScreenshot)
+            NotifyRawScreenshot?.Invoke(null, new() { Screenshot = rawScreenshot });
+    }
 
-            var sysDialog = GameMain.Instance.SysDlg;
+    private void OnGUI()
+    {
+        if (!uiActive)
+            return;
+
+        windowManager.DrawWindows();
 
-            if (!sysDialog.IsDecided && !force) return;
+        if (DropdownHelper.Visible)
+            DropdownHelper.HandleDropdown();
 
+        if (Modal.Visible)
+            Modal.Draw();
+    }
+
+    private void Initialize()
+    {
+        if (initialized)
+            return;
+
+        initialized = true;
+
+        meidoManager = new();
+        environmentManager = new();
+        messageWindowManager = new();
+        messageWindowManager.Activate();
+        lightManager = new();
+        propManager = new(meidoManager);
+        sceneManager = new(this);
+        cameraManager = new();
+
+        effectManager = new();
+        effectManager.AddManager<BloomEffectManager>();
+        effectManager.AddManager<DepthOfFieldEffectManager>();
+        effectManager.AddManager<FogEffectManager>();
+        effectManager.AddManager<VignetteEffectManager>();
+        effectManager.AddManager<SepiaToneEffectManager>();
+        effectManager.AddManager<BlurEffectManager>();
+
+        meidoManager.BeginCallMeidos += (_, _) =>
             uiActive = false;
-            active = false;
 
-            if (force)
-            {
-                Exit();
-                return;
-            }
+        meidoManager.EndCallMeidos += (_, _) =>
+            uiActive = true;
 
-            sysDialog.Show(
-                string.Format(Translation.Get("systemMessage", "exitConfirm"), pluginName),
-                SystemDialog.TYPE.OK_CANCEL,
-                Exit,
-                () =>
-                {
-                    sysDialog.Close();
-                    uiActive = true;
-                    active = true;
-                }
-            );
+        var maidSwitcherPane = new MaidSwitcherPane(meidoManager);
+        var sceneWindow = new SceneWindow(sceneManager);
 
-            void Exit()
+        windowManager = new()
+        {
+            [Constants.Window.Main] = new MainWindow(meidoManager, propManager, lightManager)
             {
-                sysDialog.Close();
-
-                meidoManager.Deactivate();
-                environmentManager.Deactivate();
-                cameraManager.Deactivate();
-                propManager.Deactivate();
-                lightManager.Deactivate();
-                effectManager.Deactivate();
-                messageWindowManager.Deactivate();
-                windowManager.Deactivate();
-                Input.Deactivate();
+                [Constants.Window.Call] = new CallWindowPane(meidoManager),
+                [Constants.Window.Pose] = new PoseWindowPane(meidoManager, maidSwitcherPane),
+                [Constants.Window.Face] = new FaceWindowPane(meidoManager, maidSwitcherPane),
+                [Constants.Window.BG] =
+                    new BGWindowPane(environmentManager, lightManager, effectManager, sceneWindow, cameraManager),
+                [Constants.Window.BG2] = new BG2WindowPane(meidoManager, propManager),
+                [Constants.Window.Settings] = new SettingsWindowPane(),
+            },
+            [Constants.Window.Message] = new MessageWindow(messageWindowManager),
+            [Constants.Window.Save] = sceneWindow,
+        };
+    }
 
-                Modal.Close();
+    private void Activate()
+    {
+        if (!GameMain.Instance.SysDlg.IsDecided)
+            return;
 
-                Configuration.Config.Save();
+        if (!initialized)
+        {
+            Initialize();
+        }
+        else
+        {
+            meidoManager.Activate();
+            environmentManager.Activate();
+            cameraManager.Activate();
+            propManager.Activate();
+            lightManager.Activate();
+            effectManager.Activate();
+            messageWindowManager.Activate();
+            windowManager.Activate();
+        }
 
-                if (EditMode) return;
+        uiActive = true;
+        active = true;
 
-                var dailyPanel = GameObject.Find("UI Root")?.transform.Find("DailyPanel")?.gameObject;
+        if (EditMode)
+        {
+            meidoManager.CallMeidos();
+        }
+        else
+        {
+            // TODO: Rework this to not use null propagation (UNT008)
+            var dailyPanel = GameObject.Find("UI Root")?.transform.Find("DailyPanel")?.gameObject;
 
-                if (dailyPanel != null)
-                    dailyPanel.SetActive(true);
-            }
+            if (dailyPanel)
+                dailyPanel.SetActive(false);
         }
     }
 
-    public class ScreenshotEventArgs : EventArgs
+    private void Deactivate(bool force = false)
     {
-        public string Path { get; set; } = string.Empty;
-        public int SuperSize { get; set; } = -1;
-        public bool HideMaids { get; set; }
-        public bool InMemory { get; set; } = false;
-        public Texture2D Screenshot { get; set; }
+        if (meidoManager.Busy || SceneManager.Busy)
+            return;
+
+        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,
+            Resume);
+
+        void Resume()
+        {
+            sysDialog.Close();
+            uiActive = true;
+            active = true;
+        }
+
+        void Exit()
+        {
+            sysDialog.Close();
+
+            meidoManager.Deactivate();
+            environmentManager.Deactivate();
+            cameraManager.Deactivate();
+            propManager.Deactivate();
+            lightManager.Deactivate();
+            effectManager.Deactivate();
+            messageWindowManager.Deactivate();
+            windowManager.Deactivate();
+            Input.Deactivate();
+
+            Modal.Close();
+
+            Configuration.Config.Save();
+
+            if (EditMode)
+                return;
+
+            // TODO: Rework this to not use null propagation (UNT008)
+            var dailyPanel = GameObject.Find("UI Root")?.transform.Find("DailyPanel")?.gameObject;
+
+            // NOTE: using is (not) for null checks on UnityEngine.Object does not work
+            if (dailyPanel)
+                dailyPanel.SetActive(true);
+        }
     }
 }

+ 54 - 41
src/MeidoPhotoStudio.Plugin/MenuFileCache.cs

@@ -1,62 +1,75 @@
-using System.IO;
 using System.Collections.Generic;
+using System.IO;
 
 /*
     All of this is pretty much stolen from COM3D2.CacheEditMenu. Thanks Mr. Horsington.
     https://git.coder.horse/ghorsington/COM3D2.CacheEditMenu
 */
-namespace MeidoPhotoStudio.Plugin
+namespace MeidoPhotoStudio.Plugin;
+
+public class MenuFileCache
 {
-    using static MenuFileUtility;
+    public static readonly string CachePath = Path.Combine(Constants.ConfigPath, "cache.dat");
+
+    private const int CacheVersion = 765;
 
-    public class MenuFileCache
+    private readonly Dictionary<string, ModItem> modItems;
+
+    private bool rebuild;
+
+    public MenuFileCache()
     {
-        private const int cacheVersion = 765;
-        public static readonly string cachePath = Path.Combine(Constants.configPath, "cache.dat");
-        private readonly Dictionary<string, ModItem> modItems;
-        private bool rebuild;
-        public ModItem this[string menu]
-        {
-            get => modItems[menu];
-            set
-            {
-                if (!modItems.ContainsKey(menu))
-                {
-                    rebuild = true;
-                    modItems[menu] = value;
-                }
-            }
-        }
+        modItems = new();
+
+        if (File.Exists(CachePath))
+            Deserialize();
+    }
 
-        public MenuFileCache()
+    public ModItem this[string menu]
+    {
+        get => modItems[menu];
+        set
         {
-            modItems = new Dictionary<string, ModItem>();
-            if (File.Exists(cachePath)) Deserialize();
+            if (modItems.ContainsKey(menu))
+                return;
+
+            rebuild = true;
+            modItems[menu] = value;
         }
+    }
+
+    public bool Has(string menuFileName) =>
+        modItems.ContainsKey(menuFileName);
+
+    public void Serialize()
+    {
+        if (!rebuild)
+            return;
+
+        using var binaryWriter = new BinaryWriter(File.OpenWrite(CachePath));
+
+        binaryWriter.Write(CacheVersion);
 
-        public bool Has(string menuFileName) => modItems.ContainsKey(menuFileName);
+        foreach (var item in modItems.Values)
+            item.Serialize(binaryWriter);
+    }
+
+    private void Deserialize()
+    {
+        using var binaryReader = new BinaryReader(File.OpenRead(CachePath));
 
-        private void Deserialize()
+        if (binaryReader.ReadInt32() is not CacheVersion)
         {
-            using BinaryReader binaryReader = new BinaryReader(File.OpenRead(cachePath));
-            if (binaryReader.ReadInt32() != cacheVersion)
-            {
-                Utility.LogInfo("Cache version out of date. Rebuilding");
-                return;
-            }
-            while (binaryReader.BaseStream.Position < binaryReader.BaseStream.Length)
-            {
-                ModItem item = ModItem.Deserialize(binaryReader);
-                modItems[item.MenuFile] = item;
-            }
+            Utility.LogInfo("Cache version out of date. Rebuilding");
+
+            return;
         }
 
-        public void Serialize()
+        while (binaryReader.BaseStream.Position < binaryReader.BaseStream.Length)
         {
-            if (!rebuild) return;
-            using BinaryWriter binaryWriter = new BinaryWriter(File.OpenWrite(cachePath));
-            binaryWriter.Write(cacheVersion);
-            foreach (ModItem item in modItems.Values) item.Serialize(binaryWriter);
+            var item = ModItem.Deserialize(binaryReader);
+
+            modItems[item.MenuFile] = item;
         }
     }
 }

+ 228 - 166
src/MeidoPhotoStudio.Plugin/MenuFileUtility.cs

@@ -1,233 +1,295 @@
 using System;
-using System.Text;
-using System.IO;
 using System.Collections;
 using System.Collections.Generic;
+using System.IO;
 using System.Linq;
+using System.Text;
+
 using UnityEngine;
 
-namespace MeidoPhotoStudio.Plugin
+namespace MeidoPhotoStudio.Plugin;
+
+public static class MenuFileUtility
 {
-    public static class MenuFileUtility
+    // TODO: What's this for?
+    public const string NoCategory = "noCategory";
+
+    public static readonly string[] MenuCategories =
     {
-        private static byte[] fileBuffer;
-        public const string noCategory = "noCategory";
+        NoCategory, "acchat", "headset", "wear", "skirt", "onepiece", "mizugi", "bra", "panz", "stkg", "shoes",
+        "acckami", "megane", "acchead", "acchana", "accmimi", "glove", "acckubi", "acckubiwa", "acckamisub", "accnip",
+        "accude", "accheso", "accashi", "accsenaka", "accshippo", "accxxx",
+    };
 
-        public static readonly string[] MenuCategories =
-        {
-            noCategory, "acchat", "headset", "wear", "skirt", "onepiece", "mizugi", "bra", "panz", "stkg", "shoes",
-            "acckami", "megane", "acchead", "acchana", "accmimi", "glove", "acckubi", "acckubiwa", "acckamisub",
-            "accnip", "accude", "accheso", "accashi", "accsenaka", "accshippo", "accxxx"
-        };
+    private static readonly HashSet<string> AccMpn = new(StringComparer.InvariantCultureIgnoreCase);
 
-        private static readonly HashSet<string> accMpn = new HashSet<string>(StringComparer.InvariantCultureIgnoreCase);
+    private static byte[] fileBuffer;
 
-        public static event EventHandler MenuFilesReadyChange;
-        public static bool MenuFilesReady { get; private set; }
+    static MenuFileUtility()
+    {
+        AccMpn.UnionWith(MenuCategories.Skip(1));
+        GameMain.Instance.StartCoroutine(CheckMenuDataBaseJob());
+    }
 
-        static MenuFileUtility()
-        {
-            accMpn.UnionWith(MenuCategories.Skip(1));
-            GameMain.Instance.StartCoroutine(CheckMenuDataBaseJob());
-        }
+    public static event EventHandler MenuFilesReadyChange;
 
-        private static IEnumerator CheckMenuDataBaseJob()
-        {
-            if (MenuFilesReady) yield break;
-            while (!GameMain.Instance.MenuDataBase.JobFinished()) yield return null;
-            MenuFilesReady = true;
-            MenuFilesReadyChange?.Invoke(null, EventArgs.Empty);
-        }
+    public static bool MenuFilesReady { get; private set; }
+
+    public static byte[] ReadAFileBase(string filename)
+    {
+        using var aFileBase = GameUty.FileOpen(filename);
 
-        private static ref byte[] GetFileBuffer(long size)
+        if (!aFileBase.IsValid() || aFileBase.GetSize() is 0)
         {
-            if (fileBuffer == null) fileBuffer = new byte[Math.Max(500000, size)];
-            else if (fileBuffer.Length < size) fileBuffer = new byte[size];
+            Utility.LogError($"AFileBase '{filename}' is invalid");
 
-            return ref fileBuffer;
+            return null;
         }
 
-        public static byte[] ReadAFileBase(string filename)
-        {
-            using AFileBase aFileBase = GameUty.FileOpen(filename);
+        // INFO: Don't really understand what this does.
+        ref var buffer = ref GetFileBuffer(aFileBase.GetSize());
 
-            if (aFileBase.IsValid() && aFileBase.GetSize() != 0)
-            {
-                ref byte[] buffer = ref GetFileBuffer(aFileBase.GetSize());
+        aFileBase.Read(ref buffer, aFileBase.GetSize());
 
-                aFileBase.Read(ref buffer, aFileBase.GetSize());
+        return buffer;
+    }
 
-                return buffer;
-            }
+    public static byte[] ReadOfficialMod(string filename)
+    {
+        using var fileStream = new FileStream(filename, FileMode.Open);
+
+        if (fileStream.Length is 0)
+        {
+            Utility.LogWarning($"Mod menu file '{filename}' is invalid");
 
-            Utility.LogError($"AFileBase '{filename}' is invalid");
             return null;
         }
 
-        public static byte[] ReadOfficialMod(string filename)
-        {
-            using var fileStream = new FileStream(filename, FileMode.Open);
+        ref var buffer = ref GetFileBuffer(fileStream.Length);
 
-            if (fileStream.Length != 0)
-            {
-                ref byte[] buffer = ref GetFileBuffer(fileStream.Length);
+        fileStream.Read(buffer, 0, (int)fileStream.Length);
 
-                fileStream.Read(buffer, 0, (int) fileStream.Length);
+        return buffer;
+    }
 
-                return buffer;
-            }
+    public static bool ParseNativeMenuFile(int menuIndex, ModItem modItem)
+    {
+        var menuDataBase = GameMain.Instance.MenuDataBase;
 
-            Utility.LogWarning($"Mod menu file '{filename}' is invalid");
-            return null;
-        }
+        menuDataBase.SetIndex(menuIndex);
+
+        if (menuDataBase.GetBoDelOnly())
+            return false;
+
+        modItem.Category = menuDataBase.GetCategoryMpnText();
+
+        if (!AccMpn.Contains(modItem.Category))
+            return false;
+
+        modItem.MenuFile = menuDataBase.GetMenuFileName().ToLower();
+
+        if (!ValidBG2MenuFile(modItem.MenuFile))
+            return false;
+
+        modItem.Name = menuDataBase.GetMenuName();
+        modItem.IconFile = menuDataBase.GetIconS();
+        modItem.Priority = menuDataBase.GetPriority();
+
+        return true;
+    }
+
+    public static void ParseMenuFile(string menuFile, ModItem modItem)
+    {
+        if (!ValidBG2MenuFile(menuFile))
+            return;
 
-        public static bool ParseNativeMenuFile(int menuIndex, ModItem modItem)
+        byte[] buffer;
+
+        try
+        {
+            buffer = ReadAFileBase(menuFile);
+        }
+        catch (Exception e)
         {
-            MenuDataBase menuDataBase = GameMain.Instance.MenuDataBase;
-            menuDataBase.SetIndex(menuIndex);
-            if (menuDataBase.GetBoDelOnly()) return false;
-            modItem.Category = menuDataBase.GetCategoryMpnText();
-            if (!accMpn.Contains(modItem.Category)) return false;
-            modItem.MenuFile = menuDataBase.GetMenuFileName().ToLower();
-            if (!ValidBG2MenuFile(modItem.MenuFile)) return false;
-            modItem.Name = menuDataBase.GetMenuName();
-            modItem.IconFile = menuDataBase.GetIconS();
-            modItem.Priority = menuDataBase.GetPriority();
-            return true;
+            Utility.LogError($"Could not read menu file '{menuFile}' because {e.Message}");
+
+            return;
         }
 
-        public static void ParseMenuFile(string menuFile, ModItem modItem)
+        try
         {
-            if (!ValidBG2MenuFile(menuFile)) return;
+            using var binaryReader = new BinaryReader(new MemoryStream(buffer), Encoding.UTF8);
 
-            byte[] buffer;
+            if (binaryReader.ReadString() is not "CM3D2_MENU")
+                return;
 
-            try { buffer = ReadAFileBase(menuFile); }
-            catch (Exception e)
-            {
-                Utility.LogError($"Could not read menu file '{menuFile}' because {e.Message}");
-                return ;
-            }
+            binaryReader.ReadInt32(); // file version
+            binaryReader.ReadString(); // txt path
+            modItem.Name = binaryReader.ReadString(); // name
+            binaryReader.ReadString(); // category
+            binaryReader.ReadString(); // description
+            binaryReader.ReadInt32(); // idk (as long)
 
-            try
+            while (true)
             {
-                using var binaryReader = new BinaryReader(new MemoryStream(buffer), Encoding.UTF8);
+                var numberOfProps = binaryReader.ReadByte();
+                var menuPropString = string.Empty;
 
-                if (binaryReader.ReadString() != "CM3D2_MENU") return;
-                binaryReader.ReadInt32(); // file version
-                binaryReader.ReadString(); // txt path
-                modItem.Name = binaryReader.ReadString(); // name
-                binaryReader.ReadString(); // category
-                binaryReader.ReadString(); // description
-                binaryReader.ReadInt32(); // idk (as long)
+                if (numberOfProps is 0)
+                    break;
+
+                for (var i = 0; i < numberOfProps; i++)
+                    menuPropString = $"{menuPropString}\"{binaryReader.ReadString()}\"";
+
+                if (string.IsNullOrEmpty(menuPropString))
+                    continue;
+
+                var header = UTY.GetStringCom(menuPropString);
+                var menuProps = UTY.GetStringList(menuPropString);
 
-                while (true)
+                if (header is "end")
+                    break;
+
+                if (header is "category")
                 {
-                    int numberOfProps = binaryReader.ReadByte();
-                    var menuPropString = string.Empty;
-
-                    if (numberOfProps == 0) break;
-
-                    for (var i = 0; i < numberOfProps; i++)
-                    {
-                        menuPropString = $"{menuPropString}\"{binaryReader.ReadString()}\"";
-                    }
-
-                    if (string.IsNullOrEmpty(menuPropString)) continue;
-
-                    var header = UTY.GetStringCom(menuPropString);
-                    string[] menuProps = UTY.GetStringList(menuPropString);
-
-                    if (header == "end") break;
-
-                    if (header == "category")
-                    {
-                        modItem.Category = menuProps[1];
-                        if (!accMpn.Contains(modItem.Category)) return;
-                    }
-                    else if (header == "icons" || header == "icon")
-                    {
-                        modItem.IconFile = menuProps[1];
-                        break;
-                    }
-                    else if (header == "priority") modItem.Priority = float.Parse(menuProps[1]);
+                    modItem.Category = menuProps[1];
+
+                    if (!AccMpn.Contains(modItem.Category))
+                        return;
+                }
+                else if (header is "icons" or "icon")
+                {
+                    modItem.IconFile = menuProps[1];
+
+                    break;
+                }
+                else if (header is "priority")
+                {
+                    modItem.Priority = float.Parse(menuProps[1]);
                 }
-            }
-            catch (Exception e)
-            {
-                Utility.LogWarning($"Could not parse menu file '{menuFile}' because {e.Message}");
             }
         }
+        catch (Exception e)
+        {
+            Utility.LogWarning($"Could not parse menu file '{menuFile}' because {e.Message}");
+        }
+    }
 
-        public static bool ParseModMenuFile(string modMenuFile, ModItem modItem)
+    public static bool ParseModMenuFile(string modMenuFile, ModItem modItem)
+    {
+        if (!ValidBG2MenuFile(modMenuFile))
+            return false;
+
+        byte[] modBuffer;
+
+        try
         {
-            if (!ValidBG2MenuFile(modMenuFile)) return false;
+            modBuffer = ReadOfficialMod(modMenuFile);
+        }
+        catch (Exception e)
+        {
+            Utility.LogError($"Could not read mod menu file '{modMenuFile} because {e.Message}'");
 
-            byte[] modBuffer;
+            return false;
+        }
 
-            try { modBuffer = ReadOfficialMod(modMenuFile); }
-            catch (Exception e)
-            {
-                Utility.LogError($"Could not read mod menu file '{modMenuFile} because {e.Message}'");
+        try
+        {
+            using var binaryReader = new BinaryReader(new MemoryStream(modBuffer), Encoding.UTF8);
+
+            if (binaryReader.ReadString() is not "CM3D2_MOD")
                 return false;
-            }
 
-            try
-            {
-                using var binaryReader = new BinaryReader(new MemoryStream(modBuffer), Encoding.UTF8);
+            binaryReader.ReadInt32();
 
-                if (binaryReader.ReadString() != "CM3D2_MOD") return false;
+            var iconName = binaryReader.ReadString();
+            var baseItemPath = binaryReader.ReadString().Replace(":", " ");
 
-                binaryReader.ReadInt32();
-                var iconName = binaryReader.ReadString();
-                var baseItemPath = binaryReader.ReadString().Replace(":", " ");
-                modItem.BaseMenuFile = Path.GetFileName(baseItemPath);
-                modItem.Name = binaryReader.ReadString();
-                modItem.Category = binaryReader.ReadString();
-                if (!accMpn.Contains(modItem.Category)) return false;
-                binaryReader.ReadString();
+            modItem.BaseMenuFile = Path.GetFileName(baseItemPath);
+            modItem.Name = binaryReader.ReadString();
+            modItem.Category = binaryReader.ReadString();
+
+            if (!AccMpn.Contains(modItem.Category))
+                return false;
 
-                var mpnValue = binaryReader.ReadString();
-                var mpn = MPN.null_mpn;
-                try { mpn = (MPN) Enum.Parse(typeof(MPN), mpnValue, true); }
-                catch { /* ignored */ }
+            binaryReader.ReadString();
 
-                if (mpn != MPN.null_mpn) binaryReader.ReadString();
+            var mpnValue = binaryReader.ReadString();
+            var mpn = MPN.null_mpn;
+
+            try
+            {
+                mpn = (MPN)Enum.Parse(typeof(MPN), mpnValue, true);
+            }
+            catch
+            {
+                // Ignored.
+            }
 
+            if (mpn is not MPN.null_mpn)
                 binaryReader.ReadString();
 
-                var entryCount = binaryReader.ReadInt32();
-                for (var i = 0; i < entryCount; i++)
-                {
-                    var key = binaryReader.ReadString();
-                    var count = binaryReader.ReadInt32();
-                    byte[] data = binaryReader.ReadBytes(count);
+            binaryReader.ReadString();
 
-                    if (!string.Equals(key, iconName, StringComparison.InvariantCultureIgnoreCase)) continue;
+            var entryCount = binaryReader.ReadInt32();
 
-                    var tex = new Texture2D(1, 1, TextureFormat.RGBA32, false);
-                    tex.LoadImage(data);
-                    modItem.Icon = tex;
-                    break;
-                }
-            }
-            catch (Exception e)
+            for (var i = 0; i < entryCount; i++)
             {
-                Utility.LogWarning($"Could not parse mod menu file '{modMenuFile}' because {e}");
-                return false;
-            }
+                var key = binaryReader.ReadString();
+                var count = binaryReader.ReadInt32();
+                var data = binaryReader.ReadBytes(count);
 
-            return true;
-        }
+                if (!string.Equals(key, iconName, StringComparison.InvariantCultureIgnoreCase))
+                    continue;
 
-        public static bool ValidBG2MenuFile(ModItem modItem)
-            => accMpn.Contains(modItem.Category) && ValidBG2MenuFile(modItem.MenuFile);
+                var tex = new Texture2D(1, 1, TextureFormat.RGBA32, false);
 
-        public static bool ValidBG2MenuFile(string menu)
+                tex.LoadImage(data);
+                modItem.Icon = tex;
+
+                break;
+            }
+        }
+        catch (Exception e)
         {
-            menu = Path.GetFileNameWithoutExtension(menu).ToLower();
-            return !(menu.EndsWith("_del") || menu.Contains("zurashi") || menu.Contains("mekure")
-                || menu.Contains("porori") || menu.Contains("moza") || menu.Contains("folder"));
+            Utility.LogWarning($"Could not parse mod menu file '{modMenuFile}' because {e}");
+
+            return false;
         }
+
+        return true;
+    }
+
+    public static bool ValidBG2MenuFile(ModItem modItem) =>
+        AccMpn.Contains(modItem.Category) && ValidBG2MenuFile(modItem.MenuFile);
+
+    public static bool ValidBG2MenuFile(string menu)
+    {
+        menu = Path.GetFileNameWithoutExtension(menu).ToLower();
+
+        return !(menu.EndsWith("_del") || menu.Contains("zurashi") || menu.Contains("mekure") || menu.Contains("porori")
+            || menu.Contains("moza") || menu.Contains("folder"));
+    }
+
+    private static IEnumerator CheckMenuDataBaseJob()
+    {
+        if (MenuFilesReady)
+            yield break;
+
+        while (!GameMain.Instance.MenuDataBase.JobFinished())
+            yield return null;
+
+        MenuFilesReady = true;
+        MenuFilesReadyChange?.Invoke(null, EventArgs.Empty);
+    }
+
+    private static ref byte[] GetFileBuffer(long size)
+    {
+        if (fileBuffer is null)
+            fileBuffer = new byte[Math.Max(500000, size)];
+        else if (fileBuffer.Length < size)
+            fileBuffer = new byte[size];
+
+        return ref fileBuffer;
     }
 }

+ 18 - 0
src/MeidoPhotoStudio.Plugin/MenuFilesEventArgs.cs

@@ -0,0 +1,18 @@
+using System;
+
+namespace MeidoPhotoStudio.Plugin;
+
+public class MenuFilesEventArgs : EventArgs
+{
+    public MenuFilesEventArgs(EventType type) =>
+        Type = type;
+
+    public enum EventType
+    {
+        HandItems,
+        MenuFiles,
+        MpnAttach,
+    }
+
+    public EventType Type { get; }
+}

+ 5 - 65
src/MeidoPhotoStudio.Plugin/MenuItem.cs

@@ -1,70 +1,10 @@
-using System.Globalization;
-using System.IO;
 using UnityEngine;
 
-namespace MeidoPhotoStudio.Plugin
-{
-    public abstract class MenuItem
-    {
-        public string IconFile { get; set; }
-        public Texture2D Icon { get; set; }
-    }
-
-    public class ModItem : MenuItem
-    {
-        public string MenuFile { get; set; }
-        public string BaseMenuFile { get; set; }
-        public string Name { get; set; }
-        public string Category { get; set; }
-        public float Priority { get; set; }
-        public bool IsMod { get; private set; }
-        public bool IsOfficialMod { get; private set; }
-
-        public static ModItem OfficialMod(string menuFile) => new ModItem()
-        {
-            MenuFile = menuFile, IsMod = true, IsOfficialMod = true, Priority = 1000f
-        };
-
-        public static ModItem Mod(string menuFile) => new ModItem() { MenuFile = menuFile, IsMod = true };
-
-        public ModItem() { }
-
-        public ModItem(string menuFile) => MenuFile = menuFile;
+namespace MeidoPhotoStudio.Plugin;
 
-        public override string ToString() => IsOfficialMod ? $"{Path.GetFileName(MenuFile)}#{BaseMenuFile}" : MenuFile;
-
-        public static ModItem Deserialize(BinaryReader binaryReader) => new ModItem()
-        {
-            MenuFile = binaryReader.ReadNullableString(),
-            BaseMenuFile = binaryReader.ReadNullableString(),
-            IconFile = binaryReader.ReadNullableString(),
-            Name = binaryReader.ReadNullableString(),
-            Category = binaryReader.ReadNullableString(),
-            Priority = float.Parse(binaryReader.ReadNullableString()),
-            IsMod = binaryReader.ReadBoolean(),
-            IsOfficialMod = binaryReader.ReadBoolean()
-        };
-
-        public void Serialize(BinaryWriter binaryWriter)
-        {
-            if (IsOfficialMod) return;
-
-            binaryWriter.WriteNullableString(MenuFile);
-            binaryWriter.WriteNullableString(BaseMenuFile);
-            binaryWriter.WriteNullableString(IconFile);
-            binaryWriter.WriteNullableString(Name);
-            binaryWriter.WriteNullableString(Category);
-            binaryWriter.WriteNullableString(Priority.ToString(CultureInfo.InvariantCulture));
-            binaryWriter.Write(IsMod);
-            binaryWriter.Write(IsOfficialMod);
-        }
-    }
-
-    public class MyRoomItem : MenuItem
-    {
-        public int ID { get; set; }
-        public string PrefabName { get; set; }
+public abstract class MenuItem
+{
+    public string IconFile { get; set; }
 
-        public override string ToString() => $"MYR_{ID}#{PrefabName}";
-    }
+    public Texture2D Icon { get; set; }
 }

+ 75 - 0
src/MeidoPhotoStudio.Plugin/ModItem.cs

@@ -0,0 +1,75 @@
+using System.Globalization;
+using System.IO;
+
+namespace MeidoPhotoStudio.Plugin;
+
+public class ModItem : MenuItem
+{
+    public ModItem()
+    {
+    }
+
+    public ModItem(string menuFile) =>
+        MenuFile = menuFile;
+
+    public string MenuFile { get; set; }
+
+    public string BaseMenuFile { get; set; }
+
+    public string Name { get; set; }
+
+    public string Category { get; set; }
+
+    public float Priority { get; set; }
+
+    public bool IsMod { get; private set; }
+
+    public bool IsOfficialMod { get; private set; }
+
+    public static ModItem OfficialMod(string menuFile) =>
+        new()
+        {
+            MenuFile = menuFile,
+            IsMod = true,
+            IsOfficialMod = true,
+            Priority = 1000f,
+        };
+
+    public static ModItem Mod(string menuFile) =>
+        new()
+        {
+            MenuFile = menuFile,
+            IsMod = true,
+        };
+
+    public static ModItem Deserialize(BinaryReader binaryReader) =>
+        new()
+        {
+            MenuFile = binaryReader.ReadNullableString(),
+            BaseMenuFile = binaryReader.ReadNullableString(),
+            IconFile = binaryReader.ReadNullableString(),
+            Name = binaryReader.ReadNullableString(),
+            Category = binaryReader.ReadNullableString(),
+            Priority = float.Parse(binaryReader.ReadNullableString()),
+            IsMod = binaryReader.ReadBoolean(),
+            IsOfficialMod = binaryReader.ReadBoolean(),
+        };
+
+    public override string ToString() =>
+        IsOfficialMod ? $"{Path.GetFileName(MenuFile)}#{BaseMenuFile}" : MenuFile;
+
+    public void Serialize(BinaryWriter binaryWriter)
+    {
+        if (IsOfficialMod)
+            return;
+
+        binaryWriter.WriteNullableString(MenuFile);
+        binaryWriter.WriteNullableString(BaseMenuFile);
+        binaryWriter.WriteNullableString(IconFile);
+        binaryWriter.WriteNullableString(Name);
+        binaryWriter.WriteNullableString(Category);
+        binaryWriter.WriteNullableString(Priority.ToString(CultureInfo.InvariantCulture));
+        binaryWriter.Write(IsMod);
+        binaryWriter.Write(IsOfficialMod);
+    }
+}

File diff suppressed because it is too large
+ 480 - 423
src/MeidoPhotoStudio.Plugin/ModelUtility.cs


+ 31 - 0
src/MeidoPhotoStudio.Plugin/MousePosition.cs

@@ -0,0 +1,31 @@
+using UnityEngine;
+
+namespace MeidoPhotoStudio.Plugin;
+
+public class MousePosition : MonoBehaviour
+{
+    private Vector3 mousePosition;
+
+    public Vector3 Position =>
+        mousePosition;
+
+    private void Awake()
+    {
+        DontDestroyOnLoad(this);
+
+        mousePosition = Input.mousePosition;
+    }
+
+    private void Update()
+    {
+        if (Input.GetMouseButton(0))
+        {
+            mousePosition.x += Input.GetAxis("Mouse X") * 20;
+            mousePosition.y += Input.GetAxis("Mouse Y") * 20;
+        }
+        else
+        {
+            mousePosition = Input.mousePosition;
+        }
+    }
+}

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

@@ -0,0 +1,14 @@
+namespace MeidoPhotoStudio.Plugin;
+
+public readonly struct MpnAttachProp
+{
+    public MpnAttachProp(MPN tag, string menuFile)
+    {
+        Tag = tag;
+        MenuFile = menuFile;
+    }
+
+    public MPN Tag { get; }
+
+    public string MenuFile { get; }
+}

+ 97 - 0
src/MeidoPhotoStudio.Plugin/MpsGui.cs

@@ -0,0 +1,97 @@
+using UnityEngine;
+
+namespace MeidoPhotoStudio.Plugin;
+
+public static class MpsGui
+{
+    public static readonly GUILayoutOption HalfSlider = GUILayout.Width(98);
+    public static readonly Texture2D White = Utility.MakeTex(2, 2, Color.white);
+    public static readonly Texture2D TransparentBlack = Utility.MakeTex(2, 2, new(0f, 0f, 0f, 0.8f));
+    public static readonly GUIStyle SliderLabelStyle;
+    public static readonly GUIStyle SliderStyle;
+    public static readonly GUIStyle SliderStyleNoLabel;
+    public static readonly GUIStyle SliderTextBoxStyle;
+    public static readonly GUIStyle SliderThumbStyle;
+    public static readonly GUIStyle SliderResetButtonStyle;
+
+    private static readonly GUIStyle LineStyleWhite;
+    private static readonly GUIStyle LineStyleBlack;
+    private static readonly GUIStyle TextureBoxStyle;
+    private static readonly GUIStyle HeaderLabelStyle;
+
+    static MpsGui()
+    {
+        GUI.skin = null;
+
+        LineStyleWhite = new(GUI.skin.box)
+        {
+            margin = new(0, 0, 8, 8),
+            normal = { background = Utility.MakeTex(2, 2, new(1f, 1f, 1f, 0.2f)) },
+        };
+
+        LineStyleWhite.padding = LineStyleWhite.border = new(0, 0, 1, 1);
+
+        LineStyleBlack = new(LineStyleWhite)
+        {
+            normal = { background = Utility.MakeTex(2, 2, new(0f, 0f, 0f, 0.3f)) },
+        };
+
+        TextureBoxStyle = new(GUI.skin.box)
+        {
+            normal = { background = Utility.MakeTex(2, 2, new(0f, 0f, 0f, 0f)) },
+        };
+
+        TextureBoxStyle.padding = TextureBoxStyle.margin = new(0, 0, 0, 0);
+
+        HeaderLabelStyle = new(GUI.skin.label)
+        {
+            padding = new(7, 0, 0, -5),
+            normal = { textColor = Color.white },
+            fontSize = 14,
+        };
+
+        SliderLabelStyle = new(GUI.skin.label)
+        {
+            alignment = TextAnchor.LowerLeft,
+            fontSize = 13,
+            normal = { textColor = Color.white },
+        };
+
+        SliderStyle = new(GUI.skin.horizontalSlider);
+        SliderStyleNoLabel = new(SliderStyle)
+        {
+            margin = { top = 10 },
+        };
+
+        SliderTextBoxStyle = new(GUI.skin.textField)
+        {
+            fontSize = 12,
+        };
+
+        SliderResetButtonStyle = new(GUI.skin.button)
+        {
+            alignment = TextAnchor.MiddleRight,
+            fontSize = 10,
+        };
+
+        SliderThumbStyle = new(GUI.skin.horizontalSliderThumb);
+    }
+
+    public static void WhiteLine() =>
+        Line(LineStyleWhite);
+
+    public static void BlackLine() =>
+        Line(LineStyleBlack);
+
+    public static void DrawTexture(Texture texture, params GUILayoutOption[] layoutOptions) =>
+        GUILayout.Box(texture, TextureBoxStyle, layoutOptions);
+
+    public static int ClampFont(int size, int min, int max) =>
+        Mathf.Clamp(Utility.GetPix(size), min, max);
+
+    public static void Header(string text, params GUILayoutOption[] layoutOptions) =>
+        GUILayout.Label(text, HeaderLabelStyle, layoutOptions);
+
+    private static void Line(GUIStyle style) =>
+        GUILayout.Box(GUIContent.none, style, GUILayout.Height(1));
+}

+ 0 - 85
src/MeidoPhotoStudio.Plugin/MyGui.cs

@@ -1,85 +0,0 @@
-using UnityEngine;
-
-namespace MeidoPhotoStudio.Plugin
-{
-    public static class MpsGui
-    {
-        public static readonly GUILayoutOption HalfSlider = GUILayout.Width(98);
-        public static readonly Texture2D white = Utility.MakeTex(2, 2, Color.white);
-        public static readonly Texture2D transparentBlack = Utility.MakeTex(2, 2, new Color(0f, 0f, 0f, 0.8f));
-        public static readonly GUIStyle SliderLabelStyle;
-        public static readonly GUIStyle SliderStyle;
-        public static readonly GUIStyle SliderStyleNoLabel;
-        public static readonly GUIStyle SliderTextBoxStyle;
-        public static readonly GUIStyle SliderThumbStyle;
-        public static readonly GUIStyle SliderResetButtonStyle;
-        private static readonly GUIStyle lineStyleWhite;
-        private static readonly GUIStyle lineStyleBlack;
-        private static readonly GUIStyle textureBoxStyle;
-        private static readonly GUIStyle headerLabelStyle;
-
-        static MpsGui()
-        {
-            GUI.skin = null;
-
-            lineStyleWhite = new GUIStyle(GUI.skin.box)
-            {
-                margin = new RectOffset(0, 0, 8, 8),
-                normal = { background = Utility.MakeTex(2, 2, new Color(1f, 1f, 1f, 0.2f)) }
-            };
-            lineStyleWhite.padding = lineStyleWhite.border = new RectOffset(0, 0, 1, 1);
-
-            lineStyleBlack = new GUIStyle(lineStyleWhite)
-            {
-                normal = { background = Utility.MakeTex(2, 2, new Color(0f, 0f, 0f, 0.3f)) }
-            };
-
-            textureBoxStyle = new GUIStyle(GUI.skin.box)
-            {
-                normal = { background = Utility.MakeTex(2, 2, new Color(0f, 0f, 0f, 0f)) }
-            };
-            textureBoxStyle.padding = textureBoxStyle.margin = new RectOffset(0, 0, 0, 0);
-
-            headerLabelStyle = new GUIStyle(GUI.skin.label)
-            {
-                padding = new RectOffset(7, 0, 0, -5),
-                normal = { textColor = Color.white },
-                fontSize = 14
-            };
-
-            SliderLabelStyle = new GUIStyle(GUI.skin.label)
-            {
-                alignment = TextAnchor.LowerLeft,
-                fontSize = 13,
-                normal = { textColor = Color.white }
-            };
-            SliderStyle = new GUIStyle(GUI.skin.horizontalSlider);
-            SliderStyleNoLabel = new GUIStyle(SliderStyle) { margin = { top = 10 } };
-            SliderTextBoxStyle = new GUIStyle(GUI.skin.textField) { fontSize = 12, };
-            SliderResetButtonStyle = new GUIStyle(GUI.skin.button)
-            {
-                alignment = TextAnchor.MiddleRight,
-                fontSize = 10
-            };
-            SliderThumbStyle = new GUIStyle(GUI.skin.horizontalSliderThumb);
-        }
-
-        private static void Line(GUIStyle style) => GUILayout.Box(GUIContent.none, style, GUILayout.Height(1));
-
-        public static void WhiteLine() => Line(lineStyleWhite);
-
-        public static void BlackLine() => Line(lineStyleBlack);
-
-        public static void DrawTexture(Texture texture, params GUILayoutOption[] layoutOptions)
-        {
-            GUILayout.Box(texture, textureBoxStyle, layoutOptions);
-        }
-
-        public static int ClampFont(int size, int min, int max) => Mathf.Clamp(Utility.GetPix(size), min, max);
-
-        public static void Header(string text, params GUILayoutOption[] layoutOptions)
-        {
-            GUILayout.Label(text, headerLabelStyle, layoutOptions);
-        }
-    }
-}

+ 11 - 0
src/MeidoPhotoStudio.Plugin/MyRoomItem.cs

@@ -0,0 +1,11 @@
+namespace MeidoPhotoStudio.Plugin;
+
+public class MyRoomItem : MenuItem
+{
+    public int ID { get; set; }
+
+    public string PrefabName { get; set; }
+
+    public override string ToString() =>
+        $"MYR_{ID}#{PrefabName}";
+}

+ 12 - 18
src/MeidoPhotoStudio.Plugin/Patchers/AllProcPropSeqStartPatcher.cs

@@ -1,24 +1,18 @@
 using System;
+using System.Diagnostics.CodeAnalysis;
+
 using HarmonyLib;
 
-namespace MeidoPhotoStudio.Plugin
-{
-    // TODO: Extend this further to potentially reduce the need for coroutines that wait for maid proc state
-    public static class AllProcPropSeqStartPatcher
-    {
-        public static event EventHandler<ProcStartEventArgs> SequenceStart;
+namespace MeidoPhotoStudio.Plugin;
 
-        [HarmonyPatch(typeof(Maid), nameof(Maid.AllProcPropSeqStart))]
-        [HarmonyPostfix]
-        private static void NotifyProcStart(Maid __instance)
-        {
-            SequenceStart?.Invoke(null, new ProcStartEventArgs(__instance));
-        }
-    }
+// TODO: Extend this further to potentially reduce the need for coroutines that wait for maid proc state
+public static class AllProcPropSeqStartPatcher
+{
+    public static event EventHandler<ProcStartEventArgs> SequenceStart;
 
-    public class ProcStartEventArgs : EventArgs
-    {
-        public readonly Maid maid;
-        public ProcStartEventArgs(Maid maid) => this.maid = maid;
-    }
+    [HarmonyPatch(typeof(Maid), nameof(Maid.AllProcPropSeqStart))]
+    [HarmonyPostfix]
+    [SuppressMessage("StyleCop.Analyzers.NamingRules", "SA1313", Justification = "Harmony parameter")]
+    private static void NotifyProcStart(Maid __instance) =>
+        SequenceStart?.Invoke(null, new(__instance));
 }

+ 17 - 14
src/MeidoPhotoStudio.Plugin/Patchers/BgMgrPatcher.cs

@@ -1,21 +1,24 @@
 using System;
+
 using HarmonyLib;
 
-namespace MeidoPhotoStudio.Plugin
+namespace MeidoPhotoStudio.Plugin;
+
+public static class BgMgrPatcher
 {
-    public static class BgMgrPatcher
-    {
-        public static event EventHandler ChangeBgBegin;
-        public static event EventHandler ChangeBgEnd;
+    public static event EventHandler ChangeBgBegin;
+
+    public static event EventHandler ChangeBgEnd;
 
-        [HarmonyPatch(typeof(BgMgr), nameof(BgMgr.ChangeBg))]
-        [HarmonyPatch(typeof(BgMgr), nameof(BgMgr.ChangeBgMyRoom))]
-        [HarmonyPrefix]
-        private static void NotifyBeginChangeBg() => ChangeBgBegin?.Invoke(null, EventArgs.Empty);
+    [HarmonyPatch(typeof(BgMgr), nameof(BgMgr.ChangeBg))]
+    [HarmonyPatch(typeof(BgMgr), nameof(BgMgr.ChangeBgMyRoom))]
+    [HarmonyPrefix]
+    private static void NotifyBeginChangeBg() =>
+        ChangeBgBegin?.Invoke(null, EventArgs.Empty);
 
-        [HarmonyPatch(typeof(BgMgr), nameof(BgMgr.ChangeBg))]
-        [HarmonyPatch(typeof(BgMgr), nameof(BgMgr.ChangeBgMyRoom))]
-        [HarmonyPostfix]
-        private static void NotifyEndChangeBg() => ChangeBgEnd?.Invoke(null, EventArgs.Empty);
-    }
+    [HarmonyPatch(typeof(BgMgr), nameof(BgMgr.ChangeBg))]
+    [HarmonyPatch(typeof(BgMgr), nameof(BgMgr.ChangeBgMyRoom))]
+    [HarmonyPostfix]
+    private static void NotifyEndChangeBg() =>
+        ChangeBgEnd?.Invoke(null, EventArgs.Empty);
 }

+ 11 - 0
src/MeidoPhotoStudio.Plugin/Patchers/ProcStartEventArgs.cs

@@ -0,0 +1,11 @@
+using System;
+
+namespace MeidoPhotoStudio.Plugin;
+
+public class ProcStartEventArgs : EventArgs
+{
+    public readonly Maid Maid;
+
+    public ProcStartEventArgs(Maid maid) =>
+        Maid = maid;
+}

+ 18 - 0
src/MeidoPhotoStudio.Plugin/PresetChangeEventArgs.cs

@@ -0,0 +1,18 @@
+using System;
+
+namespace MeidoPhotoStudio.Plugin;
+
+public class PresetChangeEventArgs : EventArgs
+{
+    public PresetChangeEventArgs(string path, string category)
+    {
+        Path = path;
+        Category = category;
+    }
+
+    public static new PresetChangeEventArgs Empty { get; } = new(string.Empty, string.Empty);
+
+    public string Category { get; }
+
+    public string Path { get; }
+}

+ 18 - 0
src/MeidoPhotoStudio.Plugin/ScreenshotEventArgs.cs

@@ -0,0 +1,18 @@
+using System;
+
+using UnityEngine;
+
+namespace MeidoPhotoStudio.Plugin;
+
+public class ScreenshotEventArgs : EventArgs
+{
+    public string Path { get; set; } = string.Empty;
+
+    public int SuperSize { get; set; } = -1;
+
+    public bool HideMaids { get; set; }
+
+    public bool InMemory { get; set; } = false;
+
+    public Texture2D Screenshot { get; set; }
+}

+ 7 - 7
src/MeidoPhotoStudio.Plugin/Serialization/ISerializer.cs

@@ -1,10 +1,10 @@
-using System.IO;
+using System.IO;
 
-namespace MeidoPhotoStudio.Plugin
+namespace MeidoPhotoStudio.Plugin;
+
+public interface ISerializer
 {
-    public interface ISerializer
-    {
-        void Serialize(object thing, BinaryWriter writer);
-        void Deserialize(object thing, BinaryReader reader, SceneMetadata metadata);
-    }
+    void Serialize(object thing, BinaryWriter writer);
+
+    void Deserialize(object thing, BinaryReader reader, SceneMetadata metadata);
 }

+ 7 - 7
src/MeidoPhotoStudio.Plugin/Serialization/ISimpleSerializer.cs

@@ -1,10 +1,10 @@
-using System.IO;
+using System.IO;
 
-namespace MeidoPhotoStudio.Plugin
+namespace MeidoPhotoStudio.Plugin;
+
+public interface ISimpleSerializer
 {
-    public interface ISimpleSerializer
-    {
-        void Serialize(object obj, BinaryWriter writer);
-        object Deserialize(BinaryReader reader, SceneMetadata metadata);
-    }
+    void Serialize(object obj, BinaryWriter writer);
+
+    object Deserialize(BinaryReader reader, SceneMetadata metadata);
 }

+ 32 - 34
src/MeidoPhotoStudio.Plugin/Serialization/SceneMetadata.cs

@@ -1,41 +1,39 @@
-using System.IO;
+using System.IO;
 
-namespace MeidoPhotoStudio.Plugin
+namespace MeidoPhotoStudio.Plugin;
+
+public class SceneMetadata
 {
-    public class SceneMetadata
-    {
-        public short Version { get; set; }
-        public bool Environment { get; set; }
-        public int MaidCount { get; set; }
-        public bool MMConverted { get; set; }
+    public short Version { get; set; }
 
-        public void WriteMetadata(BinaryWriter writer)
-        {
-            writer.Write(Version);
-            writer.Write(Environment);
-            writer.Write(MaidCount);
-            writer.Write(MMConverted);
-        }
+    public bool Environment { get; set; }
 
-        public static SceneMetadata ReadMetadata(BinaryReader reader)
-        {
-            return new()
-            {
-                Version = reader.ReadVersion(),
-                Environment = reader.ReadBoolean(),
-                MaidCount = reader.ReadInt32(),
-                MMConverted = reader.ReadBoolean()
-            };
-        }
-
-        public void Deconstruct(
-            out short version, out bool environment, out int maidCount, out bool mmConverted
-        )
+    public int MaidCount { get; set; }
+
+    public bool MMConverted { get; set; }
+
+    public static SceneMetadata ReadMetadata(BinaryReader reader) =>
+        new()
         {
-            version = Version;
-            environment = Environment;
-            mmConverted = MMConverted;
-            maidCount = MaidCount;
-        }
+            Version = reader.ReadVersion(),
+            Environment = reader.ReadBoolean(),
+            MaidCount = reader.ReadInt32(),
+            MMConverted = reader.ReadBoolean(),
+        };
+
+    public void WriteMetadata(BinaryWriter writer)
+    {
+        writer.Write(Version);
+        writer.Write(Environment);
+        writer.Write(MaidCount);
+        writer.Write(MMConverted);
+    }
+
+    public void Deconstruct(out short version, out bool environment, out int maidCount, out bool mmConverted)
+    {
+        version = Version;
+        environment = Environment;
+        mmConverted = MMConverted;
+        maidCount = MaidCount;
     }
 }

+ 34 - 28
src/MeidoPhotoStudio.Plugin/Serialization/Serialization.cs

@@ -1,42 +1,48 @@
-using System;
+using System;
 using System.Collections.Generic;
 using System.IO;
 using System.Linq;
 
-namespace MeidoPhotoStudio.Plugin
-{
-    public static class Serialization
-    {
-        private static readonly Dictionary<Type, ISerializer> Serializers;
+namespace MeidoPhotoStudio.Plugin;
 
-        private static readonly Dictionary<Type, ISimpleSerializer> SimpleSerializers;
-
-        static Serialization()
-        {
-            var types = (from t in typeof(MeidoPhotoStudio).Assembly.GetTypes()
-                let baseType = t.BaseType
-                where !t.IsAbstract && !t.IsInterface && baseType?.IsGenericType == true
-                select new { type = t, baseType }).ToArray();
+public static class Serialization
+{
+    private static readonly Dictionary<Type, ISerializer> Serializers;
 
-            Serializers = types.Where(t => t.baseType.GetGenericTypeDefinition() == typeof(Serializer<>))
-                .Select(t => new {t.type, arg = t.baseType.GetGenericArguments()[0]})
-                .ToDictionary(x => x.arg, x => (ISerializer)Activator.CreateInstance(x.type));
+    private static readonly Dictionary<Type, ISimpleSerializer> SimpleSerializers;
 
-            SimpleSerializers = types.Where(t => t.baseType.GetGenericTypeDefinition() == typeof(SimpleSerializer<>))
-                .Select(t => new {t.type, arg = t.baseType.GetGenericArguments()[0]})
-                .ToDictionary(x => x.arg, x => (ISimpleSerializer)Activator.CreateInstance(x.type));
-        }
+    static Serialization()
+    {
+        var types =
+            (from t in typeof(MeidoPhotoStudio).Assembly.GetTypes()
+             let baseType = t.BaseType
+             where !t.IsAbstract && !t.IsInterface && baseType?.IsGenericType == true
+             select new { type = t, baseType }).ToArray();
+
+        Serializers = types.Where(t => t.baseType.GetGenericTypeDefinition() == typeof(Serializer<>))
+            .Select(t => new { t.type, arg = t.baseType.GetGenericArguments()[0] })
+            .ToDictionary(x => x.arg, x => (ISerializer)Activator.CreateInstance(x.type));
+
+        SimpleSerializers = types.Where(t => t.baseType.GetGenericTypeDefinition() == typeof(SimpleSerializer<>))
+            .Select(t => new { t.type, arg = t.baseType.GetGenericArguments()[0] })
+            .ToDictionary(x => x.arg, x => (ISimpleSerializer)Activator.CreateInstance(x.type));
+    }
 
-        public static Serializer<T> Get<T>() => Serializers[typeof(T)] as Serializer<T>;
+    public static Serializer<T> Get<T>() =>
+        Serializers[typeof(T)] as Serializer<T>;
 
-        public static ISerializer Get(Type type) => Serializers[type];
+    public static ISerializer Get(Type type) =>
+        Serializers[type];
 
-        public static SimpleSerializer<T> GetSimple<T>() => SimpleSerializers[typeof(T)] as SimpleSerializer<T>;
+    public static SimpleSerializer<T> GetSimple<T>() =>
+        SimpleSerializers[typeof(T)] as SimpleSerializer<T>;
 
-        public static ISimpleSerializer GetSimple(Type type) => SimpleSerializers[type];
+    public static ISimpleSerializer GetSimple(Type type) =>
+        SimpleSerializers[type];
 
-        public static short ReadVersion(this BinaryReader reader) => reader.ReadInt16();
+    public static short ReadVersion(this BinaryReader reader) =>
+        reader.ReadInt16();
 
-        public static void WriteVersion(this BinaryWriter writer, short version) => writer.Write(version);
-    }
+    public static void WriteVersion(this BinaryWriter writer, short version) =>
+        writer.Write(version);
 }

+ 11 - 10
src/MeidoPhotoStudio.Plugin/Serialization/Serializer.cs

@@ -1,15 +1,16 @@
-using System.IO;
+using System.IO;
 
-namespace MeidoPhotoStudio.Plugin
+namespace MeidoPhotoStudio.Plugin;
+
+public abstract class Serializer<T> : ISerializer
 {
-    public abstract class Serializer<T> : ISerializer
-    {
-        void ISerializer.Serialize(object obj, BinaryWriter writer) => Serialize((T) obj, writer);
+    void ISerializer.Serialize(object obj, BinaryWriter writer) =>
+        Serialize((T)obj, writer);
+
+    void ISerializer.Deserialize(object obj, BinaryReader reader, SceneMetadata metadata) =>
+        Deserialize((T)obj, reader, metadata);
 
-        void ISerializer.Deserialize(object obj, BinaryReader reader, SceneMetadata metadata)
-            => Deserialize((T) obj, reader, metadata);
+    public abstract void Serialize(T obj, BinaryWriter writer);
 
-        public abstract void Serialize(T obj, BinaryWriter writer);
-        public abstract void Deserialize(T obj, BinaryReader reader, SceneMetadata metadata);
-    }
+    public abstract void Deserialize(T obj, BinaryReader reader, SceneMetadata metadata);
 }

+ 17 - 18
src/MeidoPhotoStudio.Plugin/Serialization/Serializers/AttachPointInfoSerializer.cs

@@ -1,27 +1,26 @@
-using System.IO;
+using System.IO;
 
-namespace MeidoPhotoStudio.Plugin
+namespace MeidoPhotoStudio.Plugin;
+
+public class AttachPointInfoSerializer : SimpleSerializer<AttachPointInfo>
 {
-    public class AttachPointInfoSerializer : SimpleSerializer<AttachPointInfo>
-    {
-        private const short version = 1;
+    private const short Version = 1;
 
-        public override void Serialize(AttachPointInfo info, BinaryWriter writer)
-        {
-            writer.WriteVersion(version);
+    public override void Serialize(AttachPointInfo info, BinaryWriter writer)
+    {
+        writer.WriteVersion(Version);
 
-            writer.Write((int) info.AttachPoint);
-            writer.Write(info.MaidIndex);
-        }
+        writer.Write((int)info.AttachPoint);
+        writer.Write(info.MaidIndex);
+    }
 
-        public override AttachPointInfo Deserialize(BinaryReader reader, SceneMetadata metadata)
-        {
-            _ = reader.ReadVersion();
+    public override AttachPointInfo Deserialize(BinaryReader reader, SceneMetadata metadata)
+    {
+        _ = reader.ReadVersion();
 
-            var attachPoint = (AttachPoint) reader.ReadInt32();
-            var maidIndex = reader.ReadInt32();
+        var attachPoint = (AttachPoint)reader.ReadInt32();
+        var maidIndex = reader.ReadInt32();
 
-            return new AttachPointInfo(attachPoint, string.Empty, maidIndex);
-        }
+        return new(attachPoint, string.Empty, maidIndex);
     }
 }

+ 20 - 21
src/MeidoPhotoStudio.Plugin/Serialization/Serializers/CameraInfoSerializer.cs

@@ -1,29 +1,28 @@
-using System.IO;
+using System.IO;
 
-namespace MeidoPhotoStudio.Plugin
+namespace MeidoPhotoStudio.Plugin;
+
+public class CameraInfoSerializer : Serializer<CameraInfo>
 {
-    public class CameraInfoSerializer : Serializer<CameraInfo>
-    {
-        private const short version = 1;
+    private const short Version = 1;
 
-        public override void Serialize(CameraInfo info, BinaryWriter writer)
-        {
-            writer.WriteVersion(version);
+    public override void Serialize(CameraInfo info, BinaryWriter writer)
+    {
+        writer.WriteVersion(Version);
 
-            writer.Write(info.TargetPos);
-            writer.Write(info.Angle);
-            writer.Write(info.Distance);
-            writer.Write(info.FOV);
-        }
+        writer.Write(info.TargetPos);
+        writer.Write(info.Angle);
+        writer.Write(info.Distance);
+        writer.Write(info.FOV);
+    }
 
-        public override void Deserialize(CameraInfo info, BinaryReader reader, SceneMetadata metadata)
-        {
-            _ = reader.ReadVersion();
+    public override void Deserialize(CameraInfo info, BinaryReader reader, SceneMetadata metadata)
+    {
+        _ = reader.ReadVersion();
 
-            info.TargetPos = reader.ReadVector3();
-            info.Angle = reader.ReadQuaternion();
-            info.Distance = reader.ReadSingle();
-            info.FOV = reader.ReadSingle();
-        }
+        info.TargetPos = reader.ReadVector3();
+        info.Angle = reader.ReadQuaternion();
+        info.Distance = reader.ReadSingle();
+        info.FOV = reader.ReadSingle();
     }
 }

+ 32 - 29
src/MeidoPhotoStudio.Plugin/Serialization/Serializers/DragPointLightSerializer.cs

@@ -1,41 +1,44 @@
-using System.IO;
+using System.IO;
 
-namespace MeidoPhotoStudio.Plugin
+namespace MeidoPhotoStudio.Plugin;
+
+public class DragPointLightSerializer : Serializer<DragPointLight>
 {
-    public class DragPointLightSerializer : Serializer<DragPointLight>
-    {
-        private const short version = 1;
-        private static Serializer<LightProperty> LightPropertySerializer => Serialization.Get<LightProperty>();
+    private const short Version = 1;
 
-        public override void Serialize(DragPointLight light, BinaryWriter writer)
-        {
-            writer.WriteVersion(version);
+    private static Serializer<LightProperty> LightPropertySerializer =>
+        Serialization.Get<LightProperty>();
 
-            LightProperty[] lightList = GetLightProperties(light);
+    public override void Serialize(DragPointLight light, BinaryWriter writer)
+    {
+        writer.WriteVersion(Version);
 
-            for (var i = 0; i < 3; i++) LightPropertySerializer.Serialize(lightList[i], writer);
+        var lightList = GetLightProperties(light);
 
-            writer.Write(light.MyObject.position);
-            writer.Write((int) light.SelectedLightType);
-            writer.Write(light.IsColourMode);
-            writer.Write(light.IsDisabled);
-        }
+        for (var i = 0; i < 3; i++)
+            LightPropertySerializer.Serialize(lightList[i], writer);
 
-        public override void Deserialize(DragPointLight light, BinaryReader reader, SceneMetadata metadata)
-        {
-            _ = reader.ReadVersion();
+        writer.Write(light.MyObject.position);
+        writer.Write((int)light.SelectedLightType);
+        writer.Write(light.IsColourMode);
+        writer.Write(light.IsDisabled);
+    }
 
-            LightProperty[] lightList = GetLightProperties(light);
-            
-            for (var i = 0; i < 3; i++) LightPropertySerializer.Deserialize(lightList[i], reader, metadata);
+    public override void Deserialize(DragPointLight light, BinaryReader reader, SceneMetadata metadata)
+    {
+        _ = reader.ReadVersion();
+
+        var lightList = GetLightProperties(light);
 
-            light.MyObject.position = reader.ReadVector3();
-            light.SetLightType((DragPointLight.MPSLightType) reader.ReadInt32());
-            light.IsColourMode = reader.ReadBoolean();
-            light.IsDisabled = reader.ReadBoolean();
-        }
+        for (var i = 0; i < 3; i++)
+            LightPropertySerializer.Deserialize(lightList[i], reader, metadata);
 
-        private static LightProperty[] GetLightProperties(DragPointLight light)
-            => Utility.GetFieldValue<DragPointLight, LightProperty[]>(light, "LightProperties");
+        light.MyObject.position = reader.ReadVector3();
+        light.SetLightType((DragPointLight.MPSLightType)reader.ReadInt32());
+        light.IsColourMode = reader.ReadBoolean();
+        light.IsDisabled = reader.ReadBoolean();
     }
+
+    private static LightProperty[] GetLightProperties(DragPointLight light) =>
+        Utility.GetFieldValue<DragPointLight, LightProperty[]>(light, "LightProperties");
 }

+ 29 - 29
src/MeidoPhotoStudio.Plugin/Serialization/Serializers/EffectSerializers/BloomEffectSerializer.cs

@@ -1,34 +1,34 @@
-using System.IO;
+using System.IO;
 
-namespace MeidoPhotoStudio.Plugin
+namespace MeidoPhotoStudio.Plugin;
+
+public class BloomEffectSerializer : Serializer<BloomEffectManager>
 {
-    public class BloomEffectSerializer : Serializer<BloomEffectManager>
+    private const short Version = 1;
+
+    public override void Serialize(BloomEffectManager effect, BinaryWriter writer)
     {
-        private const short version = 1;
-
-        public override void Serialize(BloomEffectManager effect, BinaryWriter writer)
-        {
-            writer.Write(BloomEffectManager.header);
-            writer.WriteVersion(version);
-
-            writer.Write(effect.Active);
-            writer.Write(effect.BloomValue);
-            writer.Write(effect.BlurIterations);
-            writer.Write(effect.BloomThresholdColour);
-            writer.Write(effect.BloomHDR);
-        }
-
-        public override void Deserialize(BloomEffectManager effect, BinaryReader reader, SceneMetadata metadata)
-        {
-            _ = reader.ReadVersion();
-
-            var active = reader.ReadBoolean();
-            effect.BloomValue = reader.ReadSingle();
-            effect.BlurIterations = reader.ReadInt32();
-            effect.BloomThresholdColour = reader.ReadColour();
-            effect.BloomHDR = reader.ReadBoolean();
-
-            effect.SetEffectActive(active);
-        }
+        writer.Write(BloomEffectManager.Header);
+        writer.WriteVersion(Version);
+
+        writer.Write(effect.Active);
+        writer.Write(effect.BloomValue);
+        writer.Write(effect.BlurIterations);
+        writer.Write(effect.BloomThresholdColour);
+        writer.Write(effect.BloomHDR);
+    }
+
+    public override void Deserialize(BloomEffectManager effect, BinaryReader reader, SceneMetadata metadata)
+    {
+        _ = reader.ReadVersion();
+
+        var active = reader.ReadBoolean();
+
+        effect.BloomValue = reader.ReadSingle();
+        effect.BlurIterations = reader.ReadInt32();
+        effect.BloomThresholdColour = reader.ReadColour();
+        effect.BloomHDR = reader.ReadBoolean();
+
+        effect.SetEffectActive(active);
     }
 }

+ 18 - 18
src/MeidoPhotoStudio.Plugin/Serialization/Serializers/EffectSerializers/BlurEffectSerializer.cs

@@ -1,28 +1,28 @@
-using System.IO;
+using System.IO;
 
-namespace MeidoPhotoStudio.Plugin
+namespace MeidoPhotoStudio.Plugin;
+
+public class BlurEffectSerializer : Serializer<BlurEffectManager>
 {
-    public class BlurEffectSerializer : Serializer<BlurEffectManager>
+    private const short Version = 1;
+
+    public override void Serialize(BlurEffectManager effect, BinaryWriter writer)
     {
-        private const short version = 1;
+        writer.Write(BlurEffectManager.Header);
+        writer.WriteVersion(Version);
 
-        public override void Serialize(BlurEffectManager effect, BinaryWriter writer)
-        {
-            writer.Write(BlurEffectManager.header);
-            writer.WriteVersion(version);
+        writer.Write(effect.Active);
+        writer.Write(effect.BlurSize);
+    }
 
-            writer.Write(effect.Active);
-            writer.Write(effect.BlurSize);
-        }
+    public override void Deserialize(BlurEffectManager effect, BinaryReader reader, SceneMetadata metadata)
+    {
+        _ = reader.ReadVersion();
 
-        public override void Deserialize(BlurEffectManager effect, BinaryReader reader, SceneMetadata metadata)
-        {
-            _ = reader.ReadVersion();
+        var active = reader.ReadBoolean();
 
-            var active = reader.ReadBoolean();
-            effect.BlurSize = reader.ReadSingle();
+        effect.BlurSize = reader.ReadSingle();
 
-            effect.SetEffectActive(active);
-        }
+        effect.SetEffectActive(active);
     }
 }

+ 31 - 31
src/MeidoPhotoStudio.Plugin/Serialization/Serializers/EffectSerializers/DepthOfFieldEffectSerializer.cs

@@ -1,36 +1,36 @@
-using System.IO;
+using System.IO;
 
-namespace MeidoPhotoStudio.Plugin
+namespace MeidoPhotoStudio.Plugin;
+
+public class DepthOfFieldEffectSerializer : Serializer<DepthOfFieldEffectManager>
 {
-    public class DepthOfFieldEffectSerializer : Serializer<DepthOfFieldEffectManager>
+    private const short Version = 1;
+
+    public override void Serialize(DepthOfFieldEffectManager effect, BinaryWriter writer)
     {
-        private const short version = 1;
-
-        public override void Serialize(DepthOfFieldEffectManager effect, BinaryWriter writer)
-        {
-            writer.Write(DepthOfFieldEffectManager.header);
-            writer.WriteVersion(version);
-
-            writer.Write(effect.Active);
-            writer.Write(effect.FocalLength);
-            writer.Write(effect.FocalSize);
-            writer.Write(effect.Aperture);
-            writer.Write(effect.MaxBlurSize);
-            writer.Write(effect.VisualizeFocus);
-        }
-
-        public override void Deserialize(DepthOfFieldEffectManager effect, BinaryReader reader, SceneMetadata metadata)
-        {
-            _ = reader.ReadVersion();
-
-            var active = reader.ReadBoolean();
-            effect.FocalLength = reader.ReadSingle();
-            effect.FocalSize = reader.ReadSingle();
-            effect.Aperture = reader.ReadSingle();
-            effect.MaxBlurSize = reader.ReadSingle();
-            effect.VisualizeFocus = reader.ReadBoolean();
-
-            effect.SetEffectActive(active);
-        }
+        writer.Write(DepthOfFieldEffectManager.Header);
+        writer.WriteVersion(Version);
+
+        writer.Write(effect.Active);
+        writer.Write(effect.FocalLength);
+        writer.Write(effect.FocalSize);
+        writer.Write(effect.Aperture);
+        writer.Write(effect.MaxBlurSize);
+        writer.Write(effect.VisualizeFocus);
+    }
+
+    public override void Deserialize(DepthOfFieldEffectManager effect, BinaryReader reader, SceneMetadata metadata)
+    {
+        _ = reader.ReadVersion();
+
+        var active = reader.ReadBoolean();
+
+        effect.FocalLength = reader.ReadSingle();
+        effect.FocalSize = reader.ReadSingle();
+        effect.Aperture = reader.ReadSingle();
+        effect.MaxBlurSize = reader.ReadSingle();
+        effect.VisualizeFocus = reader.ReadBoolean();
+
+        effect.SetEffectActive(active);
     }
 }

+ 31 - 31
src/MeidoPhotoStudio.Plugin/Serialization/Serializers/EffectSerializers/FogEffectSerializer.cs

@@ -1,36 +1,36 @@
-using System.IO;
+using System.IO;
 
-namespace MeidoPhotoStudio.Plugin
+namespace MeidoPhotoStudio.Plugin;
+
+public class FogEffectSerializer : Serializer<FogEffectManager>
 {
-    public class FogEffectSerializer : Serializer<FogEffectManager>
+    private const short Version = 1;
+
+    public override void Serialize(FogEffectManager effect, BinaryWriter writer)
     {
-        private const short version = 1;
-
-        public override void Serialize(FogEffectManager effect, BinaryWriter writer)
-        {
-            writer.Write(FogEffectManager.header);
-            writer.WriteVersion(version);
-
-            writer.Write(effect.Active);
-            writer.Write(effect.Distance);
-            writer.Write(effect.Density);
-            writer.Write(effect.HeightScale);
-            writer.Write(effect.Height);
-            writer.WriteColour(effect.FogColour);
-        }
-
-        public override void Deserialize(FogEffectManager effect, BinaryReader reader, SceneMetadata metadata)
-        {
-            _ = reader.ReadVersion();
-
-            var active = reader.ReadBoolean();
-            effect.Distance = reader.ReadSingle();
-            effect.Density = reader.ReadSingle();
-            effect.HeightScale = reader.ReadSingle();
-            effect.Height = reader.ReadSingle();
-            effect.FogColour = reader.ReadColour();
-
-            effect.SetEffectActive(active);
-        }
+        writer.Write(FogEffectManager.Header);
+        writer.WriteVersion(Version);
+
+        writer.Write(effect.Active);
+        writer.Write(effect.Distance);
+        writer.Write(effect.Density);
+        writer.Write(effect.HeightScale);
+        writer.Write(effect.Height);
+        writer.WriteColour(effect.FogColour);
+    }
+
+    public override void Deserialize(FogEffectManager effect, BinaryReader reader, SceneMetadata metadata)
+    {
+        _ = reader.ReadVersion();
+
+        var active = reader.ReadBoolean();
+
+        effect.Distance = reader.ReadSingle();
+        effect.Density = reader.ReadSingle();
+        effect.HeightScale = reader.ReadSingle();
+        effect.Height = reader.ReadSingle();
+        effect.FogColour = reader.ReadColour();
+
+        effect.SetEffectActive(active);
     }
 }

+ 15 - 15
src/MeidoPhotoStudio.Plugin/Serialization/Serializers/EffectSerializers/SepiaToneEffectSerializer.cs

@@ -1,24 +1,24 @@
-using System.IO;
+using System.IO;
 
-namespace MeidoPhotoStudio.Plugin
+namespace MeidoPhotoStudio.Plugin;
+
+public class SepiaToneEffectSerializer : Serializer<SepiaToneEffectManager>
 {
-    public class SepiaToneEffectSerializer : Serializer<SepiaToneEffectManger>
+    private const short Version = 1;
+
+    public override void Serialize(SepiaToneEffectManager effect, BinaryWriter writer)
     {
-        private const short version = 1;
+        writer.Write(SepiaToneEffectManager.Header);
 
-        public override void Serialize(SepiaToneEffectManger effect, BinaryWriter writer)
-        {
-            writer.Write(SepiaToneEffectManger.header);
-            writer.WriteVersion(version);
+        writer.WriteVersion(Version);
 
-            writer.Write(effect.Active);
-        }
+        writer.Write(effect.Active);
+    }
 
-        public override void Deserialize(SepiaToneEffectManger effect, BinaryReader reader, SceneMetadata metadata)
-        {
-            _ = reader.ReadVersion();
+    public override void Deserialize(SepiaToneEffectManager effect, BinaryReader reader, SceneMetadata metadata)
+    {
+        _ = reader.ReadVersion();
 
-            effect.SetEffectActive(reader.ReadBoolean());
-        }
+        effect.SetEffectActive(reader.ReadBoolean());
     }
 }

+ 29 - 29
src/MeidoPhotoStudio.Plugin/Serialization/Serializers/EffectSerializers/VignetteEffectSerializer.cs

@@ -1,34 +1,34 @@
-using System.IO;
+using System.IO;
 
-namespace MeidoPhotoStudio.Plugin
+namespace MeidoPhotoStudio.Plugin;
+
+public class VignetteEffectSerializer : Serializer<VignetteEffectManager>
 {
-    public class VignetteEffectSerializer : Serializer<VignetteEffectManager>
+    private const short Version = 1;
+
+    public override void Serialize(VignetteEffectManager manager, BinaryWriter writer)
     {
-        private const short version = 1;
-
-        public override void Serialize(VignetteEffectManager manager, BinaryWriter writer)
-        {
-            writer.Write(VignetteEffectManager.header);
-            writer.WriteVersion(version);
-
-            writer.Write(manager.Active);
-            writer.Write(manager.Intensity);
-            writer.Write(manager.Blur);
-            writer.Write(manager.BlurSpread);
-            writer.Write(manager.ChromaticAberration);
-        }
-
-        public override void Deserialize(VignetteEffectManager manager, BinaryReader reader, SceneMetadata metadata)
-        {
-            _ = reader.ReadVersion();
-
-            var active = reader.ReadBoolean();
-            manager.Intensity = reader.ReadSingle();
-            manager.Blur = reader.ReadSingle();
-            manager.BlurSpread = reader.ReadSingle();
-            manager.ChromaticAberration = reader.ReadSingle();
-
-            manager.SetEffectActive(active);
-        }
+        writer.Write(VignetteEffectManager.Header);
+        writer.WriteVersion(Version);
+
+        writer.Write(manager.Active);
+        writer.Write(manager.Intensity);
+        writer.Write(manager.Blur);
+        writer.Write(manager.BlurSpread);
+        writer.Write(manager.ChromaticAberration);
+    }
+
+    public override void Deserialize(VignetteEffectManager manager, BinaryReader reader, SceneMetadata metadata)
+    {
+        _ = reader.ReadVersion();
+
+        var active = reader.ReadBoolean();
+
+        manager.Intensity = reader.ReadSingle();
+        manager.Blur = reader.ReadSingle();
+        manager.BlurSpread = reader.ReadSingle();
+        manager.ChromaticAberration = reader.ReadSingle();
+
+        manager.SetEffectActive(active);
     }
 }

+ 24 - 25
src/MeidoPhotoStudio.Plugin/Serialization/Serializers/LightPropertySerializer.cs

@@ -1,33 +1,32 @@
-using System.IO;
+using System.IO;
 
-namespace MeidoPhotoStudio.Plugin
+namespace MeidoPhotoStudio.Plugin;
+
+public class LightPropertySerializer : Serializer<LightProperty>
 {
-    public class LightPropertySerializer : Serializer<LightProperty>
-    {
-        private const short version = 1;
+    private const short Version = 1;
 
-        public override void Serialize(LightProperty prop, BinaryWriter writer)
-        {
-            writer.WriteVersion(version);
+    public override void Serialize(LightProperty prop, BinaryWriter writer)
+    {
+        writer.WriteVersion(Version);
 
-            writer.Write(prop.Rotation);
-            writer.Write(prop.Intensity);
-            writer.Write(prop.Range);
-            writer.Write(prop.SpotAngle);
-            writer.Write(prop.ShadowStrength);
-            writer.Write(prop.LightColour);
-        }
+        writer.Write(prop.Rotation);
+        writer.Write(prop.Intensity);
+        writer.Write(prop.Range);
+        writer.Write(prop.SpotAngle);
+        writer.Write(prop.ShadowStrength);
+        writer.Write(prop.LightColour);
+    }
 
-        public override void Deserialize(LightProperty prop, BinaryReader reader, SceneMetadata metadata)
-        {
-            _ = reader.ReadVersion();
+    public override void Deserialize(LightProperty prop, BinaryReader reader, SceneMetadata metadata)
+    {
+        _ = reader.ReadVersion();
 
-            prop.Rotation = reader.ReadQuaternion();
-            prop.Intensity = reader.ReadSingle();
-            prop.Range = reader.ReadSingle();
-            prop.SpotAngle = reader.ReadSingle();
-            prop.ShadowStrength = reader.ReadSingle();
-            prop.LightColour = reader.ReadColour();
-        }
+        prop.Rotation = reader.ReadQuaternion();
+        prop.Intensity = reader.ReadSingle();
+        prop.Range = reader.ReadSingle();
+        prop.SpotAngle = reader.ReadSingle();
+        prop.ShadowStrength = reader.ReadSingle();
+        prop.LightColour = reader.ReadColour();
     }
 }

+ 38 - 32
src/MeidoPhotoStudio.Plugin/Serialization/Serializers/ManagerSerializers/CameraManagerSerializer.cs

@@ -1,50 +1,56 @@
-using System.IO;
+using System.IO;
 
-namespace MeidoPhotoStudio.Plugin
+namespace MeidoPhotoStudio.Plugin;
+
+public class CameraManagerSerializer : Serializer<CameraManager>
 {
-    public class CameraManagerSerializer : Serializer<CameraManager>
+    private const short Version = 1;
+
+    private static readonly CameraInfo DummyInfo = new();
+
+    private static Serializer<CameraInfo> InfoSerializer =>
+        Serialization.Get<CameraInfo>();
+
+    public override void Serialize(CameraManager manager, BinaryWriter writer)
     {
-        private const short version = 1;
-        private static Serializer<CameraInfo> InfoSerializer => Serialization.Get<CameraInfo>();
-        private static readonly CameraInfo dummyInfo = new();
+        writer.Write(CameraManager.Header);
+        writer.WriteVersion(Version);
 
-        public override void Serialize(CameraManager manager, BinaryWriter writer)
-        {
-            writer.Write(CameraManager.header);
-            writer.WriteVersion(version);
+        var cameraInfos = GetCameraInfos(manager);
 
-            CameraInfo[] cameraInfos = GetCameraInfos(manager);
-            cameraInfos[manager.CurrentCameraIndex].UpdateInfo(CameraUtility.MainCamera);
+        cameraInfos[manager.CurrentCameraIndex].UpdateInfo(CameraUtility.MainCamera);
 
-            writer.Write(manager.CurrentCameraIndex);
-            writer.Write(manager.CameraCount);
-            foreach (var info in cameraInfos) InfoSerializer.Serialize(info, writer);
+        writer.Write(manager.CurrentCameraIndex);
+        writer.Write(manager.CameraCount);
 
-            CameraUtility.StopAll();
-        }
+        foreach (var info in cameraInfos)
+            InfoSerializer.Serialize(info, writer);
 
-        public override void Deserialize(CameraManager manager, BinaryReader reader, SceneMetadata metadata)
-        {
-            _ = reader.ReadVersion();
+        CameraUtility.StopAll();
+    }
 
-            var camera = CameraUtility.MainCamera;
+    public override void Deserialize(CameraManager manager, BinaryReader reader, SceneMetadata metadata)
+    {
+        _ = reader.ReadVersion();
 
-            manager.CurrentCameraIndex = reader.ReadInt32();
+        var camera = CameraUtility.MainCamera;
 
-            var cameraCount = reader.ReadInt32();
+        manager.CurrentCameraIndex = reader.ReadInt32();
 
-            CameraInfo[] cameraInfos = GetCameraInfos(manager);
-            for (var i = 0; i < cameraCount; i++)
-                InfoSerializer.Deserialize(i >= manager.CameraCount ? dummyInfo : cameraInfos[i], reader, metadata);
+        var cameraCount = reader.ReadInt32();
+        var cameraInfos = GetCameraInfos(manager);
 
-            if (metadata.Environment) return;
+        for (var i = 0; i < cameraCount; i++)
+            InfoSerializer.Deserialize(i >= manager.CameraCount ? DummyInfo : cameraInfos[i], reader, metadata);
 
-            cameraInfos[manager.CurrentCameraIndex].Apply(camera);
+        if (metadata.Environment)
+            return;
 
-            CameraUtility.StopAll();
-        }
+        cameraInfos[manager.CurrentCameraIndex].Apply(camera);
 
-        private static CameraInfo[] GetCameraInfos(CameraManager manager)
-            => Utility.GetFieldValue<CameraManager, CameraInfo[]>(manager, "cameraInfos");
+        CameraUtility.StopAll();
     }
+
+    private static CameraInfo[] GetCameraInfos(CameraManager manager) =>
+        Utility.GetFieldValue<CameraManager, CameraInfo[]>(manager, "cameraInfos");
 }

+ 30 - 29
src/MeidoPhotoStudio.Plugin/Serialization/Serializers/ManagerSerializers/EffectManagerSerializer.cs

@@ -1,43 +1,44 @@
-using System;
+using System;
 using System.Collections.Generic;
 using System.IO;
 using System.Linq;
 
-namespace MeidoPhotoStudio.Plugin
+namespace MeidoPhotoStudio.Plugin;
+
+public class EffectManagerSerializer : Serializer<EffectManager>
 {
-    public class EffectManagerSerializer : Serializer<EffectManager>
+    private const short Version = 1;
+
+    public override void Serialize(EffectManager manager, BinaryWriter writer)
     {
-        private const short version = 1;
+        writer.Write(EffectManager.Header);
+        writer.WriteVersion(Version);
 
-        public override void Serialize(EffectManager manager, BinaryWriter writer)
-        {
-            writer.Write(EffectManager.header);
-            writer.WriteVersion(version);
+        foreach (var effectManager in GetEffectManagers(manager).Values)
+            Serialization.Get(effectManager.GetType()).Serialize(effectManager, writer);
 
-            foreach (var effectManager in GetEffectManagers(manager).Values)
-                Serialization.Get(effectManager.GetType()).Serialize(effectManager, writer);
+        writer.Write(EffectManager.Footer);
+    }
 
-            writer.Write(EffectManager.footer);
-        }
+    public override void Deserialize(EffectManager manager, BinaryReader reader, SceneMetadata metadata)
+    {
+        _ = reader.ReadVersion();
 
-        public override void Deserialize(EffectManager manager, BinaryReader reader, SceneMetadata metadata)
+        var headerToManager =
+            GetEffectManagers(manager).ToDictionary(
+                x => (string)x.Key.GetField("header").GetValue(null),
+                y => y.Value);
+
+        string header;
+
+        while ((header = reader.ReadString()) is not EffectManager.Footer)
         {
-            _ = reader.ReadVersion();
-
-            Dictionary<string, IEffectManager> headerToManager = GetEffectManagers(manager).ToDictionary(
-                x => (string) x.Key.GetField("header").GetValue(null),
-                y => y.Value
-            );
-
-            string header;
-            while ((header = reader.ReadString()) != EffectManager.footer)
-            {
-                var effectManager = headerToManager[header];
-                Serialization.Get(effectManager.GetType()).Deserialize(effectManager, reader, metadata);
-            }
-        }
+            var effectManager = headerToManager[header];
 
-        private static Dictionary<Type, IEffectManager> GetEffectManagers(EffectManager manager)
-            => Utility.GetFieldValue<EffectManager, Dictionary<Type, IEffectManager>>(manager, "EffectManagers");
+            Serialization.Get(effectManager.GetType()).Deserialize(effectManager, reader, metadata);
+        }
     }
+
+    private static Dictionary<Type, IEffectManager> GetEffectManagers(EffectManager manager) =>
+        Utility.GetFieldValue<EffectManager, Dictionary<Type, IEffectManager>>(manager, "EffectManagers");
 }

+ 52 - 49
src/MeidoPhotoStudio.Plugin/Serialization/Serializers/ManagerSerializers/EnvironmentManagerSerializer.cs

@@ -1,71 +1,74 @@
-using System;
-using System.Collections.Generic;
+using System;
 using System.IO;
-using UnityEngine;
 
-namespace MeidoPhotoStudio.Plugin
-{
-    public class EnvironmentManagerSerializer : Serializer<EnvironmentManager>
-    {
-        private const short version = 1;
+using UnityEngine;
 
-        private static SimpleSerializer<TransformDTO> TransformDtoSerializer => Serialization.GetSimple<TransformDTO>();
+namespace MeidoPhotoStudio.Plugin;
 
-        public override void Serialize(EnvironmentManager manager, BinaryWriter writer)
-        {
-            writer.Write(EnvironmentManager.header);
-            writer.WriteVersion(version);
-
-            writer.Write(manager.CurrentBgAsset);
+public class EnvironmentManagerSerializer : Serializer<EnvironmentManager>
+{
+    private const short Version = 1;
 
-            var bgTransform = GetBgTransform(manager);
-            var transformDto = bgTransform ? new TransformDTO(bgTransform) : new TransformDTO();
+    private static SimpleSerializer<TransformDTO> TransformDtoSerializer =>
+        Serialization.GetSimple<TransformDTO>();
 
-            TransformDtoSerializer.Serialize(transformDto, writer);
-        }
+    public override void Serialize(EnvironmentManager manager, BinaryWriter writer)
+    {
+        writer.Write(EnvironmentManager.Header);
+        writer.WriteVersion(Version);
 
-        public override void Deserialize(EnvironmentManager manager, BinaryReader reader, SceneMetadata metadata)
-        {
-            _ = reader.ReadVersion();
+        writer.Write(manager.CurrentBgAsset);
 
-            var bgAsset = reader.ReadString();
+        var bgTransform = GetBgTransform(manager);
+        var transformDto = bgTransform ? new TransformDTO(bgTransform) : new TransformDTO();
 
-            var transformDto = TransformDtoSerializer.Deserialize(reader, metadata);
+        TransformDtoSerializer.Serialize(transformDto, writer);
+    }
 
-            var creativeBg = Utility.IsGuidString(bgAsset);
+    public override void Deserialize(EnvironmentManager manager, BinaryReader reader, SceneMetadata metadata)
+    {
+        _ = reader.ReadVersion();
 
-            List<string> bgList = creativeBg
-                ? Constants.MyRoomCustomBGList.ConvertAll(kvp => kvp.Key)
-                : Constants.BGList;
+        var bgAsset = reader.ReadString();
+        var transformDto = TransformDtoSerializer.Deserialize(reader, metadata);
+        var creativeBg = Utility.IsGuidString(bgAsset);
 
-            var assetIndex = bgList.FindIndex(
-                asset => asset.Equals(bgAsset, StringComparison.InvariantCultureIgnoreCase)
-            );
+        var bgList = creativeBg
+            ? Constants.MyRoomCustomBGList.ConvertAll(kvp => kvp.Key)
+            : Constants.BGList;
 
-            var validBg = assetIndex >= 0;
+        var assetIndex =
+            bgList.FindIndex(asset => asset.Equals(bgAsset, StringComparison.InvariantCultureIgnoreCase));
 
-            if (validBg) bgAsset = bgList[assetIndex];
-            else
-            {
-                Utility.LogWarning($"Could not load BG '{bgAsset}'");
-                creativeBg = false;
-                bgAsset = EnvironmentManager.defaultBg;
-            }
+        var validBg = assetIndex >= 0;
 
-            manager.ChangeBackground(bgAsset, creativeBg);
+        if (validBg)
+        {
+            bgAsset = bgList[assetIndex];
+        }
+        else
+        {
+            Utility.LogWarning($"Could not load BG '{bgAsset}'");
+            creativeBg = false;
+            bgAsset = EnvironmentManager.DefaultBg;
+        }
 
-            if (!validBg) return;
+        manager.ChangeBackground(bgAsset, creativeBg);
 
-            var bg = GetBgTransform(manager);
+        if (!validBg)
+            return;
 
-            if (!bg) return;
+        var bg = GetBgTransform(manager);
 
-            bg.position = transformDto.Position;
-            bg.rotation = transformDto.Rotation;
-            bg.localScale = transformDto.LocalScale;
-        }
+        if (!bg)
+            return;
 
-        private static Transform GetBgTransform(EnvironmentManager manager)
-            => Utility.GetFieldValue<EnvironmentManager, Transform>(manager, "bg");
+        // TODO: Use transform.SetPositionAndRotation
+        bg.position = transformDto.Position;
+        bg.rotation = transformDto.Rotation;
+        bg.localScale = transformDto.LocalScale;
     }
+
+    private static Transform GetBgTransform(EnvironmentManager manager) =>
+        Utility.GetFieldValue<EnvironmentManager, Transform>(manager, "bg");
 }

+ 33 - 30
src/MeidoPhotoStudio.Plugin/Serialization/Serializers/ManagerSerializers/LightManagerSerializer.cs

@@ -1,43 +1,46 @@
-using System.Collections.Generic;
+using System.Collections.Generic;
 using System.IO;
 
-namespace MeidoPhotoStudio.Plugin
+namespace MeidoPhotoStudio.Plugin;
+
+public class LightManagerSerializer : Serializer<LightManager>
 {
-    public class LightManagerSerializer : Serializer<LightManager>
+    private const short Version = 1;
+
+    private static Serializer<DragPointLight> LightSerializer =>
+        Serialization.Get<DragPointLight>();
+
+    public override void Serialize(LightManager manager, BinaryWriter writer)
     {
-        private const short version = 1;
-        private static Serializer<DragPointLight> LightSerializer => Serialization.Get<DragPointLight>();
+        writer.Write(LightManager.Header);
+        writer.WriteVersion(Version);
 
-        public override void Serialize(LightManager manager, BinaryWriter writer)
-        {
-            writer.Write(LightManager.header);
-            writer.WriteVersion(version);
+        var list = GetLightList(manager);
 
-            List<DragPointLight> list = GetLightList(manager);
-            writer.Write(list.Count);
-            foreach (var light in list) LightSerializer.Serialize(light, writer);
-        }
+        writer.Write(list.Count);
 
-        public override void Deserialize(LightManager manager, BinaryReader reader, SceneMetadata metadata)
-        {
-            manager.ClearLights();
+        foreach (var light in list)
+            LightSerializer.Serialize(light, writer);
+    }
 
-            _ = reader.ReadVersion();
+    public override void Deserialize(LightManager manager, BinaryReader reader, SceneMetadata metadata)
+    {
+        manager.ClearLights();
 
-            var lightCount = reader.ReadInt32();
+        _ = reader.ReadVersion();
 
-            List<DragPointLight> list = GetLightList(manager);
-            
-            
-            LightSerializer.Deserialize(list[0], reader, metadata);
-            for (var i = 1; i < lightCount; i++)
-            {
-                manager.AddLight();
-                LightSerializer.Deserialize(list[i], reader, metadata);
-            }
-        }
+        var lightCount = reader.ReadInt32();
+        var list = GetLightList(manager);
+
+        LightSerializer.Deserialize(list[0], reader, metadata);
 
-        private static List<DragPointLight> GetLightList(LightManager manager)
-            => Utility.GetFieldValue<LightManager, List<DragPointLight>>(manager, "lightList");
+        for (var i = 1; i < lightCount; i++)
+        {
+            manager.AddLight();
+            LightSerializer.Deserialize(list[i], reader, metadata);
+        }
     }
+
+    private static List<DragPointLight> GetLightList(LightManager manager) =>
+        Utility.GetFieldValue<LightManager, List<DragPointLight>>(manager, "lightList");
 }

+ 67 - 60
src/MeidoPhotoStudio.Plugin/Serialization/Serializers/ManagerSerializers/MeidoManagerSerializer.cs

@@ -1,86 +1,93 @@
-using System.Collections.Generic;
 using System.IO;
+
 using UnityEngine;
 
-namespace MeidoPhotoStudio.Plugin
+namespace MeidoPhotoStudio.Plugin;
+
+public class MeidoManagerSerializer : Serializer<MeidoManager>
 {
-    public class MeidoManagerSerializer : Serializer<MeidoManager>
+    private const short Version = 1;
+
+    private static Serializer<Meido> MeidoSerializer =>
+        Serialization.Get<Meido>();
+
+    public override void Serialize(MeidoManager manager, BinaryWriter writer)
     {
-        private const short version = 1;
-        private static Serializer<Meido> MeidoSerializer => Serialization.Get<Meido>();
+        writer.Write(MeidoManager.Header);
+        writer.WriteVersion(Version);
 
-        public override void Serialize(MeidoManager manager, BinaryWriter writer)
-        {
-            writer.Write(MeidoManager.header);
-            writer.WriteVersion(version);
+        var meidoList = manager.ActiveMeidoList;
+
+        var meidoCount = meidoList.Count;
 
-            List<Meido> meidoList = manager.ActiveMeidoList;
+        var hairPosition = Vector3.zero;
+        var skirtPosition = Vector3.zero;
 
-            var meidoCount = meidoList.Count;
+        var hairMeidoFound = false;
+        var skirtMeidoFound = false;
 
-            var hairPosition = Vector3.zero;
-            var skirtPosition = Vector3.zero;
+        var globalGravity = manager.GlobalGravity;
 
-            var hairMeidoFound = false;
-            var skirtMeidoFound = false;
+        writer.Write(meidoCount);
 
-            var globalGravity = manager.GlobalGravity;
+        foreach (var meido in meidoList)
+        {
+            MeidoSerializer.Serialize(meido, writer);
+
+            if (!globalGravity || meidoCount <= 0)
+                continue;
 
-            writer.Write(meidoCount);
-            foreach (var meido in meidoList)
+            // Get gravity and skirt control positions to apply to meidos past the meido count
+            if (!hairMeidoFound && meido.HairGravityControl.Valid)
             {
-                MeidoSerializer.Serialize(meido, writer);
-
-                if (!globalGravity || meidoCount <= 0) continue;
-
-                // Get gravity and skirt control positions to apply to meidos past the meido count
-                if (!hairMeidoFound && meido.HairGravityControl.Valid)
-                {
-                    hairPosition = meido.HairGravityControl.Control.transform.localPosition;
-                    hairMeidoFound = true;
-                }
-                else if (!skirtMeidoFound && meido.SkirtGravityControl.Valid)
-                {
-                    skirtPosition = meido.SkirtGravityControl.Control.transform.localPosition;
-                    skirtMeidoFound = true;
-                }
+                hairPosition = meido.HairGravityControl.Control.transform.localPosition;
+                hairMeidoFound = true;
+            }
+            else if (!skirtMeidoFound && meido.SkirtGravityControl.Valid)
+            {
+                skirtPosition = meido.SkirtGravityControl.Control.transform.localPosition;
+                skirtMeidoFound = true;
             }
-
-            writer.Write(globalGravity);
-            writer.Write(hairPosition);
-            writer.Write(skirtPosition);
         }
 
-        public override void Deserialize(MeidoManager manager, BinaryReader reader, SceneMetadata metadata)
-        {
-            _ = reader.ReadVersion();
+        writer.Write(globalGravity);
+        writer.Write(hairPosition);
+        writer.Write(skirtPosition);
+    }
+
+    public override void Deserialize(MeidoManager manager, BinaryReader reader, SceneMetadata metadata)
+    {
+        _ = reader.ReadVersion();
 
-            var meidoCount = reader.ReadInt32();
-            for (var i = 0; i < meidoCount; i++)
+        var meidoCount = reader.ReadInt32();
+
+        for (var i = 0; i < meidoCount; i++)
+        {
+            if (i >= manager.ActiveMeidoList.Count)
             {
-                if (i >= manager.ActiveMeidoList.Count)
-                {
-                    reader.BaseStream.Seek(reader.ReadInt64(), SeekOrigin.Current);
-                    continue;
-                }
+                reader.BaseStream.Seek(reader.ReadInt64(), SeekOrigin.Current);
 
-                MeidoSerializer.Deserialize(manager.ActiveMeidoList[i], reader, metadata);
+                continue;
             }
 
-            var globalGravity = reader.ReadBoolean();
-            var hairPosition = reader.ReadVector3();
-            var skirtPosition = reader.ReadVector3();
-            Utility.SetFieldValue(manager, "globalGravity", globalGravity);
+            MeidoSerializer.Deserialize(manager.ActiveMeidoList[i], reader, metadata);
+        }
 
-            if (!globalGravity) return;
+        var globalGravity = reader.ReadBoolean();
+        var hairPosition = reader.ReadVector3();
+        var skirtPosition = reader.ReadVector3();
 
-            foreach (var meido in manager.ActiveMeidoList)
-            {
-                meido.HairGravityActive = true;
-                meido.SkirtGravityActive = true;
-                meido.ApplyGravity(hairPosition);
-                meido.ApplyGravity(skirtPosition, true);
-            }
+        Utility.SetFieldValue(manager, "globalGravity", globalGravity);
+
+        if (!globalGravity)
+            return;
+
+        foreach (var meido in manager.ActiveMeidoList)
+        {
+            meido.HairGravityActive = true;
+            meido.SkirtGravityActive = true;
+            meido.ApplyGravity(hairPosition);
+            meido.ApplyGravity(skirtPosition, true);
         }
     }
 }

+ 31 - 28
src/MeidoPhotoStudio.Plugin/Serialization/Serializers/ManagerSerializers/MessageWindowManagerSerializer.cs

@@ -1,33 +1,36 @@
-using System.IO;
+using System.IO;
 
-namespace MeidoPhotoStudio.Plugin
+namespace MeidoPhotoStudio.Plugin;
+
+public class MessageWindowManagerSerializer : Serializer<MessageWindowManager>
 {
-    public class MessageWindowManagerSerializer : Serializer<MessageWindowManager>
+    private const short Version = 1;
+
+    public override void Serialize(MessageWindowManager manager, BinaryWriter writer)
+    {
+        writer.Write(MessageWindowManager.Header);
+        writer.WriteVersion(Version);
+
+        writer.Write(manager.ShowingMessage);
+        writer.Write(manager.FontSize);
+        writer.Write(manager.MessageName);
+        writer.Write(manager.MessageText);
+    }
+
+    public override void Deserialize(MessageWindowManager manager, BinaryReader reader, SceneMetadata metadata)
     {
-        private const short version = 1;
-
-        public override void Serialize(MessageWindowManager manager, BinaryWriter writer)
-        {
-            writer.Write(MessageWindowManager.header);
-            writer.WriteVersion(version);
-
-            writer.Write(manager.ShowingMessage);
-            writer.Write(manager.FontSize);
-            writer.Write(manager.MessageName);
-            writer.Write(manager.MessageText);
-        }
-
-        public override void Deserialize(MessageWindowManager manager, BinaryReader reader, SceneMetadata metadata)
-        {
-            manager.CloseMessagePanel();
-
-            _ = reader.ReadVersion();
-
-            var showingMessage = reader.ReadBoolean();
-            manager.FontSize = reader.ReadInt32();
-            var messageName = reader.ReadString();
-            var messageText = reader.ReadString();
-            if (showingMessage) manager.ShowMessage(messageName, messageText);
-        }
+        manager.CloseMessagePanel();
+
+        _ = reader.ReadVersion();
+
+        var showingMessage = reader.ReadBoolean();
+
+        manager.FontSize = reader.ReadInt32();
+
+        var messageName = reader.ReadString();
+        var messageText = reader.ReadString();
+
+        if (showingMessage)
+            manager.ShowMessage(messageName, messageText);
     }
 }

+ 48 - 45
src/MeidoPhotoStudio.Plugin/Serialization/Serializers/ManagerSerializers/PropManagerSerializer.cs

@@ -1,69 +1,72 @@
-using System.Collections.Generic;
+using System.Collections.Generic;
 using System.IO;
 
-namespace MeidoPhotoStudio.Plugin
+namespace MeidoPhotoStudio.Plugin;
+
+public class PropManagerSerializer : Serializer<PropManager>
 {
-    public class PropManagerSerializer : Serializer<PropManager>
-    {
-        private const short version = 1;
+    private const short Version = 1;
 
-        private static SimpleSerializer<DragPointPropDTO> DragPointDtoSerializer
-            => Serialization.GetSimple<DragPointPropDTO>();
+    private static SimpleSerializer<DragPointPropDTO> DragPointDtoSerializer =>
+        Serialization.GetSimple<DragPointPropDTO>();
 
-        public override void Serialize(PropManager manager, BinaryWriter writer)
-        {
-            writer.Write(PropManager.header);
-            writer.WriteVersion(version);
+    public override void Serialize(PropManager manager, BinaryWriter writer)
+    {
+        writer.Write(PropManager.Header);
+        writer.WriteVersion(Version);
 
-            List<DragPointProp> propList = GetPropList(manager);
+        var propList = GetPropList(manager);
 
-            writer.Write(propList.Count);
-            foreach (var prop in propList) DragPointDtoSerializer.Serialize(new DragPointPropDTO(prop), writer);
-        }
+        writer.Write(propList.Count);
 
-        public override void Deserialize(PropManager manager, BinaryReader reader, SceneMetadata metadata)
-        {
-            _ = reader.ReadVersion();
+        foreach (var prop in propList)
+            DragPointDtoSerializer.Serialize(new DragPointPropDTO(prop), writer);
+    }
 
-            manager.DeleteAllProps();
+    public override void Deserialize(PropManager manager, BinaryReader reader, SceneMetadata metadata)
+    {
+        _ = reader.ReadVersion();
+
+        manager.DeleteAllProps();
 
-            List<DragPointProp> propList = GetPropList(manager);
+        var propList = GetPropList(manager);
+        var propCount = reader.ReadInt32();
+        var propIndex = 0;
 
-            var propCount = reader.ReadInt32();
-            var propIndex = 0;
-            for (var i = 0; i < propCount; i++)
-            {
-                var dragPointPropDto = DragPointDtoSerializer.Deserialize(reader, metadata);
+        for (var i = 0; i < propCount; i++)
+        {
+            var dragPointPropDto = DragPointDtoSerializer.Deserialize(reader, metadata);
 
-                if (!manager.AddFromPropInfo(dragPointPropDto.PropInfo)) continue;
+            if (!manager.AddFromPropInfo(dragPointPropDto.PropInfo))
+                continue;
 
-                Apply(manager, propList[propIndex], dragPointPropDto);
+            Apply(manager, propList[propIndex], dragPointPropDto);
 
-                propIndex++;
-            }
+            propIndex++;
         }
+    }
 
-        private static void Apply(PropManager manager, DragPointProp prop, DragPointPropDTO dto)
-        {
-            var (transformDto, attachPointInfo, shadowCasting) = dto;
+    private static List<DragPointProp> GetPropList(PropManager manager) =>
+        Utility.GetFieldValue<PropManager, List<DragPointProp>>(manager, "propList");
 
-            prop.ShadowCasting = shadowCasting;
+    private static void Apply(PropManager manager, DragPointProp prop, DragPointPropDTO dto)
+    {
+        var (transformDto, attachPointInfo, shadowCasting) = dto;
 
-            var transform = prop.MyObject;
+        prop.ShadowCasting = shadowCasting;
 
-            if (attachPointInfo.AttachPoint != AttachPoint.None)
-            {
-                manager.AttachProp(prop, attachPointInfo.AttachPoint, attachPointInfo.MaidIndex);
-                transform.localPosition = transformDto.LocalPosition;
-                transform.localRotation = transformDto.LocalRotation;
-            }
+        var transform = prop.MyObject;
 
-            transform.position = transformDto.Position;
-            transform.rotation = transformDto.Rotation;
-            transform.localScale = transformDto.LocalScale;
+        if (attachPointInfo.AttachPoint is not AttachPoint.None)
+        {
+            manager.AttachProp(prop, attachPointInfo.AttachPoint, attachPointInfo.MaidIndex);
+            transform.localPosition = transformDto.LocalPosition;
+            transform.localRotation = transformDto.LocalRotation;
         }
 
-        private static List<DragPointProp> GetPropList(PropManager manager)
-            => Utility.GetFieldValue<PropManager, List<DragPointProp>>(manager, "propList");
+        // TODO: Use transform.SetRotationAndPosition or whatever it's called.
+        transform.position = transformDto.Position;
+        transform.rotation = transformDto.Rotation;
+        transform.localScale = transformDto.LocalScale;
     }
 }

+ 139 - 96
src/MeidoPhotoStudio.Plugin/Serialization/Serializers/MeidoSerializer.cs

@@ -1,75 +1,50 @@
-using System.Collections.Generic;
+using System.Collections.Generic;
 using System.IO;
 using System.Linq;
 using System.Text;
+
 using UnityEngine;
 
-namespace MeidoPhotoStudio.Plugin
+namespace MeidoPhotoStudio.Plugin;
+
+public class MeidoSerializer : Serializer<Meido>
 {
-    public class MeidoSerializer : Serializer<Meido>
-    {
-        private const short version = 1;
-        private const short headVersion = 1;
-        private const short bodyVersion = 2;
-        private const short clothingVersion = 1;
+    private const short Version = 1;
+    private const short HeadVersion = 1;
+    private const short BodyVersion = 2;
+    private const short ClothingVersion = 1;
 
-        private static SimpleSerializer<PoseInfo> PoseInfoSerializer => Serialization.GetSimple<PoseInfo>();
+    private static SimpleSerializer<PoseInfo> PoseInfoSerializer =>
+        Serialization.GetSimple<PoseInfo>();
 
-        private static SimpleSerializer<TransformDTO> TransformDtoSerializer => Serialization.GetSimple<TransformDTO>();
+    private static SimpleSerializer<TransformDTO> TransformDtoSerializer =>
+        Serialization.GetSimple<TransformDTO>();
 
-        public override void Serialize(Meido meido, BinaryWriter writer)
-        {
-            var maid = meido.Maid;
+    public override void Serialize(Meido meido, BinaryWriter writer)
+    {
+        var maid = meido.Maid;
 
-            using var memoryStream = new MemoryStream();
-            using var tempWriter = new BinaryWriter(memoryStream, Encoding.UTF8);
+        using var memoryStream = new MemoryStream();
+        using var tempWriter = new BinaryWriter(memoryStream, Encoding.UTF8);
 
-            tempWriter.WriteVersion(version);
+        tempWriter.WriteVersion(Version);
 
-            TransformDtoSerializer.Serialize(new TransformDTO(maid.transform), tempWriter);
+        TransformDtoSerializer.Serialize(new TransformDTO(maid.transform), tempWriter);
 
-            SerializeHead(meido, tempWriter);
+        SerializeHead(meido, tempWriter);
 
-            SerializeBody(meido, tempWriter);
+        SerializeBody(meido, tempWriter);
 
-            SerializeClothing(meido, tempWriter);
+        SerializeClothing(meido, tempWriter);
 
-            writer.Write(memoryStream.Length);
-            writer.Write(memoryStream.ToArray());
-        }
+        writer.Write(memoryStream.Length);
+        writer.Write(memoryStream.ToArray());
 
-        public override void Deserialize(Meido meido, BinaryReader reader, SceneMetadata metadata)
-        {
-            var maid = meido.Maid;
-
-            maid.GetAnimation().Stop();
-            meido.DetachAllMpnAttach();
-            meido.StopBlink();
-
-            reader.ReadInt64(); // data length
-
-            _ = reader.ReadVersion();
-
-            var transformDto = TransformDtoSerializer.Deserialize(reader, metadata);
-            var maidTransform = maid.transform;
-            maidTransform.position = transformDto.Position;
-            maidTransform.rotation = transformDto.Rotation;
-            maidTransform.localScale = transformDto.LocalScale;
-
-            meido.IKManager.SetDragPointScale(maidTransform.localScale.x);
-
-            DeserializeHead(meido, reader, metadata);
-
-            DeserializeBody(meido, reader, metadata);
-
-            DeserializeClothing(meido, reader, metadata);
-        }
-
-        private static void SerializeHead(Meido meido, BinaryWriter writer)
+        static void SerializeHead(Meido meido, BinaryWriter writer)
         {
             var body = meido.Body;
 
-            writer.WriteVersion(headVersion);
+            writer.WriteVersion(HeadVersion);
 
             // eye direction
             writer.WriteQuaternion(body.quaDefEyeL * Quaternion.Inverse(meido.DefaultEyeRotL));
@@ -85,8 +60,10 @@ namespace MeidoPhotoStudio.Plugin
             writer.Write(meido.EyeToCam);
 
             // face
-            Dictionary<string, float> faceDict = meido.SerializeFace();
+            var faceDict = meido.SerializeFace();
+
             writer.Write(faceDict.Count);
+
             foreach (var (hash, value) in faceDict)
             {
                 writer.Write(hash);
@@ -94,31 +71,34 @@ namespace MeidoPhotoStudio.Plugin
             }
         }
 
-        private static void SerializeBody(Meido meido, BinaryWriter writer)
+        static void SerializeBody(Meido meido, BinaryWriter writer)
         {
-            writer.WriteVersion(bodyVersion);
+            writer.WriteVersion(BodyVersion);
 
             // pose
             var poseBuffer = meido.SerializePose(true);
+
             writer.Write(poseBuffer.Length);
             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
+            // TODO: Think about how to indicate code for new versions of serialization.
+            { // V2
+                // sub mune rotation
+                var body = meido.Body;
+
+                writer.WriteQuaternion(body.GetBone("Mune_L_sub").localRotation);
+                writer.WriteQuaternion(body.GetBone("Mune_R_sub").localRotation);
+            }
         }
 
-        private static void SerializeClothing(Meido meido, BinaryWriter writer)
+        static void SerializeClothing(Meido meido, BinaryWriter writer)
         {
             var maid = meido.Maid;
             var body = meido.Body;
 
-            writer.WriteVersion(clothingVersion);
+            writer.WriteVersion(ClothingVersion);
 
             // body visible
             writer.Write(body.GetMask(TBody.SlotID.body));
@@ -127,19 +107,23 @@ namespace MeidoPhotoStudio.Plugin
             foreach (var clothingSlot in MaidDressingPane.ClothingSlots)
             {
                 var value = true;
-                if (clothingSlot == TBody.SlotID.wear)
+
+                if (clothingSlot is TBody.SlotID.wear)
                 {
                     if (MaidDressingPane.WearSlots.Any(slot => body.GetSlotLoaded(slot)))
-                    {
                         value = MaidDressingPane.WearSlots.Any(slot => body.GetMask(slot));
-                    }
                 }
-                else if (clothingSlot == TBody.SlotID.megane)
+                else if (clothingSlot is TBody.SlotID.megane)
                 {
                     var slots = new[] { TBody.SlotID.megane, TBody.SlotID.accHead };
-                    if (slots.Any(slot => body.GetSlotLoaded(slot))) { value = slots.Any(slot => body.GetMask(slot)); }
+
+                    if (slots.Any(slot => body.GetSlotLoaded(slot)))
+                        value = slots.Any(slot => body.GetMask(slot));
+                }
+                else if (body.GetSlotLoaded(clothingSlot))
+                {
+                    value = body.GetMask(clothingSlot);
                 }
-                else if (body.GetSlotLoaded(clothingSlot)) value = body.GetMask(clothingSlot);
 
                 writer.Write(value);
             }
@@ -151,10 +135,12 @@ namespace MeidoPhotoStudio.Plugin
 
             // mpn attach props
             var hasKousokuUpper = body.GetSlotLoaded(TBody.SlotID.kousoku_upper);
+
             writer.Write(hasKousokuUpper);
             writer.Write(maid.GetProp(MPN.kousoku_upper).strTempFileName);
 
             var hasKousokuLower = body.GetSlotLoaded(TBody.SlotID.kousoku_lower);
+
             writer.Write(hasKousokuLower);
             writer.Write(maid.GetProp(MPN.kousoku_lower).strTempFileName);
 
@@ -165,8 +151,37 @@ namespace MeidoPhotoStudio.Plugin
             writer.Write(meido.SkirtGravityActive);
             writer.Write(meido.SkirtGravityControl.Control.transform.localPosition);
         }
+    }
 
-        private static void DeserializeHead(Meido meido, BinaryReader reader, SceneMetadata metadata)
+    public override void Deserialize(Meido meido, BinaryReader reader, SceneMetadata metadata)
+    {
+        var maid = meido.Maid;
+
+        maid.GetAnimation().Stop();
+        meido.DetachAllMpnAttach();
+        meido.StopBlink();
+
+        reader.ReadInt64(); // data length
+
+        _ = reader.ReadVersion();
+
+        var transformDto = TransformDtoSerializer.Deserialize(reader, metadata);
+        var maidTransform = maid.transform;
+
+        // TODO: use transform.SetRotationAndPosition
+        maidTransform.position = transformDto.Position;
+        maidTransform.rotation = transformDto.Rotation;
+        maidTransform.localScale = transformDto.LocalScale;
+
+        meido.IKManager.SetDragPointScale(maidTransform.localScale.x);
+
+        DeserializeHead(meido, reader, metadata);
+
+        DeserializeBody(meido, reader, metadata);
+
+        DeserializeClothing(meido, reader, metadata);
+
+        static void DeserializeHead(Meido meido, BinaryReader reader, SceneMetadata metadata)
         {
             var body = meido.Body;
 
@@ -190,7 +205,8 @@ namespace MeidoPhotoStudio.Plugin
             var offsetLookTarget = reader.ReadVector3();
             var headEulerAngle = reader.ReadVector3();
 
-            if (freeLook) body.offsetLookTarget = offsetLookTarget;
+            if (freeLook)
+                body.offsetLookTarget = offsetLookTarget;
 
             if (!metadata.MMConverted)
             {
@@ -202,49 +218,57 @@ namespace MeidoPhotoStudio.Plugin
             meido.EyeToCam = reader.ReadBoolean();
 
             var faceBlendCount = reader.ReadInt32();
+
             for (var i = 0; i < faceBlendCount; i++)
             {
                 var hash = reader.ReadString();
                 var value = reader.ReadSingle();
+
                 meido.SetFaceBlendValue(hash, value);
             }
         }
 
-        private static void DeserializeBody(Meido meido, BinaryReader reader, SceneMetadata metadata)
+        static void DeserializeBody(Meido meido, BinaryReader reader, SceneMetadata metadata)
         {
             var version = reader.ReadVersion();
 
             var muneSetting = new KeyValuePair<bool, bool>(true, true);
-            if (metadata.MMConverted) meido.IKManager.Deserialize(reader);
+
+            if (metadata.MMConverted)
+            {
+                meido.IKManager.Deserialize(reader);
+            }
             else
             {
                 var poseBufferLength = reader.ReadInt32();
-                byte[] poseBuffer = reader.ReadBytes(poseBufferLength);
+                var poseBuffer = reader.ReadBytes(poseBufferLength);
+
                 muneSetting = meido.SetFrameBinary(poseBuffer);
             }
 
             var poseInfo = PoseInfoSerializer.Deserialize(reader, metadata);
+
             Utility.SetPropertyValue(meido, nameof(Meido.CachedPose), poseInfo);
-            
+
             meido.SetMune(!muneSetting.Key, true);
             meido.SetMune(!muneSetting.Value);
 
-            if (version >= 2)
-            {
-                var muneLSubRotation = reader.ReadQuaternion();
-                var muneSubRRotation = reader.ReadQuaternion();
+            if (version < 2)
+                return;
 
-                var body = meido.Body;
+            var muneLSubRotation = reader.ReadQuaternion();
+            var muneSubRRotation = reader.ReadQuaternion();
 
-                if (muneSetting.Key)
-                    body.GetBone("Mune_L_sub").localRotation = muneLSubRotation;
+            var body = meido.Body;
 
-                if (muneSetting.Value)
-                    body.GetBone("Mune_R_sub").localRotation = muneSubRRotation;
-            }
+            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)
+        static void DeserializeClothing(Meido meido, BinaryReader reader, SceneMetadata metadata)
         {
             var body = meido.Body;
 
@@ -255,20 +279,25 @@ namespace MeidoPhotoStudio.Plugin
             foreach (var clothingSlot in MaidDressingPane.ClothingSlots)
             {
                 var value = reader.ReadBoolean();
-                if (metadata.MMConverted) continue;
 
-                if (clothingSlot == TBody.SlotID.wear)
+                if (metadata.MMConverted)
+                    continue;
+
+                if (clothingSlot is TBody.SlotID.wear)
                 {
                     body.SetMask(TBody.SlotID.wear, value);
                     body.SetMask(TBody.SlotID.mizugi, value);
                     body.SetMask(TBody.SlotID.onepiece, value);
                 }
-                else if (clothingSlot == TBody.SlotID.megane)
+                else if (clothingSlot is TBody.SlotID.megane)
                 {
                     body.SetMask(TBody.SlotID.megane, value);
                     body.SetMask(TBody.SlotID.accHead, value);
                 }
-                else if (body.GetSlotLoaded(clothingSlot)) body.SetMask(clothingSlot, value);
+                else if (body.GetSlotLoaded(clothingSlot))
+                {
+                    body.SetMask(clothingSlot, value);
+                }
             }
 
             // zurashi and mekure
@@ -278,32 +307,46 @@ namespace MeidoPhotoStudio.Plugin
 
             if (!metadata.MMConverted)
             {
-                if (meido.CurlingFront != curlingFront) meido.SetCurling(Meido.Curl.Front, curlingFront);
-                if (meido.CurlingBack != curlingBack) meido.SetCurling(Meido.Curl.Back, curlingBack);
+                if (meido.CurlingFront != curlingFront)
+                    meido.SetCurling(Meido.Curl.Front, curlingFront);
+
+                if (meido.CurlingBack != curlingBack)
+                    meido.SetCurling(Meido.Curl.Back, curlingBack);
+
                 meido.SetCurling(Meido.Curl.Shift, curlingPantsu);
             }
 
             // MPN attach upper prop
             var hasKousokuUpper = reader.ReadBoolean();
             var upperMenuFile = reader.ReadString();
-            if (hasKousokuUpper) meido.SetMpnProp(new MpnAttachProp(MPN.kousoku_upper, upperMenuFile), false);
+
+            if (hasKousokuUpper)
+                meido.SetMpnProp(new MpnAttachProp(MPN.kousoku_upper, upperMenuFile), false);
 
             // MPN attach lower prop
             var hasKousokuLower = reader.ReadBoolean();
             var lowerMenuFile = reader.ReadString();
-            if (hasKousokuLower) meido.SetMpnProp(new MpnAttachProp(MPN.kousoku_lower, lowerMenuFile), false);
+
+            if (hasKousokuLower)
+                meido.SetMpnProp(new MpnAttachProp(MPN.kousoku_lower, lowerMenuFile), false);
 
             // hair gravity
             var hairGravityActive = reader.ReadBoolean();
             var hairPosition = reader.ReadVector3();
+
             meido.HairGravityActive = hairGravityActive;
-            if (meido.HairGravityActive) meido.ApplyGravity(hairPosition);
+
+            if (meido.HairGravityActive)
+                meido.ApplyGravity(hairPosition);
 
             // skirt gravity
             var skirtGravityActive = reader.ReadBoolean();
             var skirtPosition = reader.ReadVector3();
+
             meido.SkirtGravityActive = skirtGravityActive;
-            if (meido.SkirtGravityActive) meido.ApplyGravity(skirtPosition, true);
+
+            if (meido.SkirtGravityActive)
+                meido.ApplyGravity(skirtPosition, true);
         }
     }
 }

+ 11 - 10
src/MeidoPhotoStudio.Plugin/Serialization/SimpleSerializer.cs

@@ -1,15 +1,16 @@
-using System.IO;
+using System.IO;
 
-namespace MeidoPhotoStudio.Plugin
+namespace MeidoPhotoStudio.Plugin;
+
+public abstract class SimpleSerializer<T> : ISimpleSerializer
 {
-    public abstract class SimpleSerializer<T> : ISimpleSerializer
-    {
-        void ISimpleSerializer.Serialize(object obj, BinaryWriter writer) => Serialize((T) obj, writer);
+    void ISimpleSerializer.Serialize(object obj, BinaryWriter writer) =>
+        Serialize((T)obj, writer);
+
+    object ISimpleSerializer.Deserialize(BinaryReader reader, SceneMetadata metadata) =>
+        Deserialize(reader, metadata);
 
-        object ISimpleSerializer.Deserialize(BinaryReader reader, SceneMetadata metadata)
-            => Deserialize(reader, metadata);
+    public abstract void Serialize(T obj, BinaryWriter writer);
 
-        public abstract void Serialize(T obj, BinaryWriter writer);
-        public abstract T Deserialize(BinaryReader reader, SceneMetadata metadata);
-    }
+    public abstract T Deserialize(BinaryReader reader, SceneMetadata metadata);
 }

+ 32 - 0
src/MeidoPhotoStudio.Plugin/Serialization/SimpleSerializers/DragPointPropDTO.cs

@@ -0,0 +1,32 @@
+namespace MeidoPhotoStudio.Plugin;
+
+// TODO: Extract other classes to another file
+public class DragPointPropDTO
+{
+    public DragPointPropDTO()
+    {
+    }
+
+    public DragPointPropDTO(DragPointProp dragPoint)
+    {
+        TransformDTO = new(dragPoint.MyObject.transform);
+        ShadowCasting = dragPoint.ShadowCasting;
+        AttachPointInfo = dragPoint.AttachPointInfo;
+        PropInfo = dragPoint.Info;
+    }
+
+    public TransformDTO TransformDTO { get; set; }
+
+    public AttachPointInfo AttachPointInfo { get; set; }
+
+    public PropInfo PropInfo { get; set; }
+
+    public bool ShadowCasting { get; set; }
+
+    public void Deconstruct(out TransformDTO transform, out AttachPointInfo attachPointInfo, out bool shadowCasting)
+    {
+        transform = TransformDTO;
+        attachPointInfo = AttachPointInfo;
+        shadowCasting = ShadowCasting;
+    }
+}

+ 26 - 49
src/MeidoPhotoStudio.Plugin/Serialization/SimpleSerializers/DragPointPropDTOSerializer.cs

@@ -1,66 +1,43 @@
-using System.IO;
+using System.IO;
 
-namespace MeidoPhotoStudio.Plugin
+namespace MeidoPhotoStudio.Plugin;
+
+public class DragPointPropDTOSerializer : SimpleSerializer<DragPointPropDTO>
 {
-    public class DragPointPropDTOSerializer : SimpleSerializer<DragPointPropDTO>
-    {
-        private const short version = 1;
+    private const short Version = 1;
 
-        private static SimpleSerializer<PropInfo> PropInfoSerializer => Serialization.GetSimple<PropInfo>();
-        private static SimpleSerializer<TransformDTO> TransformSerializer => Serialization.GetSimple<TransformDTO>();
+    private static SimpleSerializer<PropInfo> PropInfoSerializer =>
+        Serialization.GetSimple<PropInfo>();
 
-        private static SimpleSerializer<AttachPointInfo> AttachPointSerializer
-            => Serialization.GetSimple<AttachPointInfo>();
+    private static SimpleSerializer<TransformDTO> TransformSerializer =>
+        Serialization.GetSimple<TransformDTO>();
 
-        public override void Serialize(DragPointPropDTO dragPointDto, BinaryWriter writer)
-        {
-            writer.WriteVersion(version);
+    private static SimpleSerializer<AttachPointInfo> AttachPointSerializer =>
+        Serialization.GetSimple<AttachPointInfo>();
 
-            PropInfoSerializer.Serialize(dragPointDto.PropInfo, writer);
+    public override void Serialize(DragPointPropDTO dragPointDto, BinaryWriter writer)
+    {
+        writer.WriteVersion(Version);
 
-            TransformSerializer.Serialize(dragPointDto.TransformDTO, writer);
+        PropInfoSerializer.Serialize(dragPointDto.PropInfo, writer);
 
-            AttachPointSerializer.Serialize(dragPointDto.AttachPointInfo, writer);
+        TransformSerializer.Serialize(dragPointDto.TransformDTO, writer);
 
-            writer.Write(dragPointDto.ShadowCasting);
-        }
+        AttachPointSerializer.Serialize(dragPointDto.AttachPointInfo, writer);
 
-        public override DragPointPropDTO Deserialize(BinaryReader reader, SceneMetadata metadata)
-        {
-            _ = reader.ReadVersion();
-
-            return new DragPointPropDTO
-            {
-                PropInfo = PropInfoSerializer.Deserialize(reader, metadata),
-                TransformDTO = TransformSerializer.Deserialize(reader, metadata),
-                AttachPointInfo = AttachPointSerializer.Deserialize(reader, metadata),
-                ShadowCasting = reader.ReadBoolean()
-            };
-        }
+        writer.Write(dragPointDto.ShadowCasting);
     }
 
-    public class DragPointPropDTO
+    public override DragPointPropDTO Deserialize(BinaryReader reader, SceneMetadata metadata)
     {
-        public TransformDTO TransformDTO { get; set; }
-        public AttachPointInfo AttachPointInfo { get; set; }
-        public PropInfo PropInfo { get; set; }
-        public bool ShadowCasting { get; set; }
-
-        public DragPointPropDTO() { }
-
-        public DragPointPropDTO(DragPointProp dragPoint)
-        {
-            TransformDTO = new TransformDTO(dragPoint.MyObject.transform);
-            ShadowCasting = dragPoint.ShadowCasting;
-            AttachPointInfo = dragPoint.AttachPointInfo;
-            PropInfo = dragPoint.Info;
-        }
+        _ = reader.ReadVersion();
 
-        public void Deconstruct(out TransformDTO transform, out AttachPointInfo attachPointInfo, out bool shadowCasting)
+        return new DragPointPropDTO
         {
-            transform = TransformDTO;
-            attachPointInfo = AttachPointInfo;
-            shadowCasting = ShadowCasting;
-        }
+            PropInfo = PropInfoSerializer.Deserialize(reader, metadata),
+            TransformDTO = TransformSerializer.Deserialize(reader, metadata),
+            AttachPointInfo = AttachPointSerializer.Deserialize(reader, metadata),
+            ShadowCasting = reader.ReadBoolean(),
+        };
     }
 }

+ 16 - 17
src/MeidoPhotoStudio.Plugin/Serialization/SimpleSerializers/PoseInfoSerializer.cs

@@ -1,25 +1,24 @@
-using System.IO;
+using System.IO;
 
-namespace MeidoPhotoStudio.Plugin
+namespace MeidoPhotoStudio.Plugin;
+
+public class PoseInfoSerializer : SimpleSerializer<PoseInfo>
 {
-    public class PoseInfoSerializer : SimpleSerializer<PoseInfo>
-    {
-        private const short version = 1;
+    private const short Version = 1;
 
-        public override void Serialize(PoseInfo obj, BinaryWriter writer)
-        {
-            writer.WriteVersion(version);
+    public override void Serialize(PoseInfo obj, BinaryWriter writer)
+    {
+        writer.WriteVersion(Version);
 
-            writer.Write(obj.PoseGroup);
-            writer.Write(obj.Pose);
-            writer.Write(obj.CustomPose);
-        }
+        writer.Write(obj.PoseGroup);
+        writer.Write(obj.Pose);
+        writer.Write(obj.CustomPose);
+    }
 
-        public override PoseInfo Deserialize(BinaryReader reader, SceneMetadata metadata)
-        {
-            _ = reader.ReadVersion();
+    public override PoseInfo Deserialize(BinaryReader reader, SceneMetadata metadata)
+    {
+        _ = reader.ReadVersion();
 
-            return new PoseInfo(reader.ReadString(), reader.ReadString(), reader.ReadBoolean());
-        }
+        return new(reader.ReadString(), reader.ReadString(), reader.ReadBoolean());
     }
 }

+ 23 - 24
src/MeidoPhotoStudio.Plugin/Serialization/SimpleSerializers/PropInfoSerializer.cs

@@ -1,33 +1,32 @@
-using System.IO;
+using System.IO;
 
-namespace MeidoPhotoStudio.Plugin
+namespace MeidoPhotoStudio.Plugin;
+
+public class PropInfoSerializer : SimpleSerializer<PropInfo>
 {
-    public class PropInfoSerializer : SimpleSerializer<PropInfo>
+    private const short Version = 1;
+
+    public override void Serialize(PropInfo info, BinaryWriter writer)
     {
-        private const short version = 1;
+        writer.WriteVersion(Version);
 
-        public override void Serialize(PropInfo info, BinaryWriter writer)
-        {
-            writer.WriteVersion(version);
+        writer.Write((int)info.Type);
+        writer.WriteNullableString(info.Filename);
+        writer.WriteNullableString(info.SubFilename);
+        writer.Write(info.MyRoomID);
+        writer.WriteNullableString(info.IconFile);
+    }
 
-            writer.Write((int) info.Type);
-            writer.WriteNullableString(info.Filename);
-            writer.WriteNullableString(info.SubFilename);
-            writer.Write(info.MyRoomID);
-            writer.WriteNullableString(info.IconFile);
-        }
+    public override PropInfo Deserialize(BinaryReader reader, SceneMetadata metadata)
+    {
+        _ = reader.ReadVersion();
 
-        public override PropInfo Deserialize(BinaryReader reader, SceneMetadata metadata)
+        return new PropInfo((PropInfo.PropType)reader.ReadInt32())
         {
-            _ = reader.ReadVersion();
-
-            return new PropInfo ((PropInfo.PropType) reader.ReadInt32())
-            {
-                Filename = reader.ReadNullableString(),
-                SubFilename = reader.ReadNullableString(),
-                MyRoomID = reader.ReadInt32(),
-                IconFile = reader.ReadNullableString()
-            };
-        }
+            Filename = reader.ReadNullableString(),
+            SubFilename = reader.ReadNullableString(),
+            MyRoomID = reader.ReadInt32(),
+            IconFile = reader.ReadNullableString(),
+        };
     }
 }

+ 29 - 0
src/MeidoPhotoStudio.Plugin/Serialization/SimpleSerializers/TransformDTO.cs

@@ -0,0 +1,29 @@
+using UnityEngine;
+
+namespace MeidoPhotoStudio.Plugin;
+
+public class TransformDTO
+{
+    public TransformDTO()
+    {
+    }
+
+    public TransformDTO(Transform transform)
+    {
+        Position = transform.position;
+        LocalPosition = transform.localPosition;
+        Rotation = transform.rotation;
+        LocalRotation = transform.localRotation;
+        LocalScale = transform.localScale;
+    }
+
+    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;
+}

+ 22 - 44
src/MeidoPhotoStudio.Plugin/Serialization/SimpleSerializers/TransformDTOSerializer.cs

@@ -1,55 +1,33 @@
-using System.IO;
-using UnityEngine;
+using System.IO;
 
-namespace MeidoPhotoStudio.Plugin
-{
-    public class TransformDTOSerializer : SimpleSerializer<TransformDTO>
-    {
-        private const short version = 1;
-
-        public override void Serialize(TransformDTO transform, BinaryWriter writer)
-        {
-            writer.WriteVersion(version);
+namespace MeidoPhotoStudio.Plugin;
 
-            writer.Write(transform.Position);
-            writer.Write(transform.Rotation);
-            writer.Write(transform.LocalPosition);
-            writer.Write(transform.LocalRotation);
-            writer.Write(transform.LocalScale);
-        }
+public class TransformDTOSerializer : SimpleSerializer<TransformDTO>
+{
+    private const short Version = 1;
 
-        public override TransformDTO Deserialize(BinaryReader reader, SceneMetadata metadata)
-        {
-            _ = reader.ReadVersion();
+    public override void Serialize(TransformDTO transform, BinaryWriter writer)
+    {
+        writer.WriteVersion(Version);
 
-            return new TransformDTO
-            {
-                Position = reader.ReadVector3(),
-                Rotation = reader.ReadQuaternion(),
-                LocalPosition = reader.ReadVector3(),
-                LocalRotation = reader.ReadQuaternion(),
-                LocalScale = reader.ReadVector3()
-            };
-        }
+        writer.Write(transform.Position);
+        writer.Write(transform.Rotation);
+        writer.Write(transform.LocalPosition);
+        writer.Write(transform.LocalRotation);
+        writer.Write(transform.LocalScale);
     }
 
-    public class TransformDTO
+    public override TransformDTO Deserialize(BinaryReader reader, SceneMetadata metadata)
     {
-        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() { }
+        _ = reader.ReadVersion();
 
-        public TransformDTO(Transform transform)
+        return new TransformDTO
         {
-            Position = transform.position;
-            LocalPosition = transform.localPosition;
-            Rotation = transform.rotation;
-            LocalRotation = transform.localRotation;
-            LocalScale = transform.localScale;
-        }
+            Position = reader.ReadVector3(),
+            Rotation = reader.ReadQuaternion(),
+            LocalPosition = reader.ReadVector3(),
+            LocalRotation = reader.ReadQuaternion(),
+            LocalScale = reader.ReadVector3(),
+        };
     }
 }

+ 35 - 0
src/MeidoPhotoStudio.Plugin/StreamExtensions.cs

@@ -0,0 +1,35 @@
+using System.IO;
+
+using Ionic.Zlib;
+
+namespace MeidoPhotoStudio.Plugin;
+
+public static class StreamExtensions
+{
+    public static void CopyTo(this Stream stream, Stream outStream)
+    {
+        var buf = new byte[1024 * 32];
+
+        int length;
+
+        while ((length = stream.Read(buf, 0, buf.Length)) > 0)
+            outStream.Write(buf, 0, length);
+    }
+
+    public static MemoryStream Decompress(this MemoryStream stream)
+    {
+        var dataMemoryStream = new MemoryStream();
+
+        using var compressionStream = new DeflateStream(stream, CompressionMode.Decompress, true);
+
+        compressionStream.CopyTo(dataMemoryStream);
+        compressionStream.Flush();
+
+        dataMemoryStream.Position = 0L;
+
+        return dataMemoryStream;
+    }
+
+    public static DeflateStream GetCompressionStream(this MemoryStream stream) =>
+        new(stream, CompressionMode.Compress);
+}

+ 100 - 110
src/MeidoPhotoStudio.Plugin/Translation.cs

@@ -1,144 +1,134 @@
 using System;
-using System.Linq;
-using System.IO;
 using System.Collections.Generic;
-using Newtonsoft.Json.Linq;
+using System.IO;
+using System.Linq;
+
 using BepInEx.Configuration;
+using Newtonsoft.Json.Linq;
+
+namespace MeidoPhotoStudio.Plugin;
 
-namespace MeidoPhotoStudio.Plugin
+public static class Translation
 {
-    public static class Translation
-    {
-        private const string settingsHeader = "Translation";
-        private static readonly string[] props = { "ui", "props", "bg", "face" };
-        private static Dictionary<string, Dictionary<string, string>> Translations;
-        private static readonly ConfigEntry<string> currentLanguage;
-        private static readonly ConfigEntry<bool> suppressWarnings;
-        private static bool forceSuppressWarnings;
-        private static bool suppressWarningsCached;
-        public static bool SuppressWarnings
-        {
-            get => suppressWarningsCached;
-            set
-            {
-                suppressWarningsCached = value;
-                suppressWarnings.Value = value;
-            }
-        }
-        public static string CurrentLanguage
-        {
-            get => currentLanguage.Value;
-            set => currentLanguage.Value = value;
-        }
-        public static event EventHandler ReloadTranslationEvent;
+    private const string SettingsHeader = "Translation";
 
-        static Translation()
-        {
-            currentLanguage = Configuration.Config.Bind(
-                settingsHeader, "Language",
-                "en",
-                "Directory to pull translations from"
-                + "\nTranslations are found in the 'Translations' folder"
-            );
-
-            suppressWarnings = Configuration.Config.Bind(
-                settingsHeader, "SuppressWarnings",
-                false,
-                "Suppress translation warnings from showing up in the console"
-            );
-
-            suppressWarningsCached = !suppressWarnings.Value;
-        }
+    private static readonly string[] Props = { "ui", "props", "bg", "face" };
+    private static readonly ConfigEntry<string> CurrentLanguageConfig = Configuration.Config.Bind(
+        SettingsHeader,
+        "Language",
+        "en",
+        "Directory to pull translations from\nTranslations are found in the 'Translations' folder");
 
-        public static void Initialize(string language)
-        {
-            forceSuppressWarnings = false;
+    private static readonly ConfigEntry<bool> SuppressWarningsConfig = Configuration.Config.Bind(
+        SettingsHeader,
+        "SuppressWarnings",
+        false,
+        "Suppress translation warnings from showing up in the console");
 
-            string rootTranslationPath = Path.Combine(Constants.configPath, Constants.translationDirectory);
-            string currentTranslationPath = Path.Combine(rootTranslationPath, language);
+    private static Dictionary<string, Dictionary<string, string>> translations;
+    private static bool forceSuppressWarnings;
+    private static bool suppressWarningsCached;
 
-            Translations = new Dictionary<string, Dictionary<string, string>>(
-                StringComparer.InvariantCultureIgnoreCase
-            );
+    static Translation() =>
+        suppressWarningsCached = !SuppressWarningsConfig.Value;
 
-            if (!Directory.Exists(currentTranslationPath))
-            {
-                Utility.LogError(
-                    $"No translations found for '{language}' in '{currentTranslationPath}'"
-                );
-                forceSuppressWarnings = true;
-                return;
-            }
+    public static event EventHandler ReloadTranslationEvent;
 
-            foreach (string prop in props)
-            {
-                string translationFile = $"translation.{prop}.json";
-                try
-                {
-                    string translationPath = Path.Combine(currentTranslationPath, translationFile);
+    public static bool SuppressWarnings
+    {
+        get => suppressWarningsCached;
+        set
+        {
+            suppressWarningsCached = value;
+            SuppressWarningsConfig.Value = value;
+        }
+    }
+
+    public static string CurrentLanguage
+    {
+        get => CurrentLanguageConfig.Value;
+        set => CurrentLanguageConfig.Value = value;
+    }
 
-                    string translationJson = File.ReadAllText(translationPath);
+    public static void Initialize(string language)
+    {
+        forceSuppressWarnings = false;
 
-                    JObject translation = JObject.Parse(translationJson);
+        var rootTranslationPath = Path.Combine(Constants.ConfigPath, Constants.TranslationDirectory);
+        var currentTranslationPath = Path.Combine(rootTranslationPath, language);
 
-                    foreach (JProperty translationProp in translation.AsJEnumerable())
-                    {
-                        JToken token = translationProp.Value;
-                        Translations[translationProp.Path] = new Dictionary<string, string>(
-                            token.ToObject<Dictionary<string, string>>(), StringComparer.InvariantCultureIgnoreCase
-                        );
-                    }
-                }
-                catch
-                {
-                    forceSuppressWarnings = true;
-                    Utility.LogError($"Could not find translation file '{translationFile}'");
-                }
-            }
-        }
+        translations = new(StringComparer.InvariantCultureIgnoreCase);
 
-        public static void ReinitializeTranslation()
+        if (!Directory.Exists(currentTranslationPath))
         {
-            Initialize(CurrentLanguage);
-            ReloadTranslationEvent?.Invoke(null, EventArgs.Empty);
+            Utility.LogError($"No translations found for '{language}' in '{currentTranslationPath}'");
+            forceSuppressWarnings = true;
+
+            return;
         }
 
-        public static bool Has(string category, string text, bool warn = false)
+        foreach (var prop in Props)
         {
-            warn = !forceSuppressWarnings && !SuppressWarnings && warn;
-            if (!Translations.ContainsKey(category))
-            {
-                if (warn) Utility.LogWarning($"Could not translate '{text}': category '{category}' was not found");
-                return false;
-            }
+            var translationFile = $"translation.{prop}.json";
 
-            if (!Translations[category].ContainsKey(text))
+            try
             {
-                if (warn)
+                var translationPath = Path.Combine(currentTranslationPath, translationFile);
+                var translationJson = File.ReadAllText(translationPath);
+                var translation = JObject.Parse(translationJson);
+
+                foreach (var translationProp in translation.AsJEnumerable().Cast<JProperty>())
                 {
-                    Utility.LogWarning(
-                        $"Could not translate '{text}': '{text}' was not found in category '{category}'"
-                    );
+                    var token = translationProp.Value;
+
+                    translations[translationProp.Path] =
+                        new(token.ToObject<Dictionary<string, string>>(), StringComparer.InvariantCultureIgnoreCase);
                 }
-                return false;
             }
-
-            return true;
+            catch
+            {
+                forceSuppressWarnings = true;
+                Utility.LogError($"Could not find translation file '{translationFile}'");
+            }
         }
+    }
 
-        public static string Get(string category, string text, bool warn = true)
-        {
-            return Has(category, text, warn) ? Translations[category][text] : text;
-        }
+    public static void ReinitializeTranslation()
+    {
+        Initialize(CurrentLanguage);
+        ReloadTranslationEvent?.Invoke(null, EventArgs.Empty);
+    }
+
+    public static bool Has(string category, string text, bool warn = false)
+    {
+        warn = !forceSuppressWarnings && !SuppressWarnings && warn;
 
-        public static string[] GetArray(string category, IEnumerable<string> list)
+        if (!translations.ContainsKey(category))
         {
-            return GetList(category, list).ToArray();
+            if (warn)
+                Utility.LogWarning($"Could not translate '{text}': category '{category}' was not found");
+
+            return false;
         }
 
-        public static IEnumerable<string> GetList(string category, IEnumerable<string> list)
+        if (!translations[category].ContainsKey(text))
         {
-            return list.Select(uiName => Get(category, uiName));
+            if (warn)
+                Utility.LogWarning(
+                    $"Could not translate '{text}': '{text}' was not found in category '{category}'");
+
+            return false;
         }
+
+        return true;
     }
+
+    public static string Get(string category, string text, bool warn = true) =>
+        Has(category, text, warn) ? translations[category][text] : text;
+
+    public static string[] GetArray(string category, IEnumerable<string> list) =>
+        GetList(category, list).ToArray();
+
+    public static IEnumerable<string> GetList(string category, IEnumerable<string> list) =>
+        list.Select(uiName => Get(category, uiName));
 }

+ 185 - 346
src/MeidoPhotoStudio.Plugin/Utility.cs

@@ -1,427 +1,266 @@
 using System;
 using System.Collections.Generic;
-using System.Text.RegularExpressions;
 using System.IO;
+using System.Linq;
 using System.Reflection;
+using System.Text.RegularExpressions;
+
 using UnityEngine;
-using System.Linq;
-using Ionic.Zlib;
 
-namespace MeidoPhotoStudio.Plugin
+namespace MeidoPhotoStudio.Plugin;
+
+public static class Utility
 {
-    public static class Utility
-    {
-        private const BindingFlags bindingFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic
-            | BindingFlags.Static;
-        internal static readonly byte[] pngHeader = { 137, 80, 78, 71, 13, 10, 26, 10 };
-        internal static readonly byte[] pngEnd = System.Text.Encoding.ASCII.GetBytes("IEND");
-        internal static readonly Regex guidRegEx = new Regex(
-            @"^[a-f0-9]{8}(\-[a-f0-9]{4}){3}\-[a-f0-9]{12}$", RegexOptions.IgnoreCase
-        );
-        internal static readonly GameObject mousePositionGo;
-        internal static readonly MousePosition mousePosition;
-        public static readonly BepInEx.Logging.ManualLogSource Logger
-            = BepInEx.Logging.Logger.CreateLogSource(MeidoPhotoStudio.pluginName);
-        public enum ModKey
-        {
-            Control, Shift, Alt
-        }
-        public static string Timestamp => $"{DateTime.Now:yyyyMMddHHmmss}";
-        public static Vector3 MousePosition => mousePosition.Position;
+    public static readonly BepInEx.Logging.ManualLogSource Logger =
+        ManualLogSource;
 
-        static Utility()
-        {
-            mousePositionGo = new GameObject();
-            mousePosition = mousePositionGo.AddComponent<MousePosition>();
-        }
+    internal static readonly byte[] PngHeader = { 137, 80, 78, 71, 13, 10, 26, 10 };
+    internal static readonly byte[] PngEnd = System.Text.Encoding.ASCII.GetBytes("IEND");
+    internal static readonly Regex GuidRegEx =
+        new(@"^[a-f0-9]{8}(\-[a-f0-9]{4}){3}\-[a-f0-9]{12}$", RegexOptions.IgnoreCase);
 
-        public static void LogInfo(object data) => Logger.LogInfo(data);
+    internal static readonly GameObject MousePositionGameObject;
+    internal static readonly MousePosition MousePositionValue;
 
-        public static void LogMessage(object data) => Logger.LogMessage(data);
+    private const BindingFlags ReflectionFlags =
+        BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static;
 
-        public static void LogWarning(object data) => Logger.LogWarning(data);
+    private static readonly BepInEx.Logging.ManualLogSource ManualLogSource =
+        BepInEx.Logging.Logger.CreateLogSource(MeidoPhotoStudio.PluginName);
 
-        public static void LogError(object data) => Logger.LogError(data);
+    static Utility()
+    {
+        MousePositionGameObject = new();
+        MousePositionValue = MousePositionGameObject.AddComponent<MousePosition>();
+    }
 
-        public static void LogDebug(object data) => Logger.LogDebug(data);
+    public enum ModKey
+    {
+        Control,
+        Shift,
+        Alt,
+    }
 
-        public static int Wrap(int value, int min, int max)
-        {
-            max--;
-            return value < min ? max : value > max ? min : value;
-        }
+    public static string Timestamp =>
+        $"{DateTime.Now:yyyyMMddHHmmss}";
 
-        public static int GetPix(int num) => (int)((1f + (((Screen.width / 1280f) - 1f) * 0.6f)) * num);
+    public static Vector3 MousePosition =>
+        MousePositionValue.Position;
 
-        public static float Bound(float value, float left, float right)
-        {
-            return left > (double)right ? Mathf.Clamp(value, right, left) : Mathf.Clamp(value, left, right);
-        }
+    public static void LogInfo(object data) =>
+        Logger.LogInfo(data);
 
-        public static int Bound(int value, int left, int right)
-        {
-            return left > right ? Mathf.Clamp(value, right, left) : Mathf.Clamp(value, left, right);
-        }
+    public static void LogMessage(object data) =>
+        Logger.LogMessage(data);
 
-        public static Texture2D MakeTex(int width, int height, Color color)
-        {
-            Color[] colors = new Color[width * height];
-            for (int i = 0; i < colors.Length; i++)
-            {
-                colors[i] = color;
-            }
-            Texture2D texture2D = new Texture2D(width, height);
-            texture2D.SetPixels(colors);
-            texture2D.Apply();
-            return texture2D;
-        }
+    public static void LogWarning(object data) =>
+        Logger.LogWarning(data);
 
-        public static FieldInfo GetFieldInfo<T>(string field) => typeof(T).GetField(field, bindingFlags);
+    public static void LogError(object data) =>
+        Logger.LogError(data);
 
-        public static TValue GetFieldValue<TType, TValue>(TType instance, string field)
-        {
-            FieldInfo fieldInfo = GetFieldInfo<TType>(field);
-            if (fieldInfo == null || (!fieldInfo.IsStatic && instance == null)) return default;
-            return (TValue)fieldInfo.GetValue(instance);
-        }
+    public static void LogDebug(object data) =>
+        Logger.LogDebug(data);
 
-        public static void SetFieldValue<TType, TValue>(TType instance, string name, TValue value)
-        {
-            GetFieldInfo<TType>(name).SetValue(instance, value);
-        }
+    public static int Wrap(int value, int min, int max)
+    {
+        max--;
 
-        public static PropertyInfo GetPropertyInfo<T>(string field) => typeof(T).GetProperty(field, bindingFlags);
+        return value < min ? max : value > max ? min : value;
+    }
 
-        public static TValue GetPropertyValue<TType, TValue>(TType instance, string property)
-        {
-            var propertyInfo = GetPropertyInfo<TType>(property);
-            return propertyInfo == null ? default : (TValue) propertyInfo.GetValue(instance, null);
-        }
-        
-        public static void SetPropertyValue<TType, TValue>(TType instance, string name, TValue value) 
-            => GetPropertyInfo<TType>(name).SetValue(instance, value, null);
+    public static int GetPix(int num) =>
+        (int)((1f + (Screen.width / 1280f - 1f) * 0.6f) * num);
 
-        public static bool AnyMouseDown()
-        {
-            return Input.GetMouseButtonDown(0) || Input.GetMouseButtonDown(1) || Input.GetMouseButtonDown(2);
-        }
+    public static float Bound(float value, float left, float right) =>
+        left > (double)right ? Mathf.Clamp(value, right, left) : Mathf.Clamp(value, left, right);
 
-        public static string ScreenshotFilename()
-        {
-            string screenShotDir = Path.Combine(
-                GameMain.Instance.SerializeStorageManager.StoreDirectoryPath, "ScreenShot"
-            );
-            if (!Directory.Exists(screenShotDir)) Directory.CreateDirectory(screenShotDir);
-            return Path.Combine(screenShotDir, $"img{Timestamp}.png");
-        }
+    public static int Bound(int value, int left, int right) =>
+        left > right ? Mathf.Clamp(value, right, left) : Mathf.Clamp(value, left, right);
 
-        public static string TempScreenshotFilename()
-        {
-            return Path.Combine(Path.GetTempPath(), $"cm3d2_{Guid.NewGuid()}.png");
-        }
+    public static Texture2D MakeTex(int width, int height, Color color)
+    {
+        var colors = new Color32[width * height];
 
-        public static void ShowMouseExposition(string text, float time = 2f)
-        {
-            MouseExposition mouseExposition = MouseExposition.GetObject();
-            mouseExposition.SetText(text, time);
-        }
+        for (var i = 0; i < colors.Length; i++)
+            colors[i] = color;
 
-        public static bool IsGuidString(string guid)
-        {
-            if (string.IsNullOrEmpty(guid) || guid.Length != 36) return false;
-            return guidRegEx.IsMatch(guid);
-        }
+        var texture2D = new Texture2D(width, height);
 
-        public static string HandItemToOdogu(string menu)
-        {
-            menu = menu.Substring(menu.IndexOf('_') + 1);
-            menu = menu.Substring(0, menu.IndexOf("_i_.menu", StringComparison.OrdinalIgnoreCase));
-            menu = $"odogu_{menu}";
-            return menu;
-        }
+        texture2D.SetPixels32(colors);
+        texture2D.Apply();
 
-        public static void FixGameObjectScale(GameObject go)
-        {
-            Vector3 scale = go.transform.localScale;
-            float largest = Mathf.Max(scale.x, Mathf.Max(scale.y, scale.z));
-            go.transform.localScale = Vector3.one * (float)Math.Round(largest, 3);
-        }
+        return texture2D;
+    }
 
-        public static string SanitizePathPortion(string path)
-        {
-            char[] invalid = Path.GetInvalidFileNameChars();
-            path = path.Trim();
-            path = string.Join("_", path.Split(invalid)).Replace(".", "").Trim('_');
-            return path;
-        }
+    public static FieldInfo GetFieldInfo<T>(string field) =>
+        typeof(T).GetField(field, ReflectionFlags);
 
-        public static string GP01FbFaceHash(TMorph face, string hash)
-        {
-            if ((face.bodyskin.PartsVersion >= 120) && (hash != "eyeclose3") && hash.StartsWith("eyeclose"))
-            {
-                if (hash == "eyeclose") hash += '1';
-                hash += TMorph.crcFaceTypesStr[(int)face.GetFaceTypeGP01FB()];
-            }
-            return hash;
-        }
+    public static TValue GetFieldValue<TType, TValue>(TType instance, string field)
+    {
+        var fieldInfo = GetFieldInfo<TType>(field);
 
-        public static void ResizeToFit(Texture2D texture, int maxWidth, int maxHeight)
-        {
-            int width = texture.width;
-            int height = texture.height;
-            if (width != maxWidth || height != maxHeight)
-            {
-                float scale = Mathf.Min(maxWidth / (float)width, maxHeight / (float)height);
-                width = Mathf.RoundToInt(width * scale);
-                height = Mathf.RoundToInt(height * scale);
-                TextureScale.Bilinear(texture, width, height);
-            }
-        }
+        return fieldInfo is null || !fieldInfo.IsStatic && instance == null
+            ? default
+            : (TValue)fieldInfo.GetValue(instance);
+    }
 
-        public static bool BytesEqual(byte[] buffer, byte[] other)
-        {
-            if (buffer.Length != other.Length) return false;
-            for (int i = 0; i < buffer.Length; i++)
-            {
-                if (buffer[i] != other[i]) return false;
-            }
-            return true;
-        }
+    public static void SetFieldValue<TType, TValue>(TType instance, string name, TValue value) =>
+        GetFieldInfo<TType>(name).SetValue(instance, value);
 
-        public static bool IsPngFile(Stream stream)
-        {
-            byte[] buffer = new byte[8];
-            stream.Read(buffer, 0, 8);
-            return BytesEqual(buffer, pngHeader);
-        }
+    public static PropertyInfo GetPropertyInfo<T>(string field) =>
+        typeof(T).GetProperty(field, ReflectionFlags);
 
-        public static bool SeekPngEnd(Stream stream)
-        {
-            byte[] buffer = new byte[8];
-            stream.Read(buffer, 0, 8);
-            if (!BytesEqual(buffer, pngHeader)) return false;
-            buffer = new byte[4];
-            do
-            {
-                stream.Read(buffer, 0, 4);
-                if (BitConverter.IsLittleEndian) Array.Reverse(buffer);
-                uint length = BitConverter.ToUInt32(buffer, 0);
-                stream.Read(buffer, 0, 4);
-                stream.Seek(length + 4L, SeekOrigin.Current);
-            } while (!BytesEqual(buffer, pngEnd));
-            return true;
-        }
+    public static TValue GetPropertyValue<TType, TValue>(TType instance, string property)
+    {
+        var propertyInfo = GetPropertyInfo<TType>(property);
 
-        public static void WriteToFile(string name, System.Collections.Generic.IEnumerable<string> list)
-        {
-            if (Path.GetExtension(name) != ".txt") name += ".txt";
-            File.WriteAllLines(Path.Combine(Constants.configPath, name), list.ToArray());
-        }
+        return propertyInfo is null
+            ? default
+            : (TValue)propertyInfo.GetValue(instance, null);
+    }
 
-        public static void WriteToFile(string name, byte[] data)
-        {
-            File.WriteAllBytes(Path.Combine(Constants.configPath, name), data);
-        }
+    public static void SetPropertyValue<TType, TValue>(TType instance, string name, TValue value) =>
+        GetPropertyInfo<TType>(name).SetValue(instance, value, null);
+
+    public static bool AnyMouseDown() =>
+        Input.GetMouseButtonDown(0) || Input.GetMouseButtonDown(1) || Input.GetMouseButtonDown(2);
+
+    public static string ScreenshotFilename()
+    {
+        var screenShotDir = Path.Combine(GameMain.Instance.SerializeStorageManager.StoreDirectoryPath, "ScreenShot");
+
+        if (!Directory.Exists(screenShotDir))
+            Directory.CreateDirectory(screenShotDir);
+
+        return Path.Combine(screenShotDir, $"img{Timestamp}.png");
     }
 
-    public class MousePosition : MonoBehaviour
+    public static string TempScreenshotFilename() =>
+        Path.Combine(Path.GetTempPath(), $"cm3d2_{Guid.NewGuid()}.png");
+
+    public static void ShowMouseExposition(string text, float time = 2f)
     {
-        private Vector3 mousePosition;
-        public Vector3 Position => mousePosition;
+        var mouseExposition = MouseExposition.GetObject();
 
-        private void Awake()
-        {
-            DontDestroyOnLoad(this);
-            mousePosition = Input.mousePosition;
-        }
+        mouseExposition.SetText(text, time);
+    }
 
-        private void Update()
-        {
-            if (Input.GetMouseButton(0))
-            {
-                mousePosition.x += Input.GetAxis("Mouse X") * 20;
-                mousePosition.y += Input.GetAxis("Mouse Y") * 20;
-            }
-            else mousePosition = Input.mousePosition;
-        }
+    public static bool IsGuidString(string guid) =>
+        !string.IsNullOrEmpty(guid) && guid.Length is 36 && GuidRegEx.IsMatch(guid);
+
+    public static string HandItemToOdogu(string menu)
+    {
+        menu = menu.Substring(menu.IndexOf('_') + 1);
+        menu = menu.Substring(0, menu.IndexOf("_i_.menu", StringComparison.OrdinalIgnoreCase));
+        menu = $"odogu_{menu}";
+
+        return menu;
     }
 
-    public static class KeyValuePairExtensions
+    public static void FixGameObjectScale(GameObject go)
     {
-        public static void Deconstruct<TKey, TValue>(
-            this KeyValuePair<TKey, TValue> kvp, out TKey key, out TValue value
-        )
-        {
-            key = kvp.Key;
-            value = kvp.Value;
-        }
+        var scale = go.transform.localScale;
+        var largest = Mathf.Max(scale.x, Mathf.Max(scale.y, scale.z));
+
+        go.transform.localScale = Vector3.one * (float)Math.Round(largest, 3);
     }
 
-    public static class StreamExtensions
+    public static string SanitizePathPortion(string path)
     {
-        public static void CopyTo(this Stream stream, Stream outStream)
-        {
-            var buf = new byte[1024 * 32];
-            int length;
-            while ((length = stream.Read(buf, 0, buf.Length)) > 0) 
-                outStream.Write(buf, 0, length);
-        }
+        var invalid = Path.GetInvalidFileNameChars();
 
-        public static MemoryStream Decompress(this MemoryStream stream)
-        {
-            var dataMemoryStream = new MemoryStream();
-            using var compressionStream = new DeflateStream(stream, CompressionMode.Decompress, true);
+        path = path.Trim();
+        path = string.Join("_", path.Split(invalid)).Replace(".", string.Empty).Trim('_');
 
-            compressionStream.CopyTo(dataMemoryStream);
-            compressionStream.Flush();
+        return path;
+    }
 
-            dataMemoryStream.Position = 0L;
+    public static string GP01FbFaceHash(TMorph face, string hash)
+    {
+        if (face.bodyskin.PartsVersion < 120 || hash is "eyeclose3" || !hash.StartsWith("eyeclose"))
+            return hash;
 
-            return dataMemoryStream;
-        }
+        if (hash is "eyeclose")
+            hash += '1';
 
-        public static DeflateStream GetCompressionStream(this MemoryStream stream)
-            => new(stream, CompressionMode.Compress);
+        hash += TMorph.crcFaceTypesStr[(int)face.GetFaceTypeGP01FB()];
+
+        return hash;
     }
 
-    public static class CameraUtility
+    public static void ResizeToFit(Texture2D texture, int maxWidth, int maxHeight)
     {
-        public static CameraMain MainCamera => GameMain.Instance.MainCamera;
-        public static UltimateOrbitCamera UOCamera { get; } =
-            GameMain.Instance.MainCamera.GetComponent<UltimateOrbitCamera>();
-        
-        public static void StopSpin()
-        {
-            Utility.SetFieldValue(UOCamera, "xVelocity", 0f);
-            Utility.SetFieldValue(UOCamera, "yVelocity", 0f);
-        }
+        var width = texture.width;
+        var height = texture.height;
 
-        public static void StopMovement() => MainCamera.SetTargetPos(MainCamera.GetTargetPos());
+        if (width == maxWidth && height == maxHeight)
+            return;
 
-        public static void StopAll()
-        {
-            StopSpin();
-            StopMovement();
-        }
+        var scale = Mathf.Min(maxWidth / (float)width, maxHeight / (float)height);
 
-        public static void ForceCalcNearClip(this CameraMain camera)
-        {
-            camera.StopAllCoroutines();
-            camera.m_bCalcNearClip = false;
-            camera.camera.nearClipPlane = 0.01f;
-        }
+        width = Mathf.RoundToInt(width * scale);
+        height = Mathf.RoundToInt(height * scale);
+        TextureScale.Bilinear(texture, width, height);
+    }
 
-        public static void ResetCalcNearClip(this CameraMain camera)
-        {
-            if (camera.m_bCalcNearClip) return;
-            camera.StopAllCoroutines();
-            camera.m_bCalcNearClip = true;
-            camera.Start();
-        }
+    public static bool BytesEqual(byte[] buffer, byte[] other)
+    {
+        if (buffer.Length != other.Length)
+            return false;
+
+        for (var i = 0; i < buffer.Length; i++)
+            if (buffer[i] != other[i])
+                return false;
+
+        return true;
     }
 
-    public static class BinaryExtensions
+    public static bool IsPngFile(Stream stream)
     {
-        public static string ReadNullableString(this BinaryReader binaryReader)
-        {
-            return binaryReader.ReadBoolean() ? binaryReader.ReadString() : null;
-        }
+        var buffer = new byte[8];
 
-        public static void WriteNullableString(this BinaryWriter binaryWriter, string str)
-        {
-            binaryWriter.Write(str != null);
-            if (str != null) binaryWriter.Write(str);
-        }
+        stream.Read(buffer, 0, 8);
 
-        public static void Write(this BinaryWriter binaryWriter, Vector3 vector3)
-        {
-            binaryWriter.Write(vector3.x);
-            binaryWriter.Write(vector3.y);
-            binaryWriter.Write(vector3.z);
-        }
+        return BytesEqual(buffer, PngHeader);
+    }
 
-        public static void WriteVector3(this BinaryWriter binaryWriter, Vector3 vector3)
-        {
-            binaryWriter.Write(vector3.x);
-            binaryWriter.Write(vector3.y);
-            binaryWriter.Write(vector3.z);
-        }
+    public static bool SeekPngEnd(Stream stream)
+    {
+        var buffer = new byte[8];
 
-        public static Vector2 ReadVector2(this BinaryReader binaryReader)
-        {
-            return new Vector2(binaryReader.ReadSingle(), binaryReader.ReadSingle());
-        }
+        stream.Read(buffer, 0, 8);
 
-        public static Vector3 ReadVector3(this BinaryReader binaryReader)
-        {
-            return new Vector3(
-                binaryReader.ReadSingle(), binaryReader.ReadSingle(), binaryReader.ReadSingle()
-            );
-        }
+        if (!BytesEqual(buffer, PngHeader))
+            return false;
 
-        public static Vector4 ReadVector4(this BinaryReader binaryReader)
-        {
-            return new Vector4(
-                binaryReader.ReadSingle(), binaryReader.ReadSingle(),
-                binaryReader.ReadSingle(), binaryReader.ReadSingle()
-            );
-        }
+        buffer = new byte[4];
 
-        public static void Write(this BinaryWriter binaryWriter, Quaternion quaternion)
+        do
         {
-            binaryWriter.Write(quaternion.x);
-            binaryWriter.Write(quaternion.y);
-            binaryWriter.Write(quaternion.z);
-            binaryWriter.Write(quaternion.w);
-        }
+            stream.Read(buffer, 0, 4);
 
-        public static void WriteQuaternion(this BinaryWriter binaryWriter, Quaternion quaternion)
-        {
-            binaryWriter.Write(quaternion.x);
-            binaryWriter.Write(quaternion.y);
-            binaryWriter.Write(quaternion.z);
-            binaryWriter.Write(quaternion.w);
-        }
+            if (BitConverter.IsLittleEndian)
+                Array.Reverse(buffer);
 
-        public static Quaternion ReadQuaternion(this BinaryReader binaryReader)
-        {
-            return new Quaternion
-            (
-                binaryReader.ReadSingle(), binaryReader.ReadSingle(),
-                binaryReader.ReadSingle(), binaryReader.ReadSingle()
-            );
-        }
+            var length = BitConverter.ToUInt32(buffer, 0);
 
-        public static void Write(this BinaryWriter binaryWriter, Color colour)
-        {
-            binaryWriter.Write(colour.r);
-            binaryWriter.Write(colour.g);
-            binaryWriter.Write(colour.b);
-            binaryWriter.Write(colour.a);
+            stream.Read(buffer, 0, 4);
+            stream.Seek(length + 4L, SeekOrigin.Current);
         }
+        while (!BytesEqual(buffer, PngEnd));
 
-        public static void WriteColour(this BinaryWriter binaryWriter, Color colour)
-        {
-            binaryWriter.Write(colour.r);
-            binaryWriter.Write(colour.g);
-            binaryWriter.Write(colour.b);
-            binaryWriter.Write(colour.a);
-        }
+        return true;
+    }
 
-        public static Color ReadColour(this BinaryReader binaryReader)
-        {
-            return new Color
-            (
-                binaryReader.ReadSingle(), binaryReader.ReadSingle(),
-                binaryReader.ReadSingle(), binaryReader.ReadSingle()
-            );
-        }
+    public static void WriteToFile(string name, IEnumerable<string> list)
+    {
+        if (Path.GetExtension(name) is not ".txt")
+            name += ".txt";
 
-        public static Matrix4x4 ReadMatrix4x4(this BinaryReader binaryReader)
-        {
-            Matrix4x4 matrix = default;
-            for (var i = 0; i < 16; i++) matrix[i] = binaryReader.ReadSingle();
-            return matrix;
-        }
+        File.WriteAllLines(Path.Combine(Constants.ConfigPath, name), list.ToArray());
     }
+
+    public static void WriteToFile(string name, byte[] data) =>
+        File.WriteAllBytes(Path.Combine(Constants.ConfigPath, name), data);
 }

+ 8 - 9
src/MeidoPhotoStudio.Plugin/WindowsLogicalComparer.cs

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

+ 16 - 0
stylecop.json

@@ -0,0 +1,16 @@
+{
+  "$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json",
+  "settings": {
+    "layoutRules": {
+      "newlineAtEndOfFile": "require"
+    },
+    "indentation": {
+      "indentationSize": 4,
+      "useTabs": false
+    },
+    "orderingRules": {
+      "usingDirectivesPlacement": "outsideNamespace",
+      "blankLinesBetweenUsingGroups": "require"
+    }
+  }
+}