Browse Source

Add scene (de)serialization

Heavy testing required.
habeebweeb 4 years ago
parent
commit
94e08893e3
20 changed files with 849 additions and 60 deletions
  1. 3 0
      COM3D2.MeidoPhotoStudio.Plugin/COM3D2.MeidoPhotoStudio.Plugin.csproj
  2. 47 0
      COM3D2.MeidoPhotoStudio.Plugin/MeidoPhotoStudio/DragPoint/DragPointLight.cs
  3. 1 0
      COM3D2.MeidoPhotoStudio.Plugin/MeidoPhotoStudio/DragPoint/DragPointOther.cs
  4. 36 1
      COM3D2.MeidoPhotoStudio.Plugin/MeidoPhotoStudio/Managers/EffectManager.cs
  5. 20 0
      COM3D2.MeidoPhotoStudio.Plugin/MeidoPhotoStudio/Managers/EffectManagers/BloomEffectManager.cs
  6. 22 0
      COM3D2.MeidoPhotoStudio.Plugin/MeidoPhotoStudio/Managers/EffectManagers/DepthOfFieldManager.cs
  7. 22 0
      COM3D2.MeidoPhotoStudio.Plugin/MeidoPhotoStudio/Managers/EffectManagers/FogEffectManager.cs
  8. 1 4
      COM3D2.MeidoPhotoStudio.Plugin/MeidoPhotoStudio/Managers/EffectManagers/IEffectManager.cs
  9. 20 0
      COM3D2.MeidoPhotoStudio.Plugin/MeidoPhotoStudio/Managers/EffectManagers/VignetteEffectManager.cs
  10. 35 1
      COM3D2.MeidoPhotoStudio.Plugin/MeidoPhotoStudio/Managers/EnvironmentManager.cs
  11. 11 0
      COM3D2.MeidoPhotoStudio.Plugin/MeidoPhotoStudio/Managers/IManager.cs
  12. 26 1
      COM3D2.MeidoPhotoStudio.Plugin/MeidoPhotoStudio/Managers/LightManager.cs
  13. 31 1
      COM3D2.MeidoPhotoStudio.Plugin/MeidoPhotoStudio/Managers/MeidoManager.cs
  14. 28 1
      COM3D2.MeidoPhotoStudio.Plugin/MeidoPhotoStudio/Managers/MessageWindowManager.cs
  15. 194 49
      COM3D2.MeidoPhotoStudio.Plugin/MeidoPhotoStudio/Managers/PropManager.cs
  16. 199 0
      COM3D2.MeidoPhotoStudio.Plugin/MeidoPhotoStudio/Meido/Meido.cs
  17. 34 0
      COM3D2.MeidoPhotoStudio.Plugin/MeidoPhotoStudio/Meido/MeidoDragPointManager.cs
  18. 109 2
      COM3D2.MeidoPhotoStudio.Plugin/MeidoPhotoStudio/MeidoPhotoStudio.cs
  19. 7 0
      COM3D2.MeidoPhotoStudio.Plugin/MeidoPhotoStudio/MenuFileUtility.cs
  20. 3 0
      readme.md

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

@@ -21,6 +21,9 @@
         <Reference Include="Newtonsoft.Json">
             <HintPath>..\lib\Newtonsoft.Json.dll</HintPath>
         </Reference>
+        <Reference Include="Ionic.Zlib">
+            <HintPath>..\lib\Ionic.Zlib.dll</HintPath>
+        </Reference>
         <Reference Include="BepInEx.dll">
             <HintPath>..\lib\BepInEx.dll</HintPath>
         </Reference>

+ 47 - 0
COM3D2.MeidoPhotoStudio.Plugin/MeidoPhotoStudio/DragPoint/DragPointLight.cs

@@ -118,6 +118,30 @@ namespace COM3D2.MeidoPhotoStudio.Plugin
             }
         }
 
+        public void Serialize(System.IO.BinaryWriter binaryWriter)
+        {
+            foreach (LightProperty lightProperty in LightProperties)
+            {
+                lightProperty.Serialize(binaryWriter);
+            }
+            binaryWriter.WriteVector3(MyObject.position);
+            binaryWriter.Write((int)SelectedLightType);
+            binaryWriter.Write(IsColourMode);
+            binaryWriter.Write(IsDisabled);
+        }
+
+        public void Deserialize(System.IO.BinaryReader binaryReader)
+        {
+            for (int i = 0; i < LightProperties.Length; i++)
+            {
+                LightProperties[i] = LightProperty.Deserialize(binaryReader);
+            }
+            MyObject.position = binaryReader.ReadVector3();
+            SetLightType((MPSLightType)binaryReader.ReadInt32());
+            IsColourMode = binaryReader.ReadBoolean();
+            IsDisabled = binaryReader.ReadBoolean();
+        }
+
         public static void SetLightProperties(Light light, LightProperty prop)
         {
             light.transform.rotation = prop.Rotation;
@@ -290,5 +314,28 @@ namespace COM3D2.MeidoPhotoStudio.Plugin
         public float SpotAngle { get; set; } = 50f;
         public float ShadowStrength { get; set; } = 0.10f;
         public Color LightColour { get; set; } = Color.white;
+
+        public void Serialize(System.IO.BinaryWriter binaryWriter)
+        {
+            binaryWriter.WriteQuaternion(Rotation);
+            binaryWriter.Write(Intensity);
+            binaryWriter.Write(Range);
+            binaryWriter.Write(SpotAngle);
+            binaryWriter.Write(ShadowStrength);
+            binaryWriter.WriteColour(LightColour);
+        }
+
+        public static LightProperty Deserialize(System.IO.BinaryReader binaryReader)
+        {
+            return new LightProperty()
+            {
+                Rotation = binaryReader.ReadQuaternion(),
+                Intensity = binaryReader.ReadSingle(),
+                Range = binaryReader.ReadSingle(),
+                SpotAngle = binaryReader.ReadSingle(),
+                ShadowStrength = binaryReader.ReadSingle(),
+                LightColour = binaryReader.ReadColour()
+            };
+        }
     }
 }

+ 1 - 0
COM3D2.MeidoPhotoStudio.Plugin/MeidoPhotoStudio/DragPoint/DragPointOther.cs

@@ -25,6 +25,7 @@ namespace COM3D2.MeidoPhotoStudio.Plugin
     {
         public AttachPointInfo attachPointInfo = AttachPointInfo.Empty;
         public string Name => MyGameObject.name;
+        public string assetName = string.Empty;
 
         protected override void ApplyDragType()
         {

+ 36 - 1
COM3D2.MeidoPhotoStudio.Plugin/MeidoPhotoStudio/Managers/EffectManager.cs

@@ -3,8 +3,10 @@ using System.Collections.Generic;
 
 namespace COM3D2.MeidoPhotoStudio.Plugin
 {
-    internal class EffectManager
+    internal class EffectManager : IManager
     {
+        public const string header = "EFFECT";
+        public const string footer = "END_EFFECT";
         private Dictionary<Type, IEffectManager> EffectManagers = new Dictionary<Type, IEffectManager>();
         private BloomEffectManager bloomEffectManager;
 
@@ -30,6 +32,39 @@ namespace COM3D2.MeidoPhotoStudio.Plugin
             return manager;
         }
 
+        public void Serialize(System.IO.BinaryWriter binaryWriter)
+        {
+            binaryWriter.Write(header);
+            foreach (IEffectManager effectManager in EffectManagers.Values)
+            {
+                effectManager.Serialize(binaryWriter);
+            }
+            binaryWriter.Write(footer);
+        }
+
+        public void Deserialize(System.IO.BinaryReader binaryReader)
+        {
+            string header;
+            while ((header = binaryReader.ReadString()) != footer)
+            {
+                switch (header)
+                {
+                    case BloomEffectManager.header:
+                        Get<BloomEffectManager>().Deserialize(binaryReader);
+                        break;
+                    case DepthOfFieldEffectManager.header:
+                        Get<DepthOfFieldEffectManager>().Deserialize(binaryReader);
+                        break;
+                    case VignetteEffectManager.header:
+                        Get<VignetteEffectManager>().Deserialize(binaryReader);
+                        break;
+                    case FogEffectManager.header:
+                        Get<FogEffectManager>().Deserialize(binaryReader);
+                        break;
+                }
+            }
+        }
+
         public void Activate()
         {
             foreach (IEffectManager effectManager in EffectManagers.Values)

+ 20 - 0
COM3D2.MeidoPhotoStudio.Plugin/MeidoPhotoStudio/Managers/EffectManagers/BloomEffectManager.cs

@@ -4,6 +4,7 @@ namespace COM3D2.MeidoPhotoStudio.Plugin
 {
     internal class BloomEffectManager : IEffectManager
     {
+        public const string header = "EFFECT_BLOOM";
         private Bloom Bloom { get; set; }
         private float initialIntensity;
         private int initialBlurIterations;
@@ -67,6 +68,25 @@ namespace COM3D2.MeidoPhotoStudio.Plugin
             }
         }
 
+        public void Serialize(System.IO.BinaryWriter binaryWriter)
+        {
+            binaryWriter.Write(header);
+            binaryWriter.Write(Intensity);
+            binaryWriter.Write(BlurIterations);
+            binaryWriter.WriteColour(BloomThresholdColour);
+            binaryWriter.Write(BloomHDR);
+            binaryWriter.Write(Active);
+        }
+
+        public void Deserialize(System.IO.BinaryReader binaryReader)
+        {
+            Intensity = binaryReader.ReadSingle();
+            BlurIterations = binaryReader.ReadInt32();
+            BloomThresholdColour = binaryReader.ReadColour();
+            BloomHDR = binaryReader.ReadBoolean();
+            SetEffectActive(binaryReader.ReadBoolean());
+        }
+
         public void Activate()
         {
             if (Bloom == null)

+ 22 - 0
COM3D2.MeidoPhotoStudio.Plugin/MeidoPhotoStudio/Managers/EffectManagers/DepthOfFieldManager.cs

@@ -4,6 +4,7 @@ namespace COM3D2.MeidoPhotoStudio.Plugin
 {
     internal class DepthOfFieldEffectManager : IEffectManager
     {
+        public const string header = "EFFECT_DOF";
         private DepthOfFieldScatter DepthOfField { get; set; }
         public bool Ready { get; private set; }
         public bool Active { get; private set; }
@@ -40,6 +41,27 @@ namespace COM3D2.MeidoPhotoStudio.Plugin
             set => visualizeFocus = DepthOfField.visualizeFocus = value;
         }
 
+        public void Serialize(System.IO.BinaryWriter binaryWriter)
+        {
+            binaryWriter.Write(header);
+            binaryWriter.Write(FocalLength);
+            binaryWriter.Write(FocalSize);
+            binaryWriter.Write(Aperture);
+            binaryWriter.Write(MaxBlurSize);
+            binaryWriter.Write(VisualizeFocus);
+            binaryWriter.Write(Active);
+        }
+
+        public void Deserialize(System.IO.BinaryReader binaryReader)
+        {
+            FocalLength = binaryReader.ReadSingle();
+            FocalSize = binaryReader.ReadSingle();
+            Aperture = binaryReader.ReadSingle();
+            MaxBlurSize = binaryReader.ReadSingle();
+            VisualizeFocus = binaryReader.ReadBoolean();
+            SetEffectActive(binaryReader.ReadBoolean());
+        }
+
         public void Activate()
         {
             if (DepthOfField == null)

+ 22 - 0
COM3D2.MeidoPhotoStudio.Plugin/MeidoPhotoStudio/Managers/EffectManagers/FogEffectManager.cs

@@ -4,6 +4,7 @@ namespace COM3D2.MeidoPhotoStudio.Plugin
 {
     internal class FogEffectManager : IEffectManager
     {
+        public const string header = "EFFECT_FOG";
         private GlobalFog Fog { get; set; }
         public bool Ready { get; private set; }
         public bool Active { get; private set; }
@@ -70,6 +71,27 @@ namespace COM3D2.MeidoPhotoStudio.Plugin
             set => fogColour = Fog.globalFogColor = value;
         }
 
+        public void Serialize(System.IO.BinaryWriter binaryWriter)
+        {
+            binaryWriter.Write(header);
+            binaryWriter.Write(Distance);
+            binaryWriter.Write(Density);
+            binaryWriter.Write(HeightScale);
+            binaryWriter.Write(Height);
+            binaryWriter.WriteColour(FogColour);
+            binaryWriter.Write(Active);
+        }
+
+        public void Deserialize(System.IO.BinaryReader binaryReader)
+        {
+            Distance = binaryReader.ReadSingle();
+            Density = binaryReader.ReadSingle();
+            HeightScale = binaryReader.ReadSingle();
+            Height = binaryReader.ReadSingle();
+            FogColour = binaryReader.ReadColour();
+            SetEffectActive(binaryReader.ReadBoolean());
+        }
+
         public void Activate()
         {
             if (Fog == null)

+ 1 - 4
COM3D2.MeidoPhotoStudio.Plugin/MeidoPhotoStudio/Managers/EffectManagers/IEffectManager.cs

@@ -1,13 +1,10 @@
 namespace COM3D2.MeidoPhotoStudio.Plugin
 {
-    internal interface IEffectManager
+    internal interface IEffectManager : IManager
     {
         bool Ready { get; }
         bool Active { get; }
-        void Activate();
-        void Deactivate();
         void SetEffectActive(bool active);
         void Reset();
-        void Update();
     }
 }

+ 20 - 0
COM3D2.MeidoPhotoStudio.Plugin/MeidoPhotoStudio/Managers/EffectManagers/VignetteEffectManager.cs

@@ -2,6 +2,7 @@ namespace COM3D2.MeidoPhotoStudio.Plugin
 {
     internal class VignetteEffectManager : IEffectManager
     {
+        public const string header = "EFFECT_VIGNETTE";
         private Vignetting Vignette { get; set; }
         private float initialIntensity;
         private float initialBlur;
@@ -34,6 +35,25 @@ namespace COM3D2.MeidoPhotoStudio.Plugin
             set => chromaticAberration = Vignette.chromaticAberration = value;
         }
 
+        public void Serialize(System.IO.BinaryWriter binaryWriter)
+        {
+            binaryWriter.Write(header);
+            binaryWriter.Write(Intensity);
+            binaryWriter.Write(Blur);
+            binaryWriter.Write(BlurSpread);
+            binaryWriter.Write(ChromaticAberration);
+            binaryWriter.Write(Active);
+        }
+
+        public void Deserialize(System.IO.BinaryReader binaryReader)
+        {
+            Intensity = binaryReader.ReadSingle();
+            Blur = binaryReader.ReadSingle();
+            BlurSpread = binaryReader.ReadSingle();
+            ChromaticAberration = binaryReader.ReadSingle();
+            SetEffectActive(binaryReader.ReadBoolean());
+        }
+
         public void Activate()
         {
             if (Vignette == null)

+ 35 - 1
COM3D2.MeidoPhotoStudio.Plugin/MeidoPhotoStudio/Managers/EnvironmentManager.cs

@@ -3,8 +3,9 @@ using UnityEngine;
 
 namespace COM3D2.MeidoPhotoStudio.Plugin
 {
-    internal class EnvironmentManager
+    internal class EnvironmentManager : IManager
     {
+        public const string header = "ENVIRONMENT";
         private static bool cubeActive;
         public static bool CubeActive
         {
@@ -39,6 +40,7 @@ namespace COM3D2.MeidoPhotoStudio.Plugin
         private Transform bg;
         private CameraInfo cameraInfo;
         private DragPointBG bgDragPoint;
+        private string currentBGAsset = "Theater";
         private bool bgVisible = true;
         public bool BGVisible
         {
@@ -55,6 +57,37 @@ namespace COM3D2.MeidoPhotoStudio.Plugin
             DragPointLight.environmentManager = this;
         }
 
+        public void Serialize(System.IO.BinaryWriter binaryWriter)
+        {
+            binaryWriter.Write(header);
+            binaryWriter.Write(currentBGAsset);
+            binaryWriter.WriteVector3(this.bg.position);
+            binaryWriter.WriteQuaternion(this.bg.rotation);
+            binaryWriter.WriteVector3(this.bg.localScale);
+
+            CameraMain camera = GameMain.Instance.MainCamera;
+            binaryWriter.WriteVector3(camera.GetTargetPos());
+            binaryWriter.Write(camera.GetDistance());
+            binaryWriter.WriteQuaternion(camera.transform.rotation);
+            StopCameraSpin();
+        }
+
+        public void Deserialize(System.IO.BinaryReader binaryReader)
+        {
+            string bgAsset = binaryReader.ReadString();
+            int assetIndex = Constants.BGList.FindIndex(bg => bg == bgAsset);
+            ChangeBackground(bgAsset, assetIndex > Constants.MyRoomCustomBGIndex);
+            this.bg.position = binaryReader.ReadVector3();
+            this.bg.rotation = binaryReader.ReadQuaternion();
+            this.bg.localScale = binaryReader.ReadVector3();
+
+            CameraMain camera = GameMain.Instance.MainCamera;
+            camera.SetTargetPos(binaryReader.ReadVector3());
+            camera.SetDistance(binaryReader.ReadSingle());
+            camera.transform.rotation = binaryReader.ReadQuaternion();
+            StopCameraSpin();
+        }
+
         public void Activate()
         {
             bgObject = GameObject.Find("__GameMain__/BG");
@@ -146,6 +179,7 @@ namespace COM3D2.MeidoPhotoStudio.Plugin
 
         public void ChangeBackground(string assetName, bool creative = false)
         {
+            currentBGAsset = assetName;
             if (creative)
             {
                 GameMain.Instance.BgMgr.ChangeBgMyRoom(assetName);

+ 11 - 0
COM3D2.MeidoPhotoStudio.Plugin/MeidoPhotoStudio/Managers/IManager.cs

@@ -0,0 +1,11 @@
+namespace COM3D2.MeidoPhotoStudio.Plugin
+{
+    internal interface IManager
+    {
+        void Update();
+        void Activate();
+        void Deactivate();
+        void Serialize(System.IO.BinaryWriter binaryWriter);
+        void Deserialize(System.IO.BinaryReader binaryReader);
+    }
+}

+ 26 - 1
COM3D2.MeidoPhotoStudio.Plugin/MeidoPhotoStudio/Managers/LightManager.cs

@@ -5,8 +5,9 @@ using UnityEngine;
 
 namespace COM3D2.MeidoPhotoStudio.Plugin
 {
-    internal class LightManager
+    internal class LightManager : IManager
     {
+        public const string header = "LIGHT";
         private List<DragPointLight> lightList = new List<DragPointLight>();
         private int selectedLightIndex = 0;
         public int SelectedLightIndex
@@ -33,6 +34,28 @@ namespace COM3D2.MeidoPhotoStudio.Plugin
         public event EventHandler Select;
         // TODO: enabling and disabling gizmos for a variety of dragpoints
 
+        public void Serialize(System.IO.BinaryWriter binaryWriter)
+        {
+            binaryWriter.Write(header);
+            binaryWriter.Write(lightList.Count);
+            foreach (DragPointLight light in lightList)
+            {
+                light.Serialize(binaryWriter);
+            }
+        }
+
+        public void Deserialize(System.IO.BinaryReader binaryReader)
+        {
+            ClearLights();
+            int numberOfLights = binaryReader.ReadInt32();
+            lightList[0].Deserialize(binaryReader);
+            for (int i = 1; i < numberOfLights; i++)
+            {
+                AddLight();
+                lightList[i].Deserialize(binaryReader);
+            }
+        }
+
         public void Activate()
         {
             GameMain.Instance.MainCamera.GetComponent<Camera>().backgroundColor = Color.black;
@@ -55,6 +78,8 @@ namespace COM3D2.MeidoPhotoStudio.Plugin
             DragPointLight.SetLightProperties(mainLight, new LightProperty());
         }
 
+        public void Update() { }
+
         public void AddLight(GameObject lightGo = null, bool isMain = false)
         {
             GameObject go = lightGo ?? new GameObject();

+ 31 - 1
COM3D2.MeidoPhotoStudio.Plugin/MeidoPhotoStudio/Managers/MeidoManager.cs

@@ -5,8 +5,9 @@ using UnityEngine;
 
 namespace COM3D2.MeidoPhotoStudio.Plugin
 {
-    internal class MeidoManager
+    internal class MeidoManager : IManager
     {
+        public const string header = "MEIDO";
         private static CharacterMgr characterMgr = GameMain.Instance.CharacterMgr;
         private int undress = 0;
         private int numberOfMeidos;
@@ -59,6 +60,35 @@ namespace COM3D2.MeidoPhotoStudio.Plugin
             if (Input.GetKeyDown(KeyCode.H)) UndressAll();
         }
 
+        public void Serialize(System.IO.BinaryWriter binaryWriter)
+        {
+            binaryWriter.Write(header);
+            // Only true for MM scenes converted to MPS scenes
+            binaryWriter.Write(false);
+            binaryWriter.Write(ActiveMeidoList.Count);
+            foreach (Meido meido in ActiveMeidoList)
+            {
+                meido.Serialize(binaryWriter);
+            }
+        }
+
+        public void Deserialize(System.IO.BinaryReader binaryReader)
+        {
+            bool isMMScene = binaryReader.ReadBoolean();
+            int numberOfMaids = binaryReader.ReadInt32();
+            for (int i = 0; i < numberOfMaids; i++)
+            {
+                if (i >= ActiveMeidoList.Count)
+                {
+                    Int64 skip = binaryReader.ReadInt64(); // meido buffer length
+                    binaryReader.BaseStream.Seek(skip, System.IO.SeekOrigin.Current);
+                    continue;
+                }
+                Meido meido = ActiveMeidoList[i];
+                meido.Deserialize(binaryReader, isMMScene);
+            }
+        }
+
         private void UnloadMeidos()
         {
             SelectedMeido = 0;

+ 28 - 1
COM3D2.MeidoPhotoStudio.Plugin/MeidoPhotoStudio/Managers/MessageWindowManager.cs

@@ -1,9 +1,11 @@
+using System.Reflection;
 using UnityEngine;
 
 namespace COM3D2.MeidoPhotoStudio.Plugin
 {
-    internal class MessageWindowManager
+    internal class MessageWindowManager : IManager
     {
+        public const string header = "TEXTBOX";
         public static readonly SliderProp fontBounds = new SliderProp(25f, 60f);
         private static GameObject sysRoot;
         private MessageClass msgClass;
@@ -12,6 +14,8 @@ namespace COM3D2.MeidoPhotoStudio.Plugin
         private UILabel nameLabel;
         private GameObject msgGameObject;
         public bool ShowingMessage { get; private set; }
+        private string messageName;
+        private string messageText;
 
         public MessageWindowManager()
         {
@@ -38,6 +42,27 @@ namespace COM3D2.MeidoPhotoStudio.Plugin
             SetPhotoMessageWindowActive(false);
         }
 
+        public void Serialize(System.IO.BinaryWriter binaryWriter)
+        {
+            binaryWriter.Write(header);
+            binaryWriter.Write(ShowingMessage);
+            binaryWriter.Write(this.msgLabel.fontSize);
+            binaryWriter.WriteNullableString(messageName);
+            binaryWriter.WriteNullableString(messageText);
+        }
+
+        public void Deserialize(System.IO.BinaryReader binaryReader)
+        {
+            CloseMessagePanel();
+            bool showingMessage = binaryReader.ReadBoolean();
+            this.msgLabel.fontSize = binaryReader.ReadInt32();
+            messageName = binaryReader.ReadNullableString();
+            messageText = binaryReader.ReadNullableString();
+            if (showingMessage) ShowMessage(messageName, messageText);
+        }
+
+        public void Update() { }
+
         private void SetPhotoMessageWindowActive(bool active)
         {
             UTY.GetChildObject(this.msgGameObject, "MessageViewer/MsgParent/MessageBox", false)
@@ -69,6 +94,8 @@ namespace COM3D2.MeidoPhotoStudio.Plugin
 
         public void ShowMessage(string name, string message)
         {
+            messageName = name;
+            messageText = message;
             ShowingMessage = true;
             this.msgWnd.OpenMessageWindowPanel();
             this.msgLabel.ProcessText();

+ 194 - 49
COM3D2.MeidoPhotoStudio.Plugin/MeidoPhotoStudio/Managers/PropManager.cs

@@ -4,11 +4,12 @@ using System.Linq;
 using UnityEngine;
 using UnityEngine.Rendering;
 
-
 namespace COM3D2.MeidoPhotoStudio.Plugin
 {
-    internal class PropManager
+    using static MenuFileUtility;
+    internal class PropManager : IManager
     {
+        public const string header = "PROP";
         private MeidoManager meidoManager;
         private static bool cubeActive = true;
         public static bool CubeActive
@@ -55,7 +56,128 @@ namespace COM3D2.MeidoPhotoStudio.Plugin
         {
             this.meidoManager = meidoManager;
             this.meidoManager.BeginCallMeidos += DetachProps;
-            this.meidoManager.EndCallMeidos += ReattachProps;
+            this.meidoManager.EndCallMeidos += OnEndCall;
+        }
+
+        public void Serialize(System.IO.BinaryWriter binaryWriter)
+        {
+            binaryWriter.Write(header);
+            binaryWriter.Write(doguList.Count);
+            foreach (DragPointDogu dogu in doguList)
+            {
+                binaryWriter.Write(dogu.assetName);
+                AttachPointInfo info = dogu.attachPointInfo;
+                info.Serialize(binaryWriter);
+                binaryWriter.WriteVector3(dogu.MyObject.position);
+                binaryWriter.WriteQuaternion(dogu.MyObject.rotation);
+                binaryWriter.WriteVector3(dogu.MyObject.localScale);
+            }
+        }
+
+        public void Deserialize(System.IO.BinaryReader binaryReader)
+        {
+            Dictionary<string, string> modToModPath = null;
+            ClearDogu();
+            int numberOfProps = binaryReader.ReadInt32();
+            for (int i = 0; i < numberOfProps; i++)
+            {
+                string assetName = binaryReader.ReadString();
+                bool result = false;
+                if (assetName.EndsWith(".menu"))
+                {
+                    if (assetName.Contains('#'))
+                    {
+                        if (modToModPath == null)
+                        {
+                            modToModPath = new Dictionary<string, string>(StringComparer.InvariantCultureIgnoreCase);
+                            foreach (string mod in Menu.GetModFiles())
+                            {
+                                modToModPath.Add(System.IO.Path.GetFileName(mod), mod);
+                            }
+                        }
+
+                        string[] assetParts = assetName.Split('#');
+                        ModItem item = new ModItem()
+                        {
+                            MenuFile = modToModPath[assetParts[0]],
+                            BaseMenuFile = assetParts[1],
+                            IsMod = true,
+                            IsOfficialMod = true
+                        };
+                        result = SpawnModItemProp(item);
+                    }
+                    else
+                    {
+                        if (assetName.StartsWith("handitem")) result = SpawnObject(assetName);
+                        else result = SpawnModItemProp(new ModItem() { MenuFile = assetName });
+                    }
+                }
+                else if (assetName.StartsWith("MYR_"))
+                {
+                    string[] assetParts = assetName.Split('#');
+                    int id = int.Parse(assetParts[0].Substring(4));
+                    string prefabName;
+                    if (assetParts.Length == 2 && !string.IsNullOrEmpty(assetParts[1])) prefabName = assetParts[1];
+                    else
+                    {
+                        // deserialize modifiedMM and maybe MM 23.0+.
+                        MyRoomCustom.PlacementData.Data data = MyRoomCustom.PlacementData.GetData(id);
+                        prefabName = !string.IsNullOrEmpty(data.resourceName) ? data.resourceName : data.assetName;
+                    }
+                    result = SpawnMyRoomProp(new MyRoomItem() { ID = id, PrefabName = prefabName });
+                }
+                else if (assetName.StartsWith("BG_")) result = SpawnBG(assetName);
+                else result = SpawnObject(assetName);
+
+                AttachPointInfo info = AttachPointInfo.Deserialize(binaryReader);
+
+                Vector3 position = binaryReader.ReadVector3();
+                Quaternion rotation = binaryReader.ReadQuaternion();
+                Vector3 scale = binaryReader.ReadVector3();
+                if (result)
+                {
+                    DragPointDogu dogu = doguList[i];
+                    Transform obj = dogu.MyObject;
+                    obj.position = position;
+                    obj.rotation = rotation;
+                    obj.localScale = scale;
+                    dogu.attachPointInfo = info;
+                }
+            }
+            GameMain.Instance.StartCoroutine(DeserializeAttach());
+        }
+
+        private System.Collections.IEnumerator DeserializeAttach()
+        {
+            yield return new WaitForEndOfFrame();
+
+            foreach (DragPointDogu dogu in doguList)
+            {
+                AttachPointInfo info = dogu.attachPointInfo;
+                if (info.AttachPoint != AttachPoint.None)
+                {
+                    Meido parent = meidoManager.GetMeido(info.MaidIndex);
+                    if (parent != null)
+                    {
+                        Transform obj = dogu.MyObject;
+                        Vector3 position = obj.position;
+                        Vector3 scale = obj.localScale;
+                        Quaternion rotation = obj.rotation;
+
+                        Transform point = parent.IKManager.GetAttachPointTransform(info.AttachPoint);
+                        dogu.MyObject.SetParent(point, true);
+                        info = new AttachPointInfo(
+                            info.AttachPoint,
+                            parent.Maid.status.guid,
+                            parent.Slot
+                        );
+
+                        obj.position = position;
+                        obj.localScale = scale;
+                        obj.rotation = rotation;
+                    }
+                }
+            }
         }
 
         public void Activate()
@@ -65,13 +187,23 @@ namespace COM3D2.MeidoPhotoStudio.Plugin
 
         public void Deactivate()
         {
+            ClearDogu();
+            CubeSmallChange -= OnCubeSmall;
+        }
+
+        public void Update() { }
+
+        private void ClearDogu()
+        {
             foreach (DragPointDogu dogu in doguList)
             {
-                dogu.Delete -= DeleteDogu;
-                GameObject.Destroy(dogu.gameObject);
+                if (dogu != null)
+                {
+                    dogu.Delete -= DeleteDogu;
+                    GameObject.Destroy(dogu.gameObject);
+                }
             }
             doguList.Clear();
-            CubeSmallChange -= OnCubeSmall;
         }
 
         private GameObject GetDeploymentObject()
@@ -80,23 +212,27 @@ namespace COM3D2.MeidoPhotoStudio.Plugin
                 ?? new GameObject("Deployment Object Parent");
         }
 
-        public void SpawnModItemProp(MenuFileUtility.ModItem modItem)
+        public bool SpawnModItemProp(ModItem modItem)
         {
             GameObject dogu = MenuFileUtility.LoadModel(modItem);
-            string name = modItem.Name;
-            if (dogu != null) AttachDragPoint(dogu, name, new Vector3(0f, 0f, 0.5f));
+            string name = modItem.MenuFile;
+            if (dogu != null) AttachDragPoint(dogu, modItem.ToString(), name, new Vector3(0f, 0f, 0.5f));
+            return dogu != null;
         }
 
-        public void SpawnMyRoomProp(MenuFileUtility.MyRoomItem item)
+        public bool SpawnMyRoomProp(MyRoomItem item)
         {
             MyRoomCustom.PlacementData.Data data = MyRoomCustom.PlacementData.GetData(item.ID);
             GameObject dogu = GameObject.Instantiate(data.GetPrefab());
             string name = Translation.Get("myRoomPropNames", item.PrefabName);
+            if (dogu != null) AttachDragPoint(dogu, item.ToString(), name, new Vector3(0f, 0f, 0.5f));
             else Utility.LogInfo($"Could not load MyRoomCreative prop '{item.PrefabName}'");
+            return dogu != null;
         }
 
-        public void SpawnBG(string assetName)
+        public bool SpawnBG(string assetName)
         {
+            if (assetName.StartsWith("BG_")) assetName = assetName.Substring(3);
             GameObject obj = GameMain.Instance.BgMgr.CreateAssetBundle(assetName)
                 ?? Resources.Load<GameObject>("BG/" + assetName)
                 ?? Resources.Load<GameObject>("BG/2_0/" + assetName);
@@ -106,11 +242,12 @@ namespace COM3D2.MeidoPhotoStudio.Plugin
                 GameObject dogu = GameObject.Instantiate(obj);
                 string name = Translation.Get("bgNames", assetName);
                 dogu.transform.localScale = Vector3.one * 0.1f;
-                AttachDragPoint(dogu, name, Vector3.zero);
+                AttachDragPoint(dogu, $"BG_{assetName}", name, Vector3.zero);
             }
+            return obj != null;
         }
 
-        public void SpawnObject(string assetName)
+        public bool SpawnObject(string assetName)
         {
             // TODO: Add a couple more things to ignore list
             GameObject dogu = null;
@@ -121,10 +258,7 @@ namespace COM3D2.MeidoPhotoStudio.Plugin
             {
                 dogu = MenuFileUtility.LoadModel(assetName);
                 string handItem = Utility.HandItemToOdogu(assetName);
-                if (Translation.Has("propNames", handItem))
-                {
-                    doguName = Translation.Get("propNames", handItem);
-                }
+                if (Translation.Has("propNames", handItem)) doguName = Translation.Get("propNames", handItem);
             }
             else if (assetName.StartsWith("mirror"))
             {
@@ -159,43 +293,45 @@ namespace COM3D2.MeidoPhotoStudio.Plugin
                 string[] assetParts = assetName.Split(':');
                 GameObject obj = GameMain.Instance.BgMgr.CreateAssetBundle(assetParts[0])
                     ?? Resources.Load<GameObject>("BG/" + assetParts[0]);
-
-                GameObject bg = GameObject.Instantiate(obj);
-                int num = int.Parse(assetParts[1]);
-                dogu = bg.transform.GetChild(num).gameObject;
-                dogu.transform.SetParent(null);
-                GameObject.Destroy(bg);
-                bg.SetActive(false);
+                try
+                {
+                    GameObject bg = GameObject.Instantiate(obj);
+                    int num = int.Parse(assetParts[1]);
+                    dogu = bg.transform.GetChild(num).gameObject;
+                    dogu.transform.SetParent(null);
+                    GameObject.Destroy(bg);
+                }
+                catch { }
             }
             else
             {
                 GameObject obj = GameMain.Instance.BgMgr.CreateAssetBundle(assetName)
                     ?? Resources.Load<GameObject>("Prefab/" + assetName);
-
-                dogu = GameObject.Instantiate<GameObject>(obj);
-                dogu.transform.localPosition = Vector3.zero;
-
-                MeshRenderer[] meshRenderers = dogu.GetComponentsInChildren<MeshRenderer>();
-                for (int i = 0; i < meshRenderers.Length; i++)
+                try
                 {
-                    if (meshRenderers[i] != null
-                        && meshRenderers[i].gameObject.name.ToLower().IndexOf("castshadow") < 0
-                    )
+                    dogu = GameObject.Instantiate<GameObject>(obj);
+                    dogu.transform.localPosition = Vector3.zero;
+
+                    MeshRenderer[] meshRenderers = dogu.GetComponentsInChildren<MeshRenderer>();
+                    for (int i = 0; i < meshRenderers.Length; i++)
                     {
-                        meshRenderers[i].shadowCastingMode = ShadowCastingMode.Off;
+                        if (meshRenderers[i] != null
+                            && meshRenderers[i].gameObject.name.ToLower().IndexOf("castshadow") < 0
+                        ) meshRenderers[i].shadowCastingMode = ShadowCastingMode.Off;
                     }
-                }
 
-                Collider collider = dogu.transform.GetComponent<Collider>();
-                if (collider != null) collider.enabled = false;
-                foreach (Transform transform in dogu.transform)
-                {
-                    collider = transform.GetComponent<Collider>();
-                    if (collider != null)
+                    Collider collider = dogu.transform.GetComponent<Collider>();
+                    if (collider != null) collider.enabled = false;
+                    foreach (Transform transform in dogu.transform)
                     {
-                        collider.enabled = false;
+                        collider = transform.GetComponent<Collider>();
+                        if (collider != null)
+                        {
+                            collider.enabled = false;
+                        }
                     }
                 }
+                catch { }
                 #region particle system experiment
                 // if (asset.StartsWith("Particle/"))
                 // {
@@ -229,15 +365,17 @@ namespace COM3D2.MeidoPhotoStudio.Plugin
 
             if (dogu != null)
             {
-                AttachDragPoint(dogu, doguName, doguPosition);
+                AttachDragPoint(dogu, assetName, doguName, doguPosition);
+                return true;
             }
             else
             {
                 Utility.LogInfo($"Could not spawn object '{assetName}'");
             }
+            return false;
         }
 
-        private void AttachDragPoint(GameObject dogu, string name, Vector3 position)
+        private void AttachDragPoint(GameObject dogu, string assetName, string name, Vector3 position)
         {
             // TODO: Figure out why some props aren't centred properly
             // Doesn't happen in MM but even after copy pasting the code, it doesn't work :/
@@ -258,6 +396,7 @@ namespace COM3D2.MeidoPhotoStudio.Plugin
             dragDogu.ConstantScale = true;
             dragDogu.Delete += DeleteDogu;
             dragDogu.DragPointScale = CubeSmall ? DragPointGeneral.smallCube : 1f;
+            dragDogu.assetName = assetName;
 
             doguList.Add(dragDogu);
             OnDoguListChange();
@@ -292,10 +431,9 @@ namespace COM3D2.MeidoPhotoStudio.Plugin
                 maidIndex: meido == null ? -1 : meido.Slot
             );
 
-            worldPositionStays = meido == null ? true : worldPositionStays;
-
             Vector3 position = dogu.transform.position;
             Quaternion rotation = dogu.transform.rotation;
+            Vector3 scale = dogu.transform.localScale;
 
             dogu.transform.SetParent(attachPointTransform, worldPositionStays);
 
@@ -310,6 +448,8 @@ namespace COM3D2.MeidoPhotoStudio.Plugin
                 dogu.transform.rotation = Quaternion.identity;
             }
 
+            dogu.transform.localScale = scale;
+
             if (meido == null) Utility.FixGameObjectScale(dogu);
         }
 
@@ -324,12 +464,17 @@ namespace COM3D2.MeidoPhotoStudio.Plugin
             }
         }
 
-        private void ReattachProps(object sender, EventArgs args)
+        private void OnEndCall(object sender, EventArgs args) => ReattachProps(useGuid: true);
+
+        private void ReattachProps(bool useGuid, bool forceStay = false)
         {
             foreach (DragPointDogu dragDogu in doguList)
             {
-                Meido meido = this.meidoManager.GetMeido(dragDogu.attachPointInfo.MaidGuid);
-                bool worldPositionStays = meido == null;
+                AttachPointInfo info = dragDogu.attachPointInfo;
+                Meido meido = useGuid
+                    ? this.meidoManager.GetMeido(info.MaidGuid)
+                    : this.meidoManager.GetMeido(info.MaidIndex);
+                bool worldPositionStays = forceStay || meido == null;
                 AttachProp(dragDogu, dragDogu.attachPointInfo.AttachPoint, meido, worldPositionStays);
             }
         }

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

@@ -376,6 +376,188 @@ namespace COM3D2.MeidoPhotoStudio.Plugin
         {
             this.UpdateMeido?.Invoke(this, args ?? MeidoUpdateEventArgs.Empty);
         }
+
+        public void Serialize(BinaryWriter binaryWriter)
+        {
+            using (MemoryStream memoryStream = new MemoryStream())
+            using (BinaryWriter tempWriter = new BinaryWriter(memoryStream))
+            {
+                // transform
+                tempWriter.WriteVector3(Maid.transform.position);
+                tempWriter.WriteQuaternion(Maid.transform.rotation);
+                tempWriter.WriteVector3(Maid.transform.localScale);
+                // pose
+                byte[] poseBuffer = SerializePose(true);
+                tempWriter.Write(poseBuffer.Length);
+                tempWriter.Write(poseBuffer);
+                CachedPose.Serialize(tempWriter);
+                // eye direction
+                tempWriter.WriteQuaternion(Body.quaDefEyeL * Quaternion.Inverse(DefaultEyeRotL));
+                tempWriter.WriteQuaternion(Body.quaDefEyeR * Quaternion.Inverse(DefaultEyeRotR));
+                // free look
+                tempWriter.Write(FreeLook);
+                tempWriter.WriteVector3(Body.offsetLookTarget);
+                // face
+                SerializeFace(tempWriter);
+                // body visible
+                tempWriter.Write(Body.GetMask(SlotID.body));
+                // clothing
+                foreach (SlotID clothingSlot in MaidDressingPane.clothingSlots)
+                {
+                    bool value = true;
+                    if (clothingSlot == SlotID.wear)
+                    {
+                        if (MaidDressingPane.wearSlots.Any(slot => Body.GetSlotLoaded(slot)))
+                        {
+                            value = MaidDressingPane.wearSlots.Any(slot => Body.GetMask(slot));
+                        }
+                    }
+                    else if (clothingSlot == SlotID.megane)
+                    {
+                        SlotID[] slots = new[] { SlotID.megane, SlotID.accHead };
+                        if (slots.Any(slot => Body.GetSlotLoaded(slot)))
+                        {
+                            value = slots.Any(slot => Body.GetMask(slot));
+                        }
+                    }
+                    else if (Body.GetSlotLoaded(clothingSlot))
+                    {
+                        value = Body.GetMask(clothingSlot);
+                    }
+                    tempWriter.Write(value);
+                }
+                // zurashi and mekure
+                tempWriter.Write(CurlingFront);
+                tempWriter.Write(CurlingBack);
+                tempWriter.Write(PantsuShift);
+
+                binaryWriter.Write(memoryStream.Length);
+                binaryWriter.Write(memoryStream.ToArray());
+            }
+        }
+
+        private void SerializeFace(BinaryWriter binaryWriter)
+        {
+            binaryWriter.Write("MPS_FACE");
+            TMorph morph = Maid.body0.Face.morph;
+            bool gp01FBFace = morph.bodyskin.PartsVersion >= 120;
+            foreach (string hash in faceKeys)
+            {
+                float[] blendValues = hash.StartsWith("eyeclose") && !(gp01FBFace && (hash == "eyeclose3"))
+                    ? this.BlendValuesBackup
+                    : this.BlendValues;
+
+                string faceKey = Utility.GP01FbFaceHash(morph, hash);
+                try
+                {
+                    float value = blendValues[(int)morph.hash[hash]];
+                    binaryWriter.Write(hash);
+                    binaryWriter.Write(value);
+                }
+                catch { }
+            }
+
+            foreach (string hash in faceToggleKeys)
+            {
+                bool value = this.BlendValues[(int)morph.hash[hash]] > 0f;
+                if (hash == "nosefook") value = morph.boNoseFook;
+
+                binaryWriter.Write(hash);
+                binaryWriter.Write(value);
+            }
+            binaryWriter.Write("END_FACE");
+        }
+
+        public void Deserialize(BinaryReader binaryReader, bool mmScene = false)
+        {
+            Maid.GetAnimation().Stop();
+            binaryReader.ReadInt64(); // meido buffer length
+            // transform
+            Maid.transform.position = binaryReader.ReadVector3();
+            Maid.transform.rotation = binaryReader.ReadQuaternion();
+            Maid.transform.localScale = binaryReader.ReadVector3();
+            // pose
+            if (mmScene) IKManager.Deserialize(binaryReader);
+            else
+            {
+                int poseBufferLength = binaryReader.ReadInt32();
+                byte[] poseBuffer = binaryReader.ReadBytes(poseBufferLength);
+                GetCacheBoneData().SetFrameBinary(poseBuffer);
+            }
+            CachedPose = PoseInfo.Deserialize(binaryReader);
+            // eye direction
+            Body.quaDefEyeL = DefaultEyeRotL * binaryReader.ReadQuaternion();
+            Body.quaDefEyeR = DefaultEyeRotR * binaryReader.ReadQuaternion();
+            // free look
+            FreeLook = binaryReader.ReadBoolean();
+            Vector3 lookTarget = binaryReader.ReadVector3();
+            if (FreeLook) Body.offsetLookTarget = lookTarget;
+            // face
+            DeserializeFace(binaryReader);
+            // body visible
+            SetBodyMask(binaryReader.ReadBoolean());
+            // clothing
+            foreach (SlotID clothingSlot in MaidDressingPane.clothingSlots)
+            {
+                bool value = binaryReader.ReadBoolean();
+                if (clothingSlot == SlotID.wear)
+                {
+                    Body.SetMask(SlotID.wear, value);
+                    Body.SetMask(SlotID.mizugi, value);
+                    Body.SetMask(SlotID.onepiece, value);
+                }
+                else if (clothingSlot == SlotID.megane)
+                {
+                    Body.SetMask(SlotID.megane, value);
+                    Body.SetMask(SlotID.accHead, value);
+                }
+                else if (Body.GetSlotLoaded(clothingSlot))
+                {
+                    Body.SetMask(clothingSlot, value);
+                }
+            }
+            // zurashi and mekure
+            bool curlingFront = binaryReader.ReadBoolean();
+            bool curlingBack = binaryReader.ReadBoolean();
+            if (CurlingFront != curlingFront) SetCurling(Curl.front, curlingFront);
+            if (CurlingBack != curlingBack) SetCurling(Curl.back, curlingBack);
+            SetCurling(Curl.shift, binaryReader.ReadBoolean());
+            // OnUpdateMeido();
+        }
+
+        private void DeserializeFace(BinaryReader binaryReader)
+        {
+            binaryReader.ReadString(); // read face header
+            TMorph morph = Maid.body0.Face.morph;
+            bool gp01FBFace = morph.bodyskin.PartsVersion >= 120;
+            HashSet<string> faceKeys = new HashSet<string>(Meido.faceKeys);
+            string header;
+            while ((header = binaryReader.ReadString()) != "END_FACE")
+            {
+                if (faceKeys.Contains(header))
+                {
+                    float[] blendValues = header.StartsWith("eyeclose") && !(gp01FBFace && (header == "eyeclose3"))
+                        ? this.BlendValuesBackup
+                        : this.BlendValues;
+                    string hash = Utility.GP01FbFaceHash(morph, header);
+                    try
+                    {
+                        float value = binaryReader.ReadSingle();
+                        blendValues[(int)morph.hash[hash]] = value;
+                    }
+                    catch { }
+                }
+                else
+                {
+                    bool value = binaryReader.ReadBoolean();
+                    if (header == "nosefook") this.Maid.boNoseFook = value;
+                    else this.BlendValues[(int)morph.hash[header]] = value ? 1f : 0f;
+                }
+            }
+            Maid.boMabataki = false;
+            morph.EyeMabataki = 0f;
+            morph.FixBlendValues_Face();
+        }
     }
 
     public struct PoseInfo
@@ -389,5 +571,22 @@ namespace COM3D2.MeidoPhotoStudio.Plugin
             this.Pose = pose;
             this.CustomPose = customPose;
         }
+
+        public void Serialize(BinaryWriter binaryWriter)
+        {
+            binaryWriter.Write(PoseGroup);
+            binaryWriter.Write(Pose);
+            binaryWriter.Write(CustomPose);
+        }
+
+        public static PoseInfo Deserialize(BinaryReader binaryReader)
+        {
+            return new PoseInfo
+            (
+                binaryReader.ReadString(),
+                binaryReader.ReadString(),
+                binaryReader.ReadBoolean()
+            );
+        }
     }
 }

+ 34 - 0
COM3D2.MeidoPhotoStudio.Plugin/MeidoPhotoStudio/Meido/MeidoDragPointManager.cs

@@ -137,6 +137,25 @@ namespace COM3D2.MeidoPhotoStudio.Plugin
 
         public MeidoDragPointManager(Meido meido) => this.meido = meido;
 
+        public void Deserialize(BinaryReader binaryReader)
+        {
+            Bone[] bones = {
+                Bone.Hip, Bone.Pelvis, Bone.Spine, Bone.Spine0a, Bone.Spine1, Bone.Spine1a, Bone.Neck,
+                Bone.ClavicleL, Bone.ClavicleR, Bone.UpperArmL, Bone.UpperArmR, Bone.ForearmL, Bone.ForearmR,
+                Bone.ForearmL, Bone.ForearmR, Bone.ThighL, Bone.ThighR, Bone.CalfL, Bone.CalfR,
+                Bone.HandL, Bone.HandR, Bone.FootL, Bone.FootR
+            };
+            for (Bone bone = Bone.Finger0L; bone <= Bone.Toe2NubR; ++bone)
+            {
+                BoneTransform[bone].localRotation = binaryReader.ReadQuaternion();
+            }
+            foreach (Bone bone in bones)
+            {
+                BoneTransform[bone].rotation = binaryReader.ReadQuaternion();
+            }
+            BoneTransform[Bone.Hip].position = binaryReader.ReadVector3();
+        }
+
         public Transform GetAttachPointTransform(AttachPoint point)
         {
             if (point == AttachPoint.None) return null;
@@ -653,5 +672,20 @@ namespace COM3D2.MeidoPhotoStudio.Plugin
             this.MaidGuid = maidGuid;
             this.MaidIndex = maidIndex;
         }
+
+        public void Serialize(BinaryWriter binaryWriter)
+        {
+            binaryWriter.Write((int)AttachPoint);
+            binaryWriter.Write(MaidIndex);
+        }
+
+        public static AttachPointInfo Deserialize(BinaryReader binaryReader)
+        {
+            return new AttachPointInfo(
+                (AttachPoint)binaryReader.ReadInt32(),
+                String.Empty,
+                binaryReader.ReadInt32()
+            );
+        }
     }
 }

+ 109 - 2
COM3D2.MeidoPhotoStudio.Plugin/MeidoPhotoStudio/MeidoPhotoStudio.cs

@@ -1,7 +1,9 @@
+using System.IO;
 using System.Collections;
 using System.Collections.Generic;
 using UnityEngine;
 using UnityEngine.SceneManagement;
+using Ionic.Zlib;
 using BepInEx;
 
 namespace COM3D2.MeidoPhotoStudio.Plugin
@@ -13,6 +15,7 @@ namespace COM3D2.MeidoPhotoStudio.Plugin
         private const string pluginGuid = "com.habeebweeb.com3d2.meidophotostudio";
         public const string pluginName = "MeidoPhotoStudio";
         public const string pluginVersion = "0.0.0";
+        public const int sceneVersion = 1000;
         public static string pluginString = $"{pluginName} {pluginVersion}";
         private WindowManager windowManager;
         private MeidoManager meidoManager;
@@ -35,6 +38,104 @@ namespace COM3D2.MeidoPhotoStudio.Plugin
             SceneManager.sceneLoaded += OnSceneLoaded;
         }
 
+        public void Serialize(bool quickSave)
+        {
+            string sceneName = quickSave ? "mpstempscene" : $"mpsscene{System.DateTime.Now:yyyyMMddHHmmss}.scene";
+            string scenePath = Path.Combine(Constants.scenesPath, sceneName);
+            File.WriteAllBytes(scenePath, Serialize());
+        }
+
+        public byte[] Serialize()
+        {
+            if (meidoManager.Busy) return null;
+
+            MemoryStream memoryStream;
+
+            using (memoryStream = new MemoryStream())
+            using (DeflateStream deflateStream = new DeflateStream(memoryStream, CompressionMode.Compress))
+            using (BinaryWriter binaryWriter = new BinaryWriter(deflateStream, System.Text.Encoding.UTF8))
+            {
+                binaryWriter.Write("MPS_SCENE");
+                binaryWriter.Write(sceneVersion);
+
+                messageWindowManager.Serialize(binaryWriter);
+                effectManager.Serialize(binaryWriter);
+                environmentManager.Serialize(binaryWriter);
+                lightManager.Serialize(binaryWriter);
+                // meidomanager has to be serialized before propmanager because reattached props will be in the wrong
+                // place after a maid's pose is deserialized.
+                meidoManager.Serialize(binaryWriter);
+                propManager.Serialize(binaryWriter);
+
+                binaryWriter.Write("END");
+            }
+
+            return memoryStream.ToArray();
+        }
+
+        public void Deserialize()
+        {
+            string path = Path.Combine(Constants.scenesPath, "mpstempscene");
+            Deserialize(path);
+        }
+
+        public void Deserialize(string filePath)
+        {
+            if (meidoManager.Busy) return;
+
+            byte[] data = DeflateStream.UncompressBuffer(File.ReadAllBytes(filePath));
+
+            using (MemoryStream memoryStream = new MemoryStream(data))
+            using (BinaryReader binaryReader = new BinaryReader(memoryStream, System.Text.Encoding.UTF8))
+            {
+                try
+                {
+                    if (binaryReader.ReadString() != "MPS_SCENE") return;
+
+                    if (binaryReader.ReadInt32() > sceneVersion)
+                    {
+                        Utility.LogWarning($"'{filePath}' is made in a newer version of {pluginName}");
+                        return;
+                    }
+
+                    string previousHeader = string.Empty;
+                    string header;
+
+                    while ((header = binaryReader.ReadString()) != "END")
+                    {
+                        switch (header)
+                        {
+                            case MessageWindowManager.header:
+                                messageWindowManager.Deserialize(binaryReader);
+                                break;
+                            case EnvironmentManager.header:
+                                environmentManager.Deserialize(binaryReader);
+                                break;
+                            case MeidoManager.header:
+                                meidoManager.Deserialize(binaryReader);
+                                break;
+                            case PropManager.header:
+                                propManager.Deserialize(binaryReader);
+                                break;
+                            case LightManager.header:
+                                lightManager.Deserialize(binaryReader);
+                                break;
+                            case EffectManager.header:
+                                effectManager.Deserialize(binaryReader);
+                                break;
+                            default: throw new System.Exception($"Unknown header '{header}'. Last {previousHeader}");
+                        }
+                        previousHeader = header;
+                    }
+                }
+                catch (System.Exception e)
+                {
+                    Utility.LogError($"Failed to deserialize scene '{filePath}' because {e.Message}");
+                    return;
+                }
+            }
+        }
+
         private void Update()
         {
             if (currentScene == Constants.Scene.Daily)
@@ -46,8 +147,13 @@ namespace COM3D2.MeidoPhotoStudio.Plugin
                 }
 
                 if (active)
-                    bool qFlag = Input.GetKey(KeyCode.Q);
-                    if (!qFlag && Input.GetKeyDown(KeyCode.S))
+                {
+                    if (Utility.GetModKey(Utility.ModKey.Control))
+                    {
+                        if (Input.GetKeyDown(KeyCode.S)) Serialize(true);
+                        else if (Input.GetKeyDown(KeyCode.A)) Deserialize();
+                    }
+                    else if (!Input.GetKey(KeyCode.Q) && Input.GetKeyDown(KeyCode.S))
                     {
                         StartCoroutine(TakeScreenShot());
                     }
@@ -127,6 +233,7 @@ namespace COM3D2.MeidoPhotoStudio.Plugin
         private void OnSceneLoaded(Scene scene, LoadSceneMode sceneMode)
         {
             currentScene = (Constants.Scene)scene.buildIndex;
+            if (active) Deactivate();
             ResetCalcNearClip();
         }
 

+ 7 - 0
COM3D2.MeidoPhotoStudio.Plugin/MeidoPhotoStudio/MenuFileUtility.cs

@@ -764,6 +764,11 @@ namespace COM3D2.MeidoPhotoStudio.Plugin
             public bool IsMod { get; set; }
             public bool IsOfficialMod { get; set; }
 
+            public override string ToString()
+            {
+                return IsOfficialMod ? $"{Path.GetFileName(MenuFile)}#{BaseMenuFile}" : MenuFile;
+            }
+
             public static ModItem Deserialize(BinaryReader binaryReader)
             {
                 return new ModItem()
@@ -797,6 +802,8 @@ namespace COM3D2.MeidoPhotoStudio.Plugin
         {
             public int ID { get; set; }
             public string PrefabName { get; set; }
+
+            public override string ToString() => $"MYR_{ID}#{PrefabName}";
         }
 
         private class ModelInfo

+ 3 - 0
readme.md

@@ -6,6 +6,8 @@ A rewrite of MultipleMaids
 
 ### Required Libraries
 
+These libraries are all found in COM3D2's Managed folder
+
 Place these in a folder called `lib`
 
 * `Assembly-CSharp.dll`
@@ -13,4 +15,5 @@ Place these in a folder called `lib`
 * `Assembly-UnityScript-firstpass.dll`
 * `Newtonsoft.json.dll`
 * `UnityEngine.dll`
+* `Ionic.Zlib.dll`
 * `BepInEx.dll`