浏览代码

Initial commit

habeebweeb 4 年之前
当前提交
ba9d15fc87
共有 47 个文件被更改,包括 5628 次插入0 次删除
  1. 361 0
      .gitignore
  2. 25 0
      COM3D2.MeidoPhotoStudio.Plugin.sln
  3. 24 0
      COM3D2.MeidoPhotoStudio.Plugin/.vscode/tasks.json
  4. 36 0
      COM3D2.MeidoPhotoStudio.Plugin/COM3D2.MeidoPhotoStudio.Plugin.csproj
  5. 509 0
      COM3D2.MeidoPhotoStudio.Plugin/Config/MeidoPhotoStudio/mm_pose_list.json
  6. 545 0
      COM3D2.MeidoPhotoStudio.Plugin/Config/MeidoPhotoStudio/translations.en.json
  7. 221 0
      COM3D2.MeidoPhotoStudio.Plugin/MeidoPhotoStudio/Constants.cs
  8. 25 0
      COM3D2.MeidoPhotoStudio.Plugin/MeidoPhotoStudio/GUI/Controls/BaseControl.cs
  9. 28 0
      COM3D2.MeidoPhotoStudio.Plugin/MeidoPhotoStudio/GUI/Controls/Button.cs
  10. 295 0
      COM3D2.MeidoPhotoStudio.Plugin/MeidoPhotoStudio/GUI/Controls/DropDown.cs
  11. 36 0
      COM3D2.MeidoPhotoStudio.Plugin/MeidoPhotoStudio/GUI/Controls/MiscGUI.cs
  12. 43 0
      COM3D2.MeidoPhotoStudio.Plugin/MeidoPhotoStudio/GUI/Controls/SelectionGrid.cs
  13. 61 0
      COM3D2.MeidoPhotoStudio.Plugin/MeidoPhotoStudio/GUI/Controls/Slider.cs
  14. 19 0
      COM3D2.MeidoPhotoStudio.Plugin/MeidoPhotoStudio/GUI/Controls/TextArea.cs
  15. 20 0
      COM3D2.MeidoPhotoStudio.Plugin/MeidoPhotoStudio/GUI/Controls/TextField.cs
  16. 37 0
      COM3D2.MeidoPhotoStudio.Plugin/MeidoPhotoStudio/GUI/Controls/Toggle.cs
  17. 18 0
      COM3D2.MeidoPhotoStudio.Plugin/MeidoPhotoStudio/GUI/Panes/BasePane.cs
  18. 152 0
      COM3D2.MeidoPhotoStudio.Plugin/MeidoPhotoStudio/GUI/Panes/MaidFaceSliderPane.cs
  19. 78 0
      COM3D2.MeidoPhotoStudio.Plugin/MeidoPhotoStudio/GUI/Panes/MaidSelectorPane.cs
  20. 69 0
      COM3D2.MeidoPhotoStudio.Plugin/MeidoPhotoStudio/GUI/Panes/MaidSwitcherPane.cs
  21. 40 0
      COM3D2.MeidoPhotoStudio.Plugin/MeidoPhotoStudio/GUI/Panes/TabsPane.cs
  22. 23 0
      COM3D2.MeidoPhotoStudio.Plugin/MeidoPhotoStudio/GUI/Windows/BaseMainWindow.cs
  23. 17 0
      COM3D2.MeidoPhotoStudio.Plugin/MeidoPhotoStudio/GUI/Windows/BaseWindow.cs
  24. 20 0
      COM3D2.MeidoPhotoStudio.Plugin/MeidoPhotoStudio/GUI/Windows/MainWindows/Background2Window.cs
  25. 57 0
      COM3D2.MeidoPhotoStudio.Plugin/MeidoPhotoStudio/GUI/Windows/MainWindows/BackgroundWindow.cs
  26. 46 0
      COM3D2.MeidoPhotoStudio.Plugin/MeidoPhotoStudio/GUI/Windows/MainWindows/MaidCallWindow.cs
  27. 103 0
      COM3D2.MeidoPhotoStudio.Plugin/MeidoPhotoStudio/GUI/Windows/MainWindows/MaidFaceWindow.cs
  28. 119 0
      COM3D2.MeidoPhotoStudio.Plugin/MeidoPhotoStudio/GUI/Windows/MainWindows/MaidPoseWindow.cs
  29. 101 0
      COM3D2.MeidoPhotoStudio.Plugin/MeidoPhotoStudio/GUI/Windows/MessageWindow.cs
  30. 573 0
      COM3D2.MeidoPhotoStudio.Plugin/MeidoPhotoStudio/Managers/DragPointManager.cs
  31. 63 0
      COM3D2.MeidoPhotoStudio.Plugin/MeidoPhotoStudio/Managers/EnvironmentManager.cs
  32. 183 0
      COM3D2.MeidoPhotoStudio.Plugin/MeidoPhotoStudio/Managers/MeidoManager.cs
  33. 142 0
      COM3D2.MeidoPhotoStudio.Plugin/MeidoPhotoStudio/Managers/WindowManager.cs
  34. 162 0
      COM3D2.MeidoPhotoStudio.Plugin/MeidoPhotoStudio/Meido/IK/BaseDrag.cs
  35. 131 0
      COM3D2.MeidoPhotoStudio.Plugin/MeidoPhotoStudio/Meido/IK/DragBody.cs
  36. 147 0
      COM3D2.MeidoPhotoStudio.Plugin/MeidoPhotoStudio/Meido/IK/DragHead.cs
  37. 108 0
      COM3D2.MeidoPhotoStudio.Plugin/MeidoPhotoStudio/Meido/IK/DragJointFinger.cs
  38. 98 0
      COM3D2.MeidoPhotoStudio.Plugin/MeidoPhotoStudio/Meido/IK/DragJointForearm.cs
  39. 128 0
      COM3D2.MeidoPhotoStudio.Plugin/MeidoPhotoStudio/Meido/IK/DragJointHand.cs
  40. 100 0
      COM3D2.MeidoPhotoStudio.Plugin/MeidoPhotoStudio/Meido/IK/DragMune.cs
  41. 66 0
      COM3D2.MeidoPhotoStudio.Plugin/MeidoPhotoStudio/Meido/IK/DragPelvis.cs
  42. 50 0
      COM3D2.MeidoPhotoStudio.Plugin/MeidoPhotoStudio/Meido/IK/DragSpine.cs
  43. 93 0
      COM3D2.MeidoPhotoStudio.Plugin/MeidoPhotoStudio/Meido/IK/DragTorso.cs
  44. 211 0
      COM3D2.MeidoPhotoStudio.Plugin/MeidoPhotoStudio/Meido/Meido.cs
  45. 163 0
      COM3D2.MeidoPhotoStudio.Plugin/MeidoPhotoStudio/MeidoPhotoStudio.cs
  46. 65 0
      COM3D2.MeidoPhotoStudio.Plugin/MeidoPhotoStudio/Utility.cs
  47. 17 0
      readme.md

+ 361 - 0
.gitignore

@@ -0,0 +1,361 @@
+lib/
+## Ignore Visual Studio temporary files, build results, and
+## files generated by popular Visual Studio add-ons.
+##
+## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
+
+# User-specific files
+*.rsuser
+*.suo
+*.user
+*.userosscache
+*.sln.docstates
+
+# User-specific files (MonoDevelop/Xamarin Studio)
+*.userprefs
+
+# Mono auto generated files
+mono_crash.*
+
+# Build results
+[Dd]ebug/
+[Dd]ebugPublic/
+[Rr]elease/
+[Rr]eleases/
+x64/
+x86/
+[Ww][Ii][Nn]32/
+[Aa][Rr][Mm]/
+[Aa][Rr][Mm]64/
+bld/
+[Bb]in/
+[Oo]bj/
+[Ll]og/
+[Ll]ogs/
+
+# Visual Studio 2015/2017 cache/options directory
+.vs/
+# Uncomment if you have tasks that create the project's static files in wwwroot
+#wwwroot/
+
+# Visual Studio 2017 auto generated files
+Generated\ Files/
+
+# MSTest test Results
+[Tt]est[Rr]esult*/
+[Bb]uild[Ll]og.*
+
+# NUnit
+*.VisualState.xml
+TestResult.xml
+nunit-*.xml
+
+# Build Results of an ATL Project
+[Dd]ebugPS/
+[Rr]eleasePS/
+dlldata.c
+
+# Benchmark Results
+BenchmarkDotNet.Artifacts/
+
+# .NET Core
+project.lock.json
+project.fragment.lock.json
+artifacts/
+
+# ASP.NET Scaffolding
+ScaffoldingReadMe.txt
+
+# StyleCop
+StyleCopReport.xml
+
+# Files built by Visual Studio
+*_i.c
+*_p.c
+*_h.h
+*.ilk
+*.meta
+*.obj
+*.iobj
+*.pch
+*.pdb
+*.ipdb
+*.pgc
+*.pgd
+*.rsp
+*.sbr
+*.tlb
+*.tli
+*.tlh
+*.tmp
+*.tmp_proj
+*_wpftmp.csproj
+*.log
+*.vspscc
+*.vssscc
+.builds
+*.pidb
+*.svclog
+*.scc
+
+# Chutzpah Test files
+_Chutzpah*
+
+# Visual C++ cache files
+ipch/
+*.aps
+*.ncb
+*.opendb
+*.opensdf
+*.sdf
+*.cachefile
+*.VC.db
+*.VC.VC.opendb
+
+# Visual Studio profiler
+*.psess
+*.vsp
+*.vspx
+*.sap
+
+# Visual Studio Trace Files
+*.e2e
+
+# TFS 2012 Local Workspace
+$tf/
+
+# Guidance Automation Toolkit
+*.gpState
+
+# ReSharper is a .NET coding add-in
+_ReSharper*/
+*.[Rr]e[Ss]harper
+*.DotSettings.user
+
+# TeamCity is a build add-in
+_TeamCity*
+
+# DotCover is a Code Coverage Tool
+*.dotCover
+
+# AxoCover is a Code Coverage Tool
+.axoCover/*
+!.axoCover/settings.json
+
+# Coverlet is a free, cross platform Code Coverage Tool
+coverage*[.json, .xml, .info]
+
+# Visual Studio code coverage results
+*.coverage
+*.coveragexml
+
+# NCrunch
+_NCrunch_*
+.*crunch*.local.xml
+nCrunchTemp_*
+
+# MightyMoose
+*.mm.*
+AutoTest.Net/
+
+# Web workbench (sass)
+.sass-cache/
+
+# Installshield output folder
+[Ee]xpress/
+
+# DocProject is a documentation generator add-in
+DocProject/buildhelp/
+DocProject/Help/*.HxT
+DocProject/Help/*.HxC
+DocProject/Help/*.hhc
+DocProject/Help/*.hhk
+DocProject/Help/*.hhp
+DocProject/Help/Html2
+DocProject/Help/html
+
+# Click-Once directory
+publish/
+
+# Publish Web Output
+*.[Pp]ublish.xml
+*.azurePubxml
+# Note: Comment the next line if you want to checkin your web deploy settings,
+# but database connection strings (with potential passwords) will be unencrypted
+*.pubxml
+*.publishproj
+
+# Microsoft Azure Web App publish settings. Comment the next line if you want to
+# checkin your Azure Web App publish settings, but sensitive information contained
+# in these scripts will be unencrypted
+PublishScripts/
+
+# NuGet Packages
+*.nupkg
+# NuGet Symbol Packages
+*.snupkg
+# The packages folder can be ignored because of Package Restore
+**/[Pp]ackages/*
+# except build/, which is used as an MSBuild target.
+!**/[Pp]ackages/build/
+# Uncomment if necessary however generally it will be regenerated when needed
+#!**/[Pp]ackages/repositories.config
+# NuGet v3's project.json files produces more ignorable files
+*.nuget.props
+*.nuget.targets
+
+# Microsoft Azure Build Output
+csx/
+*.build.csdef
+
+# Microsoft Azure Emulator
+ecf/
+rcf/
+
+# Windows Store app package directories and files
+AppPackages/
+BundleArtifacts/
+Package.StoreAssociation.xml
+_pkginfo.txt
+*.appx
+*.appxbundle
+*.appxupload
+
+# Visual Studio cache files
+# files ending in .cache can be ignored
+*.[Cc]ache
+# but keep track of directories ending in .cache
+!?*.[Cc]ache/
+
+# Others
+ClientBin/
+~$*
+*~
+*.dbmdl
+*.dbproj.schemaview
+*.jfm
+*.pfx
+*.publishsettings
+orleans.codegen.cs
+
+# Including strong name files can present a security risk
+# (https://github.com/github/gitignore/pull/2483#issue-259490424)
+#*.snk
+
+# Since there are multiple workflows, uncomment next line to ignore bower_components
+# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
+#bower_components/
+
+# RIA/Silverlight projects
+Generated_Code/
+
+# Backup & report files from converting an old project file
+# to a newer Visual Studio version. Backup files are not needed,
+# because we have git ;-)
+_UpgradeReport_Files/
+Backup*/
+UpgradeLog*.XML
+UpgradeLog*.htm
+ServiceFabricBackup/
+*.rptproj.bak
+
+# SQL Server files
+*.mdf
+*.ldf
+*.ndf
+
+# Business Intelligence projects
+*.rdl.data
+*.bim.layout
+*.bim_*.settings
+*.rptproj.rsuser
+*- [Bb]ackup.rdl
+*- [Bb]ackup ([0-9]).rdl
+*- [Bb]ackup ([0-9][0-9]).rdl
+
+# Microsoft Fakes
+FakesAssemblies/
+
+# GhostDoc plugin setting file
+*.GhostDoc.xml
+
+# Node.js Tools for Visual Studio
+.ntvs_analysis.dat
+node_modules/
+
+# Visual Studio 6 build log
+*.plg
+
+# Visual Studio 6 workspace options file
+*.opt
+
+# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
+*.vbw
+
+# Visual Studio LightSwitch build output
+**/*.HTMLClient/GeneratedArtifacts
+**/*.DesktopClient/GeneratedArtifacts
+**/*.DesktopClient/ModelManifest.xml
+**/*.Server/GeneratedArtifacts
+**/*.Server/ModelManifest.xml
+_Pvt_Extensions
+
+# Paket dependency manager
+.paket/paket.exe
+paket-files/
+
+# FAKE - F# Make
+.fake/
+
+# CodeRush personal settings
+.cr/personal
+
+# Python Tools for Visual Studio (PTVS)
+__pycache__/
+*.pyc
+
+# Cake - Uncomment if you are using it
+# tools/**
+# !tools/packages.config
+
+# Tabs Studio
+*.tss
+
+# Telerik's JustMock configuration file
+*.jmconfig
+
+# BizTalk build output
+*.btp.cs
+*.btm.cs
+*.odx.cs
+*.xsd.cs
+
+# OpenCover UI analysis results
+OpenCover/
+
+# Azure Stream Analytics local run output
+ASALocalRun/
+
+# MSBuild Binary and Structured Log
+*.binlog
+
+# NVidia Nsight GPU debugger configuration file
+*.nvuser
+
+# MFractors (Xamarin productivity tool) working folder
+.mfractor/
+
+# Local History for Visual Studio
+.localhistory/
+
+# BeatPulse healthcheck temp database
+healthchecksdb
+
+# Backup folder for Package Reference Convert tool in Visual Studio 2017
+MigrationBackup/
+
+# Ionide (cross platform F# VS Code tools) working folder
+.ionide/
+
+# Fody - auto-generated XML schema
+FodyWeavers.xsd

+ 25 - 0
COM3D2.MeidoPhotoStudio.Plugin.sln

@@ -0,0 +1,25 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 16
+VisualStudioVersion = 16.0.29806.167
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "COM3D2.MeidoPhotoStudio.Plugin", "COM3D2.MeidoPhotoStudio.Plugin\COM3D2.MeidoPhotoStudio.Plugin.csproj", "{5FA40522-E0AC-459A-A571-DD0051217AB6}"
+EndProject
+Global
+	GlobalSection(SolutionConfigurationPlatforms) = preSolution
+		Debug|Any CPU = Debug|Any CPU
+		Release|Any CPU = Release|Any CPU
+	EndGlobalSection
+	GlobalSection(ProjectConfigurationPlatforms) = postSolution
+		{5FA40522-E0AC-459A-A571-DD0051217AB6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{5FA40522-E0AC-459A-A571-DD0051217AB6}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{5FA40522-E0AC-459A-A571-DD0051217AB6}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{5FA40522-E0AC-459A-A571-DD0051217AB6}.Release|Any CPU.Build.0 = Release|Any CPU
+	EndGlobalSection
+	GlobalSection(SolutionProperties) = preSolution
+		HideSolutionNode = FALSE
+	EndGlobalSection
+	GlobalSection(ExtensibilityGlobals) = postSolution
+		SolutionGuid = {0CF8022A-1ECB-4FEB-8A20-6E7E544E3DC4}
+	EndGlobalSection
+EndGlobal

+ 24 - 0
COM3D2.MeidoPhotoStudio.Plugin/.vscode/tasks.json

@@ -0,0 +1,24 @@
+{
+    // See https://go.microsoft.com/fwlink/?LinkId=733558
+    // for the documentation about the tasks.json format
+    "version": "2.0.0",
+    "tasks": [
+        {
+            "label": "build",
+            "command": "dotnet",
+            "type": "process",
+            "args": [
+                // Ask msbuild to generate full paths for file names.
+                "build",
+                "${workspaceFolder}/COM3D2.MeidoPhotoStudio.Plugin.csproj",
+                "/property:GenerateFullPaths=true",
+                "/property:Configuration=Release",
+                // Do not generate summary otherwise it leads to duplicate errors in Problems panel
+                "/consoleloggerparameters:NoSummary"
+            ],
+            "group": "build",
+            // Use the standard MS compiler pattern to detect errors, warnings and infos
+            "problemMatcher": "$msCompile"
+        }
+    ]
+}

+ 36 - 0
COM3D2.MeidoPhotoStudio.Plugin/COM3D2.MeidoPhotoStudio.Plugin.csproj

@@ -0,0 +1,36 @@
+<Project Sdk="Microsoft.Net.Sdk">
+  <PropertyGroup>
+    <TargetFramework>net35</TargetFramework>
+    <AssemblyName>COM3D2.MeidoPhotoStudio.Plugin</AssemblyName>
+    <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
+    <FrameworkPathOverride Condition="'$(TargetFramework)' == 'net35'">$(MSBuildProgramFiles32)\Reference Assemblies\Microsoft\Framework\.NETFramework\v3.5\Profile\Client</FrameworkPathOverride>
+  </PropertyGroup>
+  <ItemGroup>
+    <Reference Include="Assembly-CSharp">
+      <HintPath>..\lib\Assembly-CSharp.dll</HintPath>
+    </Reference>
+    <Reference Include="Assembly-CSharp-firstpass">
+      <HintPath>..\lib\Assembly-CSharp-firstpass.dll</HintPath>
+    </Reference>
+    <Reference Include="Assembly-UnityScript-firstpass">
+      <HintPath>..\lib\Assembly-UnityScript-firstpass.dll</HintPath>
+    </Reference>
+    <Reference Include="ExIni">
+      <HintPath>..\lib\ExIni.dll</HintPath>
+    </Reference>
+    <Reference Include="UnityEngine">
+      <HintPath>..\lib\UnityEngine.dll</HintPath>
+    </Reference>
+    <Reference Include="UnityInjector">
+      <HintPath>..\lib\UnityInjector.dll</HintPath>
+    </Reference>
+    <Reference Include="Newtonsoft.Json">
+      <HintPath>..\lib\Newtonsoft.Json.dll</HintPath>
+    </Reference>
+  </ItemGroup>
+  <ItemGroup>
+    <Content Include="Config\**">
+        <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+    </Content>
+  </ItemGroup>
+</Project>

+ 509 - 0
COM3D2.MeidoPhotoStudio.Plugin/Config/MeidoPhotoStudio/mm_pose_list.json

@@ -0,0 +1,509 @@
+[
+    {
+        "uiName": "normal",
+        "poseList": [
+            "pose_taiki_f",
+            "pose_01_f",
+            "pose_02_f",
+            "pose_03_f",
+            "pose_04_f",
+            "pose_ero_01_loop_f",
+            "pose_ero_02_loop_f",
+            "pose_ero_03_loop_f",
+            "pose_ero_04_loop_f",
+            "pose_ero_05_loop_f",
+            "pose_ero_06_loop_f",
+            "pose_kakkoii_01_loop_f",
+            "pose_kakkoii_02_loop_f",
+            "pose_kakkoii_03_loop_f",
+            "pose_kakkoii_04_loop_f",
+            "pose_kakkoii_05_loop_f",
+            "pose_kakkoii_06_loop_f",
+            "pose_kawaii_01_loop_f",
+            "pose_kawaii_02_loop_f",
+            "pose_kawaii_03_loop_f",
+            "pose_kawaii_04_loop_f",
+            "pose_kawaii_05_loop_f",
+            "pose_kawaii_06_loop_f",
+            "edit_pose21_001_f",
+            "edit_pose21_002_f",
+            "edit_pose21_003_f",
+            "edit_pose21_mune_taiki_f",
+            "edit_pose21_mune_tate_f_once_",
+            "edit_pose21_mune_yoko_f_once_"
+        ]
+    },
+    {
+        "uiName": "standing",
+        "poseList": [
+            "maid_dressroom01",
+            "kaiwa_tati_hutuu1_taiki_f",
+            "maid_dressroom02",
+            "poseizi_taiki_f",
+            "maid_dressroom03",
+            "kaiwa_tati_yorokobub_taiki_f",
+            "maid_stand02akireloop",
+            "stand_madogiwa",
+            "kaiwa_tati_akireb_taiki_f",
+            "kaiwa_tati_udekumu_taiki_f",
+            "maid_stand02listenloop",
+            "sys_munehide",
+            "poseizi2_kakusu_taiki_f",
+            "maid_stand03_base",
+            "maid_stand02",
+            "maid_stand02tere",
+            "kaiwa_tati_ubiawase_taiki_f",
+            "kaiwa_tati_teawase_taiki_f",
+            "kaiwa_tati_hakusyu_taiki_2_f",
+            "maid_view1",
+            "maid_comehome2_loop_",
+            "kaiwa_tati_hohokaki_taiki_f",
+            "kaiwa_tati_tere_taiki_f",
+            "sys_muneporo",
+            "kaiwa_tati_yorokobua_taiki_f",
+            "kaiwa_tati_odoroku_taiki_f",
+            "kaiwa_tati_tutorial_1_taiki_f",
+            "maid_stand05",
+            "kaiwa_tati_tutorial_2_taiki_f",
+            "stand_annai",
+            "kaiwa_tati_teofuru_taiki_f",
+            "kaiwa_tati_munetataku_taiki_f",
+            "kaiwa_tati_yubisasu_taiki_f",
+            "kaiwa_tati_iya_taiki_f",
+            "hinpyoukai_gattu_taiki_f",
+            "hinpyoukai_tewatasi_taiki_f",
+            "kaiwa_tati_yorokobu_taiki_f",
+            "kaiwa_tati_akire_taiki_f",
+            "momi_momi_f",
+            "soji_hakisouji",
+            "soji_hataki",
+            "soji_syokkiarai",
+            "work_ryouri_nabe_mazeru",
+            "work_sentakuhosu",
+            "soji_mop_itazurago",
+            "kaiwa_tati_hirumi_taiki_f"
+        ]
+    },
+    {
+        "uiName": "halfStand",
+        "poseList": [
+            "tennis_kamae_f",
+            "kaiwa_tati_ayamaru_taiki_f",
+            "kaiwa_tati_syazai_taiki_f",
+            "stand_akire",
+            "kaiwa_tati_ibalu_taiki_f",
+            "work_kaimono_itazurago",
+            "kaiwa_tati_kuyasi_taiki_2_f",
+            "soji_houki_itazurago",
+            "work_ryouri_houtyou",
+            "work_demukae_itazurago",
+            "kaiwa_tati_mo_taiki_f",
+            "poseizi2_zeccyougo_f",
+            "poseizi_zeccyougo_f",
+            "fukisouji1",
+            "fukisouji1_vibe",
+            "soji_tubo",
+            "soji_mop",
+            "soji_mop_vibe",
+            "work_mizuyari_itazurago",
+            "work_sentakuhosu_itazurago",
+            "soji_hataki_itazurago",
+            "fukisouji1_itazurago",
+            "maidcho_oha1"
+        ]
+    },
+    {
+        "uiName": "kneeling",
+        "poseList": [
+            "senakanagasi_f",
+            "paizuri_taiki_f",
+            "paizuri_fera_shaseigo_f",
+            "osuwariaibu1",
+            "self_ir_kansou_f",
+            "inu_pose_f",
+            "rosyutu_hounyou_taiki_f"
+        ]
+    },
+    {
+        "uiName": "sitting",
+        "poseList": [
+            "work_hansei",
+            "sex_osuwari_taiki",
+            "item_candy0_osuwari",
+            "rosyutu_pose06_f",
+            "sit_bed1",
+            "hanyou_dogeza_taiki_f",
+            "hanyou_dogeza_aisatu_f",
+            "hanyou_dogeza_f"
+        ]
+    },
+    {
+        "uiName": "onAllFours",
+        "poseList": [
+            "inu_taiki_f",
+            "soji_zoukin",
+            "soji_zoukin_itazurago",
+            "massage_f",
+            "mp_arai_taiki_f",
+            "midasinami_esthe_f2",
+            "midasinami_esthe_f"
+        ]
+    },
+    {
+        "uiName": "sittingFloor",
+        "poseList": [
+            "syagami_pose_f",
+            "hanyou_kizetu_f"
+        ]
+    },
+    {
+        "uiName": "sittingChair",
+        "poseList": [
+            "densyasuwari_taiki_f",
+            "ocha_pose_taiki_f"
+        ]
+    },
+    {
+        "uiName": "sittingSofa",
+        "poseList": [
+            "work_kaiwa",
+            "work_kaiwa_itazurago",
+            "work_hon",
+            "work_hon_itazurago",
+            "work_saihou",
+            "work_sentaku_tatamu",
+            "midasinami_kadou_f",
+            "work_mimi_f",
+            "work_mimi_itazurago_f",
+            "sit_yasumi1",
+            "sit_tukue",
+            "sleep1",
+            "kaiwa_sofa_utumuku_taiki_f",
+            "kaiwa_sofa_teawasea_taiki2_f",
+            "kaiwa_sofa_teawase_taiki_f",
+            "kaiwa_sofa_hazukasii_taiki_f",
+            "op_osyaku_taiki_f",
+            "op_wine_taiki_f",
+            "kaiwa_sofa_1_f",
+            "kaiwa_sofa_noridasu_1_taiki_f",
+            "kaiwa_sofa_noridasu_2_taiki_f",
+            "kaiwa_sofa_kangaerua_taiki_f",
+            "kaiwa_sofa_kangaerub_taiki_f",
+            "kaiwa_sofa_kangaeru_taiki_f",
+            "kaiwa_sofa_tere_taiki_f",
+            "kaiwa_sofa_konwakua_taiki_f",
+            "kaiwa_sofa_odoroki_taiki_f",
+            "kaiwa_sofa_konwaku_2_taiki_f",
+            "kaiwa_sofa_konwaku_taiki_f"
+        ]
+    },
+    {
+        "uiName": "walking",
+        "poseList": [
+            "rosyutu_aruki_f_once_,1.37",
+            "rosyutu_aruki_f_once_,2.35",
+            "rosyutu_aruki_omocya_f,1.4",
+            "rosyutu_aruki_omocya_f,2.72",
+            "rosyutu_omocya_aruki_f_once_,1.54",
+            "rosyutu_omocya_aruki_f_once_,2.33"
+        ]
+    },
+    {
+        "uiName": "other",
+        "poseList": [
+            "stand_desk1",
+            "soji_tukue",
+            "soji_tukuefuki_salon"
+        ]
+    },
+    {
+        "uiName": "danceDokiDoki",
+        "poseList": [
+            "dance_cm3d2_001_f1,14.14",
+            "dance_cm3d2_001_f1,18.72",
+            "dance_cm3d2_001_f1,15.34",
+            "dance_cm3d2_001_f1,35.20",
+            "dance_cm3d2_001_f1,36.15",
+            "dance_cm3d2_001_f1,74.72",
+            "dance_cm3d2_001_f1,74.52",
+            "dance_cm3d2_001_f1,74.13",
+            "dance_cm3d2_001_f1,63.53",
+            "dance_cm3d2_001_f1,64.41",
+            "dance_cm3d2_001_f1,80.41",
+            "dance_cm3d2_001_f1,80.62",
+            "dance_cm3d2_001_f1,81.47",
+            "dance_cm3d2_001_f1,68.36",
+            "dance_cm3d2_001_f1,68.49",
+            "dance_cm3d2_001_f1,70.25",
+            "dance_cm3d2_001_f1,70.64",
+            "dance_cm3d2_001_f1,71.36",
+            "dance_cm3d2_001_f1,72.26",
+            "dance_cm3d2_001_f1,72.45",
+            "dance_cm3d2_001_f1,73.23",
+            "dance_cm3d2_001_f1,82.98",
+            "dance_cm3d2_001_f1,83.77",
+            "dance_cm3d2_001_f1,86.05",
+            "dance_cm3d2_001_f1,94.06",
+            "dance_cm3d2_001_f1,94.52",
+            "dance_cm3d2_001_f1,95.0",
+            "dance_cm3d2_001_f1,60.32",
+            "dance_cm3d2_001_f1,60.76",
+            "dance_cm3d2_001_f1,61.36",
+            "dance_cm3d2_001_f1,150.0"
+        ]
+    },
+    {
+        "uiName": "danceEntranceToYou",
+        "poseList": [
+            "dance_cm3d_001_f1,39.25",
+            "dance_cm3d_001_f1,8.29",
+            "dance_cm3d_001_f1,11.47",
+            "dance_cm3d_001_f1,12.67",
+            "dance_cm3d_001_f1,14.42",
+            "dance_cm3d_001_f1,18.45",
+            "dance_cm3d_001_f1,24.43",
+            "dance_cm3d_001_f1,52.57",
+            "dance_cm3d_001_f1,56.83",
+            "dance_cm3d_001_f1,58.18",
+            "dance_cm3d_001_f1,62.87",
+            "dance_cm3d_001_f1,63.84",
+            "dance_cm3d_001_f1,69.52",
+            "dance_cm3d_001_f1,70.52",
+            "dance_cm3d_001_f1,71.31",
+            "dance_cm3d_001_f1,72.67",
+            "dance_cm3d_001_f1,73.94",
+            "dance_cm3d_001_f1,77.55",
+            "dance_cm3d_001_f1,79.78",
+            "dance_cm3d_001_f1,82.56",
+            "dance_cm3d_001_f1,85.71",
+            "dance_cm3d_001_f1,105.82",
+            "dance_cm3d_001_f1,107.48",
+            "dance_cm3d_001_f1,107.92"
+        ]
+    },
+    {
+        "uiName": "danceScarletLeap",
+        "poseList": [
+            "dance_cm3d_002_end_f1,50.71",
+            "dance_cm3d_002_end_f1,53.04",
+            "dance_cm3d_002_end_f1,102.88",
+            "dance_cm3d_002_end_f1,75.18",
+            "dance_cm3d_002_end_f1,79.34",
+            "dance_cm3d_002_end_f1,97.01",
+            "dance_cm3d_002_end_f1,89.85",
+            "dance_cm3d_002_end_f1,26.74",
+            "dance_cm3d_002_end_f1,100.30",
+            "dance_cm3d_002_end_f1,101.38",
+            "dance_cm3d_002_end_f1,124.85",
+            "dance_cm3d_002_end_f1,35.40",
+            "dance_cm3d_002_end_f1,107.98",
+            "dance_cm3d_002_end_f1,106.71",
+            "dance_cm3d_002_end_f1,36.51",
+            "dance_cm3d_002_end_f1,47.54",
+            "dance_cm3d_002_end_f1,118.35",
+            "dance_cm3d_002_end_f1,43.37",
+            "dance_cm3d_002_end_f1,31.22",
+            "dance_cm3d_002_end_f1,90.71",
+            "dance_cm3d_002_end_f1,25.78",
+            "dance_cm3d_002_end_f1,24.85",
+            "dance_cm3d_002_end_f1,29.21",
+            "dance_cm3d_002_end_f1,29.53",
+            "dance_cm3d_002_end_f1,29.72",
+            "dance_cm3d_002_end_f1,128.61",
+            "dance_cm3d_002_end_f1,133.56",
+            "dance_cm3d_002_end_f1,138.26",
+            "dance_cm3d_002_end_f1,63.84",
+            "dance_cm3d_002_end_f1,170"
+        ]
+    },
+    {
+        "uiName": "danceRhythmix",
+        "poseList": [
+            "dance_cm3d_003_sp2_f1,90.15",
+            "dance_cm3d_003_sp2_f1,102.35",
+            "dance_cm3d_003_sp2_f1,66.56",
+            "dance_cm3d_003_sp2_f1,103.36",
+            "dance_cm3d_003_sp2_f1,103.86",
+            "dance_cm3d_003_sp2_f1,105.19",
+            "dance_cm3d_003_sp2_f1,100.05",
+            "dance_cm3d_003_sp2_f1,99.55",
+            "dance_cm3d_003_sp2_f1,19.54",
+            "dance_cm3d_003_sp2_f1,21.34",
+            "dance_cm3d_003_sp2_f1,11.84",
+            "dance_cm3d_003_sp2_f1,14.69",
+            "dance_cm3d_003_sp2_f1,24.44",
+            "dance_cm3d_003_sp2_f1,32.47",
+            "dance_cm3d_003_sp2_f1,47.97",
+            "dance_cm3d_003_sp2_f1,48.38",
+            "dance_cm3d_003_sp2_f1,51.32",
+            "dance_cm3d_003_sp2_f1,56.47",
+            "dance_cm3d_003_sp2_f1,61.64",
+            "dance_cm3d_003_sp2_f1,68.00",
+            "dance_cm3d_003_sp2_f1,69.35",
+            "dance_cm3d_003_sp2_f1,69.80",
+            "dance_cm3d_003_sp2_f1,72.68",
+            "dance_cm3d_003_sp2_f1,77.29",
+            "dance_cm3d_003_sp2_f1,82.81",
+            "dance_cm3d_003_sp2_f1,83.98",
+            "dance_cm3d_003_sp2_f1,92.09",
+            "dance_cm3d_003_sp2_f1,101.40",
+            "dance_cm3d_003_sp2_f1,104.48",
+            "dance_cm3d_003_sp2_f1,106.61",
+            "dance_cm3d_003_sp2_f1,106.78",
+            "dance_cm3d_003_sp2_f1,108.43",
+            "dance_cm3d_003_sp2_f1,109.41",
+            "dance_cm3d_003_sp2_f1,111.23",
+            "dance_cm3d_003_sp2_f1,112.67",
+            "dance_cm3d_003_sp2_f1,112.89",
+            "dance_cm3d_003_sp2_f1,114.03",
+            "dance_cm3d_003_sp2_f1,115.61"
+        ]
+    },
+    {
+        "uiName": "dance",
+        "poseList": [
+            "dance_cm3d_001_f1",
+            "dance_cm3d_002_end_f1",
+            "dance_cm3d_003_sp2_f1",
+            "dance_cm3d2_001_f1",
+            "dance_cm3d2_001_f2",
+            "dance_cm3d2_001_f3",
+            "dance_cm3d21_001_nmf_f1",
+            "dance_cm3d21_001_nmf_f2",
+            "dance_cm3d21_001_nmf_f3",
+            "dance_cm3d21_002_bid_f1",
+            "dance_cm3d21_002_bid_f2",
+            "dance_cm3d21_002_bid_f3",
+            "dance_cm3d21_003_kad_f1",
+            "dance_cm3d21_003_kad_f2",
+            "dance_cm3d21_003_kad_f3",
+            "dance_cm3d21_004_lm_f1",
+            "dance_cm3d21_004_lm_f2",
+            "dance_cm3d21_004_lm_f3",
+            "dance_cm3d21_005_moe_f1",
+            "dance_cm3d21_005_moe_f2",
+            "dance_cm3d21_005_moe_f3"
+        ]
+    },
+    {
+        "uiName": "danceMC",
+        "poseList": [
+            "dance_mc_001_p01a_f1_once_",
+            "dance_mc_001_p01a_f2_once_",
+            "dance_mc_001_p01a_f3_once_",
+            "dance_mc_001_p01b_f1_once_",
+            "dance_mc_001_p01b_f2_once_",
+            "dance_mc_001_p01b_f3_once_",
+            "dance_mc_001_p02_f1_once_",
+            "dance_mc_001_p02_f2_once_",
+            "dance_mc_001_p02_f3_once_",
+            "dance_mc_001_p03_good_f1_once_",
+            "dance_mc_001_p03_good_f2_once_",
+            "dance_mc_001_p03_good_f3_once_",
+            "dance_mc_001_p04_bad_f1_once_",
+            "dance_mc_001_p04_bad_f2_once_",
+            "dance_mc_001_p04_bad_f3_once_",
+            "dance_mc_001_p05_f1_once_",
+            "dance_mc_001_p05_f2_once_",
+            "dance_mc_001_p05_f3_once_",
+            "dance_mc_002_p01_f1_once_",
+            "dance_mc_002_p01_f2_once_",
+            "dance_mc_002_p01_f3_once_",
+            "dance_mc_002_p02_f1_once_",
+            "dance_mc_002_p02_f2_once_",
+            "dance_mc_002_p02_f3_once_",
+            "dance_mc_002_p03_good_f1_once_",
+            "dance_mc_002_p03_good_f2_once_",
+            "dance_mc_002_p03_good_f3_once_",
+            "dance_mc_002_p04_bad_f1_once_",
+            "dance_mc_002_p04_bad_f2_once_",
+            "dance_mc_002_p04_bad_f3_once_",
+            "dance_mc_002_p05_f1_once_",
+            "dance_mc_002_p05_f2_once_",
+            "dance_mc_002_p05_f3_once_",
+            "dance_mc_003_p01_f1_once_",
+            "dance_mc_003_p01_f2_once_",
+            "dance_mc_003_p01_f3_once_",
+            "dance_mc_003_p02_f1_once_",
+            "dance_mc_003_p02_f2_once_",
+            "dance_mc_003_p02_f3_once_",
+            "dance_mc_003_p03_good_f1_once_",
+            "dance_mc_003_p03_good_f2_once_",
+            "dance_mc_003_p03_good_f3_once_",
+            "dance_mc_003_p04_bad_f1_once_",
+            "dance_mc_003_p04_bad_f2_once_",
+            "dance_mc_003_p04_bad_f3_once_",
+            "dance_mc_003_p05_f1_once_",
+            "dance_mc_003_p05_f2_once_",
+            "dance_mc_003_p05_f3_once_",
+            "dance_mc_004_p01_f1_once_",
+            "dance_mc_004_p01_f2_once_",
+            "dance_mc_004_p01_f3_once_",
+            "dance_mc_004_p02_f1_once_",
+            "dance_mc_004_p02_f2_once_",
+            "dance_mc_004_p02_f3_once_",
+            "dance_mc_004_p03_good_f1_once_",
+            "dance_mc_004_p03_good_f2_once_",
+            "dance_mc_004_p03_good_f3_once_",
+            "dance_mc_004_p04_bad_f1_once_",
+            "dance_mc_004_p04_bad_f2_once_",
+            "dance_mc_004_p04_bad_f3_once_",
+            "dance_mc_004_p05_f1_once_",
+            "dance_mc_004_p05_f2_once_",
+            "dance_mc_004_p05_f3_once_",
+            "dance_mc_005_p01_f1_once_",
+            "dance_mc_005_p01_f2_once_",
+            "dance_mc_005_p01_f3_once_",
+            "dance_mc_005_p02_f1_once_",
+            "dance_mc_005_p02_f2_once_",
+            "dance_mc_005_p02_f3_once_",
+            "dance_mc_005_p03_good_f1_once_",
+            "dance_mc_005_p03_good_f2_once_",
+            "dance_mc_005_p03_good_f3_once_",
+            "dance_mc_005_p04_bad_f1_once_",
+            "dance_mc_005_p04_bad_f2_once_",
+            "dance_mc_005_p04_bad_f3_once_",
+            "dance_mc_005_p05_f1_once_",
+            "dance_mc_005_p05_f2_once_",
+            "dance_mc_005_p05_f3_once_"
+        ]
+    },
+    {
+        "uiName": "restraint",
+        "poseList": [
+            "turusi_sex_in_taiki_f",
+            "turusi_sex_shaseigo_naka_f",
+            "turusi_sex_shaseigo_soto_f",
+            "mokuba_sissin_f",
+            "poseizi2_taiki_f",
+            "hentai_pose_03_f",
+            "osuwariaibu2",
+            "rosyutu_pose03_f",
+            "kousoku_aibu_hibu_sissin_taiki_f"
+        ]
+    },
+    {
+        "uiName": "ero",
+        "poseList": [
+            "rosyutu_pose01_f",
+            "rosyutu_pose02_f",
+            "rosyutu_pose04_f",
+            "rosyutu_pose05_f",
+            "rosyutu_taiki_omocya_f",
+            "rosyutu_omocya_taiki_f",
+            "rosyutu_omocya_zeccyougo_f",
+            "rosyutu_tati_vibe_onani_zeccyougo_f",
+            "ran3p_housi_taiki_f",
+            "ran3p_housi_shaseigo_f",
+            "manguri_in_taiki_f",
+            "manguri_shaseigo_naka_f",
+            "manguri_taiki_f",
+            "manguri_shaseigo_soto_f",
+            "ran3p_seijyoui_kuti_shaseigo_soto_f",
+            "ran3p_seijyoui_kuti_sissin_taiki_f",
+            "nefera_shasei_kuti_nomi02_b_f",
+            "ran3p_2ana_in_taiki_f",
+            "haimenrituia_zikkyou_1_f"
+        ]
+    }
+]

+ 545 - 0
COM3D2.MeidoPhotoStudio.Plugin/Config/MeidoPhotoStudio/translations.en.json

@@ -0,0 +1,545 @@
+{
+    "maidCallWindow": {
+        "clearButton": "Clear",
+        "callButton": "Call"
+    },
+    "placementDropdown": {
+        "okButton": "OK",
+        "normal": "Normal",
+        "horizontalRow": "Horizontal Row",
+        "diagonal": "Diagonal",
+        "circleOut": "Circle (outer)",
+        "circleIn": "Circle (Inner)",
+        "fan": "Fan",
+        "v": "V",
+        "^": "^",
+        "m": "M",
+        "w": "W"
+    },
+    "maidPoseWindow": {
+        "ikToggle": "IK",
+        "releaseToggle": "Release",
+        "boneToggle": "Bone",
+        "flipToggle": "Flip IK",
+        "fixSkirtToggle": "Fix Skirt"
+    },
+    "poseGroupDropdown": {
+        "normal": "Normal",
+        "standing": "Standing",
+        "halfStand": "Half-Standing",
+        "kneeling": "Kneeling",
+        "sitting": "Sitting",
+        "onAllFours": "On All Fours",
+        "sittingFloor": "Sitting (Floor)",
+        "sittingChair": "Sitting (Chair)",
+        "sittingSofa": "Sitting (Sofa)",
+        "walking": "Walking",
+        "other": "Other",
+        "danceDokiDoki": "Doki Doki ☆ Fallin' Love",
+        "danceEntranceToYou": "Entrance To You",
+        "danceScarletLeap": "Scarlet Leap",
+        "danceRhythmix": "Rhythmix to You",
+        "dance": "Dance",
+        "danceMC": "Dance (MC)",
+        "restraint": "Restraint",
+        "ero": "Ero"
+    },
+    "poseSave": {
+        "saveToggle": "Save Pose",
+        "saveButton": "Add",
+        "deleteButton": "D"
+    },
+    "freeLook": {
+        "freeLookToggle": "F-Look",
+        "x": "Look X",
+        "y": "Look Y"
+    },
+    "clothing": {
+        "top": "Top",
+        "bottom": "Bottom",
+        "bra": "Bra",
+        "panties": "Panties",
+        "headwear": "Headwear",
+        "glasses": "Glasses",
+        "arms": "Arms",
+        "gloves": "Gloves",
+        "back": "Back",
+        "socks": "Socks",
+        "maid": "Maid",
+        "curlingFront": "Curling",
+        "curlingBack": "Curling",
+        "shiftPanties": "Shift"
+    },
+    "hands": {
+        "rightHand": "R Hand",
+        "leftHand": "L Hand"
+    },
+    "copyIK": {
+        "copyLabel": "copy",
+        "copyButton": "OK"
+    },
+    "voiceLines": {
+        "speak": "Speak",
+        "speakEro": "H Lines"
+    },
+    "maidFaceWindow": {
+        "faceLock": "L"
+    },
+    "faceBlendPresetsDropdown": {
+        "通常": "General: Normal",
+        "微笑み": "General: Half Smile",
+        "笑顔": "General: Big Smile",
+        "にっこり": "General: Sweet Smile",
+        "優しさ": "General: Gentle",
+        "発情": "General: Lust",
+        "ジト目": "General: Glare",
+        "閉じ目": "General: Eyes Closed",
+        "思案伏せ目": "General: Consideration",
+        "ドヤ顔": "General: Proud",
+        "引きつり笑顔": "General: Awkward Smile",
+        "苦笑い": "General: Bitter Smile",
+        "困った": "General: Distressed",
+        "疑問": "General: Curious",
+        "ぷんすか": "General: Upset",
+        "むー": "General: Mmmm..",
+        "泣き": "General: Cry",
+        "拗ね": "General: Sulk",
+        "照れ": "General: Bashful",
+        "悲しみ2": "General: Sad",
+        "きょとん": "General: Confused",
+        "びっくり": "General: Surprised",
+        "少し怒り": "General: Kinda Angry",
+        "怒り": "General: Angry",
+        "照れ叫び": "General: Shout",
+        "誘惑": "General: Temptation",
+        "接吻": "General: Kiss",
+        "居眠り安眠": "General: Sleep",
+        "まぶたギュ": "General: Eyes Shut",
+        "目を見開いて": "General: Eyes Wide Open",
+        "痛みで目を見開いて": "General: Eyes Open Pain",
+        "恥ずかしい": "General: Embarrassed",
+        "ためいき": "General: Sigh",
+        "目口閉じ": "General: Eyes and Mouth Closed",
+        "ウインク照れ": "General: Shy Wink",
+        "ダンス目つむり": "Dance: Eyes and Mouth Closed",
+        "ダンスあくび": "Dance: Yawn",
+        "ダンスびっくり": "Dance: Surprised",
+        "ダンス微笑み": "Dance: Half Smile",
+        "ダンス目あけ": "Dance: Eyes Open",
+        "ダンス目とじ": "Dance: Eyes Closed",
+        "ダンス誘惑": "Dance: Temptation",
+        "ダンス困り顔": "Dance: Troubled",
+        "ダンスウインク": "Dance: Wink",
+        "ダンス真剣": "Dance: Serious",
+        "ダンス憂い": "Dance: Sorrow",
+        "ダンスジト目": "Dance: Glare",
+        "ダンスキス": "Dance: Kiss",
+        "エロ通常1": "Ero: General 1",
+        "エロ通常2": "Ero: General 2",
+        "エロ通常3": "Ero: General 3",
+        "エロ興奮0": "Ero: Excitement 1",
+        "エロ興奮1": "Ero: Excitement 2",
+        "エロ興奮2": "Ero: Excitement 3",
+        "エロ興奮3": "Ero: Excitement 4",
+        "エロ好感1": "Ero: Favourable 1",
+        "エロ好感2": "Ero: Favourable 2",
+        "エロ好感3": "Ero: Favourable 3",
+        "エロ期待": "Ero: Anticipation",
+        "エロ羞恥1": "Ero: Bashful 1",
+        "エロ羞恥2": "Ero: Bashful 2",
+        "エロ羞恥3": "Ero: Bashful 3",
+        "エロ緊張": "Ero: Nervous",
+        "エロ我慢1": "Ero: Endurance 1",
+        "エロ我慢2": "Ero: Endurance 2",
+        "エロ我慢3": "Ero: Endurance 3",
+        "エロ嫌悪1": "Ero: Hate",
+        "エロ痛み1": "Ero: Pain 1",
+        "エロ痛み2": "Ero: Pain 2",
+        "エロ痛み3": "Ero: Pain 3",
+        "エロ痛み我慢": "Ero: Enduring 1",
+        "エロ痛み我慢2": "Ero: Enduring 2",
+        "エロ痛み我慢3": "Ero: Enduring 3",
+        "エロ怯え": "Ero: Afraid",
+        "エロメソ泣き": "Ero: Lewd Cry",
+        "あーん": "Ero: Ahhnn",
+        "エロ舌責": "Ero: Tongue Out",
+        "エロ舌責快楽": "Ero: Tongue Out Pleasure",
+        "エロ舌責嫌悪": "Ero: Tongue Out Hate",
+        "エロ舐め通常": "Ero: Lick Normal 1",
+        "エロ舐め通常2": "Ero: Lick Normal 2",
+        "エロ舐め愛情": "Ero: Lick Love 1",
+        "エロ舐め愛情2": "Ero: Lick Love 2",
+        "エロ舐め快楽": "Ero: Lick Pleasure 1",
+        "エロ舐め快楽2": "Ero: Lick Pleasure 2",
+        "エロ舐め嫌悪": "Ero: Lick Hate 1",
+        "エロ舐め嫌悪2": "Ero: Lick Hate 2",
+        "エロフェラ通常": "Ero: Fellatio Normal",
+        "エロフェラ愛情": "Ero: Fellatio Love",
+        "エロフェラ快楽": "Ero: Fellatio Pleasure",
+        "エロフェラ嫌悪": "Ero: Fellatio Hate",
+        "閉じ舐め通常": "Ero: Lick Normal 1 (close)",
+        "閉じ舐め通常2": "Ero: Lick Normal 2 (close)",
+        "閉じ舐め愛情": "Ero: Lick Love 1 (close)",
+        "閉じ舐め愛情2": "Ero: Lick Love 2 (close)",
+        "閉じ舐め快楽": "Ero:  Lick Pleasure 1 (close)",
+        "閉じ舐め快楽2": "Ero: Lick Pleasure 2 (close)",
+        "閉じ舐め嫌悪": "Ero: Lick Hate 1 (close)",
+        "閉じ舐め嫌悪2": "Ero: Lick Hate 2 (close)",
+        "閉じフェラ通常": "Ero: Fellatio Normal (close)",
+        "閉じフェラ愛情": "Ero: Fellatio Love (close)",
+        "閉じフェラ快楽": "Ero: Fellatio Pleasure (close)",
+        "閉じフェラ嫌悪": "Ero: Fellatio Hate (close)",
+        "通常射精後1": "Ero: After Ejaculation 1",
+        "通常射精後2": "Ero: After Ejaculation 2",
+        "絶頂射精後1": "Ero: After Ejaculation Orgasm 1",
+        "絶頂射精後2": "Ero: After Ejaculation Orgasm 2",
+        "興奮射精後1": "Ero: After Ejaculation Aroused 1",
+        "興奮射精後2": "Ero: After Ejaculation Aroused 2",
+        "余韻弱": "Ero: Afterglow",
+        "エロ絶頂": "Ero: Orgasm",
+        "エロ放心": "Ero: Absent Minded"
+    },
+    "faceBlendValues": {
+        "eyeclose": "Eye Shut",
+        "eyeclose2": "Eye Smile",
+        "eyeclose3": "Glare",
+        "eyebig": "Eyes Widen",
+        "eyeclose6": "Wink Smile",
+        "eyeclose5": "Wink Shut",
+        "hitomih": "Highlight",
+        "hitomis": "Pupil Size",
+        "mayuha": "Brows Angle",
+        "mayuw": "Brows Sad",
+        "mayuup": "Brows Up",
+        "mayuv": "Brows Angry 1",
+        "mayuvhalf": "Brows Angry 2",
+        "moutha": "Mouth Open 1",
+        "mouths": "Mouth Open 2",
+        "mouthc": "Mouth Narrow",
+        "mouthi": "Mouth Widen",
+        "mouthup": "Smile",
+        "mouthdw": "Frown",
+        "mouthhe": "Pout",
+        "mouthuphalf": "Grin",
+        "tangout": "Tongue Out",
+        "tangup": "Tongue Up",
+        "tangopen": "Tongue Base",
+        "hoho2": "Blush",
+        "shock": "Shade",
+        "nosefook": "Nose Up",
+        "namida": "Tears",
+        "yodare": "Drool",
+        "toothoff": "Teeth",
+        "tear1": "Cry 1",
+        "tear2": "Cry 2",
+        "tear3": "Cry 3",
+        "hohos": "Blush 1",
+        "hoho": "Blush 2",
+        "hohol": "Blush 3"
+    },
+    "faceSave": {
+        "saveButton": "Add",
+        "deleteButton": "D"
+    },
+    "backgroundWindow": {
+        "saveLoadButton": "Save\nLoad",
+        "movementCubeLabel": "Movement Cube",
+        "lightLabel": "Light",
+        "red": "Red",
+        "green": "Green",
+        "blue": "Blue",
+        "onToggle": "On"
+    },
+    "bgDropdown": {
+        "adultshop": "Adult Shop",
+        "aquarium": "Aquarium",
+        "aquarium_isu": "Aquarium (No Chairs)",
+        "BackStage": "Backstage",
+        "Bar": "Bar",
+        "BarLounge": "Bar Lounge",
+        "Bathroom": "Bathroom",
+        "BigSight": "Cosplay Convention",
+        "BigSight_Night": "Cosplay Convention (Night)",
+        "boutique": "Boutique",
+        "Casino": "Casino",
+        "CasinoMini": "Mini Casino",
+        "cathedral": "Cathedral",
+        "ClassRoom": "Classroom",
+        "ClassRoom_Play": "Classroom (Yotogi)",
+        "com3d2pool": "Pool",
+        "com3d2pool_night": "Pool (Night)",
+        "DanceRoom": "Training",
+        "DressRoom_NoMirror": "Dressing Room",
+        "empireclub_elevator": "Empire Club Elevator",
+        "EmpireClub_Entrance": "Empire Club Entrance",
+        "empireclub_hallway": "Empire Club Hallway",
+        "EmpireClub_Rotary": "Empire Club Rotary",
+        "EmpireClub_Rotary_Night": "Empire Club Rotary (Night)",
+        "fantasyinn": "Fantasy Inn",
+        "fantasyinn_night": "Fantasy Inn (Night)",
+        "GameShop": "Game Shop",
+        "gelaende": "Ski Slope",
+        "gelaende_night": "Ski Slope (Night)",
+        "HeroineRoom_A": "Innocence Room",
+        "HeroineRoom_A1": "Pure Room",
+        "HeroineRoom_A1_Night": "Pure Room",
+        "HeroineRoom_A_Night": "Innocence Room",
+        "HeroineRoom_B": "Kuudere Room",
+        "HeroineRoom_B1": "Serious Room",
+        "HeroineRoom_B1_Night": "Serious Room",
+        "HeroineRoom_B_Night": "Kuudere Room",
+        "HeroineRoom_C": "Tsundere",
+        "HeroineRoom_C1": "Rindere Room",
+        "HeroineRoom_C1_Night": "Rindere Room",
+        "HeroineRoom_C_Night": "Tsundere",
+        "HeroineRoom_D": "Yandere Room",
+        "HeroineRoom_D1": "Bookworm Room",
+        "HeroineRoom_D1_Night": "Bookworm Room",
+        "HeroineRoom_D_Night": "Yandere Room",
+        "HeroineRoom_E": "Oneechan Room",
+        "HeroineRoom_E1": "Koakuma Room",
+        "HeroineRoom_E1_Night": "Koakuma Room",
+        "HeroineRoom_E_Night": "Oneechan Room",
+        "HeroineRoom_F": "Genki Room",
+        "HeroineRoom_F1": "Anesan Room",
+        "HeroineRoom_F1_Night": "Anesan Room",
+        "HeroineRoom_F_Night": "Genki Room",
+        "HeroineRoom_G": "Sadist Room",
+        "HeroineRoom_G1": "Secretary Room",
+        "HeroineRoom_G1_Night": "Secretary Room",
+        "HeroineRoom_G_Night": "Sadist Room",
+        "HeroineRoom_H1": "Imouto Room",
+        "HeroineRoom_H1_Night": "Imouto Room",
+        "HeroineRoom_J1": "Wary Room",
+        "HeroineRoom_J1_Night": "Wary Room",
+        "homelesstents": "Homeless Tent",
+        "HoneymoonRoom": "Honeymoon Room",
+        "izakaya": "Japanese Bar",
+        "izakaya_play": "Japanese Bar Messy",
+        "japanesehouse": "Japanese Style House",
+        "japanesehouse_night": "Japanese Style House (Night)",
+        "KaraokeRoom": "Karaoke Room",
+        "Kitchen": "Kitchen",
+        "Kitchen_Night": "Kitchen (Night)",
+        "LargeBathRoom": "Large Bathroom",
+        "LiveStage": "Live Stage",
+        "LiveStage_Side": "Live Stage (On)",
+        "LiveStage_use_dance": "Live Stage (Off)",
+        "LockerRoom": "Locker ROom",
+        "luxurytoilet": "Luxury Bathroom",
+        "machikado": "Street Corner",
+        "machikado_night": "Street Corner (Night)",
+        "MaidRoom": "Maid Room",
+        "MainKitchen": "Main Kitchen",
+        "MainKitchen_LightOff": "Main Kitchen (Lights Off)",
+        "MainKitchen_Night": "Kitchen (Night)",
+        "MusicShop": "Music Shop",
+        "MyBedRoom": "Master's Room",
+        "MyBedRoom_Night": "Master's Room (Night)",
+        "MyBedRoom_NightOff": "Master's Room (Lights Off)",
+        "MyRoom": "Master's Room",
+        "MyRoom_Night": "Master's Room Night",
+        "Oheya": "Tatami Mat Room",
+        "OiranRoom": "Oiran Style Room",
+        "OpemCafe": "Open Cafe",
+        "opemcafe_aikiss": "Ai Kiss Collab Cafe",
+        "opemcafe_aikiss_night": "Ai Kiss Collab Cafe (Night)",
+        "opemcafe_cristalia": "CRYSTALiA Collab Cafe",
+        "opemcafe_cristalia_night": "CRYSTALiA Collab Cafe (Night)",
+        "opemcafe_evenicle2": "Evenicle Ⅱ Collab Cafe",
+        "opemcafe_evenicle2_night": "Evenicle Ⅱ Collab Cafe (Night)",
+        "opemcafe_inuyome2": "Wanko no Yomeiri Collab Cafe 2",
+        "opemcafe_inuyome2_night": "Wanko no Yomeiri Collab Cafe 2 (Night)",
+        "opemcafe_korolum": "Korolum Collab Cafe",
+        "opemcafe_korolum2": "Korolum Collab Cafe 2",
+        "opemcafe_korolum2_night": "Korolum Collab Cafe 2 (Night)",
+        "opemcafe_korolum_night": "Korolum Collab Cafe (Night)",
+        "opemcafe_laplacian": "Future Radio and Artificial Pigeons Collab Cafe",
+        "opemcafe_laplacian_night": "Future Radio and Artificial Pigeons Collab Cafe (Night)",
+        "opemcafe_nekow": "Nekopara Collab Cafe",
+        "opemcafe_nekow_night": "Nekopara Collab Cafe (Night)",
+        "OpemCafe_Night": "Open Cafe (Night)",
+        "opemcafe_nitro": "Minikui Mojika no Ko Collab Cafe",
+        "opemcafe_nitro_night": "Minikui Mojika no Ko Collab Cafe (Night)",
+        "opemcafe_pencil": "Mary-san Collab Cafe",
+        "opemcafe_pencil_night": "Mary-san Collab Cafe (Night)",
+        "opemcafe_rance10": "RanceX Collab Cafe",
+        "opemcafe_rance10_night": "RanceX Collab Cafe (Night)",
+        "opemcafe_raspberry": "Raspberry Cube Collab Cafe",
+        "opemcafe_raspberry_night": "Raspberry Cube Collab Cafe (Night)",
+        "opemcafe_riddlejoker": "Riddle Joker Collab Cafe",
+        "opemcafe_riddlejoker_night": "Riddle Joker Collab Cafe (Night)",
+        "opemcafe_sagapla": "Kin-iro Loveriche GT Collab Cafe",
+        "opemcafe_sagapla_night": "Kin-iro Loveriche GT Collab Cafe (Night)",
+        "opemcafe_wanko": "Wanko no Yomeiri Collab Cafe",
+        "opemcafe_wanko_night": "Wanko no Yomeiri Collab Cafe (Night)",
+        "OutletPark": "Outlet Park",
+        "park": "Park (No Food Trucks)",
+        "park_car": "Park",
+        "park_car_night": "Park (Night)",
+        "park_night": "Park (Night No Food Trucks)",
+        "Penthouse": "Penthouse",
+        "PlayRoom": "Playroom",
+        "PlayRoom2": "Playroom 2",
+        "poledancestage": "Pole Dance Stage",
+        "Pool": "Pool",
+        "PrivateRoom": "Private Room",
+        "privateroom2": "Private Room 2",
+        "privateroom2_night": "Private Room 2 (Night)",
+        "privateroom2_nightoff": "Private Room 2 (Lights Off)",
+        "PrivateRoom_Night": "Private Room (Night)",
+        "Restaurant": "Restaurant",
+        "Restaurant_Night": "Restaurant (Night)",
+        "Rotenburo": "Open-Air Bath",
+        "Rotenburo_Night": "Open-Air Bath (Night)",
+        "Salon": "Salon",
+        "Salon_Day": "Salon Day",
+        "Salon_Entrance": "Salon Entrance",
+        "Salon_Garden": "Courtyard",
+        "Sea": "Beach",
+        "seacafe": "Beach Cafe",
+        "seacafe_night": "Beach Cafe (Night)",
+        "Sea_Night": "Beach (Night)",
+        "Sea_VR": "Beach (VR)",
+        "Sea_VR_Night": "Beach (VR Night)",
+        "ShinShitsumu": "Office",
+        "ShinShitsumu_ChairRot": "Office (Chair)",
+        "ShinShitsumu_Night": "Office (Night)",
+        "Shitsumu": "Office",
+        "Shitsumu_ChairRot": "Office 2 (Chair)",
+        "Shitsumu_ChairRot_Night": "Office 2 (Chair Night)",
+        "Shitsumu_Night": "Office (Night)",
+        "ShoppingMall": "Shopping Mall",
+        "ShoppingMall_Night": "Shopping Mall (Night)",
+        "shrine": "Shrine",
+        "shrine_night": "Shrine (Night)",
+        "Shukuhakubeya_BedRoom": "Bedroom",
+        "Shukuhakubeya_BedRoom_Night": "Bedroom (Night)",
+        "Shukuhakubeya_Living": "Living Room",
+        "Shukuhakubeya_Living_Night": "Living Room (Night)",
+        "Shukuhakubeya_Other_BedRoom": "Bedroom 2",
+        "Shukuhakubeya_Toilet": "Bathroom",
+        "Shukuhakubeya_Toilet_Night": "Bathroom (Night)",
+        "Shukuhakubeya_WashRoom": "Washroom",
+        "Shukuhakubeya_WashRoom_Night": "Washroom (Night)",
+        "SMClub": "SM Club",
+        "SMRoom": "SM Room",
+        "SMRoom2": "Basement",
+        "Soap": "Soapland",
+        "Spa": "Spa",
+        "Spa_Night": "Spa (Night)",
+        "springgarden": "Spring Garden (No Stalls)",
+        "springgarden_night": "Spring Garden (Night No Stalls)",
+        "springgarden_yatai": "Spring Garden",
+        "springgarden_yatai_night": "Spring Garden (Night)",
+        "Syosai": "Study",
+        "Syosai_Night": "Study (Night)",
+        "Theater": "Theater",
+        "Theater_LightOff": "Theater (Night)",
+        "Toilet": "Toilet",
+        "Town": "Town",
+        "Train": "Train",
+        "Villa": "Villa 1F",
+        "Villa_BedRoom": "Villa 2f",
+        "Villa_BedRoom_Night": "Villa 2f (Night)",
+        "Villa_Farm": "Garden",
+        "Villa_Farm_Night": "Garden (Night)",
+        "Villa_Night": "Villa 1F (Night)",
+        "Yashiki": "Inn (Night)",
+        "Yashiki_Day": "Inn",
+        "Yashiki_Pillow": "Inn (Night with Pillow)"
+    },
+    "props": {
+        "props1Label": "Props 1",
+        "props2Label": "Props 2",
+        "props1AddButton": "Add",
+        "props2AddButton": "Add"
+    },
+    "movementCube": {
+        "label": "Movement Cube",
+        "props": "Props",
+        "small": "Small",
+        "maid": "Maid",
+        "bg": "BG"
+    },
+    "lights": {
+        "labelNormal": "Norm",
+        "labelSpot": "Spot",
+        "labelPoint": "Point",
+        "labelColor": "Color",
+        "x": "Light X",
+        "y": "Light Y",
+        "brightness": "Brightness",
+        "range": "Range",
+        "shadow": "Shadow"
+    },
+    "effectBloom": {
+        "strength": "Strength",
+        "blur": "Blur",
+        "hdrToggle": "HDR"
+    },
+    "effectDOF": {
+        "focalLength": "Focal Length",
+        "focalArea": "Focal Area",
+        "aperture": "Aperture",
+        "blur": "Blur",
+        "thicknessToggle": "Thickness"
+    },
+    "effectBlur": {
+        "strength": "Strength",
+        "blur1": "Blur 1",
+        "blur2": "Blur 2",
+        "aberration": "Aberration"
+    },
+    "effectFog": {
+        "range": "Range",
+        "opacity": "Opacity",
+        "strength": "Strength",
+        "height": "Height"
+    },
+    "effectSepia": {
+        "toggle": "Sepia",
+        "blur": "Blur"
+    },
+    "background2Window": {
+        "envButton": "Environments",
+        "itemLabel": "Item",
+        "smallBGLabel": "BG (Prop)",
+        "propLabel": "Clothes",
+        "modsToggle": "Mods"
+    },
+    "clothingDropdown": {
+        "myRoom": "My Room Custom",
+        "hat": "Hat",
+        "headset": "Headset",
+        "top": "Top",
+        "bottom": "Bottom",
+        "onepiece": "One-Piece",
+        "swimsuit": "Swimsuit",
+        "bra": "Bra",
+        "panties": "Panties",
+        "socks": "Socks",
+        "shoes": "Shoes",
+        "hairAccessory": "Hair Accessory",
+        "glasses": "Glasses",
+        "eyeMask": "Eye Mask",
+        "nose": "Nose",
+        "ear": "Ear",
+        "gloves": "Gloves",
+        "necklace": "Necklace",
+        "choker": "Choker",
+        "ribbon": "Ribbon",
+        "nipple": "Nipple",
+        "arms": "Arms",
+        "belly": "Belly",
+        "anlke": "Ankle",
+        "back": "Back",
+        "tail": "Tail",
+        "genitals": "Genitals"
+    },
+    "messageWindow": {
+        "name": "Name",
+        "fontSize": "Font Size",
+        "okButton": "OK"
+    }
+}

+ 221 - 0
COM3D2.MeidoPhotoStudio.Plugin/MeidoPhotoStudio/Constants.cs

@@ -0,0 +1,221 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using UnityEngine;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+
+namespace COM3D2.MeidoPhotoStudio.Plugin
+{
+    public class Constants
+    {
+        public static readonly string customPosePath;
+        public static readonly string scenesPath;
+        public static readonly string kankyoPath;
+        public static readonly string configPath;
+        public static readonly int mainWindowID = 765;
+        public static readonly int messageWindowID = 961;
+        public static readonly int sceneManagerWindowID = 876;
+        public static readonly int sceneManagerModalID = 283;
+        public static readonly int dropdownWindowID = 777;
+        public enum Window
+        {
+            Call, Pose, Face, BG, BG2, Message, Save, SaveModal
+        }
+        public enum Scene
+        {
+            Daily = 3, Edit = 5
+        }
+        public static readonly List<string> PoseGroupList;
+        public static readonly Dictionary<string, List<string>> PoseDict;
+        public static readonly Dictionary<string, List<KeyValuePair<string, string>>> CustomPoseDict;
+        public static int CustomPoseGroupsIndex { get; private set; }
+        public static readonly List<string> FaceBlendList;
+        public static readonly List<string> BGList;
+
+        static Constants()
+        {
+            string modsPath = Path.Combine(Path.GetFullPath(".\\"), @"Mod\MeidoPhotoStudio");
+            customPosePath = Path.Combine(modsPath, "Custom Poses");
+            scenesPath = Path.Combine(modsPath, "Scenes");
+            kankyoPath = Path.Combine(modsPath, "Environments");
+            configPath = Path.Combine(Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location), @"Config\MeidoPhotoStudio");
+
+            PoseDict = new Dictionary<string, List<string>>();
+            PoseGroupList = new List<string>();
+            CustomPoseDict = new Dictionary<string, List<KeyValuePair<string, string>>>();
+
+            FaceBlendList = new List<string>();
+
+            BGList = new List<string>();
+        }
+
+        public static void Initialize()
+        {
+            foreach (string dir in new[] { customPosePath, scenesPath, kankyoPath, configPath })
+            {
+                if (!Directory.Exists(dir)) Directory.CreateDirectory(dir);
+            }
+
+            // Load Poses
+            string poseListJson = File.ReadAllText(Path.Combine(configPath, "mm_pose_list.json"));
+            List<SerializePoseList> poseLists = JsonConvert.DeserializeObject<List<SerializePoseList>>(poseListJson);
+
+            foreach (SerializePoseList poseList in poseLists)
+            {
+                PoseDict[poseList.UIName] = poseList.PoseList;
+                PoseGroupList.Add(poseList.UIName);
+                CustomPoseGroupsIndex++;
+            }
+
+            Action<string> GetPoses = (directory) =>
+            {
+                List<KeyValuePair<string, string>> poseList = new List<KeyValuePair<string, string>>();
+                foreach (string file in Directory.GetFiles(directory))
+                {
+                    if (Path.GetExtension(file) == ".anm")
+                    {
+                        string fileName = Path.GetFileNameWithoutExtension(file);
+                        poseList.Add(new KeyValuePair<string, string>(fileName, file));
+                    }
+                }
+                if (poseList.Count > 0)
+                {
+                    string poseGroupName = new DirectoryInfo(directory).Name;
+                    PoseGroupList.Add(poseGroupName);
+                    CustomPoseDict[poseGroupName] = poseList;
+                }
+            };
+
+            GetPoses(customPosePath);
+
+            // TODO: Get rest of poses
+
+            foreach (string directory in Directory.GetDirectories(customPosePath))
+            {
+                GetPoses(directory);
+            }
+
+            // Load Face Blends Presets
+            using (CsvParser csvParser = OpenCsvParser("phot_face_list.nei"))
+            {
+                for (int cell_y = 1; cell_y < csvParser.max_cell_y; cell_y++)
+                {
+                    if (csvParser.IsCellToExistData(3, cell_y))
+                    {
+                        string blendValue = csvParser.GetCellAsString(3, cell_y);
+                        FaceBlendList.Add(blendValue);
+                    }
+                }
+            }
+
+            // Load BGs
+            PhotoBGData.Create();
+            List<PhotoBGData> photList = PhotoBGData.data;
+
+            // COM3D2 BGs
+            foreach (PhotoBGData bgData in photList)
+            {
+                if (!string.IsNullOrEmpty(bgData.create_prefab_name))
+                {
+                    string bg = bgData.create_prefab_name;
+                    BGList.Add(bg);
+                }
+            }
+
+            // CM3D2 BGs
+            if (GameUty.IsEnabledCompatibilityMode)
+            {
+                using (CsvParser csvParser = OpenCsvParser("phot_bg_list.nei", GameUty.FileSystemOld))
+                {
+                    for (int cell_y = 1; cell_y < csvParser.max_cell_y; cell_y++)
+                    {
+                        if (csvParser.IsCellToExistData(3, cell_y))
+                        {
+                            string bg = csvParser.GetCellAsString(3, cell_y);
+                            BGList.Add(bg);
+                        }
+                    }
+                }
+            }
+        }
+
+        private static CsvParser OpenCsvParser(string nei, AFileSystemBase fs)
+        {
+            try
+            {
+                if (fs.IsExistentFile(nei))
+                {
+                    AFileBase file = fs.FileOpen(nei);
+                    CsvParser csvParser = new CsvParser();
+                    if (csvParser.Open(file)) return csvParser;
+                }
+            }
+            catch { }
+            return null;
+        }
+
+        private static CsvParser OpenCsvParser(string nei)
+        {
+            return OpenCsvParser(nei, GameUty.FileSystem);
+        }
+    }
+
+    public class SerializePoseList
+    {
+        public string UIName { get; set; }
+        public List<string> PoseList { get; set; }
+    }
+
+    public static class Translation
+    {
+        public static Dictionary<string, Dictionary<string, string>> Translations;
+        public static string CurrentLanguage { get; set; }
+
+        public static void Initialize(string language)
+        {
+            CurrentLanguage = language;
+
+            string translationFile = $"translations.{language}.json";
+            string translationPath = Path.Combine(Constants.configPath, translationFile);
+            string translationJson = File.ReadAllText(translationPath);
+
+            JObject translation = JObject.Parse(translationJson);
+
+            Translations = new Dictionary<string, Dictionary<string, string>>(StringComparer.InvariantCultureIgnoreCase);
+
+            foreach (JProperty translationProp in translation.AsJEnumerable())
+            {
+                JToken token = translationProp.Value;
+                Translations[translationProp.Path] = token.ToObject<Dictionary<string, string>>();
+            }
+        }
+
+        public static string Get(string category, string text)
+        {
+            if (!Translations.ContainsKey(category))
+            {
+                Debug.LogWarning($"Could not find category '{category}'");
+                return null;
+            }
+
+            if (!Translations[category].ContainsKey(text))
+            {
+                Debug.LogWarning($"Could not find translation for '{text}'");
+                return null;
+            }
+            return Translations[category][text];
+        }
+
+        public static string[] GetList(string category, IEnumerable<string> list)
+        {
+            return list.Select(uiName => Get(category, uiName) ?? uiName).ToArray();
+        }
+
+        public static string[] GetList(string category, IEnumerable<KeyValuePair<string, string>> list)
+        {
+            return list.Select(kvp => Get(category, kvp.Key) ?? kvp.Key).ToArray();
+        }
+    }
+}

+ 25 - 0
COM3D2.MeidoPhotoStudio.Plugin/MeidoPhotoStudio/GUI/Controls/BaseControl.cs

@@ -0,0 +1,25 @@
+using System;
+using UnityEngine;
+using UnityEngine.SceneManagement;
+
+namespace COM3D2.MeidoPhotoStudio.Plugin
+{
+    public abstract class BaseControl
+    {
+        public event EventHandler ControlEvent;
+        public bool Enabled { get; set; } = true;
+        public bool Visible { get; set; } = true;
+        public virtual void Draw(params GUILayoutOption[] layoutOptions) { }
+        public virtual void Update() { }
+        public virtual void Awake() { }
+        public virtual void OnSceneLoaded(Scene scene, LoadSceneMode sceneMode) { }
+        public virtual void OnControlEvent(EventArgs args)
+        {
+            EventHandler handler = ControlEvent;
+            if (handler != null)
+            {
+                handler(this, args);
+            }
+        }
+    }
+}

+ 28 - 0
COM3D2.MeidoPhotoStudio.Plugin/MeidoPhotoStudio/GUI/Controls/Button.cs

@@ -0,0 +1,28 @@
+using System;
+using UnityEngine;
+
+namespace COM3D2.MeidoPhotoStudio.Plugin
+{
+    public class Button : BaseControl
+    {
+        public string Label { get; set; }
+        public Button(string label)
+        {
+            this.Label = label;
+        }
+        public void Draw(GUIStyle buttonStyle, params GUILayoutOption[] layoutOptions)
+        {
+            if (!Visible) return;
+            bool clicked = false;
+            clicked = GUILayout.Button(Label, buttonStyle, layoutOptions);
+            if (clicked) OnControlEvent(EventArgs.Empty);
+        }
+
+        public override void Draw(params GUILayoutOption[] layoutOptions)
+        {
+            GUIStyle buttonStyle = new GUIStyle(GUI.skin.button);
+            buttonStyle.fontSize = 14;
+            Draw(buttonStyle, layoutOptions);
+        }
+    }
+}

+ 295 - 0
COM3D2.MeidoPhotoStudio.Plugin/MeidoPhotoStudio/GUI/Controls/DropDown.cs

@@ -0,0 +1,295 @@
+using System;
+using UnityEngine;
+namespace COM3D2.MeidoPhotoStudio.Plugin
+{
+    using DropdownSelectArgs = DropdownHelper.DropdownSelectArgs;
+    using DropdownCloseArgs = DropdownHelper.DropdownCloseArgs;
+    public class Dropdown : BaseControl
+    {
+        public event EventHandler SelectionChange;
+        public event EventHandler DropdownOpen;
+        public event EventHandler DropdownClose;
+        private bool clickedYou = false;
+        private bool showDropdown = false;
+        public string[] DropdownList { get; private set; }
+        public int DropdownID { get; private set; }
+        private Vector2 scrollPos;
+        public Vector2 ScrollPos
+        {
+            get => scrollPos;
+            private set => scrollPos = value;
+        }
+        private Rect buttonRect;
+        public Rect ButtonRect
+        {
+            get => buttonRect;
+            private set => buttonRect = value;
+        }
+        private Vector2 elementSize;
+        public Vector2 ElementSize
+        {
+            get => elementSize;
+            set => elementSize = value;
+        }
+        private int selectedItemIndex = 0;
+        public int SelectedItemIndex
+        {
+            get => selectedItemIndex;
+            set
+            {
+                this.selectedItemIndex = Mathf.Clamp(value, 0, DropdownList.Length);
+                OnDropdownEvent(SelectionChange);
+            }
+        }
+
+        public Dropdown(string[] itemList, int selectedItemIndex = 0)
+        {
+            DropdownID = DropdownHelper.DropdownID;
+            SetDropdownItems(itemList, selectedItemIndex);
+
+            DropdownHelper.SelectionChange += OnChangeSelection;
+            DropdownHelper.DropdownClose += OnCloseDropdown;
+        }
+
+        ~Dropdown()
+        {
+            DropdownHelper.SelectionChange -= OnChangeSelection;
+            DropdownHelper.DropdownClose -= OnCloseDropdown;
+        }
+
+        public void SetDropdownItems(string[] itemList, int selectedItemIndex = 0)
+        {
+            this.scrollPos = (this.elementSize = Vector2.zero);
+            this.DropdownList = itemList;
+            this.SelectedItemIndex = selectedItemIndex;
+        }
+        public void Step(int dir)
+        {
+            dir = (int)Mathf.Sign(dir);
+            this.SelectedItemIndex = Utility.Wrap(this.SelectedItemIndex + dir, 0, this.DropdownList.Length);
+        }
+
+        public void Draw(GUIStyle buttonStyle, params GUILayoutOption[] layoutOptions)
+        {
+            bool clicked = GUILayout.Button(DropdownList[selectedItemIndex], buttonStyle, layoutOptions);
+
+            if (clicked)
+            {
+                showDropdown = !clickedYou;
+                clickedYou = false;
+            }
+
+            if (showDropdown)
+            {
+                if (Event.current.type == EventType.Repaint) InitializeDropdown();
+            }
+        }
+
+        public override void Draw(params GUILayoutOption[] layoutOptions)
+        {
+            GUIStyle buttonStyle = new GUIStyle(GUI.skin.button);
+            buttonStyle.alignment = TextAnchor.MiddleLeft;
+            this.Draw(buttonStyle, layoutOptions);
+        }
+
+
+        private void OnChangeSelection(object sender, DropdownSelectArgs args)
+        {
+            if (args.DropdownID == this.DropdownID)
+            {
+                SelectedItemIndex = args.SelectedItemIndex;
+            }
+        }
+
+        private void OnCloseDropdown(object sender, DropdownCloseArgs args)
+        {
+            if (args.DropdownID == this.DropdownID)
+            {
+                scrollPos = args.ScrollPos;
+                clickedYou = args.ClickedYou;
+
+                if (clickedYou) OnDropdownEvent(SelectionChange);
+
+                OnDropdownEvent(DropdownClose);
+            }
+        }
+        private void InitializeDropdown()
+        {
+            showDropdown = false;
+
+            this.buttonRect = GUILayoutUtility.GetLastRect();
+            Vector2 rectPos = GUIUtility.GUIToScreenPoint(new Vector2(buttonRect.x, buttonRect.y));
+            buttonRect.x = rectPos.x;
+            buttonRect.y = rectPos.y;
+            if (this.elementSize == Vector2.zero)
+            {
+                this.elementSize = DropdownHelper.CalculateElementSize(this.DropdownList);
+            }
+            DropdownHelper.Set(this);
+
+            OnDropdownEvent(DropdownOpen);
+        }
+
+        private void OnDropdownEvent(EventHandler handler)
+        {
+            if (handler != null) handler(this, EventArgs.Empty);
+        }
+    }
+
+    public static class DropdownHelper
+    {
+        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 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 = false;
+        public static bool Visible { get; private set; }
+        private static bool onScrollBar = false;
+        public static Rect dropdownRect;
+        public static Vector2 CalculateElementSize(string[] list)
+        {
+            if (!initialized) InitializeStyle();
+
+            Vector2 calculatedSize = dropdownStyle.CalcSize(new GUIContent(list[0]));
+            for (int i = 1; i < list.Length; i++)
+            {
+                string word = list[i];
+                Vector2 calcSize = dropdownStyle.CalcSize(new GUIContent(word));
+                if (calcSize.x > calculatedSize.x) calculatedSize = calcSize;
+            }
+
+            return calculatedSize;
+        }
+
+        public static void Set(Dropdown dropdown)
+        {
+            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)
+            {
+                dropdownRect = new Rect(buttonRect.x, buttonRect.y - rectHeight, rectWidth + 18, rectHeight);
+            }
+            else
+            {
+                if (calculatedListHeight > heightBelow) rectHeight -= calculatedSize.y;
+                dropdownRect = new Rect(buttonRect.x, buttonRect.y + buttonRect.height, rectWidth + 18, rectHeight);
+            }
+
+            dropdownRect.x = Mathf.Clamp(dropdownRect.x, 0, Screen.width - rectWidth - 18);
+
+            Visible = true;
+        }
+
+        public static void HandleDropdown()
+        {
+            if (Visible)
+            {
+                GUILayout.Window(Constants.dropdownWindowID, dropdownRect, GUIFunc, "", windowStyle);
+            }
+        }
+        private static void GUIFunc(int id)
+        {
+            bool clicked = false;
+
+            if (Event.current.type == EventType.MouseUp)
+            {
+                clicked = true;
+            }
+
+            scrollPos = GUILayout.BeginScrollView(scrollPos);
+            int selection = GUILayout.SelectionGrid(selectedItemIndex, dropdownList, 1, dropdownStyle);
+            GUILayout.EndScrollView();
+
+            bool clickedYou = false;
+            if (Utility.AnyMouseDown())
+            {
+                Vector2 mousePos = GUIUtility.GUIToScreenPoint(Event.current.mousePosition);
+                bool clickedMe = dropdownRect.Contains(mousePos);
+                onScrollBar = mousePos.x > dropdownRect.x + dropdownRect.width - 12f;
+                if (buttonRect.Contains(mousePos)) clickedYou = true;
+                if (!clickedMe) Visible = false;
+            }
+
+            if (selection != selectedItemIndex || (clicked && !onScrollBar))
+            {
+                EventHandler<DropdownSelectArgs> handler = SelectionChange;
+                if (handler != null)
+                    handler(null, new DropdownSelectArgs(currentDropdownID, selection));
+                Visible = false;
+            }
+
+            if (!Visible)
+            {
+                EventHandler<DropdownCloseArgs> handler = DropdownClose;
+                if (handler != null)
+                    handler(null, new DropdownCloseArgs(currentDropdownID, scrollPos, clickedYou));
+            }
+        }
+
+        private static void InitializeStyle()
+        {
+            dropdownStyle = new GUIStyle(GUI.skin.button);
+            dropdownStyle.alignment = TextAnchor.MiddleLeft;
+            dropdownStyle.margin = new RectOffset(0, 0, 0, 0);
+            dropdownStyle.padding.top = dropdownStyle.padding.bottom = 2;
+            dropdownStyle.normal.background = Utility.MakeTex(2, 2, new Color(0f, 0f, 0f, 0.5f));
+            dropdownStyle.onHover.background = dropdownStyle.hover.background = dropdownStyle.onNormal.background = new Texture2D(2, 2);
+            dropdownStyle.onHover.textColor = dropdownStyle.onNormal.textColor = dropdownStyle.hover.textColor = Color.black;
+
+            windowStyle = new GUIStyle(GUI.skin.box);
+            windowStyle.padding = new RectOffset(0, 0, 0, 0);
+            windowStyle.alignment = TextAnchor.UpperRight;
+            initialized = true;
+        }
+
+        public class DropdownEventArgs : EventArgs
+        {
+            public int DropdownID { get; }
+            public DropdownEventArgs(int dropdownID)
+            {
+                this.DropdownID = dropdownID;
+            }
+        }
+
+        public class DropdownSelectArgs : DropdownEventArgs
+        {
+            public int SelectedItemIndex { get; }
+            public DropdownSelectArgs(int dropdownID, int selection) : base(dropdownID)
+            {
+                this.SelectedItemIndex = selection;
+            }
+        }
+
+        public class DropdownCloseArgs : DropdownEventArgs
+        {
+            public Vector2 ScrollPos { get; }
+            public bool ClickedYou { get; }
+            public DropdownCloseArgs(int dropdownID, Vector2 scrollPos, bool clickedYou = false) : base(dropdownID)
+            {
+                this.ScrollPos = scrollPos;
+                this.ClickedYou = clickedYou;
+            }
+        }
+    }
+}

+ 36 - 0
COM3D2.MeidoPhotoStudio.Plugin/MeidoPhotoStudio/GUI/Controls/MiscGUI.cs

@@ -0,0 +1,36 @@
+using System;
+using UnityEngine;
+
+namespace COM3D2.MeidoPhotoStudio.Plugin
+{
+    public static class MiscGUI
+    {
+        private static GUIStyle lineStyleWhite;
+        private static GUIStyle lineStyleBlack;
+        private static GUIStyle textureBoxStyle;
+        static MiscGUI()
+        {
+            lineStyleWhite = new GUIStyle(GUI.skin.box);
+            lineStyleWhite.padding = lineStyleWhite.border = new RectOffset(0, 0, 1, 1);
+            lineStyleWhite.margin = new RectOffset(0, 0, 8, 8);
+            lineStyleWhite.normal.background = Utility.MakeTex(2, 2, new Color(1f, 1f, 1f, 0.2f));
+
+            lineStyleBlack = new GUIStyle(lineStyleWhite);
+            lineStyleBlack.normal.background = Utility.MakeTex(2, 2, new Color(0f, 0f, 0f, 0.3f));
+
+            textureBoxStyle = new GUIStyle(GUI.skin.box);
+            textureBoxStyle.normal.background = Utility.MakeTex(2, 2, new Color(0f, 0f, 0f, 0f));
+        }
+
+        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);
+        }
+    }
+}

+ 43 - 0
COM3D2.MeidoPhotoStudio.Plugin/MeidoPhotoStudio/GUI/Controls/SelectionGrid.cs

@@ -0,0 +1,43 @@
+using System;
+using UnityEngine;
+
+namespace COM3D2.MeidoPhotoStudio.Plugin
+{
+    public class SelectionGrid : BaseControl
+    {
+        public string[] Items { get; set; }
+        public int XCount { get; set; }
+        private int selectedItem;
+        public int SelectedItem
+        {
+            get => selectedItem;
+            set
+            {
+                this.selectedItem = value;
+                OnControlEvent(EventArgs.Empty);
+            }
+        }
+
+        public SelectionGrid(string[] items, int xCount, int selectedTab = 0)
+        {
+            Items = items;
+            XCount = xCount;
+            this.selectedItem = selectedTab;
+        }
+
+        public void Draw(GUIStyle gridStyle, params GUILayoutOption[] layoutOptions)
+        {
+            if (!Visible) return;
+            GUILayout.BeginHorizontal();
+            int selected;
+            selected = GUILayout.SelectionGrid(SelectedItem, Items, XCount, gridStyle, layoutOptions);
+            GUILayout.EndHorizontal();
+            if (selected != SelectedItem) SelectedItem = selected;
+        }
+
+        public override void Draw(params GUILayoutOption[] layoutOptions)
+        {
+            this.Draw(new GUIStyle(GUI.skin.button));
+        }
+    }
+}

+ 61 - 0
COM3D2.MeidoPhotoStudio.Plugin/MeidoPhotoStudio/GUI/Controls/Slider.cs

@@ -0,0 +1,61 @@
+using System;
+using UnityEngine;
+
+namespace COM3D2.MeidoPhotoStudio.Plugin
+{
+    public class Slider : BaseControl
+    {
+        private bool hasLabel;
+        private string label;
+        public string Label
+        {
+            get => label;
+            set
+            {
+                label = value;
+                hasLabel = !string.IsNullOrEmpty(label);
+            }
+        }
+        private float value;
+        public float Value
+        {
+            get => value;
+            set
+            {
+                this.value = Mathf.Clamp(value, Min, Max);
+                OnControlEvent(EventArgs.Empty);
+            }
+        }
+        public float Min { get; set; }
+        public float Max { get; set; }
+
+        public Slider(string label, float min, float max, float value = 0)
+        {
+            Label = label;
+            Min = min;
+            Max = max;
+            this.value = Mathf.Clamp(value, min, max);
+        }
+        public Slider(float min, float max, float value = 0) : this("", min, max, value) { }
+        public override void Draw(params GUILayoutOption[] layoutOptions)
+        {
+
+            if (!Visible) return;
+            GUIStyle sliderStyle = new GUIStyle(GUI.skin.horizontalSlider);
+            if (hasLabel)
+            {
+                GUILayout.BeginVertical(GUILayout.ExpandWidth(false));
+                GUIStyle sliderLabelStyle = new GUIStyle(GUI.skin.label);
+                sliderLabelStyle.padding.bottom = -5;
+                sliderLabelStyle.margin = new RectOffset(0, 0, 0, 0);
+                sliderLabelStyle.alignment = TextAnchor.LowerLeft;
+                sliderLabelStyle.fontSize = 13;
+                GUILayout.Label(Label, sliderLabelStyle, GUILayout.ExpandWidth(false));
+            }
+            else sliderStyle.margin.top = 10;
+            float value = GUILayout.HorizontalSlider(Value, Min, Max, sliderStyle, GUI.skin.horizontalSliderThumb, layoutOptions);
+            if (hasLabel) GUILayout.EndVertical();
+            if (value != Value) Value = value;
+        }
+    }
+}

+ 19 - 0
COM3D2.MeidoPhotoStudio.Plugin/MeidoPhotoStudio/GUI/Controls/TextArea.cs

@@ -0,0 +1,19 @@
+using System;
+using UnityEngine;
+
+namespace COM3D2.MeidoPhotoStudio.Plugin
+{
+    public class TextArea : BaseControl
+    {
+        public string Value { get; set; } = string.Empty;
+        public void Draw(GUIStyle textAreaStyle, params GUILayoutOption[] layoutOptions)
+        {
+            if (!Visible) return;
+            Value = GUILayout.TextArea(Value, textAreaStyle, layoutOptions);
+        }
+        public override void Draw(params GUILayoutOption[] layoutOptions)
+        {
+            Draw(new GUIStyle(GUI.skin.textArea), layoutOptions);
+        }
+    }
+}

+ 20 - 0
COM3D2.MeidoPhotoStudio.Plugin/MeidoPhotoStudio/GUI/Controls/TextField.cs

@@ -0,0 +1,20 @@
+using System;
+using UnityEngine;
+
+namespace COM3D2.MeidoPhotoStudio.Plugin
+{
+    public class TextField : BaseControl
+    {
+        public string Value { get; set; } = string.Empty;
+        public void Draw(GUIStyle textFieldStyle, params GUILayoutOption[] layoutOptions)
+        {
+            if (!Visible) return;
+            Value = GUILayout.TextField(Value, textFieldStyle, layoutOptions);
+        }
+
+        public override void Draw(params GUILayoutOption[] layoutOptions)
+        {
+            Draw(new GUIStyle(GUI.skin.textField), layoutOptions);
+        }
+    }
+}

+ 37 - 0
COM3D2.MeidoPhotoStudio.Plugin/MeidoPhotoStudio/GUI/Controls/Toggle.cs

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

+ 18 - 0
COM3D2.MeidoPhotoStudio.Plugin/MeidoPhotoStudio/GUI/Panes/BasePane.cs

@@ -0,0 +1,18 @@
+using System.Collections.Generic;
+using UnityEngine;
+
+namespace COM3D2.MeidoPhotoStudio.Plugin
+{
+    public abstract class BasePane : BaseControl
+    {
+        protected List<BaseControl> Controls { get; set; }
+        protected List<BasePane> Panes { get; set; }
+        protected bool updating = false;
+
+        public BasePane()
+        {
+            Controls = new List<BaseControl>();
+            Panes = new List<BasePane>();
+        }
+    }
+}

+ 152 - 0
COM3D2.MeidoPhotoStudio.Plugin/MeidoPhotoStudio/GUI/Panes/MaidFaceSliderPane.cs

@@ -0,0 +1,152 @@
+using System;
+using System.Collections.Generic;
+using UnityEngine;
+
+namespace COM3D2.MeidoPhotoStudio.Plugin
+{
+    public class MaidFaceSliderPane : BasePane
+    {
+        private MeidoManager meidoManager;
+
+        private static readonly Dictionary<string, float[]> SliderRange = new Dictionary<string, float[]>()
+        {
+            ["eyeclose"] = new[] { 0f, 1f },
+            ["eyeclose2"] = new[] { 0f, 1f },
+            ["eyeclose3"] = new[] { 0f, 1f },
+            ["eyebig"] = new[] { 0f, 1f },
+            ["eyeclose6"] = new[] { 0f, 1f },
+            ["eyeclose5"] = new[] { 0f, 1f },
+            ["hitomih"] = new[] { 0f, 2f },
+            ["hitomis"] = new[] { 0f, 1f },
+            ["mayuha"] = new[] { 0f, 1f },
+            ["mayuw"] = new[] { 0f, 1f },
+            ["mayuup"] = new[] { 0f, 0.8f },
+            ["mayuv"] = new[] { 0f, 0.8f },
+            ["mayuvhalf"] = new[] { 0f, 0.9f },
+            ["moutha"] = new[] { 0f, 1f },
+            ["mouths"] = new[] { 0f, 0.9f },
+            ["mouthc"] = new[] { 0f, 1f },
+            ["mouthi"] = new[] { 0f, 1f },
+            ["mouthup"] = new[] { 0f, 1.4f },
+            ["mouthdw"] = new[] { 0f, 1f },
+            ["mouthhe"] = new[] { 0f, 1f },
+            ["mouthuphalf"] = new[] { 0f, 2f },
+            ["tangout"] = new[] { 0f, 1f },
+            ["tangup"] = new[] { 0f, 0.7f },
+            ["tangopen"] = new[] { 0f, 1f }
+        };
+
+        public static readonly string[] faceKeys = new string[24]
+        {
+            "eyeclose", "eyeclose2", "eyeclose3", "eyebig", "eyeclose6", "eyeclose5", "hitomih",
+            "hitomis", "mayuha", "mayuw", "mayuup", "mayuv", "mayuvhalf", "moutha", "mouths",
+            "mouthc", "mouthi", "mouthup", "mouthdw", "mouthhe", "mouthuphalf", "tangout",
+            "tangup", "tangopen"
+        };
+
+        public static readonly string[] faceToggleKeys = new string[12]
+        {
+            "hoho2", "shock", "nosefook", "namida", "yodare", "toothoff",
+            "tear1", "tear2", "tear3", "hohos", "hoho", "hohol"
+        };
+
+        public MaidFaceSliderPane(MeidoManager meidoManager)
+        {
+            this.meidoManager = meidoManager;
+
+            for (int i = 0; i < faceKeys.Length; i++)
+            {
+                string key = faceKeys[i];
+                string uiName = Translation.Get("faceBlendValues", key);
+                Slider slider = new Slider(uiName, SliderRange[key][0], SliderRange[key][1]);
+                int myIndex = i;
+                slider.ControlEvent += (s, a) => this.SetFaceValue(faceKeys[myIndex], slider.Value);
+                this.Controls.Add(slider);
+            }
+
+            for (int i = 0; i < faceToggleKeys.Length; i++)
+            {
+                string uiName = Translation.Get("faceBlendValues", faceToggleKeys[i]);
+                Toggle toggle = new Toggle(uiName);
+                int myIndex = i;
+                toggle.ControlEvent += (s, a) => this.SetFaceValue(faceToggleKeys[myIndex], toggle.Value);
+                this.Controls.Add(toggle);
+            }
+        }
+
+        public void SetFaceValue(string key, float value)
+        {
+            if (updating) return;
+            this.meidoManager.ActiveMeido.SetFaceBlendValue(key, value);
+        }
+
+        public void SetFaceValue(string key, bool value)
+        {
+            float max = (key == "hoho" || key == "hoho2") ? 0.5f : 1f;
+            if (key == "toothoff") value = !value;
+            SetFaceValue(key, value ? max : 0f);
+        }
+
+        public void SetControlValues()
+        {
+            this.updating = true;
+            TMorph morph = this.meidoManager.ActiveMeido.Maid.body0.Face.morph;
+            float[] blendValues = Utility.GetFieldValue<TMorph, float[]>(morph, "BlendValues");
+            float[] blendValuesBackup = Utility.GetFieldValue<TMorph, float[]>(morph, "BlendValuesBackup");
+            for (int i = 0; i < faceKeys.Length; i++)
+            {
+                string hash = faceKeys[i];
+                Slider slider = this.Controls[i] as Slider;
+                try
+                {
+                    if (hash.StartsWith("eyeclose"))
+                        slider.Value = blendValuesBackup[(int)morph.hash[hash]];
+                    else
+                        slider.Value = blendValues[(int)morph.hash[hash]];
+                }
+                catch { }
+            }
+
+            for (int i = 0; i < faceToggleKeys.Length; i++)
+            {
+                string hash = faceToggleKeys[i];
+                Toggle toggle = this.Controls[24 + i] as Toggle;
+                if (hash == "nosefook") toggle.Value = morph.boNoseFook;
+                else toggle.Value = blendValues[(int)morph.hash[hash]] > 0f;
+                if (hash == "toothoff") toggle.Value = !toggle.Value;
+            }
+            this.updating = false;
+        }
+
+        public override void Draw(params GUILayoutOption[] layoutOptions)
+        {
+            for (int i = 0; i < faceKeys.Length; i += 2)
+            {
+                GUILayout.BeginHorizontal();
+                for (int j = 0; j < 2; j++)
+                {
+                    Controls[i + j].Draw(GUILayout.Width(90));
+                    if (i + j == 12 || i + j == 23)
+                    {
+                        i--;
+                        break;
+                    }
+
+                }
+                GUILayout.EndHorizontal();
+            }
+
+            MiscGUI.WhiteLine();
+
+            for (int i = 0; i < faceToggleKeys.Length; i += 3)
+            {
+                GUILayout.BeginHorizontal();
+                for (int j = 0; j < 3; j++)
+                {
+                    Controls[24 + i + j].Draw();
+                }
+                GUILayout.EndHorizontal();
+            }
+        }
+    }
+}

+ 78 - 0
COM3D2.MeidoPhotoStudio.Plugin/MeidoPhotoStudio/GUI/Panes/MaidSelectorPane.cs

@@ -0,0 +1,78 @@
+using System;
+using System.Collections.Generic;
+using UnityEngine;
+
+namespace COM3D2.MeidoPhotoStudio.Plugin
+{
+    public class MaidSelectorPane : BasePane
+    {
+        private MeidoManager meidoManager;
+        public List<int> selectedMaidList { get; private set; }
+        private Vector2 maidListScrollPos;
+        private Button clearMaidsButton;
+        private Button callMaidsButton;
+        public event EventHandler MaidCall;
+        public MaidSelectorPane(MeidoManager meidoManager) : base()
+        {
+            this.meidoManager = meidoManager;
+            selectedMaidList = new List<int>();
+            clearMaidsButton = new Button("Clear");
+            clearMaidsButton.ControlEvent += (s, a) => selectedMaidList.Clear();
+            Controls.Add(clearMaidsButton);
+
+            callMaidsButton = new Button("Call");
+            callMaidsButton.ControlEvent += (s, a) =>
+            {
+                EventHandler handler = MaidCall;
+                if (handler != null) handler(this, EventArgs.Empty);
+            };
+            Controls.Add(callMaidsButton);
+        }
+
+        public override void Draw(params GUILayoutOption[] layoutOptions)
+        {
+            clearMaidsButton.Draw();
+            callMaidsButton.Draw();
+
+            GUIStyle labelStyle = new GUIStyle(GUI.skin.label);
+            labelStyle.fontSize = 14;
+            GUIStyle selectLabelStyle = new GUIStyle(labelStyle);
+            selectLabelStyle.normal.textColor = Color.black;
+            selectLabelStyle.alignment = TextAnchor.UpperRight;
+            GUIStyle labelSelectedStyle = new GUIStyle(labelStyle);
+            labelSelectedStyle.normal.textColor = Color.black;
+
+            float windowHeight = Screen.height * 0.8f;
+            int buttonHeight = 85;
+            int buttonWidth = 190;
+            Rect positionRect = new Rect(5, 115, 208, windowHeight - 140);
+            Rect viewRect = new Rect(0, 0, 185, buttonHeight * meidoManager.meidos.Length + 5);
+            maidListScrollPos = GUI.BeginScrollView(positionRect, maidListScrollPos, viewRect);
+
+            for (int i = 0; i < meidoManager.meidos.Length; i++)
+            {
+                Meido meido = meidoManager.meidos[i];
+                float y = i * buttonHeight;
+                bool selectedMaid = selectedMaidList.Contains(i);
+
+                if (GUI.Button(new Rect(0, y, buttonWidth, buttonHeight), ""))
+                {
+                    if (selectedMaid) selectedMaidList.Remove(i);
+                    else selectedMaidList.Add(i);
+                }
+
+                if (selectedMaid)
+                {
+                    int selectedIndex = selectedMaidList.IndexOf(i) + 1;
+                    GUI.DrawTexture(new Rect(5, y + 5, buttonWidth - 10, buttonHeight - 10), Texture2D.whiteTexture);
+                    GUI.Label(new Rect(0, y + 5, buttonWidth - 10, buttonHeight), selectedIndex.ToString(), selectLabelStyle);
+                }
+
+                GUI.DrawTexture(new Rect(5, y, buttonHeight, buttonHeight), meido.Image);
+                GUI.Label(new Rect(95, y + 30, buttonWidth - 80, buttonHeight), meido.NameJP, selectedMaid ? labelSelectedStyle : labelStyle);
+
+            }
+            GUI.EndScrollView();
+        }
+    }
+}

+ 69 - 0
COM3D2.MeidoPhotoStudio.Plugin/MeidoPhotoStudio/GUI/Panes/MaidSwitcherPane.cs

@@ -0,0 +1,69 @@
+using System;
+using UnityEngine;
+
+namespace COM3D2.MeidoPhotoStudio.Plugin
+{
+    public class MaidSwitcherPane : BasePane
+    {
+        public static MeidoManager meidoManager;
+        private static Button PreviousButton;
+        private static Button NextButton;
+        public static event EventHandler<MeidoChangeEventArgs> MaidChange;
+        private static int SelectedMeido => meidoManager.SelectedMeido;
+        static MaidSwitcherPane()
+        {
+            PreviousButton = new Button("<");
+            PreviousButton.ControlEvent += (s, a) => ChangeMaid(-1);
+
+            NextButton = new Button(">");
+            NextButton.ControlEvent += (s, a) => ChangeMaid(1);
+        }
+
+        public static void Draw()
+        {
+            GUIStyle boxStyle = new GUIStyle(GUI.skin.box);
+            GUIStyle buttonStyle = new GUIStyle(GUI.skin.button);
+            GUIStyle labelStyle = new GUIStyle(GUI.skin.label);
+            boxStyle.padding.top = -15;
+            buttonStyle.margin.top = 20;
+            labelStyle.alignment = TextAnchor.UpperLeft;
+
+            GUILayout.BeginHorizontal();
+
+            bool previousState = GUI.enabled;
+            GUI.enabled = meidoManager.HasActiveMeido;
+
+            PreviousButton.Draw(buttonStyle, GUILayout.Height(40), GUILayout.ExpandWidth(false));
+
+            if (meidoManager.HasActiveMeido)
+                MiscGUI.DrawTexture(meidoManager.ActiveMeido.Image, GUILayout.Width(70), GUILayout.Height(70));
+            else
+                GUILayout.Box("", boxStyle, GUILayout.Height(70), GUILayout.Width(70));
+
+
+            GUILayout.BeginVertical();
+            GUILayout.Space(30);
+            GUILayout.Label(meidoManager.HasActiveMeido ? meidoManager.ActiveMeido.NameJP : "", labelStyle, GUILayout.ExpandWidth(false));
+            GUILayout.EndVertical();
+
+            NextButton.Draw(buttonStyle, GUILayout.Height(40), GUILayout.ExpandWidth(false));
+
+            GUI.enabled = previousState;
+
+            GUILayout.EndHorizontal();
+        }
+
+        private static void ChangeMaid(int dir)
+        {
+            dir = (int)Mathf.Sign(dir);
+            int selected = Utility.Wrap(SelectedMeido + dir, 0, meidoManager.ActiveMeidoList.Count);
+            OnMaidChange(new MeidoChangeEventArgs(selected));
+        }
+
+        private static void OnMaidChange(MeidoChangeEventArgs args)
+        {
+            EventHandler<MeidoChangeEventArgs> handler = MaidChange;
+            if (handler != null) handler(null, args);
+        }
+    }
+}

+ 40 - 0
COM3D2.MeidoPhotoStudio.Plugin/MeidoPhotoStudio/GUI/Panes/TabsPane.cs

@@ -0,0 +1,40 @@
+using System;
+using UnityEngine;
+
+namespace COM3D2.MeidoPhotoStudio.Plugin
+{
+    public class TabsPane : BasePane
+    {
+        private static SelectionGrid Tabs;
+        private static Constants.Window selectedTab;
+        public static Constants.Window SelectedTab
+        {
+            get => selectedTab;
+            set => Tabs.SelectedItem = (int)value;
+
+        }
+        public static event EventHandler TabChange;
+        static TabsPane()
+        {
+            string[] tabs = { "Call", "Pose", "Face", "BG", "BG2" };
+            Tabs = new SelectionGrid(tabs, tabs.Length);
+            Tabs.ControlEvent += (s, a) => OnChangeTab();
+        }
+
+        private static void OnChangeTab()
+        {
+            selectedTab = (Constants.Window)Tabs.SelectedItem;
+            EventHandler handler = TabChange;
+            if (handler != null) handler(null, EventArgs.Empty);
+        }
+
+        public static void Draw()
+        {
+            GUIStyle tabStyle = new GUIStyle(GUI.skin.toggle);
+            tabStyle.padding.right = -6;
+            Tabs.Draw(tabStyle, GUILayout.ExpandWidth(false));
+            MiscGUI.BlackLine();
+        }
+
+    }
+}

+ 23 - 0
COM3D2.MeidoPhotoStudio.Plugin/MeidoPhotoStudio/GUI/Windows/BaseMainWindow.cs

@@ -0,0 +1,23 @@
+using UnityEngine;
+
+namespace COM3D2.MeidoPhotoStudio.Plugin
+{
+    public abstract class BaseMainWindow : BaseWindow
+    {
+        public BaseMainWindow() : base() { }
+        public override void OnGUI(int id)
+        {
+            TabsPane.Draw();
+
+            Draw();
+
+            GUILayout.FlexibleSpace();
+            GUIStyle labelStyle = new GUIStyle(GUI.skin.label);
+            labelStyle.fontSize = 10;
+            labelStyle.alignment = TextAnchor.LowerLeft;
+
+            GUILayout.Label("MeidoPhotoStudio 1.0.0", labelStyle);
+            GUI.DragWindow();
+        }
+    }
+}

+ 17 - 0
COM3D2.MeidoPhotoStudio.Plugin/MeidoPhotoStudio/GUI/Windows/BaseWindow.cs

@@ -0,0 +1,17 @@
+using System;
+using System.Collections.Generic;
+using UnityEngine;
+
+namespace COM3D2.MeidoPhotoStudio.Plugin
+{
+    public abstract class BaseWindow : BasePane
+    {
+        protected Vector2 scrollPos;
+        public BaseWindow() : base() { }
+        public virtual void OnGUI(int id)
+        {
+            Draw();
+            GUI.DragWindow();
+        }
+    }
+}

+ 20 - 0
COM3D2.MeidoPhotoStudio.Plugin/MeidoPhotoStudio/GUI/Windows/MainWindows/Background2Window.cs

@@ -0,0 +1,20 @@
+using System.Collections;
+using System.Collections.Generic;
+using UnityEngine;
+
+namespace COM3D2.MeidoPhotoStudio.Plugin
+{
+    public class Background2Window : BaseMainWindow
+    {
+        EnvironmentManager environmentManager;
+        public Background2Window(EnvironmentManager environmentManager)
+        {
+            this.environmentManager = environmentManager;
+        }
+        public override void Draw(params GUILayoutOption[] layoutOptions)
+        {
+
+            GUILayout.Label("bg2");
+        }
+    }
+}

+ 57 - 0
COM3D2.MeidoPhotoStudio.Plugin/MeidoPhotoStudio/GUI/Windows/MainWindows/BackgroundWindow.cs

@@ -0,0 +1,57 @@
+using System.Collections;
+using System.Collections.Generic;
+using System.Linq;
+using UnityEngine;
+
+namespace COM3D2.MeidoPhotoStudio.Plugin
+{
+    public class BackgroundWindow : BaseMainWindow
+    {
+        private EnvironmentManager environmentManager;
+        private Dropdown bgDropdown;
+        private Button prevBGButton;
+        private Button nextBGButton;
+
+        public BackgroundWindow(EnvironmentManager environmentManager)
+        {
+            this.environmentManager = environmentManager;
+
+            int theaterIndex = Constants.BGList.FindIndex(bg => bg == "Theater");
+
+            this.bgDropdown = new Dropdown(Translation.GetList("bgDropdown", Constants.BGList), theaterIndex);
+            this.bgDropdown.SelectionChange += (s, a) =>
+            {
+                string bg = Constants.BGList[this.bgDropdown.SelectedItemIndex];
+                environmentManager.ChangeBackground(bg);
+            };
+
+            this.prevBGButton = new Button("<");
+            this.prevBGButton.ControlEvent += (s, a) => this.bgDropdown.Step(-1);
+
+            this.nextBGButton = new Button(">");
+            this.nextBGButton.ControlEvent += (s, a) => this.bgDropdown.Step(1);
+        }
+
+        public override void Draw(params GUILayoutOption[] layoutOptions)
+        {
+            float arrowButtonSize = 30;
+            GUILayoutOption[] arrowLayoutOptions = {
+                GUILayout.Width(arrowButtonSize),
+                GUILayout.Height(arrowButtonSize)
+            };
+
+            float dropdownButtonHeight = arrowButtonSize;
+            float dropdownButtonWidth = 143f;
+            GUILayoutOption[] dropdownLayoutOptions = new GUILayoutOption[] {
+                GUILayout.Height(dropdownButtonHeight),
+                GUILayout.Width(dropdownButtonWidth)
+            };
+
+            GUILayout.BeginHorizontal();
+            this.prevBGButton.Draw(arrowLayoutOptions);
+            this.bgDropdown.Draw(dropdownLayoutOptions);
+            this.nextBGButton.Draw(arrowLayoutOptions);
+            GUILayout.EndHorizontal();
+        }
+    }
+}

+ 46 - 0
COM3D2.MeidoPhotoStudio.Plugin/MeidoPhotoStudio/GUI/Windows/MainWindows/MaidCallWindow.cs

@@ -0,0 +1,46 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using UnityEngine;
+
+namespace COM3D2.MeidoPhotoStudio.Plugin
+{
+    public class MaidCallWindow : BaseMainWindow
+    {
+        private MeidoManager meidoManager;
+        private MaidSelectorPane maidSelectorPane;
+        private Button placementButton;
+        private Button placementOKButton;
+        public MaidCallWindow(MeidoManager meidoManager) : base()
+        {
+            this.meidoManager = meidoManager;
+            placementButton = new Button("Normal");
+            placementButton.ControlEvent += (o, a) => Debug.Log("Change placement");
+            Controls.Add(placementButton);
+
+            placementOKButton = new Button("OK");
+            placementOKButton.ControlEvent += (o, a) => Debug.Log("Placement changed");
+            Controls.Add(placementOKButton);
+
+            maidSelectorPane = new MaidSelectorPane(meidoManager);
+            maidSelectorPane.MaidCall += (s, e) =>
+            {
+                this.meidoManager.IsFade = true;
+                GameMain.Instance.MainCamera.FadeOut(0.01f, false, () =>
+                {
+                    this.meidoManager.CallMeidos(maidSelectorPane.selectedMaidList);
+                }, false);
+            };
+        }
+
+        public override void Draw(params GUILayoutOption[] layoutOptions)
+        {
+            GUILayout.BeginHorizontal();
+            placementButton.Draw(GUILayout.Width(150));
+            placementOKButton.Draw();
+            GUILayout.EndHorizontal();
+
+            maidSelectorPane.Draw();
+        }
+    }
+}

+ 103 - 0
COM3D2.MeidoPhotoStudio.Plugin/MeidoPhotoStudio/GUI/Windows/MainWindows/MaidFaceWindow.cs

@@ -0,0 +1,103 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Linq;
+using UnityEngine;
+
+namespace COM3D2.MeidoPhotoStudio.Plugin
+{
+    public class MaidFaceWindow : BaseMainWindow
+    {
+        private MeidoManager meidoManager;
+        private MaidFaceSliderPane maidFaceSliderPane;
+        private Dropdown faceBlendDropdown;
+        private Button facePrevButton;
+        private Button faceNextButton;
+
+        public MaidFaceWindow(MeidoManager meidoManager)
+        {
+            this.meidoManager = meidoManager;
+            this.meidoManager.SelectMeido += SelectMeido;
+
+            TabsPane.TabChange += ChangeTab;
+
+            this.maidFaceSliderPane = new MaidFaceSliderPane(this.meidoManager);
+
+            this.faceBlendDropdown = new Dropdown(Translation.GetList("faceBlendPresetsDropdown", Constants.FaceBlendList));
+            this.faceBlendDropdown.SelectionChange += (s, a) =>
+            {
+                string faceBlend = Constants.FaceBlendList[this.faceBlendDropdown.SelectedItemIndex];
+                this.meidoManager.ActiveMeido.SetFaceBlend(faceBlend);
+                this.UpdateFace();
+            };
+
+            this.facePrevButton = new Button("<");
+            this.facePrevButton.ControlEvent += (s, a) => this.faceBlendDropdown.Step(-1);
+
+            this.faceNextButton = new Button(">");
+            this.faceNextButton.ControlEvent += (s, a) => this.faceBlendDropdown.Step(1);
+        }
+
+        ~MaidFaceWindow()
+        {
+            TabsPane.TabChange -= ChangeTab;
+        }
+
+        public override void Draw(params GUILayoutOption[] layoutOptions)
+        {
+            float arrowButtonSize = 30;
+            GUILayoutOption[] arrowLayoutOptions = {
+                GUILayout.Width(arrowButtonSize),
+                GUILayout.Height(arrowButtonSize)
+            };
+
+            float dropdownButtonHeight = arrowButtonSize;
+            float dropdownButtonWidth = 143f;
+            GUILayoutOption[] dropdownLayoutOptions = new GUILayoutOption[] {
+                GUILayout.Height(dropdownButtonHeight),
+                GUILayout.Width(dropdownButtonWidth)
+            };
+
+            MaidSwitcherPane.Draw();
+
+            bool previousState = GUI.enabled;
+            GUI.enabled = this.meidoManager.HasActiveMeido;
+
+            GUILayout.BeginHorizontal();
+            this.facePrevButton.Draw(arrowLayoutOptions);
+            this.faceBlendDropdown.Draw(dropdownLayoutOptions);
+            this.faceNextButton.Draw(arrowLayoutOptions);
+            GUILayout.EndHorizontal();
+
+            this.scrollPos = GUILayout.BeginScrollView(this.scrollPos);
+
+            this.maidFaceSliderPane.Draw();
+
+            GUILayout.EndScrollView();
+
+            GUI.enabled = previousState;
+        }
+
+        private void UpdateFace()
+        {
+            if (this.meidoManager.HasActiveMeido)
+            {
+                this.meidoManager.ActiveMeido.Maid.boMabataki = false;
+                this.meidoManager.ActiveMeido.Maid.body0.Face.morph.EyeMabataki = 0f;
+                this.maidFaceSliderPane.SetControlValues();
+            }
+        }
+
+        private void SelectMeido(object sender, MeidoChangeEventArgs args)
+        {
+            if (TabsPane.SelectedTab == Constants.Window.Face)
+                UpdateFace();
+        }
+
+        private void ChangeTab(object sender, EventArgs args)
+        {
+            if (TabsPane.SelectedTab == Constants.Window.Face)
+                UpdateFace();
+        }
+    }
+}

+ 119 - 0
COM3D2.MeidoPhotoStudio.Plugin/MeidoPhotoStudio/GUI/Windows/MainWindows/MaidPoseWindow.cs

@@ -0,0 +1,119 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Linq;
+using UnityEngine;
+
+namespace COM3D2.MeidoPhotoStudio.Plugin
+{
+    public class MaidPoseWindow : BaseMainWindow
+    {
+        private MeidoManager meidoManager;
+        private Button poseLeftButton;
+        private Button poseRightButton;
+        private Button poseGroupLeftButton;
+        private Button poseGroupRightButton;
+        private Dropdown poseGroupDropdown;
+        private Dropdown poseDropdown;
+        private string selectedPoseGroup;
+        private int selectedPose;
+        public MaidPoseWindow(MeidoManager meidoManager)
+        {
+            this.meidoManager = meidoManager;
+            this.meidoManager.SelectMeido += SelectMeido;
+
+            this.poseGroupDropdown = new Dropdown(Translation.GetList("poseGroupDropdown", Constants.PoseGroupList));
+            this.poseGroupDropdown.SelectionChange += ChangePoseGroup;
+
+            this.poseDropdown = new Dropdown(Constants.PoseDict[Constants.PoseGroupList[0]].ToArray());
+            this.poseDropdown.SelectionChange += ChangePose;
+
+            this.poseGroupLeftButton = new Button("<");
+            this.poseGroupLeftButton.ControlEvent += (s, a) => poseGroupDropdown.Step(-1);
+
+            this.poseGroupRightButton = new Button(">");
+            this.poseGroupRightButton.ControlEvent += (s, a) => poseGroupDropdown.Step(1);
+
+            this.poseLeftButton = new Button("<");
+            this.poseLeftButton.ControlEvent += (s, a) => poseDropdown.Step(-1);
+
+            this.poseRightButton = new Button(">");
+            this.poseRightButton.ControlEvent += (s, a) => poseDropdown.Step(1);
+        }
+
+        private void ChangePoseGroup(object sender, EventArgs args)
+        {
+            string newPoseGroup = Constants.PoseGroupList[this.poseGroupDropdown.SelectedItemIndex];
+            if (selectedPoseGroup == newPoseGroup)
+            {
+                this.poseDropdown.SelectedItemIndex = 0;
+            }
+            else
+            {
+                selectedPoseGroup = newPoseGroup;
+                if (this.poseGroupDropdown.SelectedItemIndex >= Constants.CustomPoseGroupsIndex)
+                {
+                    List<KeyValuePair<string, string>> pairList = Constants.CustomPoseDict[selectedPoseGroup];
+                    string[] poseList = pairList.Select(pair => pair.Key).ToArray();
+                    this.poseDropdown.SetDropdownItems(poseList);
+                }
+                else
+                {
+                    this.poseDropdown.SetDropdownItems(Constants.PoseDict[selectedPoseGroup].ToArray());
+                }
+            }
+        }
+
+        private void ChangePose(object sender, EventArgs args)
+        {
+            selectedPose = poseDropdown.SelectedItemIndex;
+            string poseName;
+            if (this.poseGroupDropdown.SelectedItemIndex >= Constants.CustomPoseGroupsIndex)
+                poseName = Constants.CustomPoseDict[selectedPoseGroup][selectedPose].Value;
+            else
+                poseName = Constants.PoseDict[selectedPoseGroup][selectedPose];
+
+            meidoManager.ActiveMeido.SetPose(poseName);
+        }
+
+        public override void Draw(params GUILayoutOption[] layoutOptions)
+        {
+            float arrowButtonSize = 30;
+            GUILayoutOption[] arrowLayoutOptions = {
+                GUILayout.Width(arrowButtonSize),
+                GUILayout.Height(arrowButtonSize)
+            };
+
+            float dropdownButtonHeight = arrowButtonSize;
+            float dropdownButtonWidth = 143f;
+            GUILayoutOption[] dropdownLayoutOptions = new GUILayoutOption[] {
+                GUILayout.Height(dropdownButtonHeight),
+                GUILayout.Width(dropdownButtonWidth)
+            };
+
+            MaidSwitcherPane.Draw();
+
+            bool previousState = GUI.enabled;
+            GUI.enabled = meidoManager.HasActiveMeido;
+
+            GUILayout.BeginHorizontal();
+            this.poseGroupLeftButton.Draw(arrowLayoutOptions);
+            this.poseGroupDropdown.Draw(dropdownLayoutOptions);
+            this.poseGroupRightButton.Draw(arrowLayoutOptions);
+            GUILayout.EndHorizontal();
+
+            GUILayout.BeginHorizontal();
+            this.poseLeftButton.Draw(arrowLayoutOptions);
+            this.poseDropdown.Draw(dropdownLayoutOptions);
+            this.poseRightButton.Draw(arrowLayoutOptions);
+            GUILayout.EndHorizontal();
+
+            GUI.enabled = previousState;
+        }
+
+        private void SelectMeido(object sender, EventArgs args)
+        {
+
+        }
+    }
+}

+ 101 - 0
COM3D2.MeidoPhotoStudio.Plugin/MeidoPhotoStudio/GUI/Windows/MessageWindow.cs

@@ -0,0 +1,101 @@
+using System;
+using UnityEngine;
+
+namespace COM3D2.MeidoPhotoStudio.Plugin
+{
+    public class MessageWindow : BaseWindow
+    {
+        TextField nameTextField;
+        Slider fontSizeSlider;
+        TextArea messageTextArea;
+        Button okButton;
+        private int fontSize = 25;
+        private bool showingMessage = false;
+
+        public MessageWindow() : base()
+        {
+            nameTextField = new TextField();
+            Controls.Add(nameTextField);
+
+            fontSizeSlider = new Slider(25, 60);
+            fontSizeSlider.ControlEvent += ChangeFontSize;
+            Controls.Add(fontSizeSlider);
+
+            messageTextArea = new TextArea();
+            Controls.Add(messageTextArea);
+
+            okButton = new Button("OK");
+            okButton.ControlEvent += SetMessage;
+            Controls.Add(okButton);
+        }
+
+        public void SetVisibility()
+        {
+            if (showingMessage)
+            {
+                GameObject messageGameObject = GameObject.Find("__GameMain__/SystemUI Root").transform.Find("MessageWindowPanel").gameObject;
+                MessageWindowMgr messageWindowMgr = GameMain.Instance.ScriptMgr.adv_kag.MessageWindowMgr;
+                messageWindowMgr.CloseMessageWindowPanel();
+                showingMessage = false;
+            }
+            else
+            {
+                Visible = !Visible;
+            }
+        }
+
+        private void ChangeFontSize(object sender, EventArgs args)
+        {
+            fontSize = (int)fontSizeSlider.Value;
+
+            GameObject gameObject = GameObject.Find("__GameMain__/SystemUI Root").transform.Find("MessageWindowPanel").gameObject;
+            UILabel uiLabel = UTY.GetChildObject(gameObject, "MessageViewer/MsgParent/Message", false).GetComponent<UILabel>();
+            Utility.SetFieldValue<UILabel, int>(uiLabel, "mFontSize", fontSize);
+        }
+
+        private void SetMessage(object sender, EventArgs args)
+        {
+            Visible = false;
+            showingMessage = true;
+            GameObject messageGameObject = GameObject.Find("__GameMain__/SystemUI Root").transform.Find("MessageWindowPanel").gameObject;
+            MessageWindowMgr messageWindowMgr = GameMain.Instance.ScriptMgr.adv_kag.MessageWindowMgr;
+            messageWindowMgr.OpenMessageWindowPanel();
+
+            UILabel component = UTY.GetChildObject(messageGameObject, "MessageViewer/MsgParent/Message", false).GetComponent<UILabel>();
+            UILabel nameComponent = UTY.GetChildObject(messageGameObject, "MessageViewer/MsgParent/SpeakerName/Name", false).GetComponent<UILabel>();
+
+            MessageClass inst = new MessageClass(messageGameObject, messageWindowMgr);
+            // Fix for ENG version: reconfigure MessageClass to behave as in JP game
+            inst.subtitles_manager_.visible = false;
+            inst.subtitles_manager_ = null;
+            component.gameObject.SetActive(true);
+            nameComponent.gameObject.SetActive(true);
+            UTY.GetChildObject(messageGameObject, "MessageViewer/MsgParent/MessageBox", false).SetActive(true);
+            Utility.SetFieldValue<MessageClass, UILabel>(inst, "message_label_", component);
+            Utility.SetFieldValue<MessageClass, UILabel>(inst, "name_label_", nameComponent);
+
+            component.ProcessText();
+            Utility.SetFieldValue<UILabel, int>(component, "mFontSize", fontSize);
+
+            inst.SetText(nameTextField.Value, messageTextArea.Value, "", 0, AudioSourceMgr.Type.System);
+            inst.FinishChAnime();
+        }
+
+        public override void Draw(params GUILayoutOption[] layoutOptions)
+        {
+            GUILayout.BeginHorizontal();
+            GUILayout.Label("Name", GUILayout.ExpandWidth(false));
+            nameTextField.Draw(GUILayout.Width(120));
+
+            GUILayout.Space(30);
+
+            GUILayout.Label("Font Size", GUILayout.ExpandWidth(false));
+            fontSizeSlider.Draw(GUILayout.Width(120), GUILayout.ExpandWidth(false));
+            GUILayout.Label($"{(int)fontSize}pt");
+            GUILayout.EndHorizontal();
+
+            messageTextArea.Draw(GUILayout.MinHeight(90));
+            okButton.Draw(GUILayout.ExpandWidth(false), GUILayout.Width(30));
+        }
+    }
+}

+ 573 - 0
COM3D2.MeidoPhotoStudio.Plugin/MeidoPhotoStudio/Managers/DragPointManager.cs

@@ -0,0 +1,573 @@
+using System;
+using System.Collections.Generic;
+using UnityEngine;
+
+namespace COM3D2.MeidoPhotoStudio.Plugin
+{
+    using ModKey = Utility.ModKey;
+    public class DragPointManager
+    {
+        enum IKMode
+        {
+            None, UpperLock, Mune, RotLocal, BodyTransform, FingerRotLocalY, FingerRotLocalXZ, BodySelect
+        }
+        enum Bone
+        {
+            Head, HeadNub, ClavicleL, ClavicleR,
+            UpperArmL, UpperArmR, ForearmL, ForearmR,
+            HandL, HandR, IKHandL, IKHandR,
+            MuneL, MuneSubL, MuneR, MuneSubR,
+            Neck, Spine, Spine0a, Spine1, Spine1a, ThighL, ThighR,
+            Pelvis, Hip,
+            CalfL, CalfR, FootL, FootR,
+            // Dragpoint specific
+            Cube, Body, Torso,
+            // Fingers
+            Finger0L, Finger01L, Finger02L, Finger0NubL,
+            Finger1L, Finger11L, Finger12L, Finger1NubL,
+            Finger2L, Finger21L, Finger22L, Finger2NubL,
+            Finger3L, Finger31L, Finger32L, Finger3NubL,
+            Finger4L, Finger41L, Finger42L, Finger4NubL,
+            Finger0R, Finger01R, Finger02R, Finger0NubR,
+            Finger1R, Finger11R, Finger12R, Finger1NubR,
+            Finger2R, Finger21R, Finger22R, Finger2NubR,
+            Finger3R, Finger31R, Finger32R, Finger3NubR,
+            Finger4R, Finger41R, Finger42R, Finger4NubR,
+            // Toes
+            Toe0L, Toe01L, Toe0NubL,
+            Toe1L, Toe11L, Toe1NubL,
+            Toe2L, Toe21L, Toe2NubL,
+            Toe0R, Toe01R, Toe0NubR,
+            Toe1R, Toe11R, Toe1NubR,
+            Toe2R, Toe21R, Toe2NubR
+        }
+        private static readonly Dictionary<IKMode, Bone[]> IKGroup = new Dictionary<IKMode, Bone[]>()
+        {
+            [IKMode.None] = new[]
+            {
+                Bone.UpperArmL, Bone.ForearmL, Bone.HandL, Bone.UpperArmR,
+                Bone.ForearmR, Bone.HandR, Bone.CalfL, Bone.FootL, Bone.CalfR, Bone.FootR
+            },
+            [IKMode.UpperLock] = new[] { Bone.HandL, Bone.HandR, Bone.FootL, Bone.FootR },
+            [IKMode.Mune] = new[] { Bone.Head, Bone.MuneL, Bone.MuneR },
+            [IKMode.RotLocal] = new[]
+            {
+                Bone.Head, Bone.CalfL, Bone.CalfR, Bone.Torso,
+                Bone.Pelvis, Bone.HandL, Bone.HandR, Bone.FootL, Bone.FootR
+            },
+            [IKMode.BodyTransform] = new[] { Bone.Body, Bone.Cube },
+            [IKMode.BodySelect] = new[] { Bone.Head, Bone.Body },
+            [IKMode.FingerRotLocalXZ] = new[]
+            {
+                Bone.Finger01L, Bone.Finger02L, Bone.Finger0NubL,
+                Bone.Finger11L, Bone.Finger12L, Bone.Finger1NubL,
+                Bone.Finger21L, Bone.Finger22L, Bone.Finger2NubL,
+                Bone.Finger31L, Bone.Finger32L, Bone.Finger3NubL,
+                Bone.Finger41L, Bone.Finger42L, Bone.Finger4NubL,
+                Bone.Finger01R, Bone.Finger02R, Bone.Finger0NubR,
+                Bone.Finger11R, Bone.Finger12R, Bone.Finger1NubR,
+                Bone.Finger21R, Bone.Finger22R, Bone.Finger2NubR,
+                Bone.Finger31R, Bone.Finger32R, Bone.Finger3NubR,
+                Bone.Finger41R, Bone.Finger42R, Bone.Finger4NubR,
+                Bone.Toe01L, Bone.Toe0NubL, Bone.Toe11L, Bone.Toe1NubL,
+                Bone.Toe21L, Bone.Toe2NubL, Bone.Toe01R, Bone.Toe0NubR,
+                Bone.Toe11R, Bone.Toe1NubR, Bone.Toe21R, Bone.Toe2NubR
+            },
+            [IKMode.FingerRotLocalY] = new[] {
+                Bone.Finger01L, Bone.Finger11L, Bone.Finger21L, Bone.Finger31L, Bone.Finger41L,
+                Bone.Finger01R, Bone.Finger11R, Bone.Finger21R, Bone.Finger31R, Bone.Finger41R,
+                Bone.Toe01L, Bone.Toe11L, Bone.Toe21L, Bone.Toe01R, Bone.Toe11R, Bone.Toe21R
+            }
+        };
+        private Meido meido;
+        private Maid maid;
+        private Dictionary<Bone, GameObject> DragPoint;
+        private Dictionary<Bone, Transform> BoneTransform;
+        private IKMode ikMode;
+        private IKMode ikModeOld = IKMode.None;
+        public event EventHandler<MeidoChangeEventArgs> SelectMaid;
+        public bool Initialized { get; private set; }
+        public DragPointManager(Meido meido)
+        {
+            meido.BodyLoad += Initialize;
+            this.meido = meido;
+            this.maid = meido.Maid;
+        }
+
+        public void Initialize(object sender, EventArgs args)
+        {
+            if (Initialized) return;
+
+            InitializeBones();
+            InitializeDragPoints();
+            Initialized = true;
+            meido.BodyLoad -= Initialize;
+        }
+
+        public void Destroy()
+        {
+            foreach (KeyValuePair<Bone, GameObject> dragPoint in DragPoint)
+            {
+                GameObject.Destroy(dragPoint.Value);
+            }
+            DragPoint = null;
+            BoneTransform = null;
+            Initialized = false;
+        }
+
+        public void Deactivate()
+        {
+            foreach (KeyValuePair<Bone, GameObject> dragPoint in DragPoint)
+            {
+                dragPoint.Value.SetActive(false);
+            }
+        }
+
+        public void Activate()
+        {
+            ikMode = ikModeOld = IKMode.None;
+            UpdateIK();
+        }
+
+        public void Update()
+        {
+            if (Input.GetKey(KeyCode.Z) || Input.GetKey(KeyCode.X) || Input.GetKey(KeyCode.C))
+            {
+                ikMode = IKMode.BodyTransform;
+            }
+            else if (Input.GetKey(KeyCode.A))
+            {
+                ikMode = IKMode.BodySelect;
+            }
+            else if (Utility.GetModKey(ModKey.Control) && Utility.GetModKey(ModKey.Alt))
+            {
+                ikMode = IKMode.Mune;
+            }
+            else if (Utility.GetModKey(ModKey.Shift) && Input.GetKey(KeyCode.Space))
+            {
+                ikMode = IKMode.FingerRotLocalY;
+            }
+            else if (Utility.GetModKey(ModKey.Alt) /* && Utility.GetModKey(ModKey.Shift) */)
+            {
+                ikMode = IKMode.RotLocal;
+            }
+            else if (Input.GetKey(KeyCode.Space))
+            {
+                ikMode = IKMode.FingerRotLocalXZ;
+            }
+            else if (Utility.GetModKey(ModKey.Control))
+            {
+                ikMode = IKMode.UpperLock;
+            }
+            else
+            {
+                ikMode = IKMode.None;
+            }
+
+            if (ikMode != ikModeOld) UpdateIK();
+
+            ikModeOld = ikMode;
+        }
+
+        private void UpdateIK()
+        {
+            Deactivate();
+
+            foreach (Bone bone in IKGroup[ikMode])
+            {
+                DragPoint[bone].SetActive(true);
+            }
+        }
+
+        // TODO: Rework this a little to reduce number of needed BaseDrag derived components
+        private void InitializeDragPoints()
+        {
+            DragPoint = new Dictionary<Bone, GameObject>();
+
+            Vector3 limbDragPointSize = Vector3.one * 0.12f;
+            Vector3 fingerDragPointSize = Vector3.one * 0.015f;
+
+            Material transparentBlue = new Material(Shader.Find("Transparent/Diffuse"))
+            {
+                color = new Color(0.4f, 0.4f, 1f, 0.3f)
+            };
+
+            Material transparentBlue2 = new Material(Shader.Find("Transparent/Diffuse"))
+            {
+                color = new Color(0.5f, 0.5f, 1f, 0.8f)
+            };
+
+            Func<PrimitiveType, Vector3, Material, GameObject> MakeDragPoint = (primitive, scale, material) =>
+            {
+                GameObject dragPoint = GameObject.CreatePrimitive(primitive);
+                dragPoint.transform.localScale = scale;
+                if (material != null) dragPoint.GetComponent<Renderer>().material = material;
+                dragPoint.layer = 8;
+                return dragPoint;
+            };
+
+            Func<Transform[], Transform[], Transform[], bool, GameObject[]> MakeIKChainDragPoint = (upper, middle, lower, leg) =>
+            {
+                GameObject[] dragPoints = new GameObject[3];
+                for (int i = 0; i < dragPoints.Length; i++)
+                {
+                    dragPoints[i] = MakeDragPoint(PrimitiveType.Sphere, limbDragPointSize, transparentBlue);
+                }
+
+                DragJointForearm dragUpper = dragPoints[0].AddComponent<DragJointForearm>();
+                dragUpper.Initialize(upper, false, maid, () => upper[2].position, () => Vector3.zero);
+                DragJointForearm dragMiddle = dragPoints[1].AddComponent<DragJointForearm>();
+                dragMiddle.Initialize(middle, leg, maid, () => middle[2].position, () => Vector3.zero);
+                DragJointHand dragLower = dragPoints[2].AddComponent<DragJointHand>();
+                dragLower.Initialize(lower, leg, maid, () => lower[2].position, () => Vector3.zero);
+                return dragPoints;
+            };
+
+            DragPoint[Bone.Cube] = MakeDragPoint(PrimitiveType.Cube, new Vector3(0.12f, 0.12f, 0.12f), transparentBlue2);
+
+            DragPoint[Bone.Cube].AddComponent<DragBody>()
+                .Initialize(maid,
+                    () => maid.transform.position,
+                    () => maid.transform.eulerAngles
+                );
+
+            DragPoint[Bone.Body] = MakeDragPoint(PrimitiveType.Capsule, new Vector3(0.2f, 0.3f, 0.24f), transparentBlue);
+
+            DragBody dragBody = DragPoint[Bone.Body].AddComponent<DragBody>();
+            dragBody.Initialize(maid,
+                () => new Vector3(
+                    (BoneTransform[Bone.Hip].position.x + BoneTransform[Bone.Spine0a].position.x) / 2f,
+                    (BoneTransform[Bone.Spine1].position.y + BoneTransform[Bone.Spine0a].position.y) / 2f,
+                    (BoneTransform[Bone.Spine0a].position.z + BoneTransform[Bone.Hip].position.z) / 2f
+                ),
+                () => new Vector3(
+                    BoneTransform[Bone.Spine0a].transform.eulerAngles.x,
+                    BoneTransform[Bone.Spine0a].transform.eulerAngles.y,
+                    BoneTransform[Bone.Spine0a].transform.eulerAngles.z + 90f
+                )
+            );
+            dragBody.Select += (s, e) => OnMeidoSelect(new MeidoChangeEventArgs(meido.ActiveSlot, true));
+
+            DragPoint[Bone.Head] = MakeDragPoint(PrimitiveType.Sphere, new Vector3(0.2f, 0.24f, 0.2f), transparentBlue);
+            DragHead dragHead = DragPoint[Bone.Head].AddComponent<DragHead>();
+            dragHead.Initialize(BoneTransform[Bone.Neck], maid,
+                () => new Vector3(
+                    BoneTransform[Bone.Head].position.x,
+                    (BoneTransform[Bone.Head].position.y * 1.2f + BoneTransform[Bone.HeadNub].position.y * 0.8f) / 2f,
+                    BoneTransform[Bone.Head].position.z
+                ),
+                () => new Vector3(BoneTransform[Bone.Head].eulerAngles.x, BoneTransform[Bone.Head].eulerAngles.y, BoneTransform[Bone.Head].eulerAngles.z + 90f)
+            );
+            dragHead.Select += (s, a) => OnMeidoSelect(new MeidoChangeEventArgs(meido.ActiveSlot, true, false));
+
+            DragPoint[Bone.Torso] = MakeDragPoint(PrimitiveType.Capsule, new Vector3(0.2f, 0.19f, 0.24f), transparentBlue);
+            Transform spineTrans1 = BoneTransform[Bone.Spine1];
+            Transform spineTrans2 = BoneTransform[Bone.Spine1a];
+            Transform[] spineParts = new Transform[4] {
+                BoneTransform[Bone.Spine1a],
+                BoneTransform[Bone.Spine1],
+                BoneTransform[Bone.Spine0a],
+                BoneTransform[Bone.Spine]
+            };
+            DragPoint[Bone.Torso].AddComponent<DragTorso>()
+                .Initialize(maid, spineParts,
+                    () => new Vector3(
+                        spineTrans1.position.x,
+                        (spineTrans2.position.y * 2f) / 2f,
+                        spineTrans1.position.z
+                    ),
+                    () => new Vector3(
+                        spineTrans1.eulerAngles.x,
+                        spineTrans1.eulerAngles.y,
+                        spineTrans1.eulerAngles.z + 90f
+                    )
+                );
+
+            DragPoint[Bone.Pelvis] = MakeDragPoint(PrimitiveType.Capsule, new Vector3(0.2f, 0.15f, 0.24f), transparentBlue);
+            Transform pelvisTrans = BoneTransform[Bone.Pelvis];
+            Transform spineTrans = BoneTransform[Bone.Spine];
+            DragPoint[Bone.Pelvis].AddComponent<DragPelvis>()
+                .Initialize(maid, BoneTransform[Bone.Pelvis],
+                    () => new Vector3(
+                        pelvisTrans.position.x,
+                        (pelvisTrans.position.y + spineTrans.position.y) / 2f,
+                        pelvisTrans.position.z
+                    ),
+                    () => new Vector3(
+                        pelvisTrans.eulerAngles.x + 90f,
+                        pelvisTrans.eulerAngles.y + 90f,
+                        pelvisTrans.eulerAngles.z
+                    )
+                );
+
+            DragPoint[Bone.MuneL] = MakeDragPoint(PrimitiveType.Sphere, new Vector3(0.12f, 0.12f, 0.12f), transparentBlue);
+            DragMune dragMuneL = DragPoint[Bone.MuneL].AddComponent<DragMune>();
+            Transform[] muneIKChainL = new Transform[3] {
+                BoneTransform[Bone.MuneL],
+                BoneTransform[Bone.MuneL],
+                BoneTransform[Bone.MuneSubL]
+            };
+            dragMuneL.Initialize(muneIKChainL, maid,
+                () => (BoneTransform[Bone.MuneL].position + BoneTransform[Bone.MuneSubL].position) / 2f,
+                () => Vector3.zero
+            );
+
+            DragPoint[Bone.MuneR] = MakeDragPoint(PrimitiveType.Sphere, new Vector3(0.12f, 0.12f, 0.12f), transparentBlue);
+            DragMune dragMuneR = DragPoint[Bone.MuneR].AddComponent<DragMune>();
+            Transform[] muneIKChainR = new Transform[3] {
+                BoneTransform[Bone.MuneR],
+                BoneTransform[Bone.MuneR],
+                BoneTransform[Bone.MuneSubR]
+            };
+            dragMuneR.Initialize(muneIKChainR, maid,
+                () => (BoneTransform[Bone.MuneR].position + BoneTransform[Bone.MuneSubR].position) / 2f,
+                () => Vector3.zero
+            );
+
+            GameObject[] ikChainArmL = MakeIKChainDragPoint(
+                new Transform[3] {
+                    BoneTransform[Bone.ClavicleL],
+                    BoneTransform[Bone.ClavicleL],
+                    BoneTransform[Bone.UpperArmL]
+                },
+                new Transform[3] {
+                    BoneTransform[Bone.UpperArmL],
+                    BoneTransform[Bone.UpperArmL],
+                    BoneTransform[Bone.ForearmL]
+                },
+                new Transform[3] {
+                    BoneTransform[Bone.UpperArmL],
+                    BoneTransform[Bone.ForearmL],
+                    BoneTransform[Bone.HandL]
+                },
+                false
+            );
+            DragPoint[Bone.UpperArmL] = ikChainArmL[0];
+            DragPoint[Bone.ForearmL] = ikChainArmL[1];
+            DragPoint[Bone.HandL] = ikChainArmL[2];
+
+            GameObject[] ikChainArmR = MakeIKChainDragPoint(
+                new Transform[3] {
+                    BoneTransform[Bone.ClavicleR],
+                    BoneTransform[Bone.ClavicleR],
+                    BoneTransform[Bone.UpperArmR]
+                },
+                new Transform[3] {
+                    BoneTransform[Bone.UpperArmR],
+                    BoneTransform[Bone.UpperArmR],
+                    BoneTransform[Bone.ForearmR]
+                },
+                new Transform[3] {
+                    BoneTransform[Bone.UpperArmR],
+                    BoneTransform[Bone.ForearmR],
+                    BoneTransform[Bone.HandR]
+                },
+                false
+            );
+            DragPoint[Bone.UpperArmR] = ikChainArmR[0];
+            DragPoint[Bone.ForearmR] = ikChainArmR[1];
+            DragPoint[Bone.HandR] = ikChainArmR[2];
+
+            GameObject[] ikChainLegL = MakeIKChainDragPoint(
+                new Transform[3] {
+                    BoneTransform[Bone.ThighL],
+                    BoneTransform[Bone.CalfL],
+                    BoneTransform[Bone.FootL]
+                },
+                new Transform[3] {
+                    BoneTransform[Bone.ThighL],
+                    BoneTransform[Bone.ThighL],
+                    BoneTransform[Bone.CalfL]
+                },
+                new Transform[3] {
+                    BoneTransform[Bone.ThighL],
+                    BoneTransform[Bone.CalfL],
+                    BoneTransform[Bone.FootL]
+                },
+                true
+            );
+            DragPoint[Bone.CalfL] = ikChainLegL[1];
+            DragPoint[Bone.FootL] = ikChainLegL[2];
+
+            GameObject[] ikChainLegR = MakeIKChainDragPoint(
+                new Transform[3] {
+                    BoneTransform[Bone.ThighR],
+                    BoneTransform[Bone.CalfR],
+                    BoneTransform[Bone.FootR]
+                },
+                new Transform[3] {
+                    BoneTransform[Bone.ThighR],
+                    BoneTransform[Bone.ThighR],
+                    BoneTransform[Bone.CalfR]
+                },
+                new Transform[3] {
+                    BoneTransform[Bone.ThighR],
+                    BoneTransform[Bone.CalfR],
+                    BoneTransform[Bone.FootR]
+                },
+                true
+            );
+            DragPoint[Bone.CalfR] = ikChainLegR[1];
+            DragPoint[Bone.FootR] = ikChainLegR[2];
+
+            // destroy unused thigh dragpoints 
+            GameObject.Destroy(ikChainLegL[0]);
+            GameObject.Destroy(ikChainLegR[0]);
+
+            for (Bone bone = Bone.Neck; bone <= Bone.ThighR; ++bone)
+            {
+                Transform pos = BoneTransform[bone];
+                DragPoint[bone] = MakeDragPoint(PrimitiveType.Sphere, limbDragPointSize, transparentBlue);
+                DragPoint[bone].AddComponent<DragSpine>()
+                    .Initialize(BoneTransform[bone], maid,
+                        () => pos.position,
+                        () => Vector3.zero
+                    );
+            }
+
+            for (Bone finger = Bone.Finger0L; finger <= Bone.Finger4R; finger += 4)
+            {
+                for (int i = 0; i < 3; i++)
+                {
+                    Bone bone = finger + 1 + i; // Bone.Finger01
+                    DragPoint[bone] = MakeDragPoint(PrimitiveType.Sphere, fingerDragPointSize, transparentBlue);
+                    Transform[] trans = new Transform[3] {
+                        BoneTransform[bone - 1],
+                        BoneTransform[bone - 1],
+                        BoneTransform[bone]
+                    };
+                    Func<Vector3> pos = () => BoneTransform[bone].position;
+                    bool baseFinger = i == 0;
+                    // if (i == 0)
+                    //     DragPoint[bone].AddComponent<DragJointForearm>().Initialize(trans, true, maid, pos, () => Vector3.zero);
+                    // else
+                    DragPoint[bone].AddComponent<DragJointFinger>().Initialize(trans, baseFinger, maid, pos, () => Vector3.zero);
+                }
+            }
+
+            for (Bone toe = Bone.Toe0L; toe <= Bone.Toe2R; toe += 3)
+            {
+                for (int i = 0; i < 2; i++)
+                {
+                    Bone bone = toe + 1 + i; // Bone.Toe01
+                    DragPoint[bone] = MakeDragPoint(PrimitiveType.Sphere, fingerDragPointSize, transparentBlue);
+                    Transform[] trans = new Transform[3] {
+                        BoneTransform[bone - 1],
+                        BoneTransform[bone - 1],
+                        BoneTransform[bone]
+                    };
+                    Func<Vector3> pos = () => BoneTransform[bone].position;
+                    bool baseFinger = i == 0;
+                    DragPoint[bone].AddComponent<DragJointFinger>().Initialize(trans, baseFinger, maid, pos, () => Vector3.zero);
+                }
+            }
+
+            ikModeOld = IKMode.None;
+            ikMode = IKMode.None;
+
+            UpdateIK();
+        }
+
+        private void InitializeBones()
+        {
+            BoneTransform = new Dictionary<Bone, Transform>()
+            {
+                [Bone.Head] = CMT.SearchObjName(maid.body0.m_Bones.transform, "Bip01 Head", true),
+                [Bone.Neck] = CMT.SearchObjName(maid.body0.m_Bones.transform, "Bip01 Neck", true),
+                [Bone.HeadNub] = CMT.SearchObjName(maid.body0.m_Bones.transform, "Bip01 HeadNub", true),
+                [Bone.IKHandL] = CMT.SearchObjName(maid.body0.m_Bones.transform, "_IK_handL", true),
+                [Bone.IKHandR] = CMT.SearchObjName(maid.body0.m_Bones.transform, "_IK_handR", true),
+                [Bone.MuneL] = CMT.SearchObjName(maid.body0.m_Bones.transform, "Mune_L", true),
+                [Bone.MuneSubL] = CMT.SearchObjName(maid.body0.m_Bones.transform, "Mune_L_sub", true),
+                [Bone.MuneR] = CMT.SearchObjName(maid.body0.m_Bones.transform, "Mune_R", true),
+                [Bone.MuneSubR] = CMT.SearchObjName(maid.body0.m_Bones.transform, "Mune_R_sub", true),
+                [Bone.Pelvis] = CMT.SearchObjName(maid.body0.m_Bones.transform, "Bip01 Pelvis", true),
+                [Bone.Hip] = CMT.SearchObjName(maid.body0.m_Bones.transform, "Bip01", true),
+                [Bone.Spine] = CMT.SearchObjName(maid.body0.m_Bones.transform, "Bip01 Spine", true),
+                [Bone.Spine0a] = CMT.SearchObjName(maid.body0.m_Bones.transform, "Bip01 Spine0a", true),
+                [Bone.Spine1] = CMT.SearchObjName(maid.body0.m_Bones.transform, "Bip01 Spine1", true),
+                [Bone.Spine1a] = CMT.SearchObjName(maid.body0.m_Bones.transform, "Bip01 Spine1a", true),
+                [Bone.ClavicleL] = CMT.SearchObjName(maid.body0.m_Bones.transform, "Bip01 L Clavicle", true),
+                [Bone.ClavicleR] = CMT.SearchObjName(maid.body0.m_Bones.transform, "Bip01 R Clavicle", true),
+                [Bone.UpperArmL] = CMT.SearchObjName(maid.body0.m_Bones.transform, "Bip01 L UpperArm", true),
+                [Bone.ForearmL] = CMT.SearchObjName(maid.body0.m_Bones.transform, "Bip01 L Forearm", true),
+                [Bone.HandL] = CMT.SearchObjName(maid.body0.m_Bones.transform, "Bip01 L Hand", true),
+                [Bone.UpperArmR] = CMT.SearchObjName(maid.body0.m_Bones.transform, "Bip01 R UpperArm", true),
+                [Bone.ForearmR] = CMT.SearchObjName(maid.body0.m_Bones.transform, "Bip01 R Forearm", true),
+                [Bone.HandR] = CMT.SearchObjName(maid.body0.m_Bones.transform, "Bip01 R Hand", true),
+                [Bone.ThighL] = CMT.SearchObjName(maid.body0.m_Bones.transform, "Bip01 L Thigh", true),
+                [Bone.CalfL] = CMT.SearchObjName(maid.body0.m_Bones.transform, "Bip01 L Calf", true),
+                [Bone.FootL] = CMT.SearchObjName(maid.body0.m_Bones.transform, "Bip01 L Foot", true),
+                [Bone.ThighR] = CMT.SearchObjName(maid.body0.m_Bones.transform, "Bip01 R Thigh", true),
+                [Bone.CalfR] = CMT.SearchObjName(maid.body0.m_Bones.transform, "Bip01 R Calf", true),
+                [Bone.FootR] = CMT.SearchObjName(maid.body0.m_Bones.transform, "Bip01 R Foot", true),
+                // fingers
+                [Bone.Finger0L] = CMT.SearchObjName(maid.body0.m_Bones.transform, "Bip01 L Finger0", true),
+                [Bone.Finger01L] = CMT.SearchObjName(maid.body0.m_Bones.transform, "Bip01 L Finger01", true),
+                [Bone.Finger02L] = CMT.SearchObjName(maid.body0.m_Bones.transform, "Bip01 L Finger02", true),
+                [Bone.Finger0NubL] = CMT.SearchObjName(maid.body0.m_Bones.transform, "Bip01 L Finger0Nub", true),
+                [Bone.Finger1L] = CMT.SearchObjName(maid.body0.m_Bones.transform, "Bip01 L Finger1", true),
+                [Bone.Finger11L] = CMT.SearchObjName(maid.body0.m_Bones.transform, "Bip01 L Finger11", true),
+                [Bone.Finger12L] = CMT.SearchObjName(maid.body0.m_Bones.transform, "Bip01 L Finger12", true),
+                [Bone.Finger1NubL] = CMT.SearchObjName(maid.body0.m_Bones.transform, "Bip01 L Finger1Nub", true),
+                [Bone.Finger2L] = CMT.SearchObjName(maid.body0.m_Bones.transform, "Bip01 L Finger2", true),
+                [Bone.Finger21L] = CMT.SearchObjName(maid.body0.m_Bones.transform, "Bip01 L Finger21", true),
+                [Bone.Finger22L] = CMT.SearchObjName(maid.body0.m_Bones.transform, "Bip01 L Finger22", true),
+                [Bone.Finger2NubL] = CMT.SearchObjName(maid.body0.m_Bones.transform, "Bip01 L Finger2Nub", true),
+                [Bone.Finger3L] = CMT.SearchObjName(maid.body0.m_Bones.transform, "Bip01 L Finger3", true),
+                [Bone.Finger31L] = CMT.SearchObjName(maid.body0.m_Bones.transform, "Bip01 L Finger31", true),
+                [Bone.Finger32L] = CMT.SearchObjName(maid.body0.m_Bones.transform, "Bip01 L Finger32", true),
+                [Bone.Finger3NubL] = CMT.SearchObjName(maid.body0.m_Bones.transform, "Bip01 L Finger3Nub", true),
+                [Bone.Finger4L] = CMT.SearchObjName(maid.body0.m_Bones.transform, "Bip01 L Finger4", true),
+                [Bone.Finger41L] = CMT.SearchObjName(maid.body0.m_Bones.transform, "Bip01 L Finger41", true),
+                [Bone.Finger42L] = CMT.SearchObjName(maid.body0.m_Bones.transform, "Bip01 L Finger42", true),
+                [Bone.Finger4NubL] = CMT.SearchObjName(maid.body0.m_Bones.transform, "Bip01 L Finger4Nub", true),
+                [Bone.Finger0R] = CMT.SearchObjName(maid.body0.m_Bones.transform, "Bip01 R Finger0", true),
+                [Bone.Finger01R] = CMT.SearchObjName(maid.body0.m_Bones.transform, "Bip01 R Finger01", true),
+                [Bone.Finger02R] = CMT.SearchObjName(maid.body0.m_Bones.transform, "Bip01 R Finger02", true),
+                [Bone.Finger0NubR] = CMT.SearchObjName(maid.body0.m_Bones.transform, "Bip01 R Finger0Nub", true),
+                [Bone.Finger1R] = CMT.SearchObjName(maid.body0.m_Bones.transform, "Bip01 R Finger1", true),
+                [Bone.Finger11R] = CMT.SearchObjName(maid.body0.m_Bones.transform, "Bip01 R Finger11", true),
+                [Bone.Finger12R] = CMT.SearchObjName(maid.body0.m_Bones.transform, "Bip01 R Finger12", true),
+                [Bone.Finger1NubR] = CMT.SearchObjName(maid.body0.m_Bones.transform, "Bip01 R Finger1Nub", true),
+                [Bone.Finger2R] = CMT.SearchObjName(maid.body0.m_Bones.transform, "Bip01 R Finger2", true),
+                [Bone.Finger21R] = CMT.SearchObjName(maid.body0.m_Bones.transform, "Bip01 R Finger21", true),
+                [Bone.Finger22R] = CMT.SearchObjName(maid.body0.m_Bones.transform, "Bip01 R Finger22", true),
+                [Bone.Finger2NubR] = CMT.SearchObjName(maid.body0.m_Bones.transform, "Bip01 R Finger2Nub", true),
+                [Bone.Finger3R] = CMT.SearchObjName(maid.body0.m_Bones.transform, "Bip01 R Finger3", true),
+                [Bone.Finger31R] = CMT.SearchObjName(maid.body0.m_Bones.transform, "Bip01 R Finger31", true),
+                [Bone.Finger32R] = CMT.SearchObjName(maid.body0.m_Bones.transform, "Bip01 R Finger32", true),
+                [Bone.Finger3NubR] = CMT.SearchObjName(maid.body0.m_Bones.transform, "Bip01 R Finger3Nub", true),
+                [Bone.Finger4R] = CMT.SearchObjName(maid.body0.m_Bones.transform, "Bip01 R Finger4", true),
+                [Bone.Finger41R] = CMT.SearchObjName(maid.body0.m_Bones.transform, "Bip01 R Finger41", true),
+                [Bone.Finger42R] = CMT.SearchObjName(maid.body0.m_Bones.transform, "Bip01 R Finger42", true),
+                [Bone.Finger4NubR] = CMT.SearchObjName(maid.body0.m_Bones.transform, "Bip01 R Finger4Nub", true),
+                // Toes
+                [Bone.Toe0L] = CMT.SearchObjName(maid.body0.m_Bones.transform, "Bip01 L Toe0", true),
+                [Bone.Toe01L] = CMT.SearchObjName(maid.body0.m_Bones.transform, "Bip01 L Toe01", true),
+                [Bone.Toe0NubL] = CMT.SearchObjName(maid.body0.m_Bones.transform, "Bip01 L Toe0Nub", true),
+                [Bone.Toe1L] = CMT.SearchObjName(maid.body0.m_Bones.transform, "Bip01 L Toe1", true),
+                [Bone.Toe11L] = CMT.SearchObjName(maid.body0.m_Bones.transform, "Bip01 L Toe11", true),
+                [Bone.Toe1NubL] = CMT.SearchObjName(maid.body0.m_Bones.transform, "Bip01 L Toe1Nub", true),
+                [Bone.Toe2L] = CMT.SearchObjName(maid.body0.m_Bones.transform, "Bip01 L Toe2", true),
+                [Bone.Toe21L] = CMT.SearchObjName(maid.body0.m_Bones.transform, "Bip01 L Toe21", true),
+                [Bone.Toe2NubL] = CMT.SearchObjName(maid.body0.m_Bones.transform, "Bip01 L Toe2Nub", true),
+                [Bone.Toe0R] = CMT.SearchObjName(maid.body0.m_Bones.transform, "Bip01 R Toe0", true),
+                [Bone.Toe01R] = CMT.SearchObjName(maid.body0.m_Bones.transform, "Bip01 R Toe01", true),
+                [Bone.Toe0NubR] = CMT.SearchObjName(maid.body0.m_Bones.transform, "Bip01 R Toe0Nub", true),
+                [Bone.Toe1R] = CMT.SearchObjName(maid.body0.m_Bones.transform, "Bip01 R Toe1", true),
+                [Bone.Toe11R] = CMT.SearchObjName(maid.body0.m_Bones.transform, "Bip01 R Toe11", true),
+                [Bone.Toe1NubR] = CMT.SearchObjName(maid.body0.m_Bones.transform, "Bip01 R Toe1Nub", true),
+                [Bone.Toe2R] = CMT.SearchObjName(maid.body0.m_Bones.transform, "Bip01 R Toe2", true),
+                [Bone.Toe21R] = CMT.SearchObjName(maid.body0.m_Bones.transform, "Bip01 R Toe21", true),
+                [Bone.Toe2NubR] = CMT.SearchObjName(maid.body0.m_Bones.transform, "Bip01 R Toe2Nub", true)
+            };
+        }
+
+        private void OnMeidoSelect(MeidoChangeEventArgs args)
+        {
+            EventHandler<MeidoChangeEventArgs> handler = SelectMaid;
+            if (handler != null) handler(this, args);
+        }
+    }
+}

+ 63 - 0
COM3D2.MeidoPhotoStudio.Plugin/MeidoPhotoStudio/Managers/EnvironmentManager.cs

@@ -0,0 +1,63 @@
+using System;
+using System.Collections.Generic;
+using UnityEngine;
+
+namespace COM3D2.MeidoPhotoStudio.Plugin
+{
+    public class EnvironmentManager
+    {
+        private GameObject cameraObject;
+        private Camera subCamera;
+        private GameObject bgObject;
+        private Transform bg;
+        public void ChangeBackground(string assetName)
+        {
+            GameMain.Instance.BgMgr.ChangeBg(assetName);
+            if (assetName == "KaraokeRoom")
+            {
+                bg.transform.position = bgObject.transform.position;
+                bg.transform.localPosition = new Vector3(1f, 0f, 4f);
+                bg.transform.localRotation = Quaternion.Euler(new Vector3(0f, 90f, 0f));
+            }
+        }
+
+        public void Initialize()
+        {
+            if (!bgObject)
+            {
+                bgObject = GameObject.Find("__GameMain__/BG");
+                bg = bgObject.transform;
+            }
+
+            GameObject.Destroy(cameraObject);
+            GameObject.Destroy(subCamera);
+
+            if (cameraObject == null)
+            {
+                cameraObject = new GameObject("subCamera");
+                subCamera = cameraObject.AddComponent<Camera>();
+                subCamera.CopyFrom(Camera.main);
+                cameraObject.SetActive(true);
+                subCamera.clearFlags = CameraClearFlags.Depth;
+                subCamera.cullingMask = 256;
+                subCamera.depth = 1f;
+                subCamera.transform.parent = GameMain.Instance.MainCamera.transform;
+            }
+
+            bgObject.SetActive(true);
+            GameMain.Instance.BgMgr.ChangeBg("Theater");
+
+            GameMain.Instance.MainCamera.GetComponent<Camera>().backgroundColor = new Color(0.0f, 0.0f, 0.0f);
+            UltimateOrbitCamera UOCamera = Utility.GetFieldValue<CameraMain, UltimateOrbitCamera>(GameMain.Instance.MainCamera, "m_UOCamera");
+            UOCamera.enabled = true;
+
+            GameMain.Instance.MainLight.Reset();
+            GameMain.Instance.CharacterMgr.ResetCharaPosAll();
+
+            CameraMain cameraMain = GameMain.Instance.MainCamera;
+            cameraMain.Reset(CameraMain.CameraType.Target, true);
+            cameraMain.SetTargetPos(new Vector3(0f, 0.9f, 0f), true);
+            cameraMain.SetDistance(3f, true);
+        }
+    }
+}

+ 183 - 0
COM3D2.MeidoPhotoStudio.Plugin/MeidoPhotoStudio/Managers/MeidoManager.cs

@@ -0,0 +1,183 @@
+using System;
+using System.Collections.Generic;
+using UnityEngine;
+
+namespace COM3D2.MeidoPhotoStudio.Plugin
+{
+    public class MeidoManager
+    {
+        private static CharacterMgr characterMgr = GameMain.Instance.CharacterMgr;
+        private int undress = 0;
+        public Meido[] meidos { get; private set; }
+        public List<Meido> ActiveMeidoList { get; private set; }
+        public Meido ActiveMeido { get; private set; }
+        public bool HasActiveMeido { get => ActiveMeido != null; }
+        public bool IsFade { get; set; } = false;
+        public int numberOfMeidos;
+        public event EventHandler<MeidoChangeEventArgs> SelectMeido;
+        public event EventHandler CalledMeidos;
+        private int selectedMeido = 0;
+        public int SelectedMeido
+        {
+            get => selectedMeido;
+            set
+            {
+                int max = Math.Max(ActiveMeidoList.Count, 0);
+                selectedMeido = Mathf.Clamp(value, 0, max);
+                ActiveMeido = ActiveMeidoList.Count > 0 ? ActiveMeidoList[selectedMeido] : null;
+            }
+        }
+
+        public bool IsBusy
+        {
+            get
+            {
+                foreach (Meido meido in ActiveMeidoList)
+                {
+                    if (meido.Maid.IsBusy)
+                    {
+                        Debug.Log(meido.NameEN + " is busy!");
+                        return true;
+                    }
+                }
+                return false;
+            }
+        }
+
+        public MeidoManager()
+        {
+            numberOfMeidos = characterMgr.GetStockMaidCount();
+            ActiveMeidoList = new List<Meido>();
+            meidos = new Meido[numberOfMeidos];
+
+            MaidSwitcherPane.MaidChange += ChangeMeido;
+            MaidSwitcherPane.meidoManager = this;
+
+            ActiveMeido = null;
+
+            for (int stockMaidIndex = 0; stockMaidIndex < numberOfMeidos; stockMaidIndex++)
+            {
+                meidos[stockMaidIndex] = new Meido(stockMaidIndex);
+                meidos[stockMaidIndex].SelectMeido += ChangeMeido;
+                meidos[stockMaidIndex].BodyLoad += EndCallMeidos;
+            }
+        }
+
+        ~MeidoManager()
+        {
+            MaidSwitcherPane.MaidChange -= ChangeMeido;
+        }
+
+        public void Update()
+        {
+            if (Input.GetKeyDown(KeyCode.H)) UndressAll();
+
+            foreach (Meido activeMeido in ActiveMeidoList)
+            {
+                activeMeido.Update();
+            }
+        }
+
+        private void UndressAll()
+        {
+            undress = Utility.Wrap(undress + 1, 0, 3);
+            TBody.MaskMode maskMode = TBody.MaskMode.None;
+            switch (undress)
+            {
+                case 0: maskMode = TBody.MaskMode.None; break;
+                case 1: maskMode = TBody.MaskMode.Underwear; break;
+                case 2: maskMode = TBody.MaskMode.Nude; break;
+            }
+
+            foreach (Meido activeMeido in ActiveMeidoList)
+            {
+                activeMeido.Maid.body0.SetMaskMode(maskMode);
+            }
+        }
+
+        public void UnloadMeidos()
+        {
+            foreach (Meido meido in ActiveMeidoList)
+            {
+                meido.Unload();
+            }
+            ActiveMeidoList.Clear();
+        }
+
+        public void DeactivateMeidos()
+        {
+            foreach (Meido meido in meidos)
+            {
+                meido.Deactivate();
+            }
+            ActiveMeidoList.Clear();
+        }
+
+        public void CallMeidos(List<int> selectedMaids)
+        {
+            IsFade = true;
+
+            UnloadMeidos();
+
+            foreach (int slot in selectedMaids)
+            {
+                Meido meido = meidos[slot];
+                ActiveMeidoList.Add(meido);
+            }
+
+            for (int i = 0; i < ActiveMeidoList.Count; i++)
+            {
+                Meido meido = ActiveMeidoList[i];
+                meido.Load(i);
+            }
+
+            if (selectedMaids.Count == 0)
+            {
+                EndCallMeidos(this, EventArgs.Empty);
+                return;
+            }
+
+            SelectedMeido = 0;
+        }
+
+        public void SetMeidoPose(string pose, int meidoIndex = -1)
+        {
+            Meido meido = meidoIndex == -1 ? ActiveMeido : ActiveMeidoList[meidoIndex];
+            meido.SetPose(pose);
+        }
+
+        private void ChangeMeido(object sender, MeidoChangeEventArgs args)
+        {
+            SelectedMeido = args.selected;
+            // if (args.fromMeido)
+            // {
+            EventHandler<MeidoChangeEventArgs> handler = SelectMeido;
+            if (handler != null) handler(this, args);
+            // }
+        }
+
+        private void EndCallMeidos(object sender, EventArgs args)
+        {
+            if (!IsBusy)
+            {
+                IsFade = false;
+                GameMain.Instance.MainCamera.FadeIn(1f);
+                EventHandler handler = CalledMeidos;
+                if (handler != null)
+                    handler(this, EventArgs.Empty);
+            }
+        }
+    }
+    public class MeidoChangeEventArgs : EventArgs
+    {
+        public int selected;
+        public bool isBody;
+        public bool fromMeido = false;
+        public MeidoChangeEventArgs(int selected, bool fromMaid = false, bool isBody = true)
+        {
+            this.selected = selected;
+            this.isBody = isBody;
+            this.fromMeido = fromMaid;
+        }
+    }
+}

+ 142 - 0
COM3D2.MeidoPhotoStudio.Plugin/MeidoPhotoStudio/Managers/WindowManager.cs

@@ -0,0 +1,142 @@
+using System;
+using System.Collections.Generic;
+using UnityEngine;
+
+namespace COM3D2.MeidoPhotoStudio.Plugin
+{
+    using Window = Constants.Window;
+    public class WindowManager
+    {
+        private Dictionary<Window, BaseWindow> Windows;
+        private static Window currentWindow = Window.Call;
+        private static Window CurrentWindow
+        {
+            get => currentWindow;
+            set
+            {
+                if (value > Window.BG2) currentWindow = Window.BG2;
+                else if (value < Window.Call) currentWindow = Window.Call;
+                else currentWindow = value;
+            }
+        }
+        private Rect mainWindowRect;
+        private Rect messageWindowRect;
+        private MeidoManager meidoManager;
+        private bool initializeWindows = false;
+        public bool Visible { get; set; }
+        public WindowManager(MeidoManager meidoManager, EnvironmentManager environmentManager)
+        {
+            TabsPane.TabChange += ChangeTab;
+            this.meidoManager = meidoManager;
+            this.meidoManager.SelectMeido += MeidoSelect;
+            this.meidoManager.CalledMeidos += (s, a) => Visible = true;
+
+            mainWindowRect.y = Screen.height * 0.08f;
+            mainWindowRect.x = Screen.width;
+            Windows = new Dictionary<Window, BaseWindow>()
+            {
+                [Window.Call] = new MaidCallWindow(meidoManager),
+                [Window.Pose] = new MaidPoseWindow(meidoManager),
+                [Window.Face] = new MaidFaceWindow(meidoManager),
+                [Window.BG] = new BackgroundWindow(environmentManager),
+                [Window.BG2] = new Background2Window(environmentManager),
+                [Window.Message] = new MessageWindow()
+            };
+            Windows[Window.Message].Visible = false;
+        }
+
+        ~WindowManager()
+        {
+            TabsPane.TabChange -= ChangeTab;
+        }
+
+        private void MeidoSelect(object sender, MeidoChangeEventArgs args)
+        {
+            if (args.fromMeido)
+                TabsPane.SelectedTab = args.isBody ? Window.Pose : Window.Face;
+        }
+
+        private void ChangeTab(object sender, EventArgs args)
+        {
+            CurrentWindow = TabsPane.SelectedTab;
+        }
+
+        public void Update()
+        {
+            if (Input.GetKeyDown(KeyCode.M))
+            {
+                (Windows[Window.Message] as MessageWindow).SetVisibility();
+            }
+
+            if (Input.GetKeyDown(KeyCode.Tab))
+            {
+                Visible = !Visible;
+            }
+
+            if (this.meidoManager.IsFade) Visible = false;
+
+            HandleZoom();
+        }
+
+        private void HandleZoom()
+        {
+            bool mainWindowVisible = Windows[currentWindow].Visible;
+            bool dropdownVisible = DropdownHelper.Visible;
+            bool messageWindowVisible = Windows[currentWindow].Visible;
+            if (mainWindowVisible || dropdownVisible || messageWindowVisible)
+            {
+                if (Input.mouseScrollDelta.y != 0f)
+                {
+                    Vector2 mousePos = Event.current.mousePosition;
+                    if (mainWindowVisible && mainWindowRect.Contains(mousePos)
+                        || dropdownVisible && DropdownHelper.dropdownRect.Contains(mousePos)
+                        || messageWindowVisible && messageWindowRect.Contains(mousePos)
+                    )
+                    {
+                        GameMain.Instance.MainCamera.SetControl(false);
+                        Input.ResetInputAxes();
+                    }
+
+                }
+            }
+        }
+
+        public void OnGUI()
+        {
+            if (!Visible) return;
+
+            GUIStyle windowStyle = new GUIStyle(GUI.skin.box);
+            GameMain.Instance.MainCamera.SetControl(true);
+
+            if (Windows[currentWindow].Visible)
+            {
+                mainWindowRect.width = 220;
+                mainWindowRect.height = Screen.height * 0.8f;
+
+                mainWindowRect.x = Mathf.Clamp(mainWindowRect.x, 0, Screen.width - mainWindowRect.width);
+                mainWindowRect.y = Mathf.Clamp(mainWindowRect.y, -mainWindowRect.height + 30, Screen.height - 50);
+
+                mainWindowRect = GUI.Window(Constants.mainWindowID, mainWindowRect, Windows[CurrentWindow].OnGUI, "", windowStyle);
+            }
+            if (Windows[Window.Message].Visible)
+            {
+                messageWindowRect.width = Mathf.Clamp(Screen.width * 0.4f, 440, Mathf.Infinity);
+                messageWindowRect.height = Mathf.Clamp(Screen.height * 0.15f, 150, Mathf.Infinity);
+
+                messageWindowRect.x = Mathf.Clamp(messageWindowRect.x, -messageWindowRect.width + Utility.GetPix(20), Screen.width - Utility.GetPix(20));
+                messageWindowRect.y = Mathf.Clamp(messageWindowRect.y, -messageWindowRect.height + Utility.GetPix(20), Screen.height - Utility.GetPix(20));
+
+                if (!initializeWindows)
+                {
+                    messageWindowRect.x = Screen.width / 2 - messageWindowRect.width / 2;
+                    messageWindowRect.y = Screen.height - messageWindowRect.height;
+                    initializeWindows = true;
+                }
+
+                messageWindowRect = GUI.Window(Constants.messageWindowID, messageWindowRect, Windows[Window.Message].OnGUI, "", windowStyle);
+            }
+
+            DropdownHelper.HandleDropdown();
+        }
+    }
+}

+ 162 - 0
COM3D2.MeidoPhotoStudio.Plugin/MeidoPhotoStudio/Meido/IK/BaseDrag.cs

@@ -0,0 +1,162 @@
+using System;
+using UnityEngine;
+
+namespace COM3D2.MeidoPhotoStudio.Plugin
+{
+    public abstract class BaseDrag : MonoBehaviour
+    {
+        private const float doubleClickSensitivity = 0.3f;
+        protected const int upperArm = 0;
+        protected const int foreArm = 1;
+        protected const int hand = 2;
+        protected const int upperArmRot = 0;
+        protected const int handRot = 1;
+        protected Maid maid;
+        protected Func<Vector3> position;
+        protected Func<Vector3> rotation;
+        protected Renderer dragPointRenderer;
+        protected Collider dragPointCollider;
+        protected Vector3 worldPoint;
+        protected Vector3 mousePos;
+        protected DragType dragType = DragType.None;
+        protected DragType dragTypeOld;
+        protected float doubleClickStart = 0f;
+        protected bool reInitDrag = false;
+        protected bool isPlaying;
+        protected GizmoRender gizmo;
+        public bool gizmoVisible;
+        public bool Visible
+        {
+            get => dragPointRenderer.enabled;
+            set => dragPointRenderer.enabled = value;
+        }
+        public float DragPointScale
+        {
+            set => transform.localScale = new Vector3(value, value, value);
+        }
+        public float GizmoScale
+        {
+            set
+            {
+                if (gizmo != null)
+                {
+                    gizmo.offsetScale = value;
+                }
+            }
+        }
+        private static bool IsGizmoDrag
+        {
+            get => Utility.GetFieldValue<GizmoRender, bool>(null, "is_drag_");
+        }
+
+        protected enum DragType
+        {
+            None, Select,
+            MoveXZ, MoveY, RotLocalXZ, RotY, RotLocalY,
+            Scale
+        }
+
+        public virtual void Initialize(Maid maid, Func<Vector3> position, Func<Vector3> rotation)
+        {
+            this.maid = maid;
+            this.position = position;
+            this.rotation = rotation;
+            dragPointRenderer = GetComponent<Renderer>();
+            dragPointCollider = GetComponent<Collider>();
+            dragPointRenderer.enabled = true;
+
+            isPlaying = maid.GetAnimation().isPlaying;
+        }
+
+        protected void InitializeGizmo(GameObject target, float scale = 0.25f)
+        {
+            gizmo = target.gameObject.AddComponent<GizmoRender>();
+            gizmo.eRotate = true;
+            gizmo.offsetScale = scale;
+            gizmo.lineRSelectedThick = 0.25f;
+            gizmo.Visible = false;
+        }
+
+        protected void InitializeGizmo(Transform target, float scale = 0.25f)
+        {
+            InitializeGizmo(target.gameObject, scale);
+        }
+
+        protected virtual void InitializeDrag()
+        {
+            worldPoint = Camera.main.WorldToScreenPoint(transform.position);
+            mousePos = Input.mousePosition;
+
+            isPlaying = maid.GetAnimation().isPlaying;
+        }
+
+        protected virtual void DoubleClick() { }
+        protected abstract void Drag();
+        protected abstract void GetDragType();
+        private void OnMouseUp()
+        {
+            if ((Time.time - doubleClickStart) < doubleClickSensitivity)
+            {
+                doubleClickStart = -1;
+                DoubleClick();
+            }
+            else
+            {
+                doubleClickStart = Time.time;
+            }
+        }
+
+        private void Update()
+        {
+            GetDragType();
+
+            reInitDrag = dragType != dragTypeOld;
+
+            dragTypeOld = dragType;
+
+            transform.position = position();
+            transform.eulerAngles = rotation();
+
+            if (gizmo != null)
+            {
+                gizmo.Visible = gizmoVisible;
+
+                if (gizmoVisible)
+                {
+                    if (isPlaying && IsGizmoDrag)
+                    {
+                        maid.GetAnimation().Stop();
+                        isPlaying = false;
+                    }
+                }
+            }
+        }
+
+        private void OnMouseDown()
+        {
+            InitializeDrag();
+        }
+
+        private void OnMouseDrag()
+        {
+            if (dragType == DragType.Select) return;
+
+            if (reInitDrag)
+            {
+                reInitDrag = false;
+                InitializeDrag();
+            }
+
+            if (mousePos != Input.mousePosition) Drag();
+        }
+
+        private void OnEnable()
+        {
+            if (position != null)
+            {
+                transform.position = position();
+                transform.eulerAngles = rotation();
+            }
+        }
+    }
+}

+ 131 - 0
COM3D2.MeidoPhotoStudio.Plugin/MeidoPhotoStudio/Meido/IK/DragBody.cs

@@ -0,0 +1,131 @@
+using System;
+using UnityEngine;
+
+namespace COM3D2.MeidoPhotoStudio.Plugin
+{
+    public class DragBody : BaseDrag
+    {
+        private Vector3 off;
+        private Vector3 off2;
+        private Vector3 mousePos2;
+        private float maidScale;
+        private Vector3 maidRot;
+        public bool renderBody = true;
+        public event EventHandler Select;
+
+        protected override void GetDragType()
+        {
+            bool holdShift = Utility.GetModKey(Utility.ModKey.Shift);
+            if (Input.GetKey(KeyCode.A))
+            {
+                dragType = DragType.Select;
+            }
+            else if (Input.GetKey(KeyCode.Z))
+            {
+                if (Utility.GetModKey(Utility.ModKey.Control)) dragType = DragType.MoveY;
+                else dragType = holdShift ? DragType.RotY : DragType.MoveXZ;
+            }
+            else if (Input.GetKey(KeyCode.X))
+            {
+                dragType = holdShift ? DragType.RotLocalY : DragType.RotLocalXZ;
+            }
+            else if (Input.GetKey(KeyCode.C))
+            {
+                dragType = DragType.Scale;
+            }
+            else
+            {
+                dragType = DragType.None;
+            }
+        }
+        protected override void InitializeDrag()
+        {
+            if (dragType == DragType.Select)
+            {
+                EventHandler handler = Select;
+                if (handler != null) handler(this, EventArgs.Empty);
+                return;
+            }
+
+            base.InitializeDrag();
+
+            maidScale = maid.transform.localScale.x;
+            maidRot = maid.transform.localEulerAngles;
+            off = transform.position - Camera.main.ScreenToWorldPoint(new Vector3(Input.mousePosition.x, Input.mousePosition.y, worldPoint.z));
+            off2 = new Vector3(
+                transform.position.x - maid.transform.position.x,
+                transform.position.y - maid.transform.position.y,
+                transform.position.z - maid.transform.position.z);
+        }
+
+        protected override void DoubleClick()
+        {
+            if (dragType == DragType.Scale) maid.transform.localScale = new Vector3(1f, 1f, 1f);
+            if (dragType == DragType.RotLocalY || dragType == DragType.RotLocalXZ)
+                maid.transform.eulerAngles = new Vector3(0f, maid.transform.eulerAngles.y, 0f);
+        }
+
+        protected override void Drag()
+        {
+            Vector3 pos = Camera.main.ScreenToWorldPoint(new Vector3(Input.mousePosition.x, Input.mousePosition.y, worldPoint.z)) + off - off2;
+
+            if (dragType == DragType.MoveXZ)
+            {
+                maid.transform.position = new Vector3(pos.x, maid.transform.position.y, pos.z);
+            }
+
+            if (dragType == DragType.MoveY)
+            {
+                maid.transform.position = new Vector3(maid.transform.position.x, pos.y, maid.transform.position.z);
+            }
+
+            if (dragType == DragType.RotY)
+            {
+                Vector3 posOther = Input.mousePosition - mousePos;
+                maid.transform.eulerAngles =
+                    new Vector3(maid.transform.eulerAngles.x, maidRot.y - posOther.x / 3f, maid.transform.eulerAngles.z);
+
+            }
+
+            if (dragType == DragType.RotLocalXZ)
+            {
+                Vector3 posOther = Input.mousePosition - mousePos;
+                Transform transform = Camera.main.transform;
+                Vector3 vector3_3 = transform.TransformDirection(Vector3.right);
+                Vector3 vector3_4 = transform.TransformDirection(Vector3.forward);
+                transform.TransformDirection(Vector3.forward);
+                if (mousePos2 != Input.mousePosition)
+                {
+                    maid.transform.localEulerAngles = maidRot;
+                    maid.transform.RotateAround(maid.transform.position, new Vector3(vector3_3.x, 0.0f, vector3_3.z), posOther.y / 4f);
+                    maid.transform.RotateAround(maid.transform.position, new Vector3(vector3_4.x, 0.0f, vector3_4.z), (-posOther.x / 6.0f));
+                }
+                mousePos2 = Input.mousePosition;
+            }
+
+            if (dragType == DragType.RotLocalY)
+            {
+                Vector3 posOther = Input.mousePosition - mousePos;
+                Transform transform = Camera.main.transform;
+                Vector3 vector3_3 = transform.TransformDirection(Vector3.right);
+                transform.TransformDirection(Vector3.forward);
+                if (mousePos2 != Input.mousePosition)
+                {
+                    maid.transform.localEulerAngles = maidRot;
+                    maid.body0.transform.localRotation = Quaternion.Euler(maid.transform.localEulerAngles)
+                        * Quaternion.AngleAxis((-posOther.x / 2.2f), Vector3.up);
+                }
+
+                mousePos2 = Input.mousePosition;
+            }
+
+            if (dragType == DragType.Scale)
+            {
+                Vector3 posOther = Input.mousePosition - mousePos;
+                float scale = maidScale + posOther.y / 200f;
+                if (scale < 0.1f) scale = 0.1f;
+                maid.transform.localScale = new Vector3(scale, scale, scale);
+            }
+        }
+    }
+}

+ 147 - 0
COM3D2.MeidoPhotoStudio.Plugin/MeidoPhotoStudio/Meido/IK/DragHead.cs

@@ -0,0 +1,147 @@
+using System;
+using UnityEngine;
+
+namespace COM3D2.MeidoPhotoStudio.Plugin
+{
+    public class DragHead : BaseDrag
+    {
+        private Transform head;
+        private Vector3 rotate;
+        private Vector3 eyeRotL;
+        private Vector3 eyeRotR;
+        private Vector3 defEyeRotL;
+        private Vector3 defEyeRotR;
+        private Vector3 mousePosOther;
+        private bool shodaiFlg;
+        public event EventHandler Select;
+
+        public void Initialize(Transform head, Maid maid, Func<Vector3> posFunc, Func<Vector3> rotFunc)
+        {
+            base.Initialize(maid, posFunc, rotFunc);
+            this.head = head;
+
+            // default eye rotations
+            defEyeRotL = this.maid.body0.quaDefEyeL.eulerAngles;
+            defEyeRotR = this.maid.body0.quaDefEyeR.eulerAngles;
+
+            // Check for "Shodai" faces
+            try
+            {
+                shodaiFlg = false;
+                TMorph morph = maid.body0.Face.morph;
+                float throwAway = Utility.GetFieldValue<TMorph, float[]>(morph, "BlendValues")
+                    [(int)morph.hash["tangopen"]];
+            }
+            catch
+            {
+                shodaiFlg = true;
+            }
+
+            InitializeGizmo(this.head);
+        }
+
+        protected override void GetDragType()
+        {
+            bool shift = Utility.GetModKey(Utility.ModKey.Shift);
+            if (Utility.GetModKey(Utility.ModKey.Alt) && Utility.GetModKey(Utility.ModKey.Control))
+            {
+                // eyes
+                dragType = DragType.MoveXZ;
+            }
+            else if (Input.GetKey(KeyCode.LeftAlt))
+            {
+                // head
+                dragType = shift ? DragType.RotLocalY : DragType.RotLocalXZ;
+            }
+            else if (Input.GetKey(KeyCode.A))
+            {
+                dragType = DragType.Select;
+            }
+            else
+            {
+                dragType = DragType.None;
+            }
+        }
+        protected override void DoubleClick()
+        {
+            if (dragType == DragType.MoveXZ)
+            {
+                maid.body0.quaDefEyeL.eulerAngles = defEyeRotL;
+                maid.body0.quaDefEyeR.eulerAngles = defEyeRotR;
+            }
+        }
+
+        protected override void InitializeDrag()
+        {
+            if (dragType == DragType.Select)
+            {
+                EventHandler handler = Select;
+                if (handler != null) handler(this, EventArgs.Empty);
+                return;
+            }
+
+            base.InitializeDrag();
+
+            rotate = head.localEulerAngles;
+
+            eyeRotL = maid.body0.quaDefEyeL.eulerAngles;
+            eyeRotR = maid.body0.quaDefEyeR.eulerAngles;
+            mousePosOther = Input.mousePosition - mousePos;
+        }
+
+        protected override void Drag()
+        {
+            if (dragType == DragType.None || dragType == DragType.Select) return;
+
+            if (dragType != DragType.MoveXZ)
+            {
+                if (isPlaying)
+                {
+                    maid.GetAnimation().Stop();
+                }
+            }
+
+            Vector3 pos = new Vector3(Input.mousePosition.x, Input.mousePosition.y, worldPoint.z);
+            Vector3 vec31 = Input.mousePosition - mousePos;
+            Transform t = GameMain.Instance.MainCamera.gameObject.transform;
+            Vector3 vec32 = t.TransformDirection(Vector3.right);
+            Vector3 vec33 = t.TransformDirection(Vector3.forward);
+
+            if (dragType == DragType.RotLocalXZ)
+            {
+                head.localEulerAngles = rotate;
+                head.RotateAround(head.position, new Vector3(vec32.x, 0.0f, vec32.z), vec31.y / 3f);
+                head.RotateAround(head.position, new Vector3(vec33.x, 0.0f, vec33.z), (-vec31.x / 4.5f));
+            }
+
+            if (dragType == DragType.RotLocalY)
+            {
+                head.localEulerAngles = rotate;
+                head.localRotation = Quaternion.Euler(head.localEulerAngles) * Quaternion.AngleAxis(vec31.x / 3f, Vector3.right);
+            }
+
+            if (dragType == DragType.MoveXZ)
+            {
+                Vector3 vec34 = new Vector3(eyeRotR.x, eyeRotR.y + vec31.x / 10f, eyeRotR.z + vec31.y / 10f);
+
+                if (shodaiFlg)
+                {
+                    if (vec34.z < 345.7f && vec34.z > 335.6f)
+                        mousePosOther.y = vec31.y;
+                    if (vec34.y < 347.6f && vec34.y > 335.6f)
+                        mousePosOther.x = vec31.x;
+                }
+                else
+                {
+                    if (vec34.z < 354.8f && vec34.z > 344.8f)
+                        mousePosOther.y = vec31.y;
+                    if (vec34.y < 354.0f && vec34.y > 342.0f)
+                        mousePosOther.x = vec31.x;
+                }
+
+                maid.body0.quaDefEyeL.eulerAngles = new Vector3(eyeRotL.x, eyeRotL.y - mousePosOther.x / 10f, eyeRotL.z - mousePosOther.y / 10f);
+                maid.body0.quaDefEyeR.eulerAngles = new Vector3(eyeRotR.x, eyeRotR.y + mousePosOther.x / 10f, eyeRotR.z + mousePosOther.y / 10f);
+            }
+        }
+    }
+}

+ 108 - 0
COM3D2.MeidoPhotoStudio.Plugin/MeidoPhotoStudio/Meido/IK/DragJointFinger.cs

@@ -0,0 +1,108 @@
+using System;
+using UnityEngine;
+
+namespace COM3D2.MeidoPhotoStudio.Plugin
+{
+    public class DragJointFinger : BaseDrag
+    {
+        private readonly TBody.IKCMO IK = new TBody.IKCMO();
+        private readonly GameObject[] otherIK = new GameObject[3];
+        private Transform[] ikChain;
+        private Vector3[] jointRotation = new Vector3[2];
+        private Vector3 off;
+        private Vector3 off2;
+        private bool baseFinger;
+
+        public void Initialize(Transform[] ikChain, bool baseFinger, Maid maid, Func<Vector3> position, Func<Vector3> rotation)
+        {
+            base.Initialize(maid, position, rotation);
+            this.ikChain = ikChain;
+            this.baseFinger = baseFinger;
+
+            InitializeIK();
+            InitializeIK2();
+        }
+        public void InitializeIK()
+        {
+            IK.Init(ikChain[upperArm], ikChain[foreArm], ikChain[hand], maid.body0);
+        }
+
+        private void InitializeIK2()
+        {
+            for (int i = 0; i < otherIK.Length; i++)
+            {
+                otherIK[i] = new GameObject();
+                otherIK[i].transform.position = this.ikChain[i].position;
+                otherIK[i].transform.localRotation = this.ikChain[i].localRotation;
+            }
+        }
+
+        protected override void GetDragType()
+        {
+            if (Utility.GetModKey(Utility.ModKey.Shift))
+            {
+                dragType = DragType.RotLocalY;
+            }
+            else
+            {
+                dragType = DragType.None;
+            }
+        }
+
+        protected override void InitializeDrag()
+        {
+            base.InitializeDrag();
+
+            IKCtrlData ikData = maid.body0.IKCtrl.GetIKData("左手");
+            off = transform.position - Camera.main.ScreenToWorldPoint(new Vector3(Input.mousePosition.x, Input.mousePosition.y, worldPoint.z));
+            off2 = new Vector3(
+                transform.position.x - ikChain[hand].position.x,
+                transform.position.y - ikChain[hand].position.y,
+                transform.position.z - ikChain[hand].position.z);
+
+            jointRotation[upperArmRot] = ikChain[upperArm].localEulerAngles;
+            jointRotation[handRot] = ikChain[hand].localEulerAngles;
+            InitializeIK();
+        }
+
+        protected override void Drag()
+        {
+            if (isPlaying)
+            {
+                maid.GetAnimation().Stop();
+            }
+
+            IKCtrlData ikData = maid.body0.IKCtrl.GetIKData("左手");
+            Vector3 pos = Camera.main.ScreenToWorldPoint(new Vector3(Input.mousePosition.x, Input.mousePosition.y, worldPoint.z)) + off - off2;
+
+            if (dragType == DragType.None)
+            {
+                IK.Porc(ikChain[upperArm], ikChain[foreArm], ikChain[hand], pos, Vector3.zero, ikData);
+
+                if (baseFinger)
+                {
+                    jointRotation[handRot] = ikChain[hand].localEulerAngles;
+                    jointRotation[upperArmRot] = ikChain[upperArm].localEulerAngles;
+                    ikChain[upperArm].localEulerAngles = jointRotation[upperArm];
+                    ikChain[hand].localEulerAngles = jointRotation[handRot];
+                }
+                else
+                {
+                    ikChain[hand].localEulerAngles = new Vector3(jointRotation[handRot].x, jointRotation[handRot].y, ikChain[hand].localEulerAngles.z);
+                    ikChain[upperArm].localEulerAngles = new Vector3(jointRotation[upperArmRot].x, jointRotation[upperArmRot].y, ikChain[upperArm].localEulerAngles.z);
+                }
+            }
+            else
+            {
+                if (dragType == DragType.RotLocalY)
+                {
+                    Vector3 vec31 = Input.mousePosition - mousePos;
+                    ikChain[upperArm].localEulerAngles = jointRotation[upperArmRot];
+                    ikChain[upperArm].localRotation = Quaternion.Euler(ikChain[upperArm].localEulerAngles)
+                        * Quaternion.AngleAxis((-vec31.x / 1.5f), Vector3.right);
+                }
+            }
+
+        }
+    }
+}

+ 98 - 0
COM3D2.MeidoPhotoStudio.Plugin/MeidoPhotoStudio/Meido/IK/DragJointForearm.cs

@@ -0,0 +1,98 @@
+using System;
+using UnityEngine;
+
+namespace COM3D2.MeidoPhotoStudio.Plugin
+{
+    public class DragJointForearm : BaseDrag
+    {
+        private readonly TBody.IKCMO IK = new TBody.IKCMO();
+        private readonly GameObject[] otherIK = new GameObject[3];
+        private Transform[] ikChain;
+        private Vector3[] jointRotation = new Vector3[2];
+        private Vector3 off;
+        private Vector3 off2;
+        private bool knee = false;
+
+        public void Initialize(Transform[] ikChain, bool knee, Maid maid, Func<Vector3> position, Func<Vector3> rotation)
+        {
+            base.Initialize(maid, position, rotation);
+            this.ikChain = ikChain;
+            this.knee = knee;
+
+            for (int i = 0; i < otherIK.Length; i++)
+            {
+                otherIK[i] = new GameObject();
+                otherIK[i].transform.position = this.ikChain[i].position;
+                otherIK[i].transform.localRotation = this.ikChain[i].localRotation;
+            }
+
+            InitializeIK();
+
+            InitializeGizmo(this.ikChain[hand]);
+        }
+
+        public void InitializeIK()
+        {
+            IK.Init(ikChain[upperArm], ikChain[foreArm], ikChain[hand], maid.body0);
+        }
+
+        protected override void GetDragType()
+        {
+            if (knee && Utility.GetModKey(Utility.ModKey.Shift) && Utility.GetModKey(Utility.ModKey.Alt))
+            {
+                dragType = DragType.RotLocalY;
+            }
+            else
+            {
+                dragType = DragType.None;
+            }
+        }
+
+        protected override void InitializeDrag()
+        {
+            base.InitializeDrag();
+
+            off = transform.position - Camera.main.ScreenToWorldPoint(new Vector3(Input.mousePosition.x, Input.mousePosition.y, worldPoint.z));
+            off2 = new Vector3(
+                transform.position.x - ikChain[hand].position.x,
+                transform.position.y - ikChain[hand].position.y,
+                transform.position.z - ikChain[hand].position.z);
+
+            jointRotation[upperArmRot] = ikChain[upperArm].localEulerAngles;
+            jointRotation[handRot] = ikChain[hand].localEulerAngles;
+            InitializeIK();
+        }
+
+        protected override void Drag()
+        {
+            if (isPlaying)
+            {
+                maid.GetAnimation().Stop();
+            }
+
+            IKCtrlData ikData = maid.body0.IKCtrl.GetIKData("左手");
+            Vector3 pos = Camera.main.ScreenToWorldPoint(new Vector3(Input.mousePosition.x, Input.mousePosition.y, worldPoint.z)) + off - off2;
+
+            if (dragType == DragType.None)
+            {
+                IK.Porc(ikChain[upperArm], ikChain[foreArm], ikChain[hand], pos, Vector3.zero, ikData);
+
+                jointRotation[handRot] = ikChain[hand].localEulerAngles;
+                jointRotation[upperArmRot] = ikChain[upperArm].localEulerAngles;
+                ikChain[upperArm].localEulerAngles = jointRotation[upperArm];
+                ikChain[hand].localEulerAngles = jointRotation[handRot];
+            }
+            else
+            {
+                Vector3 vec31 = Input.mousePosition - mousePos;
+
+                if (dragType == DragType.RotLocalY)
+                {
+                    ikChain[upperArm].localEulerAngles = jointRotation[upperArmRot];
+                    ikChain[upperArm].localRotation = Quaternion.Euler(ikChain[upperArm].localEulerAngles)
+                        * Quaternion.AngleAxis((-vec31.x / 1.5f), Vector3.right);
+                }
+            }
+        }
+    }
+}

+ 128 - 0
COM3D2.MeidoPhotoStudio.Plugin/MeidoPhotoStudio/Meido/IK/DragJointHand.cs

@@ -0,0 +1,128 @@
+using System;
+using UnityEngine;
+
+namespace COM3D2.MeidoPhotoStudio.Plugin
+{
+    public class DragJointHand : BaseDrag
+    {
+        private readonly TBody.IKCMO IK = new TBody.IKCMO();
+        private readonly GameObject[] otherIK = new GameObject[3];
+        private Transform[] ikChain;
+        private Transform[] ikChainLock;
+        private Vector3[] jointRotation = new Vector3[2];
+        private Vector3 off;
+        private Vector3 off2;
+        private int foot = 1;
+
+        public void Initialize(Transform[] ikChain, bool foot, Maid maid, Func<Vector3> position, Func<Vector3> rotation)
+        {
+            base.Initialize(maid, position, rotation);
+            this.ikChain = ikChain;
+            this.foot = foot ? -1 : 1;
+            this.ikChainLock = new Transform[3] {
+                ikChain[foreArm],
+                ikChain[foreArm],
+                ikChain[hand]
+            };
+
+            InitializeIK();
+            InitializeIK2();
+            InitializeGizmo(this.ikChain[hand]);
+        }
+        public void InitializeIK()
+        {
+            IK.Init(ikChain[upperArm], ikChain[foreArm], ikChain[hand], maid.body0);
+        }
+
+        private void InitializeIK2()
+        {
+            for (int i = 0; i < otherIK.Length; i++)
+            {
+                otherIK[i] = new GameObject();
+                otherIK[i].transform.position = this.ikChain[i].position;
+                otherIK[i].transform.localRotation = this.ikChain[i].localRotation;
+            }
+        }
+
+        protected override void GetDragType()
+        {
+            if (Input.GetKey(KeyCode.LeftShift) && Input.GetKey(KeyCode.LeftAlt))
+            {
+                dragType = DragType.RotLocalY;
+            }
+            else if (Input.GetKey(KeyCode.LeftAlt))
+            {
+                dragType = DragType.RotLocalXZ;
+            }
+            else if (Input.GetKey(KeyCode.LeftControl))
+            {
+                dragType = DragType.MoveXZ;
+            }
+            else
+            {
+                dragType = DragType.None;
+            }
+        }
+
+        protected override void InitializeDrag()
+        {
+            base.InitializeDrag();
+
+            Transform[] ikChain = dragType == DragType.MoveXZ ? this.ikChainLock : this.ikChain;
+
+            IKCtrlData ikData = maid.body0.IKCtrl.GetIKData("左手");
+            off = transform.position - Camera.main.ScreenToWorldPoint(new Vector3(Input.mousePosition.x, Input.mousePosition.y, worldPoint.z));
+            off2 = new Vector3(
+                transform.position.x - ikChain[hand].position.x,
+                transform.position.y - ikChain[hand].position.y,
+                transform.position.z - ikChain[hand].position.z);
+
+            jointRotation[upperArmRot] = ikChain[upperArm].localEulerAngles;
+            jointRotation[handRot] = ikChain[hand].localEulerAngles;
+            InitializeIK();
+        }
+
+        protected override void Drag()
+        {
+            if (isPlaying)
+            {
+                maid.GetAnimation().Stop();
+            }
+
+            IKCtrlData ikData = maid.body0.IKCtrl.GetIKData("左手");
+            Vector3 pos = Camera.main.ScreenToWorldPoint(new Vector3(Input.mousePosition.x, Input.mousePosition.y, worldPoint.z)) + off - off2;
+
+            if (dragType == DragType.None || dragType == DragType.MoveXZ)
+            {
+                Transform[] ikChain = dragType == DragType.MoveXZ ? this.ikChainLock : this.ikChain;
+
+                IK.Porc(ikChain[upperArm], ikChain[foreArm], ikChain[hand], pos, Vector3.zero, ikData);
+
+                jointRotation[handRot] = ikChain[hand].localEulerAngles;
+                jointRotation[upperArmRot] = ikChain[upperArm].localEulerAngles;
+                ikChain[upperArm].localEulerAngles = jointRotation[upperArm];
+                ikChain[hand].localEulerAngles = jointRotation[handRot];
+            }
+            else
+            {
+                Vector3 vec31 = Input.mousePosition - mousePos;
+
+                if (dragType == DragType.RotLocalY)
+                {
+                    ikChain[hand].localEulerAngles = jointRotation[handRot];
+                    ikChain[hand].localRotation = Quaternion.Euler(ikChain[hand].localEulerAngles)
+                        * Quaternion.AngleAxis(vec31.x / 1.5f, Vector3.right);
+                }
+
+                if (dragType == DragType.RotLocalXZ)
+                {
+                    ikChain[hand].localEulerAngles = jointRotation[handRot];
+                    ikChain[hand].localRotation = Quaternion.Euler(ikChain[hand].localEulerAngles)
+                        * Quaternion.AngleAxis(foot * vec31.x / 1.5f, Vector3.up);
+                    ikChain[hand].localRotation = Quaternion.Euler(ikChain[hand].localEulerAngles)
+                        * Quaternion.AngleAxis(foot * vec31.y / 1.5f, Vector3.forward);
+                }
+            }
+        }
+    }
+}

+ 100 - 0
COM3D2.MeidoPhotoStudio.Plugin/MeidoPhotoStudio/Meido/IK/DragMune.cs

@@ -0,0 +1,100 @@
+using System;
+using UnityEngine;
+
+namespace COM3D2.MeidoPhotoStudio.Plugin
+{
+    public class DragMune : BaseDrag
+    {
+        private readonly TBody.IKCMO IK = new TBody.IKCMO();
+        private readonly GameObject[] things = new GameObject[3];
+        private Transform[] ikChain;
+        private Vector3[] jointRotation = new Vector3[2];
+        private Vector3 off;
+        private Vector3 off2;
+
+        public void Initialize(Transform[] ikChain, Maid maid, Func<Vector3> position, Func<Vector3> rotation)
+        {
+            base.Initialize(maid, position, rotation);
+            this.ikChain = ikChain;
+
+            for (int i = 0; i < things.Length; i++)
+            {
+                things[i] = new GameObject();
+                things[i].transform.position = this.ikChain[i].position;
+                things[i].transform.localRotation = this.ikChain[i].localRotation;
+            }
+
+            InitializeIK();
+        }
+
+        public void InitializeIK()
+        {
+            IK.Init(ikChain[upperArm], ikChain[foreArm], ikChain[hand], maid.body0);
+        }
+
+        protected override void GetDragType()
+        {
+            if (Input.GetKey(KeyCode.LeftControl) && Input.GetKey(KeyCode.LeftAlt))
+            {
+                dragType = DragType.RotLocalXZ;
+            }
+            else
+            {
+                dragType = DragType.None;
+            }
+        }
+
+        protected override void DoubleClick()
+        {
+            if (dragType == DragType.RotLocalXZ)
+            {
+                maid.body0.MuneYureL(1f);
+                maid.body0.MuneYureR(1f);
+                maid.body0.jbMuneL.enabled = true;
+                maid.body0.jbMuneR.enabled = true;
+            }
+        }
+
+        protected override void InitializeDrag()
+        {
+            base.InitializeDrag();
+
+            off = transform.position - Camera.main.ScreenToWorldPoint(new Vector3(Input.mousePosition.x, Input.mousePosition.y, worldPoint.z));
+            off2 = new Vector3(
+                transform.position.x - ikChain[hand].position.x,
+                transform.position.y - ikChain[hand].position.y,
+                transform.position.z - ikChain[hand].position.z);
+
+            jointRotation[upperArmRot] = ikChain[upperArm].localEulerAngles;
+            jointRotation[handRot] = ikChain[hand].localEulerAngles;
+            maid.body0.MuneYureL(0f);
+            maid.body0.MuneYureR(0f);
+            maid.body0.jbMuneL.enabled = false;
+            maid.body0.jbMuneR.enabled = false;
+        }
+
+        protected override void Drag()
+        {
+            if (dragType == DragType.None) return;
+
+            if (isPlaying)
+            {
+                maid.GetAnimation().Stop();
+            }
+
+            IKCtrlData ikData = maid.body0.IKCtrl.GetIKData("左手");
+            Vector3 pos = Camera.main.ScreenToWorldPoint(new Vector3(Input.mousePosition.x, Input.mousePosition.y, worldPoint.z)) + off - off2;
+
+            if (dragType == DragType.RotLocalXZ)
+            {
+                IK.Porc(ikChain[upperArm], ikChain[foreArm], ikChain[hand], pos, Vector3.zero, ikData);
+                // IK.Porc(ikChain[upperArm], ikChain[foreArm], ikChain[hand], pos + (pos - ikChain[hand].position), Vector3.zero, ikData);
+
+                jointRotation[handRot] = ikChain[hand].localEulerAngles;
+                jointRotation[upperArmRot] = ikChain[upperArm].localEulerAngles;
+                ikChain[upperArm].localEulerAngles = jointRotation[upperArm];
+                ikChain[hand].localEulerAngles = jointRotation[handRot];
+            }
+        }
+    }
+}

+ 66 - 0
COM3D2.MeidoPhotoStudio.Plugin/MeidoPhotoStudio/Meido/IK/DragPelvis.cs

@@ -0,0 +1,66 @@
+using System;
+using UnityEngine;
+
+namespace COM3D2.MeidoPhotoStudio.Plugin
+{
+    public class DragPelvis : BaseDrag
+    {
+        private Transform pelvis;
+        private Vector3 pelvisRotation;
+
+        public void Initialize(Maid maid, Transform pelvis, Func<Vector3> position, Func<Vector3> rotation)
+        {
+            base.Initialize(maid, position, rotation);
+            this.pelvis = pelvis;
+        }
+
+        protected override void GetDragType()
+        {
+            bool shift = Input.GetKey(KeyCode.LeftShift);
+            if (Input.GetKey(KeyCode.LeftAlt))
+            {
+                dragType = shift ? DragType.RotLocalY : DragType.RotLocalXZ;
+            }
+            else
+            {
+                dragType = DragType.None;
+            }
+        }
+
+        protected override void InitializeDrag()
+        {
+            base.InitializeDrag();
+            pelvisRotation = pelvis.localEulerAngles;
+        }
+
+        protected override void Drag()
+        {
+            if (dragType == DragType.None) return;
+
+            if (isPlaying)
+            {
+                maid.GetAnimation().Stop();
+            }
+
+            Vector3 pos = new Vector3(Input.mousePosition.x, Input.mousePosition.y, worldPoint.z);
+            Vector3 vec31 = Input.mousePosition - mousePos;
+            Transform t = GameMain.Instance.MainCamera.gameObject.transform;
+            Vector3 vec32 = t.TransformDirection(Vector3.right);
+            Vector3 vec33 = t.TransformDirection(Vector3.forward);
+
+            if (dragType == DragType.RotLocalXZ)
+            {
+                pelvis.localEulerAngles = pelvisRotation;
+                pelvis.RotateAround(pelvis.position, new Vector3(vec32.x, 0.0f, vec32.z), vec31.y / 4f);
+                pelvis.RotateAround(pelvis.position, new Vector3(vec33.x, 0.0f, vec33.z), vec31.x / 6f);
+            }
+
+            if (dragType == DragType.RotLocalY)
+            {
+                pelvis.localEulerAngles = pelvisRotation;
+                pelvis.localRotation = Quaternion.Euler(pelvis.localEulerAngles)
+                    * Quaternion.AngleAxis(vec31.x / 3f, Vector3.right);
+            }
+        }
+    }
+}

+ 50 - 0
COM3D2.MeidoPhotoStudio.Plugin/MeidoPhotoStudio/Meido/IK/DragSpine.cs

@@ -0,0 +1,50 @@
+using System;
+using UnityEngine;
+
+namespace COM3D2.MeidoPhotoStudio.Plugin
+{
+    public class DragSpine : BaseDrag
+    {
+        private Transform spine;
+        private Vector3 rotate;
+
+        public void Initialize(Transform spine, Maid maid, Func<Vector3> position, Func<Vector3> rotation)
+        {
+            base.Initialize(maid, position, rotation);
+            this.spine = spine;
+
+            InitializeGizmo(this.spine);
+        }
+
+        protected override void GetDragType()
+        {
+            dragType = DragType.None;
+        }
+
+        protected override void InitializeDrag()
+        {
+            base.InitializeDrag();
+            rotate = spine.localEulerAngles;
+        }
+
+        protected override void Drag()
+        {
+            if (isPlaying)
+            {
+                maid.GetAnimation().Stop();
+            }
+
+            if (dragType == DragType.None)
+            {
+                Vector3 vec31 = Input.mousePosition - mousePos;
+                Transform t = GameMain.Instance.MainCamera.gameObject.transform;
+                Vector3 vec32 = t.TransformDirection(Vector3.right);
+                Vector3 vec33 = t.TransformDirection(Vector3.forward);
+
+                spine.localEulerAngles = rotate;
+                spine.RotateAround(spine.position, new Vector3(vec32.x, 0.0f, vec32.z), vec31.y / 3f);
+                spine.RotateAround(spine.position, new Vector3(vec33.x, 0.0f, vec33.z), (-vec31.x / 4.5f));
+            }
+        }
+    }
+}

+ 93 - 0
COM3D2.MeidoPhotoStudio.Plugin/MeidoPhotoStudio/Meido/IK/DragTorso.cs

@@ -0,0 +1,93 @@
+using System;
+using UnityEngine;
+
+namespace COM3D2.MeidoPhotoStudio.Plugin
+{
+    public class DragTorso : BaseDrag
+    {
+        private Transform[] spine;
+        private Vector3[] spineRotation = new Vector3[4];
+
+        public void Initialize(Maid maid, Transform[] spine, Func<Vector3> position, Func<Vector3> rotation)
+        {
+            base.Initialize(maid, position, rotation);
+            this.spine = spine;
+        }
+
+        protected override void GetDragType()
+        {
+            bool shift = Input.GetKey(KeyCode.LeftShift);
+            if (Input.GetKey(KeyCode.LeftAlt))
+            {
+                dragType = shift ? DragType.RotLocalY : DragType.RotLocalXZ;
+            }
+            else
+            {
+                dragType = DragType.None;
+            }
+        }
+
+        protected override void InitializeDrag()
+        {
+            base.InitializeDrag();
+
+            for (int i = 0; i < spine.Length; i++)
+            {
+                spineRotation[i] = spine[i].localEulerAngles;
+            }
+        }
+
+        protected override void Drag()
+        {
+            if (dragType == DragType.None) return;
+
+            if (isPlaying)
+            {
+                maid.GetAnimation().Stop();
+            }
+
+            Vector3 pos = new Vector3(Input.mousePosition.x, Input.mousePosition.y, worldPoint.z);
+            Vector3 vec31 = Input.mousePosition - mousePos;
+            Transform t = GameMain.Instance.MainCamera.gameObject.transform;
+            Vector3 vec32 = t.TransformDirection(Vector3.right);
+            Vector3 vec33 = t.TransformDirection(Vector3.forward);
+
+            if (dragType == DragType.RotLocalXZ)
+            {
+                for (int i = 0; i < 4; i++)
+                {
+                    spine[i].localEulerAngles = spineRotation[i];
+                }
+
+                float num1 = 1.5f;
+                float num2 = 1f;
+                float num3 = 0.03f;
+                float num4 = 0.1f;
+                float num5 = 0.09f;
+                float num6 = 0.07f;
+                spine[0].RotateAround(spine[0].position, new Vector3(vec32.x, 0f, vec32.z), vec31.y / num2 * num3);
+                spine[0].RotateAround(spine[0].position, new Vector3(vec33.x, 0f, vec33.z), -vec31.x / num1 * num3);
+                spine[1].RotateAround(spine[1].position, new Vector3(vec32.x, 0f, vec32.z), vec31.y / num2 * num4);
+                spine[1].RotateAround(spine[1].position, new Vector3(vec33.x, 0f, vec33.z), -vec31.x / num1 * num4);
+                spine[2].RotateAround(spine[2].position, new Vector3(vec32.x, 0f, vec32.z), vec31.y / num2 * num5);
+                spine[2].RotateAround(spine[2].position, new Vector3(vec33.x, 0f, vec33.z), -vec31.x / num1 * num5);
+                spine[3].RotateAround(spine[3].position, new Vector3(vec32.x, 0f, vec32.z), vec31.y / num2 * num6);
+                spine[3].RotateAround(spine[3].position, new Vector3(vec33.x, 0f, vec33.z), -vec31.x / num1 * num6);
+            }
+
+            if (dragType == DragType.RotLocalY)
+            {
+                for (int i = 0; i < 4; i++)
+                {
+                    spine[i].localEulerAngles = spineRotation[i];
+                }
+                spine[0].localRotation = Quaternion.Euler(spine[0].localEulerAngles)
+                    * Quaternion.AngleAxis(vec31.x / 1.5f * 0.08f, Vector3.right);
+                spine[2].localRotation = Quaternion.Euler(spine[2].localEulerAngles)
+                    * Quaternion.AngleAxis(vec31.x / 1.5f * 0.15f, Vector3.right);
+                spine[3].localRotation = Quaternion.Euler(spine[3].localEulerAngles)
+                    * Quaternion.AngleAxis(vec31.x / 1.5f * 0.15f, Vector3.right);
+            }
+        }
+    }
+}

+ 211 - 0
COM3D2.MeidoPhotoStudio.Plugin/MeidoPhotoStudio/Meido/Meido.cs

@@ -0,0 +1,211 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using UnityEngine;
+
+namespace COM3D2.MeidoPhotoStudio.Plugin
+{
+    public class Meido
+    {
+        private static CharacterMgr characterMgr = GameMain.Instance.CharacterMgr;
+        public readonly int stockNo;
+        public Maid Maid { get; private set; }
+        public Texture2D Image { get; private set; }
+        public string FirstName { get; private set; }
+        public string LastName { get; private set; }
+        public string NameJP => $"{LastName}\n{FirstName}";
+        public string NameEN => $"{FirstName}\n{LastName}";
+        public int ActiveSlot { get; private set; }
+        private DragPointManager dragPointManager;
+        public event EventHandler<MeidoChangeEventArgs> SelectMeido;
+        public event EventHandler BodyLoad;
+        private bool isLoading = false;
+        public bool Visible
+        {
+            get => Maid.Visible;
+            set => Maid.Visible = value;
+        }
+
+        public Meido(int stockMaidIndex)
+        {
+            this.Maid = characterMgr.GetStockMaid(stockMaidIndex);
+            this.stockNo = stockMaidIndex;
+            this.Image = Maid.GetThumIcon();
+            this.FirstName = Maid.status.firstName;
+            this.LastName = Maid.status.lastName;
+            Maid.boAllProcPropBUSY = false;
+        }
+
+        public void Update()
+        {
+            if (isLoading)
+            {
+                if (!Maid.IsBusy)
+                {
+                    isLoading = false;
+                    OnBodyLoad();
+                }
+                return;
+            }
+            dragPointManager.Update();
+        }
+
+        public Maid Load(int activeSlot)
+        {
+            isLoading = true;
+            this.ActiveSlot = activeSlot;
+
+            Maid.Visible = true;
+
+            if (!Maid.body0.isLoadedBody)
+            {
+                Maid.DutPropAll();
+                Maid.AllProcPropSeqStart();
+            }
+            else
+            {
+                SetPose("pose_taiki_f");
+            }
+
+            if (dragPointManager == null)
+            {
+                dragPointManager = new DragPointManager(this);
+                dragPointManager.SelectMaid += (sender, meidoChangeArgs) => OnMeidoSelect(meidoChangeArgs);
+            }
+            else
+            {
+                dragPointManager.Activate();
+            }
+
+            Maid.body0.boHeadToCam = true;
+            Maid.body0.boEyeToCam = true;
+            Maid.body0.SetBoneHitHeightY(-1000f);
+
+
+            return Maid;
+        }
+
+        public void Unload()
+        {
+            if (Maid.body0.isLoadedBody)
+            {
+                Maid.body0.MuneYureL(1f);
+                Maid.body0.MuneYureR(1f);
+                Maid.body0.jbMuneL.enabled = true;
+                Maid.body0.jbMuneR.enabled = true;
+            }
+
+            Maid.body0.SetMaskMode(TBody.MaskMode.None);
+
+            Maid.body0.trsLookTarget = GameMain.Instance.MainCamera.transform;
+
+            Maid.Visible = false;
+
+            if (dragPointManager != null) dragPointManager.Deactivate();
+        }
+
+        public void Deactivate()
+        {
+            Unload();
+            if (dragPointManager != null) dragPointManager.Destroy();
+            Maid.SetPos(Vector3.zero);
+            Maid.SetRot(Vector3.zero);
+            Maid.SetPosOffset(Vector3.zero);
+            if (Maid.body0 != null)
+            {
+                Maid.body0.SetBoneHitHeightY(0f);
+            }
+
+            Maid.Visible = false;
+            Maid.ActiveSlotNo = -1;
+            Maid.DelPrefabAll();
+        }
+
+        public void SetPose(string pose)
+        {
+            if (pose.StartsWith(Constants.customPosePath))
+            {
+                SetPoseCustom(pose);
+                return;
+            }
+
+            string[] poseComponents = pose.Split(',');
+            pose = poseComponents[0] + ".anm";
+
+            Maid.CrossFade(pose, false, true, false, 0f);
+            Maid.SetAutoTwistAll(true);
+            Maid.GetAnimation().Play();
+
+            if (poseComponents.Length > 1)
+            {
+                Maid.GetAnimation()[pose].time = float.Parse(poseComponents[1]);
+                Maid.GetAnimation()[pose].speed = 0f;
+            }
+
+            if (pose.Contains("_momi") || pose.Contains("paizuri_"))
+            {
+                Maid.body0.MuneYureL(0f);
+                Maid.body0.MuneYureR(0f);
+            }
+            else
+            {
+                Maid.body0.MuneYureL(1f);
+                Maid.body0.MuneYureR(1f);
+            }
+        }
+
+        public void SetPoseCustom(string path)
+        {
+            byte[] bytes = File.ReadAllBytes(path);
+            string hash = Path.GetFileName(path).GetHashCode().ToString();
+            Maid.body0.CrossFade(hash, bytes, false, true, false, 0f);
+            Maid.SetAutoTwistAll(true);
+        }
+
+        public void SetFaceBlend(string blendValue)
+        {
+            Maid.boMabataki = false;
+            TMorph morph = Maid.body0.Face.morph;
+            morph.EyeMabataki = 0f;
+            morph.MulBlendValues(blendValue, 1f);
+            morph.FixBlendValues_Face();
+        }
+
+        public void SetFaceBlendValue(string hash, float value)
+        {
+            TMorph morph = Maid.body0.Face.morph;
+            if (hash == "nosefook")
+            {
+                morph.boNoseFook = value > 0f;
+                Maid.boNoseFook = morph.boNoseFook;
+            }
+            else
+            {
+                float[] blendValues = hash.StartsWith("eyeclose")
+                    ? Utility.GetFieldValue<TMorph, float[]>(morph, "BlendValuesBackup")
+                    : Utility.GetFieldValue<TMorph, float[]>(morph, "BlendValues");
+
+                try
+                {
+                    blendValues[(int)morph.hash[hash]] = value;
+                }
+                catch { }
+            }
+            Maid.boMabataki = false;
+            morph.EyeMabataki = 0f;
+            morph.FixBlendValues_Face();
+        }
+
+        private void OnBodyLoad()
+        {
+            EventHandler handler = BodyLoad;
+            if (handler != null) handler(this, EventArgs.Empty);
+        }
+
+        private void OnMeidoSelect(MeidoChangeEventArgs args)
+        {
+            EventHandler<MeidoChangeEventArgs> handler = SelectMeido;
+            if (handler != null) handler(this, args);
+        }
+    }
+}

+ 163 - 0
COM3D2.MeidoPhotoStudio.Plugin/MeidoPhotoStudio/MeidoPhotoStudio.cs

@@ -0,0 +1,163 @@
+using System;
+using System.Collections.Generic;
+using UnityEngine;
+using UnityEngine.SceneManagement;
+using UnityInjector;
+using UnityInjector.Attributes;
+
+namespace COM3D2.MeidoPhotoStudio.Plugin
+{
+    [PluginName("Meido Photo Studio"), PluginVersion("0.0.0")]
+    public class MeidoPhotoStudio : PluginBase
+    {
+        private static MonoBehaviour instance;
+        private WindowManager windowManager;
+        private MeidoManager meidoManager;
+        private EnvironmentManager environmentManager;
+        private Constants.Scene currentScene;
+        private bool initialized = false;
+        private bool isActive = false;
+        private MeidoPhotoStudio()
+        {
+            MeidoPhotoStudio.instance = this;
+        }
+        private void Awake()
+        {
+            DontDestroyOnLoad(this);
+            Translation.Initialize("en");
+            Constants.Initialize();
+        }
+        private void Start()
+        {
+            SceneManager.sceneLoaded += OnSceneLoaded;
+        }
+        private void Update()
+        {
+            if (currentScene == Constants.Scene.Daily)
+            {
+                if (Input.GetKeyDown(KeyCode.F6))
+                {
+                    if (!initialized)
+                    {
+                        Initialize();
+                        windowManager.Visible = true;
+                    }
+                    else
+                    {
+                        ReturnToMenu();
+                    }
+                }
+
+                if (isActive)
+                {
+                    meidoManager.Update();
+                    windowManager.Update();
+                }
+            }
+        }
+        private void OnGUI()
+        {
+            if (isActive)
+            {
+                windowManager.OnGUI();
+            }
+        }
+        private void OnSceneLoaded(Scene scene, LoadSceneMode sceneMode)
+        {
+            currentScene = (Constants.Scene)scene.buildIndex;
+
+            // if (currentScene == Constants.Scene.Daily)
+            // {
+            //     if (initialized)
+            //     {
+
+            //     }
+            // }
+            // else
+            // {
+            //     if (initialized)
+            //     {
+            //         initialized = false;
+            //     }
+            // }
+        }
+        private void ReturnToMenu()
+        {
+            if (meidoManager.IsBusy) return;
+            meidoManager.DeactivateMeidos();
+
+            isActive = false;
+            initialized = false;
+            windowManager.Visible = false;
+            GameMain.Instance.SoundMgr.PlayBGM("bgm009.ogg", 1f, true);
+            GameObject go = GameObject.Find("UI Root").transform.Find("DailyPanel").gameObject;
+            go.SetActive(true);
+            bool isNight = GameMain.Instance.CharacterMgr.status.GetFlag("時間帯") == 3;
+
+            if (isNight)
+            {
+                GameMain.Instance.BgMgr.ChangeBg("ShinShitsumu_ChairRot_Night");
+            }
+            else
+            {
+                GameMain.Instance.BgMgr.ChangeBg("ShinShitsumu_ChairRot");
+            }
+
+            GameMain.Instance.MainCamera.Reset(CameraMain.CameraType.Target, true);
+            GameMain.Instance.MainCamera.SetTargetPos(new Vector3(0.5609447f, 1.380762f, -1.382336f), true);
+            GameMain.Instance.MainCamera.SetDistance(1.6f, true);
+            GameMain.Instance.MainCamera.SetAroundAngle(new Vector2(245.5691f, 6.273283f), true);
+        }
+
+        private void Initialize()
+        {
+            initialized = true;
+            meidoManager = new MeidoManager();
+            environmentManager = new EnvironmentManager();
+            windowManager = new WindowManager(meidoManager, environmentManager);
+
+            environmentManager.Initialize();
+
+            isActive = true;
+
+            #region maid stuff
+            // if (maid)
+            // {
+            //     maid.StopKuchipakuPattern();
+            //     maid.body0.trsLookTarget = GameMain.Instance.MainCamera.transform;
+
+            //     if (maid.Visible && maid.body0.isLoadedBody)
+            //     {
+            //         maid.CrossFade("pose_taiki_f.anm", false, true, false, 0f);
+            //         maid.SetAutoTwistAll(true);
+            //         maid.body0.MuneYureL(1f);
+            //         maid.body0.MuneYureR(1f);
+            //         maid.body0.jbMuneL.enabled = true;
+            //         maid.body0.jbMuneR.enabled = true;
+            //     }
+
+            //     maid.body0.SetMask(TBody.SlotID.wear, true);
+            //     maid.body0.SetMask(TBody.SlotID.skirt, true);
+            //     maid.body0.SetMask(TBody.SlotID.bra, true);
+            //     maid.body0.SetMask(TBody.SlotID.panz, true);
+            //     maid.body0.SetMask(TBody.SlotID.mizugi, true);
+            //     maid.body0.SetMask(TBody.SlotID.onepiece, true);
+            //     if (maid.body0.isLoadedBody)
+            //     {
+            //         for (int i = 0; i < maid.body0.goSlot.Count; i++)
+            //         {
+            //             List<THair1> fieldValue = Utility.GetFieldValue<TBoneHair_, List<THair1>>(maid.body0.goSlot[i].bonehair, "hair1list");
+            //             for (int j = 0; j < fieldValue.Count; ++j)
+            //             {
+            //                 fieldValue[j].SoftG = new Vector3(0.0f, -3f / 1000f, 0.0f);
+            //             }
+            //         }
+            //     }
+            // }
+            #endregion
+
+            GameObject dailyPanel = GameObject.Find("UI Root").transform.Find("DailyPanel").gameObject;
+            dailyPanel.SetActive(false);
+        }
+    }
+}

+ 65 - 0
COM3D2.MeidoPhotoStudio.Plugin/MeidoPhotoStudio/Utility.cs

@@ -0,0 +1,65 @@
+using System.Reflection;
+using UnityEngine;
+
+namespace COM3D2.MeidoPhotoStudio.Plugin
+{
+    public static class Utility
+    {
+        public enum ModKey
+        {
+            Control, Shift, Alt
+        }
+        internal static int Wrap(int value, int min, int max)
+        {
+            max -= 1;
+            return value < min ? max : value > max ? min : value;
+        }
+        internal static int GetPix(int num)
+        {
+            return (int)((1f + (Screen.width / 1280f - 1f) * 0.6f) * num);
+        }
+        internal 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;
+        }
+        internal static FieldInfo GetFieldInfo<T>(string field)
+        {
+            BindingFlags bindingFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static;
+            return typeof(T).GetField(field, bindingFlags);
+        }
+        internal static TValue GetFieldValue<TType, TValue>(TType instance, string field)
+        {
+            FieldInfo fieldInfo = GetFieldInfo<TType>(field);
+            if (fieldInfo == null || !fieldInfo.IsStatic && instance == null) return default(TValue);
+            return (TValue)fieldInfo.GetValue(instance);
+        }
+        internal static void SetFieldValue<TType, TValue>(TType instance, string name, TValue value)
+        {
+            FieldInfo fieldInfo = GetFieldInfo<TType>(name);
+            fieldInfo.SetValue(instance, value);
+        }
+
+        internal static bool GetModKey(ModKey key)
+        {
+            switch (key)
+            {
+                case ModKey.Control: return Input.GetKey(KeyCode.LeftControl) || Input.GetKey(KeyCode.RightControl);
+                case ModKey.Alt: return Input.GetKey(KeyCode.LeftAlt) || Input.GetKey(KeyCode.RightAlt);
+                case ModKey.Shift: return Input.GetKey(KeyCode.LeftShift) || Input.GetKey(KeyCode.RightShift);
+                default: return false;
+            }
+        }
+        internal static bool AnyMouseDown()
+        {
+            return Input.GetMouseButtonDown(0) || Input.GetMouseButtonDown(1) || Input.GetMouseButtonDown(2);
+        }
+    }
+}

+ 17 - 0
readme.md

@@ -0,0 +1,17 @@
+# MeidoPhotoStudio
+
+A rewrite of MultipleMaids
+
+## Building
+
+### Required Libraries
+
+Place these in a folder called `lib`
+
+* `Assembly-CSharp.dll`
+* `Assembly-CSharp-firstpass.dll`
+* `Assembly-UnityScript-firstpass.dll`
+* `Newtonsoft.json.dll`
+* `UnityEngine.dll`
+* `Exini.dll`
+* `UnityInjector.dll`