Browse Source

Merge branch 'converter-rewrite'

habeebweeb 1 year ago
parent
commit
67d8780bf4

+ 7 - 0
src/MeidoPhotoStudio.Converter/Converters/IConverter.cs

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

+ 134 - 0
src/MeidoPhotoStudio.Converter/Converters/MMConverter.cs

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

+ 101 - 0
src/MeidoPhotoStudio.Converter/Converters/MMPngConverter.cs

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

+ 53 - 0
src/MeidoPhotoStudio.Converter/MPSSceneSerializer.cs

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

+ 5 - 1
src/MeidoPhotoStudio.Converter/MeidoPhotoStudio.Converter.csproj

@@ -1,8 +1,9 @@
 <Project Sdk="Microsoft.NET.Sdk">
   <PropertyGroup>
     <TargetFramework>net35</TargetFramework>
-    <FrameworkPathOverride Condition="'$(TargetFramework)' == 'net35'">$(MSBuildProgramFiles32)\Reference Assemblies\Microsoft\Framework\.NETFramework\v3.5\Profile\Client</FrameworkPathOverride>
     <ProjectGuid>{19D28B0C-3537-4FEE-B7B3-1ABF70B16D5E}</ProjectGuid>
+    <LangVersion>9</LangVersion>
+    <Nullable>enable</Nullable>
   </PropertyGroup>
   <ItemGroup>
     <Reference Include="..\..\lib\Assembly-CSharp.dll" />
@@ -15,4 +16,7 @@
   <ItemGroup>
     <ProjectReference Include="..\MeidoPhotoStudio.Plugin\MeidoPhotoStudio.Plugin.csproj" />
   </ItemGroup>
+  <ItemGroup>
+    <PackageReference Include="LZMA-SDK" Version="18.1.0" />
+  </ItemGroup>
 </Project>

+ 40 - 0
src/MeidoPhotoStudio.Converter/MultipleMaids/ConversionUtility.cs

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

+ 39 - 0
src/MeidoPhotoStudio.Converter/MultipleMaids/MMConstants.cs

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

+ 14 - 0
src/MeidoPhotoStudio.Converter/MultipleMaids/MMScene.cs

@@ -0,0 +1,14 @@
+namespace MeidoPhotoStudio.Converter.MultipleMaids
+{
+    public class MMScene
+    {
+        public readonly string Data;
+        public readonly string? ScreenshotBase64;
+
+        public MMScene(string data, string? screenshotBase64)
+        {
+            Data = data;
+            ScreenshotBase64 = screenshotBase64;
+        }
+    }
+}

+ 819 - 0
src/MeidoPhotoStudio.Converter/MultipleMaids/MMSceneConverter.cs

@@ -0,0 +1,819 @@
+using System;
+using System.IO;
+using System.Linq;
+using System.Text;
+using MeidoPhotoStudio.Plugin;
+using MyRoomCustom;
+using UnityEngine;
+
+namespace MeidoPhotoStudio.Converter.MultipleMaids
+{
+    public static class MMSceneConverter
+    {
+        private const int ClavicleLIndex = 68;
+        private static readonly int[] BodyRotationIndices =
+        {
+            71, // Hip
+            44, // Pelvis
+            40, // Spine
+            41, // Spine0a
+            42, // Spine1
+            43, // Spine1a
+            57, // Neck
+            ClavicleLIndex, // Clavicle L
+            69, // Clavicle R
+            46, // UpperArm L
+            49, // UpperArm R
+            47, // ForeArm L
+            50, // ForeArm R
+            52, // Thigh L
+            55, // Thigh R
+            53, // Calf L
+            56, // Calf R
+            92, // Mune L
+            94, // Mune R
+            93, // MuneSub L
+            95, // MuneSub R
+            45, // Hand L
+            48, // Hand R
+            51, // Foot L
+            54, // Foot R
+        };
+        private static readonly int[] BodyRotationIndices64 =
+            BodyRotationIndices.Where(rotation => rotation < 64).ToArray();
+        private static readonly CameraInfo DefaultCameraInfo = new();
+        private static readonly LightProperty DefaultLightProperty = new();
+
+        public static byte[] Convert(string data, bool environment = false)
+        {
+            var dataSegments = data.Split('_');
+
+            using var memoryStream = new MemoryStream();
+            using var dataWriter = new BinaryWriter(memoryStream, Encoding.UTF8);
+
+            if (!environment)
+            {
+                ConvertMeido(dataSegments, dataWriter);
+                ConvertMessage(dataSegments, dataWriter);
+                ConvertCamera(dataSegments, dataWriter);
+            }
+
+            ConvertLight(dataSegments, dataWriter);
+            ConvertEffect(dataSegments, dataWriter);
+            ConvertEnvironment(dataSegments, dataWriter);
+            ConvertProps(dataSegments, dataWriter);
+
+            dataWriter.Write("END");
+
+            return memoryStream.ToArray();
+        }
+
+        public static SceneMetadata GetSceneMetadata(string data, bool environment = false)
+        {
+            var dataSegments = data.Split('_');
+            var strArray2 = dataSegments[1].Split(';');
+
+            var meidoCount = environment ? MeidoPhotoStudio.Plugin.MeidoPhotoStudio.kankyoMagic : strArray2.Length;
+
+            return new()
+            {
+                Version = 1,
+                Environment = environment,
+                MaidCount = meidoCount,
+                MMConverted = true,
+            };
+        }
+
+        private static void ConvertMeido(string[] data, BinaryWriter writer)
+        {
+            var strArray2 = data[1].Split(';');
+
+            writer.Write(MeidoManager.header);
+            // MeidoManagerSerializer version
+            writer.WriteVersion(1);
+
+            var meidoCount = strArray2.Length;
+
+            writer.Write(meidoCount);
+
+            var transformSerializer = Serialization.GetSimple<TransformDTO>();
+
+            foreach (var rawData in strArray2)
+            {
+                using var memoryStream = new MemoryStream();
+                using var tempWriter = new BinaryWriter(memoryStream, Encoding.UTF8);
+
+                var maidData = rawData.Split(':');
+
+                tempWriter.WriteVersion(1);
+
+                transformSerializer.Serialize(
+                    new()
+                    {
+                        Position = ConversionUtility.ParseVector3(maidData[59]),
+                        Rotation = ConversionUtility.ParseEulerAngle(maidData[58]),
+                        LocalScale = ConversionUtility.ParseVector3(maidData[60]),
+                    }, tempWriter
+                );
+
+                ConvertHead(maidData, tempWriter);
+                ConvertBody(maidData, tempWriter);
+                ConvertClothing(maidData, tempWriter);
+
+                writer.Write(memoryStream.Length);
+                writer.Write(memoryStream.ToArray());
+            }
+
+            ConvertGravity(data[0].Split(','), writer);
+
+            static void ConvertHead(string[] maidData, BinaryWriter writer)
+            {
+                // MeidoSerializer -> Head version
+                writer.WriteVersion(1);
+
+                var sixtyFourFlag = maidData.Length == 64;
+
+                // eye direction
+                // MM saves eye rotation directly which is garbage data for meido that don't use the same face model.
+                // A lot of users associate scenes with specific meido though so keeping the data is desirable.
+                var eyeRotationL = Quaternion.identity;
+                var eyeRotationR = Quaternion.identity;
+
+                if (!sixtyFourFlag)
+                {
+                    eyeRotationL = ConversionUtility.ParseEulerAngle(maidData[90]);
+                    eyeRotationR = ConversionUtility.ParseEulerAngle(maidData[91]);
+                }
+
+                writer.Write(eyeRotationL);
+                writer.Write(eyeRotationR);
+
+                // free look
+                if (sixtyFourFlag)
+                {
+                    writer.Write(false);
+                    writer.Write(new Vector3(0f, 1f, 0f));
+                }
+                else
+                {
+                    var freeLookData = maidData[64].Split(',');
+                    var isFreeLook = int.Parse(freeLookData[0]) == 1;
+
+                    writer.Write(isFreeLook);
+
+                    var offsetTarget = isFreeLook
+                        ? new(float.Parse(freeLookData[2]), 1f, float.Parse(freeLookData[1]))
+                        : new Vector3(0f, 1f, 0f);
+
+                    writer.Write(offsetTarget);
+                }
+
+                // HeadEulerAngle is used to save the head's facing rotation
+                // MM does not have this data.
+                writer.Write(Vector3.zero);
+
+                // head/eye to camera (Not changed by MM so always true)
+                writer.Write(true);
+                writer.Write(true);
+
+                // face
+                var faceValues = maidData[63].Split(',');
+                writer.Write(faceValues.Length);
+
+                for (var i = 0; i < faceValues.Length; i++)
+                {
+                    writer.Write(MMConstants.FaceKeys[i]);
+                    writer.Write(float.Parse(faceValues[i]));
+                }
+            }
+
+            static void ConvertBody(string[] maidData, BinaryWriter writer)
+            {
+                // MeidoSerializer -> Body version
+                writer.WriteVersion(1);
+
+                var sixtyFourFlag = maidData.Length == 64;
+
+                writer.Write(sixtyFourFlag);
+
+                // finger rotations
+                for (var i = 0; i < 40; i++)
+                    writer.Write(ConversionUtility.ParseEulerAngle(maidData[i]));
+
+                if (!sixtyFourFlag)
+                {
+                    // toe rotations
+                    for (var i = 0; i < 2; i++)
+                    for (var j = 72 + i; j < 90; j += 2)
+                        writer.Write(ConversionUtility.ParseEulerAngle(maidData[j]));
+                }
+
+                var rotationIndices = sixtyFourFlag ? BodyRotationIndices64 : BodyRotationIndices;
+
+                // body rotations
+                foreach (var index in rotationIndices)
+                {
+                    var rotation = Quaternion.identity;
+                    var data = maidData[index];
+
+                    // check special case for ClavicleL
+                    if (index == ClavicleLIndex)
+                    {
+                        /*
+                         * Versions of MM possibly serialized ClavicleL improperly.
+                         * At least I think that's what happened otherwise why would they make this check at all.
+                         * https://git.coder.horse/meidomustard/modifiedMM/src/master/MultipleMaids/CM3D2/MultipleMaids/Plugin/MultipleMaids.Update.cs#L4355
+                         *
+                         * Look at the way MM serializes rotations.
+                         * https://git.coder.horse/meidomustard/modifiedMM/src/master/MultipleMaids/CM3D2/MultipleMaids/Plugin/MultipleMaids.Update.cs#L2364
+                         * It is most definitely possible MM dev missed a component.
+                         *
+                         * Also why is strArray9.Length == 2 acceptable? If the length were only 2,
+                         * float.Parse(strArray9[2]) would throw an index out of range exception???
+                         */
+                        writer.Write(ConversionUtility.TryParseEulerAngle(data, out rotation));
+                    }
+                    else
+                        rotation = ConversionUtility.ParseEulerAngle(data);
+
+                    writer.Write(rotation);
+                }
+
+                // hip position
+                writer.Write(sixtyFourFlag ? Vector3.zero : ConversionUtility.ParseVector3(maidData[96]));
+
+                Serialization.GetSimple<PoseInfo>().Serialize(PoseInfo.DefaultPose, writer);
+            }
+
+            static void ConvertClothing(string[] maidData, BinaryWriter writer)
+            {
+                // MeidoSerializer -> Clothing version
+                writer.WriteVersion(1);
+
+                // MM does not serialize body visibility
+                writer.Write(true);
+
+                // MM does not serialize clothing visibility
+                for (var i = 0; i < MaidDressingPane.ClothingSlots.Length; i++)
+                    writer.Write(true);
+
+                // MM does not serialize curling/shift
+                writer.Write(false);
+                writer.Write(false);
+                writer.Write(false);
+
+                // MPN attach props
+                var kousokuUpperMenu = string.Empty;
+                var kousokuLowerMenu = string.Empty;
+
+                var sixtyFourFlag = maidData.Length == 64;
+
+                if (!sixtyFourFlag)
+                {
+                    var mpnIndex = int.Parse(maidData[65].Split(',')[0]);
+
+                    if (mpnIndex >= 9 && mpnIndex <= 16)
+                    {
+                        var actualIndex = mpnIndex - 9;
+
+                        if (mpnIndex == 12)
+                        {
+                            kousokuUpperMenu = MMConstants.MpnAttachProps[actualIndex];
+                            kousokuLowerMenu = MMConstants.MpnAttachProps[actualIndex - 1];
+                        }
+                        else if (mpnIndex == 13)
+                        {
+                            kousokuUpperMenu = MMConstants.MpnAttachProps[actualIndex + 1];
+                            kousokuLowerMenu = MMConstants.MpnAttachProps[actualIndex];
+                        }
+                        else
+                        {
+                            if (mpnIndex > 13) actualIndex++;
+                            var kousokuMenu = MMConstants.MpnAttachProps[actualIndex];
+
+                            if (MMConstants.MpnAttachProps[actualIndex][7] == 'u') kousokuUpperMenu = kousokuMenu;
+                            else kousokuLowerMenu = kousokuMenu;
+                        }
+                    }
+                }
+
+                writer.Write(!string.IsNullOrEmpty(kousokuUpperMenu));
+                writer.Write(kousokuUpperMenu);
+
+                writer.Write(!string.IsNullOrEmpty(kousokuLowerMenu));
+                writer.Write(kousokuLowerMenu);
+
+                // hair/skirt gravity
+                // If gravity is enabled at all in MM, it affects all maids.
+                // So it's like global gravity is enabled which overrides individual maid gravity settings.
+                writer.Write(false);
+                writer.Write(Vector3.zero);
+                writer.Write(false);
+                writer.Write(Vector3.zero);
+            }
+
+            static void ConvertGravity(string[] data, BinaryWriter writer)
+            {
+                var softG = new Vector3(
+                    float.Parse(data[12]), float.Parse(data[13]), float.Parse(data[14])
+                );
+
+                var hairGravityActive = softG != MMConstants.DefaultSoftG;
+                writer.Write(hairGravityActive);
+                // an approximation for hair gravity position
+                writer.Write(softG * 90f);
+                // MM does not serialize skirt gravity
+                writer.Write(Vector3.zero);
+            }
+        }
+
+        private static void ConvertMessage(string[] data, BinaryWriter writer)
+        {
+            const string newLine = "&kaigyo";
+
+            writer.Write(MessageWindowManager.header);
+            // MessageWindowManagerSerializer version
+            writer.WriteVersion(1);
+
+            var showingMessage = false;
+            var name = string.Empty;
+            var message = string.Empty;
+
+            var strArray3 = data[0].Split(',');
+
+            if (strArray3.Length > 16)
+            {
+                showingMessage = int.Parse(strArray3[34]) == 1;
+                name = strArray3[35];
+                message = strArray3[36].Replace(newLine, "\n");
+                // MM does not serialize message font size
+            }
+
+            writer.Write(showingMessage);
+            writer.Write((int)MessageWindowManager.FontBounds.Left);
+            writer.Write(name);
+            writer.Write(message);
+        }
+
+        private static void ConvertCamera(string[] data, BinaryWriter writer)
+        {
+            writer.Write(CameraManager.header);
+            // CameraManagerSerializer version
+            writer.WriteVersion(1);
+
+            // MM only has one camera
+            // current camera index
+            writer.Write(0);
+            // number of camera slots
+            writer.Write(1);
+
+            var strArray3 = data[0].Split(',');
+
+            var cameraTargetPos = DefaultCameraInfo.TargetPos;
+            var cameraDistance = DefaultCameraInfo.Distance;
+            var cameraRotation = DefaultCameraInfo.Angle;
+
+            if (strArray3.Length > 16)
+            {
+                cameraTargetPos = new(
+                    float.Parse(strArray3[27]), float.Parse(strArray3[28]), float.Parse(strArray3[29])
+                );
+
+                cameraDistance = float.Parse(strArray3[30]);
+
+                cameraRotation = Quaternion.Euler(
+                    float.Parse(strArray3[31]), float.Parse(strArray3[32]), float.Parse(strArray3[33])
+                );
+            }
+
+            Serialization.Get<CameraInfo>().Serialize(
+                new()
+                {
+                    TargetPos = cameraTargetPos,
+                    Angle = cameraRotation,
+                    Distance = cameraDistance,
+                }, writer
+            );
+        }
+
+        private static void ConvertLight(string[] data, BinaryWriter writer)
+        {
+            writer.Write(LightManager.header);
+            // LightManagerSerializer version
+            writer.WriteVersion(1);
+
+            var strArray3 = data[0].Split(',');
+            var greaterThan5 = data.Length >= 5;
+            var strArray4 = greaterThan5 ? data[2].Split(',') : null;
+            var strArray5 = greaterThan5 ? data[3].Split(';') : null;
+            var strArray7 = data.Length >= 6 ? data[5].Split(';') : null;
+
+            var numberOfLights = 1 + (strArray5?.Length - 1 ?? 0);
+
+            writer.Write(numberOfLights);
+
+            var lightPropertySerializer = Serialization.Get<LightProperty>();
+
+            /* Light Types
+                0 = Directional
+                1 = Spot
+                2 = Point
+                3 = Directional (Colour Mode)
+            */
+
+            if (strArray3.Length > 16)
+            {
+                // Main Light
+                var spotAngle = float.Parse(strArray3[25]);
+
+                var lightProperty = new LightProperty
+                {
+                    Rotation = Quaternion.Euler(
+                        float.Parse(strArray3[21]), float.Parse(strArray3[22]), float.Parse(strArray3[23])
+                    ),
+                    Intensity = float.Parse(strArray3[24]),
+                    // MM uses spotAngle for both range and spotAngle based on which light type is used
+                    SpotAngle = spotAngle,
+                    Range = spotAngle / 5f,
+                    ShadowStrength = strArray4 is null ? 0.098f : float.Parse(strArray4[0]),
+                    LightColour = new(
+                        float.Parse(strArray3[18]), float.Parse(strArray3[19]), float.Parse(strArray3[20]), 1f
+                    ),
+                };
+
+                var lightType = int.Parse(strArray3[17]);
+
+                // DragPointLightSerializer version
+                writer.WriteVersion(1);
+
+                for (var i = 0; i < 3; i++)
+                {
+                    if (i == lightType || i == 0 && lightType == 3)
+                        lightPropertySerializer.Serialize(lightProperty, writer);
+                    else
+                        lightPropertySerializer.Serialize(DefaultLightProperty, writer);
+                }
+
+                var lightPosition = strArray7 is null
+                    ? LightProperty.DefaultPosition
+                    : ConversionUtility.ParseVector3(strArray7[0]);
+
+                writer.Write(lightPosition);
+                // light type. 3 is colour mode which uses directional light type.
+                writer.Write(lightType == 3 ? 0 : lightType);
+                // colour mode
+                writer.Write(lightType == 3);
+                // MM lights cannot be disabled
+                writer.Write(false);
+            }
+            else
+            {
+                // Just write defaults if missing
+                // DragPointLightSerializer version
+                writer.WriteVersion(1);
+
+                for (var i = 0; i < 3; i++)
+                    lightPropertySerializer.Serialize(DefaultLightProperty, writer);
+
+                writer.Write(LightProperty.DefaultPosition);
+                writer.Write(0);
+                writer.Write(false);
+                writer.Write(false);
+            }
+
+            if (strArray5 is null)
+                return;
+
+            for (var i = 0; i < strArray5.Length - 1; i++)
+            {
+                var lightProperties = strArray5[i].Split(',');
+
+                var spotAngle = float.Parse(lightProperties[7]);
+
+                var lightProperty = new LightProperty
+                {
+                    Rotation = Quaternion.Euler(
+                        float.Parse(lightProperties[4]), float.Parse(lightProperties[5]), 18f
+                    ),
+                    Intensity = float.Parse(lightProperties[6]),
+                    SpotAngle = spotAngle,
+                    Range = spotAngle / 5f,
+                    // MM does not save shadow strength for other lights 
+                    ShadowStrength = 0.098f,
+                    LightColour = new(
+                        float.Parse(lightProperties[1]), float.Parse(lightProperties[2]),
+                        float.Parse(lightProperties[3]), 1f
+                    ),
+                };
+
+                var lightType = int.Parse(lightProperties[0]);
+
+                // DragPointLightSerializer version
+                writer.WriteVersion(1);
+
+                for (var j = 0; j < 3; j++)
+                    lightPropertySerializer.Serialize(j == lightType ? lightProperty : DefaultLightProperty, writer);
+
+                var lightPosition = strArray7 is null
+                    ? LightProperty.DefaultPosition
+                    : ConversionUtility.ParseVector3(strArray7[i + 1]);
+
+                writer.Write(lightPosition);
+                // light type. 3 is colour mode which uses directional light type.
+                writer.Write(lightType == 3 ? 0 : lightType);
+                // colour mode only applies to the main light
+                writer.Write(false);
+                // MM lights cannot be disabled
+                writer.Write(false);
+            }
+        }
+
+        private static void ConvertEffect(string[] data, BinaryWriter writer)
+        {
+            if (data.Length < 5) return;
+
+            writer.Write(EffectManager.header);
+            // EffectManagerSerializer version
+            writer.WriteVersion(1);
+
+            var effectData = data[2].Split(',');
+
+            // bloom
+            writer.Write(BloomEffectManager.header);
+            writer.WriteVersion(1);
+
+            writer.Write(int.Parse(effectData[1]) == 1); // active
+            writer.Write(float.Parse(effectData[2]) / 5.7f * 100f); // intensity
+            writer.Write((int)float.Parse(effectData[3])); // blur iterations
+
+            writer.WriteColour(
+                new(
+                    1f - float.Parse(effectData[4]), 1f - float.Parse(effectData[5]), 1f - float.Parse(effectData[6]),
+                    1f
+                )
+            ); // bloom threshold colour
+
+            writer.Write(int.Parse(effectData[7]) == 1); // hdr
+
+            // vignetting
+            writer.Write(VignetteEffectManager.header);
+            writer.WriteVersion(1);
+
+            writer.Write(int.Parse(effectData[8]) == 1); // active
+            writer.Write(float.Parse(effectData[9])); // intensity
+            writer.Write(float.Parse(effectData[10])); // blur
+            writer.Write(float.Parse(effectData[11])); // blur spread
+            writer.Write(float.Parse(effectData[12])); // chromatic aberration
+
+            // blur 
+            writer.Write(BlurEffectManager.header);
+            writer.WriteVersion(1);
+
+            var blurSize = float.Parse(effectData[13]);
+            writer.Write(blurSize > 0f); // active
+            writer.Write(blurSize); // blur size
+
+            // Sepia Tone
+            writer.Write(SepiaToneEffectManger.header);
+            writer.WriteVersion(1);
+
+            writer.Write(int.Parse(effectData[29]) == 1);
+
+            if (effectData.Length > 15)
+            {
+                // depth of field
+                writer.Write(DepthOfFieldEffectManager.header);
+                writer.WriteVersion(1);
+
+                writer.Write(int.Parse(effectData[15]) == 1); // active
+                writer.Write(float.Parse(effectData[16])); // focal length
+                writer.Write(float.Parse(effectData[17])); // focal size
+                writer.Write(float.Parse(effectData[18])); // aperture
+                writer.Write(float.Parse(effectData[19])); // max blur size
+                writer.Write(int.Parse(effectData[20]) == 1); // visualize focus
+
+                // fog
+                writer.Write(FogEffectManager.header);
+                writer.WriteVersion(1);
+
+                writer.Write(int.Parse(effectData[21]) == 1); // active
+                writer.Write(float.Parse(effectData[22])); // fog distance
+                writer.Write(float.Parse(effectData[23])); // density
+                writer.Write(float.Parse(effectData[24])); // height scale
+                writer.Write(float.Parse(effectData[25])); // height
+
+                // fog colour
+                writer.WriteColour(
+                    new(
+                        float.Parse(effectData[26]), float.Parse(effectData[27]), float.Parse(effectData[28]), 1f
+                    )
+                );
+            }
+
+            writer.Write(EffectManager.footer);
+        }
+
+        private static void ConvertEnvironment(string[] data, BinaryWriter writer)
+        {
+            writer.Write(EnvironmentManager.header);
+            // EnvironmentManagerSerializer version
+            writer.WriteVersion(1);
+
+            var environmentData = data[0].Split(',');
+
+            var bgAsset = EnvironmentManager.defaultBg;
+
+            if (!int.TryParse(environmentData[2], out _))
+                bgAsset = environmentData[2].Replace(' ', '_');
+
+            writer.Write(bgAsset);
+
+            Serialization.GetSimple<TransformDTO>()
+                .Serialize(
+                    new()
+                    {
+                        Position = new(
+                            float.Parse(environmentData[6]), float.Parse(environmentData[7]),
+                            float.Parse(environmentData[8])
+                        ),
+                        Rotation = Quaternion.Euler(
+                            float.Parse(environmentData[3]), float.Parse(environmentData[4]),
+                            float.Parse(environmentData[5])
+                        ),
+                        LocalScale = new(
+                            float.Parse(environmentData[9]), float.Parse(environmentData[10]),
+                            float.Parse(environmentData[11])
+                        ),
+                    }, writer
+                );
+        }
+
+        private static void ConvertProps(string[] data, BinaryWriter writer)
+        {
+            var strArray3 = data[0].Split(',');
+            var strArray6 = data.Length >= 5 ? data[4].Split(';') : null;
+
+            var hasWProp = strArray3.Length > 37 && !string.IsNullOrEmpty(strArray3[37]);
+            var propCount = strArray6?.Length - 1 ?? 0;
+            propCount += hasWProp ? 1 : 0;
+
+            writer.Write(PropManager.header);
+
+            // PropManagerSerializer version
+            writer.WriteVersion(1);
+
+            writer.Write(propCount);
+
+            var propSerializer = Serialization.GetSimple<DragPointPropDTO>();
+
+            if (hasWProp)
+            {
+                // Props that are spawned by pushing (shift +) W.
+                writer.WriteVersion(1);
+
+                var propDto = new DragPointPropDTO
+                {
+                    TransformDTO = new()
+                    {
+                        Position = new(
+                            float.Parse(strArray3[41]), float.Parse(strArray3[42]), float.Parse(strArray3[43])
+                        ),
+                        Rotation = Quaternion.Euler(
+                            float.Parse(strArray3[38]), float.Parse(strArray3[39]), float.Parse(strArray3[40])
+                        ),
+                        LocalScale =
+                            new(float.Parse(strArray3[44]), float.Parse(strArray3[45]), float.Parse(strArray3[46])),
+                    },
+                    AttachPointInfo = AttachPointInfo.Empty,
+                    PropInfo = AssetToPropInfo(strArray3[37]),
+                    ShadowCasting = false,
+                };
+
+                propSerializer.Serialize(propDto, writer);
+            }
+
+            if (strArray6 is null)
+                return;
+
+            for (var i = 0; i < strArray6.Length - 1; i++)
+            {
+                var prop = strArray6[i];
+                var assetParts = prop.Split(',');
+
+                var propInfo = AssetToPropInfo(assetParts[0]);
+
+                var propDto = new DragPointPropDTO
+                {
+                    PropInfo = propInfo,
+                    TransformDTO = new()
+                    {
+                        Position = new(
+                            float.Parse(assetParts[4]), float.Parse(assetParts[5]), float.Parse(assetParts[6])
+                        ),
+                        Rotation = Quaternion.Euler(
+                            float.Parse(assetParts[1]), float.Parse(assetParts[2]), float.Parse(assetParts[3])
+                        ),
+                        LocalScale =
+                            new(float.Parse(assetParts[7]), float.Parse(assetParts[8]), float.Parse(assetParts[9])),
+                    },
+                    AttachPointInfo = AttachPointInfo.Empty,
+                    ShadowCasting = propInfo.Type == PropInfo.PropType.Mod,
+                };
+
+                propSerializer.Serialize(propDto, writer);
+            }
+
+            static PropInfo AssetToPropInfo(string asset)
+            {
+                const string mmMyRoomPrefix = "creative_";
+                const string mm23MyRoomPrefix = "MYR_";
+                const string bgOdoguPrefix = "BGodogu";
+                const string bgAsPropPrefix = "BG_";
+
+                asset = ConvertSpaces(asset);
+
+                if (asset.StartsWith(mmMyRoomPrefix))
+                {
+                    // modifiedMM my room creative prop
+                    // modifiedMM serializes the prefabName rather than the ID.
+                    // Kinda dumb tbh who's idea was this anyway?
+
+                    asset = asset.Replace(mmMyRoomPrefix, string.Empty);
+
+                    return new(PropInfo.PropType.MyRoom)
+                    {
+                        MyRoomID = MMConstants.MyrAssetNameToData[asset].ID,
+                        Filename = asset,
+                    };
+                }
+
+                if (asset.StartsWith(mm23MyRoomPrefix))
+                {
+                    // MM 23.0+ my room creative prop
+                    var assetID = int.Parse(asset.Replace(mm23MyRoomPrefix, string.Empty));
+                    var placementData = PlacementData.GetData(assetID);
+
+                    var filename = string.IsNullOrEmpty(placementData.assetName)
+                        ? placementData.resourceName
+                        : placementData.assetName;
+
+                    return new(PropInfo.PropType.MyRoom)
+                    {
+                        MyRoomID = assetID,
+                        Filename = filename,
+                    };
+                }
+
+                if (asset.Contains('#'))
+                {
+                    if (!asset.Contains(".menu"))
+                    {
+                        // MM's dumb way of using one data structure to store both a human readable name and asset name
+                        // ex. 'Pancakes                    #odogu_pancake'
+                        return new(PropInfo.PropType.Odogu) { Filename = asset.Split('#')[1] };
+                    }
+
+                    // modifiedMM official COM3D2 mod prop
+                    var modComponents = asset.Split('#');
+                    var baseMenuFile = ConvertSpaces(modComponents[0]);
+                    var modMenuFile = ConvertSpaces(modComponents[1]);
+
+                    return new(PropInfo.PropType.Mod)
+                    {
+                        Filename = modMenuFile,
+                        SubFilename = baseMenuFile,
+                    };
+                }
+
+                if (asset.EndsWith(".menu"))
+                {
+                    var propType = PropInfo.PropType.Mod;
+
+                    // hand items are treated as game props (Odogu) in MPS
+                    if (asset.StartsWith("handitem", StringComparison.OrdinalIgnoreCase)
+                        || asset.StartsWith("kousoku", StringComparison.OrdinalIgnoreCase)
+                    ) propType = PropInfo.PropType.Odogu;
+
+                    return new(propType) { Filename = asset };
+                }
+
+                if (asset.StartsWith(bgOdoguPrefix, StringComparison.OrdinalIgnoreCase))
+                {
+                    // MM prepends BG to certain prop asset names. Don't know why.
+                    return new(PropInfo.PropType.Odogu) { Filename = asset.Substring(2) };
+                }
+
+                if (asset.StartsWith(bgAsPropPrefix))
+                {
+                    // game bg as prop
+                    return new(PropInfo.PropType.Bg) { Filename = asset.Substring(3) };
+                }
+
+                return new(PropInfo.PropType.Odogu) { Filename = asset };
+            }
+
+            // MM uses '_' as a separator for different parts of serialized data so it converts all '_' to spaces
+            static string ConvertSpaces(string @string) => @string.Replace(' ', '_');
+        }
+    }
+}

+ 46 - 0
src/MeidoPhotoStudio.Converter/Plugin.cs

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

+ 38 - 0
src/MeidoPhotoStudio.Converter/PluginCore.cs

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

+ 0 - 806
src/MeidoPhotoStudio.Converter/Program.cs

@@ -1,806 +0,0 @@
-using System;
-using System.IO;
-using System.Collections.Generic;
-using System.Linq;
-using Ionic.Zlib;
-using ExIni;
-using BepInEx;
-using UnityEngine;
-using UnityEngine.SceneManagement;
-using MyRoomCustom;
-
-namespace MeidoPhotoStudio.Converter
-{
-    using static Plugin.BinaryExtensions;
-
-    [BepInPlugin(pluginGuid, pluginName, pluginVersion)]
-    public class SceneConverter : BaseUnityPlugin
-    {
-        private const string pluginGuid = "com.habeebweeb.com3d2.meidophotostudio.converter";
-        public const string pluginName = "MeidoPhotoStudio Converter";
-        public const string pluginVersion = "0.0.0";
-        private static readonly byte[] noThumb = Convert.FromBase64String(
-            "iVBORw0KGgoAAAANSUhEUgAAADIAAAAyCAIAAACRXR/mAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7D" +
-            "AcdvqGQAAAFOSURBVFhH3dJbjoMwEETRLIRP9r+zrCGpqJABY+x+2Ua5ys9EcteJNK/3sj7ws7E+j2ln8Q9+O7eE2Vjpq4kdJTsLTZRl" +
-            "jBMLTZFdDTkLDZYVAQUWGia7Wy+z0ABZZfqWhbrK6rs1Fuoka442WChcJllss1CgTDgnYqEQmXxLykJOmWpIwUJmmXZFx0IGmWFCzUKq" +
-            "J7b7FhYSvjIfN7JQ86Hnsp2FKm+dZ10sVHzuv+lloexCyMEAFkpHoq7FsBDuBJ76a1Y6EnXtT//li8/9N12sylvnWTur+dBz2cgSvjIf" +
-            "t7BUT2z31azePwOpWQYT064oWGYTUw1JWU4Tk2+JWCEmJpxrswJNTLLYYIWbWHO0xupkYvXdW1ZXE6tMl1kDTOxuvcAaZmJFQM4abGJX" +
-            "w4k1xcQyxs6aaGJHycaabmIJ82M9xMTo2VjP+izrF8NPHwq3SYqeAAAAAElFTkSuQmCC"
-        );
-        private static readonly Dictionary<string, PlacementData.Data> myrAssetNameToData
-            = new Dictionary<string, PlacementData.Data>(StringComparer.InvariantCultureIgnoreCase);
-        private static readonly string[] faceKeys = {
-            "eyeclose", "eyeclose2", "eyeclose3", "eyeclose6", "hitomih", "hitomis", "mayuha",
-            "mayuup", "mayuv", "mayuvhalf", "moutha", "mouths", "mouthdw", "mouthup", "tangout",
-            "tangup", "eyebig", "eyeclose5", "mayuw", "mouthhe", "mouthc", "mouthi", "mouthuphalf",
-            "tangopen",
-            "namida", "tear1", "tear2", "tear3", "shock", "yodare", "hoho", "hoho2", "hohos", "hohol",
-            "toothoff", "nosefook"
-        };
-        private static readonly string[] mpnAttachProps = {
-            /* "", "", "", "", "", "", "", "", "", */
-            "kousokuu_tekaseone_i_.menu", "kousokuu_tekasetwo_i_.menu", "kousokul_ashikaseup_i_.menu",
-            "kousokuu_tekasetwo_i_.menu", "kousokul_ashikasedown_i_.menu", "kousokuu_tekasetwodown_i_.menu",
-            "kousokuu_ushirode_i_.menu", "kousokuu_smroom_haritsuke_i_.menu"
-        };
-        private static readonly int[] bodyRotations =
-        {
-            71, 44, 40, 41, 42, 43, 57, 68, 69, 46, 49, 47, 50, 52, 55, 53, 56, 92, 94, 93, 95, 45, 48, 51, 54
-        };
-        private static BepInEx.Logging.ManualLogSource Log;
-        private static readonly string scenesPath = Plugin.Constants.scenesPath;
-        private static readonly Vector3 DefaultSoftG = new Vector3(0f, -3f / 1000f, 0f);
-        private bool active;
-        private Rect windowRect = new Rect(30f, 30f, 300f, 200f);
-
-        private void Awake()
-        {
-            DontDestroyOnLoad(this);
-
-            if (!Directory.Exists(scenesPath)) Directory.CreateDirectory(scenesPath);
-            Log = Logger;
-        }
-
-        private void Start()
-        {
-            SceneManager.sceneLoaded += OnSceneLoaded;
-            foreach (var data in PlacementData.GetAllDatas(false))
-            {
-                string assetName = string.IsNullOrEmpty(data.assetName) ? data.resourceName : data.assetName;
-                myrAssetNameToData[assetName] = data;
-            }
-        }
-
-        private void OnSceneLoaded(Scene scene, LoadSceneMode sceneMode)
-        {
-            int index = scene.buildIndex;
-            active = index == 9 || index == 3;
-        }
-
-        private void OnGUI()
-        {
-            if (active)
-            {
-                windowRect.width = 300f;
-                windowRect.height = 200f;
-                windowRect.x = Mathf.Clamp(windowRect.x, 0, Screen.width - windowRect.width);
-                windowRect.y = Mathf.Clamp(windowRect.y, 0, Screen.height - windowRect.height);
-                windowRect = GUI.Window(0xEA4040, windowRect, GUIFunc, pluginName);
-            }
-        }
-
-        private void GUIFunc(int id)
-        {
-            if (GUILayout.Button("Convert ModifiedMM")) ProcessModifedMM();
-            if (GUILayout.Button("Convert ModifiedMM (Scene Manager)")) ProcessModifiedMMPng();
-
-            GUILayout.Space(30f);
-
-            if (GUILayout.Button("Convert modifedMM quickSave")) ProcessQuickSave();
-
-            GUILayout.FlexibleSpace();
-
-            if (GUILayout.Button("Close (Reopening requires restart)")) Destroy(this);
-
-            GUI.DragWindow();
-        }
-
-        private void ProcessModifiedMMPng()
-        {
-            string modPath = Path.Combine(Paths.GameRootPath, "Mod");
-            string scenePath = Path.Combine(modPath, "MultipleMaidsScene");
-            string kankyoPath = Path.Combine(modPath, "MultipleMaidsScene");
-        }
-
-        private string GetModifiedMMSceneData(string pngPath)
-        {
-            return string.Empty;
-        }
-
-        private void ProcessQuickSave()
-        {
-            string sybarisPath = Path.Combine(Paths.GameRootPath, "Sybaris");
-            string iniPath = BepInEx.Utility.CombinePaths(sybarisPath, "UnityInjector", "Config", "MultipleMaids.ini");
-
-            IniFile mmIniFile = IniFile.FromFile(iniPath);
-
-            IniSection sceneSection = mmIniFile.GetSection("scene");
-
-            if (sceneSection != null)
-            {
-                if (sceneSection.HasKey("s9999"))
-                {
-                    string sceneData = sceneSection["s9999"].Value;
-
-                    if (!string.IsNullOrEmpty(sceneData))
-                    {
-                        byte[] convertedSceneData = ProcessScene(sceneData, false);
-                        string path = Path.Combine(scenesPath, $"mmtempscene_{GetMMDateString(sceneData)}.png");
-                        SaveSceneToFile(path, convertedSceneData, noThumb);
-                    }
-                }
-            }
-        }
-
-        private void ProcessModifedMM()
-        {
-            string sybarisPath = Path.Combine(Paths.GameRootPath, "Sybaris");
-            string iniPath = BepInEx.Utility.CombinePaths(sybarisPath, "UnityInjector", "Config", "MultipleMaids.ini");
-
-            IniFile mmIniFile = IniFile.FromFile(iniPath);
-
-            IniSection sceneSection = mmIniFile.GetSection("scene");
-
-            if (sceneSection != null)
-            {
-                foreach (IniKey iniKey in sceneSection.Keys)
-                {
-                    if (iniKey.Key.StartsWith("ss")) continue;
-
-                    int sceneIndex = int.Parse(iniKey.Key.Substring(1));
-                    bool kankyo = sceneIndex >= 10000;
-                    string sceneData = iniKey.Value;
-
-                    if (!string.IsNullOrEmpty(sceneData))
-                    {
-                        byte[] convertedSceneData = ProcessScene(sceneData, kankyo);
-
-                        string prefix = kankyo
-                            ? "mmkankyo"
-                            : sceneIndex == 9999
-                                ? "mmtempscene" : $"mmscene{sceneIndex}";
-
-                        string path = Path.Combine(scenesPath, $"{prefix}_{GetMMDateString(sceneData)}.png");
-
-                        byte[] thumbnail = noThumb;
-
-                        string screenshotKey = $"s{iniKey.Key}";
-                        if (sceneSection.HasKey(screenshotKey))
-                        {
-                            string screenshotBase64 = sceneSection[screenshotKey].Value;
-                            if (!string.IsNullOrEmpty(screenshotBase64))
-                            {
-                                thumbnail = Convert.FromBase64String(screenshotBase64);
-                            }
-                        }
-
-                        SaveSceneToFile(path, convertedSceneData, thumbnail);
-                    }
-                }
-            }
-        }
-
-        public static void SaveSceneToFile(string path, byte[] sceneData, byte[] thumbnailData)
-        {
-            using (FileStream fileStream = File.Create(path))
-            {
-                fileStream.Write(thumbnailData, 0, thumbnailData.Length);
-                fileStream.Write(sceneData, 0, sceneData.Length);
-            }
-        }
-
-        public static string GetMMDateString(string sceneData)
-        {
-            string dateString = sceneData.Split('_')[0].Split(',')[0];
-            DateTime date = DateTime.Parse(dateString);
-            return $"{date:yyyyMMddHHmm}";
-        }
-
-        public static byte[] ProcessScene(string sceneData, bool kankyo)
-        {
-            string[] strArray1 = sceneData.Split('_');
-            string[] strArray2 = strArray1[1].Split(';');
-            string[] strArray3 = strArray1[0].Split(',');
-            string[] strArray4 = null;
-            string[] strArray5 = null;
-            string[] strArray6 = null;
-            string[] strArray7 = null;
-
-            if (strArray1.Length >= 5)
-            {
-                strArray4 = strArray1[2].Split(',');
-                strArray5 = strArray1[3].Split(';');
-                strArray6 = strArray1[4].Split(';');
-            }
-
-            if (strArray1.Length >= 6)
-            {
-                strArray7 = strArray1[5].Split(';');
-            }
-
-            using (MemoryStream 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(Plugin.MeidoPhotoStudio.sceneVersion);
-
-                binaryWriter.Write(kankyo ? Plugin.MeidoPhotoStudio.kankyoMagic : int.Parse(strArray3[1]));
-
-                SerializeEnvironment(strArray3, binaryWriter, kankyo);
-                SerializeLights(strArray3, strArray4, strArray5, strArray7, binaryWriter);
-                SerializeEffect(strArray4, binaryWriter);
-                SerializeProp(strArray3, strArray6, binaryWriter);
-
-                if (!kankyo)
-                {
-                    SerializeMessage(strArray3, binaryWriter);
-                    SerializeMaid(strArray2, strArray3, binaryWriter);
-                }
-
-                binaryWriter.Write("END");
-
-                deflateStream.Close();
-
-                return memoryStream.ToArray();
-            }
-        }
-
-        private static void SerializeMaid(string[] strArray2, string[] strArray3, BinaryWriter binaryWriter)
-        {
-            binaryWriter.Write(Plugin.MeidoManager.header);
-            // MM scene converted to MPS
-            binaryWriter.Write(true);
-
-            // binaryWriter.Write(Plugin.Meido.meidoDataVersion);
-
-            int numberOfMaids = strArray2.Length;
-
-            binaryWriter.Write(numberOfMaids);
-
-            /*
-                TODO: Investigate why serialized maid data may only have 64 items.
-                https://git.coder.horse/meidomustard/modifiedMM/src/master/MultipleMaids/CM3D2/MultipleMaids/Plugin/MultipleMaids.Update.cs#L3745
-                
-                The difference affects whether or not rotations are local or world. 
-                Certain body rotations would be missing as well particularly the toes.
-                Other data like free look and attached items like hand/vag/anl would be missing.
-            */
-
-            bool gravityEnabled = false;
-
-            for (int i = 0; i < numberOfMaids; i++)
-            {
-                using (MemoryStream memoryStream = new MemoryStream())
-                using (BinaryWriter tempWriter = new BinaryWriter(memoryStream))
-                {
-                    string[] maidData = strArray2[i].Split(':');
-                    tempWriter.WriteVector3(Utility.Vector3String(maidData[59])); // position
-                    tempWriter.WriteQuaternion(Utility.EulerString(maidData[58])); // rotation
-                    tempWriter.WriteVector3(Utility.Vector3String(maidData[60])); // scale
-
-                    // fingers
-                    for (int j = 0; j < 40; j++)
-                    {
-                        tempWriter.WriteQuaternion(Utility.EulerString(maidData[j]));
-                    }
-
-                    // toes
-                    for (int k = 0; k < 2; k++)
-                    {
-                        for (int j = 72 + k; j < 90; j += 2)
-                        {
-                            tempWriter.WriteQuaternion(Utility.EulerString(maidData[j]));
-                        }
-                    }
-
-                    // the rest of the limbs
-                    foreach (int j in bodyRotations)
-                    {
-                        tempWriter.WriteQuaternion(Utility.EulerString(maidData[j]));
-                    }
-
-                    tempWriter.WriteVector3(Utility.Vector3String(maidData[96])); // hip position
-
-                    // cached pose stuff
-                    tempWriter.Write("normal");
-                    tempWriter.Write("maid_stand01");
-                    tempWriter.Write(false);
-
-                    // eye rotation delta
-                    // MM saves the rotations directly so just save the identity
-                    tempWriter.WriteQuaternion(Quaternion.identity);
-                    tempWriter.WriteQuaternion(Quaternion.identity);
-
-                    string[] freeLookData = maidData[64].Split(',');
-
-                    bool isFreeLook = int.Parse(freeLookData[0]) == 1;
-                    tempWriter.Write(isFreeLook);
-                    if (isFreeLook)
-                    {
-                        tempWriter.WriteVector3(new Vector3(
-                            float.Parse(freeLookData[2]), 1f, float.Parse(freeLookData[1])
-                        ));
-                    }
-
-                    // head/eye to camera
-                    // MM does not changes these values so they're always true
-                    tempWriter.Write(true);
-                    tempWriter.Write(true);
-
-                    string[] faceValues = maidData[63].Split(',');
-
-                    tempWriter.Write("MPS_FACE");
-                    for (int j = 0; j < faceKeys.Length - 2; j++)
-                    {
-                        tempWriter.Write(faceKeys[j]);
-                        tempWriter.Write(float.Parse(faceValues[j]));
-                    }
-
-                    if (faceValues.Length > 65)
-                    {
-                        tempWriter.Write(faceKeys[faceKeys.Length - 1]);
-                        tempWriter.Write(float.Parse(faceValues[faceValues.Length - 1]));
-                    }
-                    tempWriter.Write("END_FACE");
-
-                    tempWriter.Write(true); // body visible
-
-                    // MM does not serialize clothing
-                    for (int j = 0; j < 29; j++) tempWriter.Write(true);
-
-                    Vector3 softG = new Vector3(
-                        float.Parse(strArray3[12]), float.Parse(strArray3[13]), float.Parse(strArray3[14])
-                    );
-
-                    bool hairGravityActive = softG != DefaultSoftG;
-                    tempWriter.Write(hairGravityActive);
-                    if (hairGravityActive)
-                    {
-                        // MM gravity affects all maids
-                        gravityEnabled = true;
-                        tempWriter.WriteVector3(softG * 90f);
-                    }
-
-                    // MM doesn't serialize skirt gravity
-                    tempWriter.Write(false);
-
-                    // MM does not serialize curling
-                    tempWriter.Write(false);
-                    tempWriter.Write(false);
-                    tempWriter.Write(false);
-
-                    string kousokuUpperMenu = string.Empty;
-                    string kousokuLowerMenu = string.Empty;
-
-                    int mpnIndex = int.Parse(maidData[65].Split(',')[0]);
-
-                    // MM can attach accvag, accanl and handitem stuff as well as kousoku_upper/lower
-                    // MPS attach prop is preferred for non kousoku_upper/lower props because unlike kousoku_upper/lower
-                    // props, accvag etc. props attach only to a single place.
-                    if (mpnIndex >= 9 && mpnIndex <= 16)
-                    {
-                        int actualIndex = mpnIndex - 9;
-                        if (mpnIndex == 12)
-                        {
-                            kousokuUpperMenu = mpnAttachProps[actualIndex];
-                            kousokuLowerMenu = mpnAttachProps[actualIndex - 1];
-                        }
-                        else if (mpnIndex == 13)
-                        {
-                            kousokuUpperMenu = mpnAttachProps[actualIndex + 1];
-                            kousokuLowerMenu = mpnAttachProps[actualIndex];
-                        }
-                        else
-                        {
-                            if (mpnIndex > 13) actualIndex++;
-                            string kousokuMenu = mpnAttachProps[actualIndex];
-                            if (mpnAttachProps[actualIndex][7] == 'u') kousokuUpperMenu = kousokuMenu;
-                            else kousokuLowerMenu = kousokuMenu;
-                        }
-                    }
-
-                    bool kousokuUpper = !string.IsNullOrEmpty(kousokuUpperMenu);
-                    tempWriter.Write(kousokuUpper);
-                    if (kousokuUpper) tempWriter.Write(kousokuUpperMenu);
-
-                    bool kousokuLower = !string.IsNullOrEmpty(kousokuLowerMenu);
-                    tempWriter.Write(kousokuLower);
-                    if (kousokuLower) tempWriter.Write(kousokuLowerMenu);
-
-                    binaryWriter.Write(memoryStream.Length);
-                    binaryWriter.Write(memoryStream.ToArray());
-                }
-            }
-
-            binaryWriter.Write(gravityEnabled);
-        }
-
-        private static void SerializeProp(string[] strArray3, string[] strArray6, BinaryWriter binaryWriter)
-        {
-            binaryWriter.Write(Plugin.PropManager.header);
-
-            // binaryWriter.Write(Plugin.PropManager.propDataVersion);
-
-            bool hasWProp = strArray3.Length > 37 && !string.IsNullOrEmpty(strArray3[37]);
-            int numberOfProps = hasWProp ? 1 : 0;
-            numberOfProps += strArray6 == null ? 0 : strArray6.Length - 1;
-
-            binaryWriter.Write(numberOfProps);
-
-            if (hasWProp)
-            {
-                // For the prop that spawns when you push (shift +) W
-
-                binaryWriter.WriteVector3(new Vector3(
-                    float.Parse(strArray3[41]), float.Parse(strArray3[42]), float.Parse(strArray3[43])
-                ));
-
-                binaryWriter.WriteQuaternion(Quaternion.Euler(
-                    float.Parse(strArray3[38]), float.Parse(strArray3[39]), float.Parse(strArray3[40])
-                ));
-
-                binaryWriter.WriteVector3(new Vector3(
-                    float.Parse(strArray3[44]), float.Parse(strArray3[45]), float.Parse(strArray3[46])
-                ));
-
-                SerializeAttachPoint(binaryWriter);
-
-                binaryWriter.Write(false); // shadow casting
-
-                binaryWriter.Write(strArray3[37].Replace(' ', '_'));
-            }
-
-            if (strArray6 != null)
-            {
-                for (int i = 0; i < strArray6.Length - 1; i++)
-                {
-                    string[] assetParts = strArray6[i].Split(',');
-                    string assetName = assetParts[0].Replace(' ', '_');
-                    bool shadowCasting = assetName.EndsWith(".menu");
-
-                    if (assetName.StartsWith("creative_"))
-                    {
-                        // modifiedMM my room creative prop
-                        // modifiedMM serializes the prefabName rather than the ID.
-                        assetName = assetName.Replace("creative_", String.Empty);
-                        assetName = $"MYR_{myrAssetNameToData[assetName].ID}#{assetName}";
-                    }
-                    else if (assetName.StartsWith("MYR_"))
-                    {
-                        // MM 23.0+ my room creative prop
-                        int assetID = int.Parse(assetName.Replace("MYR_", string.Empty));
-                        PlacementData.Data data = PlacementData.GetData(assetID);
-                        string asset = string.IsNullOrEmpty(data.assetName) ? data.resourceName : data.assetName;
-
-                        assetName = $"{assetName}#{asset}";
-                    }
-                    else if (assetName.Contains('#'))
-                    {
-                        if (assetName.Contains(".menu"))
-                        {
-                            // modifiedMM official mod prop
-                            string[] modComponents = assetParts[0].Split('#');
-                            string baseMenuFile = modComponents[0].Replace(' ', '_');
-                            string modItem = modComponents[1].Replace(' ', '_');
-                            assetName = $"{modItem}#{baseMenuFile}";
-                        }
-                        else
-                        {
-                            assetName = assetName.Split('#')[1].Replace(' ', '_');
-                        }
-                    }
-                    else if (assetName.StartsWith("BGOdogu", StringComparison.InvariantCultureIgnoreCase))
-                    {
-                        // I don't know why multiplemaids even prepends BG
-                        assetName = assetName.Substring(2);
-                    }
-
-                    binaryWriter.WriteVector3(new Vector3(
-                        float.Parse(assetParts[4]), float.Parse(assetParts[5]), float.Parse(assetParts[6])
-                    ));
-
-                    binaryWriter.WriteQuaternion(Quaternion.Euler(
-                        float.Parse(assetParts[1]), float.Parse(assetParts[2]), float.Parse(assetParts[3])
-                    ));
-
-                    binaryWriter.WriteVector3(new Vector3(
-                        float.Parse(assetParts[7]), float.Parse(assetParts[8]), float.Parse(assetParts[9])
-                    ));
-
-                    SerializeAttachPoint(binaryWriter);
-
-                    binaryWriter.Write(shadowCasting);
-
-                    binaryWriter.Write(assetName);
-                }
-            }
-        }
-
-        private static void SerializeEffect(string[] strArray4, BinaryWriter binaryWriter)
-        {
-            binaryWriter.Write(Plugin.EffectManager.header);
-
-            if (strArray4 != null)
-            {
-                // bloom
-                binaryWriter.Write(Plugin.BloomEffectManager.header);
-                binaryWriter.Write(float.Parse(strArray4[2]) / 5.7f * 100f); // intensity
-                binaryWriter.Write((int)float.Parse(strArray4[3])); // blur iterations
-                binaryWriter.WriteColour(new Color( // bloom threshold colour
-                    1f - float.Parse(strArray4[4]), 1f - float.Parse(strArray4[5]), 1f - float.Parse(strArray4[6]), 1f
-                ));
-                binaryWriter.Write(int.Parse(strArray4[7]) == 1); // hdr
-                binaryWriter.Write(int.Parse(strArray4[1]) == 1); // active
-
-                // vignetting
-                binaryWriter.Write(Plugin.VignetteEffectManager.header);
-                binaryWriter.Write(float.Parse(strArray4[9])); // intensity
-                binaryWriter.Write(float.Parse(strArray4[10])); // blur
-                binaryWriter.Write(float.Parse(strArray4[11])); // blur spread
-                binaryWriter.Write(float.Parse(strArray4[12])); // chromatic aberration
-                binaryWriter.Write(int.Parse(strArray4[8]) == 1); // active
-
-                // bokashi 
-                binaryWriter.Write(Plugin.BlurEffectManager.header);
-                float blurSize = float.Parse(strArray4[13]);
-                binaryWriter.Write(blurSize);
-                binaryWriter.Write(blurSize > 0f);
-
-                binaryWriter.Write(Plugin.SepiaToneEffectManger.header);
-                binaryWriter.Write(int.Parse(strArray4[29]) == 1);
-
-                if (strArray4.Length > 15)
-                {
-                    binaryWriter.Write(Plugin.DepthOfFieldEffectManager.header);
-                    binaryWriter.Write(float.Parse(strArray4[16])); // focal length
-                    binaryWriter.Write(float.Parse(strArray4[17])); // focal size
-                    binaryWriter.Write(float.Parse(strArray4[18])); // aperture
-                    binaryWriter.Write(float.Parse(strArray4[19])); // max blur size
-                    binaryWriter.Write(int.Parse(strArray4[20]) == 1); // visualize focus
-                    binaryWriter.Write(int.Parse(strArray4[15]) == 1); // active
-
-                    binaryWriter.Write(Plugin.FogEffectManager.header);
-                    binaryWriter.Write(float.Parse(strArray4[22])); // fog distance
-                    binaryWriter.Write(float.Parse(strArray4[23])); // density
-                    binaryWriter.Write(float.Parse(strArray4[24])); // height scale
-                    binaryWriter.Write(float.Parse(strArray4[25])); // height
-                    binaryWriter.WriteColour(new Color( // fog colour
-                        float.Parse(strArray4[26]), float.Parse(strArray4[27]), float.Parse(strArray4[28]), 1f
-                    ));
-                    binaryWriter.Write(int.Parse(strArray4[21]) == 1); // active
-                }
-            }
-
-            binaryWriter.Write(Plugin.EffectManager.footer);
-        }
-
-        private static void SerializeMessage(string[] strArray3, BinaryWriter binaryWriter)
-        {
-            binaryWriter.Write(Plugin.MessageWindowManager.header);
-
-            bool showingMessage = false;
-            string name = "Maid";
-            string message = "Hello world";
-
-            if (strArray3.Length > 16)
-            {
-                showingMessage = int.Parse(strArray3[34]) == 1;
-                name = strArray3[35];
-                message = strArray3[36].Replace("&kaigyo", "\n");
-                // MM does not serialize message font size
-            }
-
-            binaryWriter.Write(showingMessage);
-            binaryWriter.Write(25);
-            binaryWriter.WriteNullableString(name);
-            binaryWriter.WriteNullableString(message);
-        }
-
-        private static void SerializeLights(string[] strArray3, string[] strArray4, string[] strArray5, string[] strArray7, BinaryWriter binaryWriter)
-        {
-            // Lights
-            binaryWriter.Write(Plugin.LightManager.header);
-
-            int numberOfLights = 1;
-            numberOfLights += strArray5 == null ? 0 : strArray5.Length - 1;
-
-            binaryWriter.Write(numberOfLights);
-
-            if (strArray3.Length > 16)
-            {
-                // Main Light
-                /*
-                    0 = Directional
-                    1 = Spot
-                    2 = Point
-                    3 = Directional (Colour Mode)
-                */
-                int lightType = int.Parse(strArray3[17]);
-                Color lightColour = new Color(
-                    float.Parse(strArray3[18]), float.Parse(strArray3[19]), float.Parse(strArray3[20]), 1f
-                );
-
-                Quaternion lightRotation = Quaternion.Euler(
-                    float.Parse(strArray3[21]), float.Parse(strArray3[22]), float.Parse(strArray3[23])
-                );
-
-                // MM uses spotAngle for both range and spotAngle based on which light type is used
-                float intensity = float.Parse(strArray3[24]);
-                float spotAngle = float.Parse(strArray3[25]);
-                float range = spotAngle / 5f;
-                float shadowStrength = 0.098f;
-                if (strArray4 != null) shadowStrength = float.Parse(strArray4[0]);
-
-                for (int i = 0; i < 3; i++)
-                {
-                    if (i == lightType || (i == 0 && lightType == 3))
-                    {
-                        SerializeLightProperty(
-                            binaryWriter, lightRotation, lightColour, intensity, range, spotAngle, shadowStrength
-                        );
-                    }
-                    else SerializeDefaultLight(binaryWriter);
-                }
-
-                if (strArray7 != null)
-                {
-                    binaryWriter.WriteVector3(Utility.Vector3String(strArray7[0]));
-                }
-                else binaryWriter.WriteVector3(new Vector3(0f, 1.9f, 0.4f));
-                binaryWriter.Write(lightType == 3 ? 0 : lightType);
-                binaryWriter.Write(lightType == 3);
-                binaryWriter.Write(false);
-                // lightKage[0] is the only value that's serialized
-            }
-
-            if (strArray5 != null)
-            {
-                int otherLights = strArray5.Length - 1;
-                for (int i = 0; i < otherLights; i++)
-                {
-                    string[] lightProperties = strArray5[i].Split(',');
-
-                    int lightType = int.Parse(lightProperties[0]);
-
-                    Color lightColour = new Color(
-                        float.Parse(lightProperties[1]), float.Parse(lightProperties[2]),
-                        float.Parse(lightProperties[3]), 1f
-                    );
-
-                    Quaternion lightRotation = Quaternion.Euler(
-                        float.Parse(lightProperties[4]), float.Parse(lightProperties[5]), 18f
-                    );
-
-                    float intensity = float.Parse(lightProperties[6]);
-                    float spotAngle = float.Parse(lightProperties[7]);
-                    float range = spotAngle / 5f;
-                    float shadowStrength = 0.098f;
-                    for (int j = 0; j < 3; j++)
-                    {
-                        if (j == lightType)
-                        {
-                            SerializeLightProperty(
-                                binaryWriter, lightRotation, lightColour, intensity, range, spotAngle, shadowStrength
-                            );
-                        }
-                        else SerializeDefaultLight(binaryWriter);
-                    }
-                    if (strArray7 != null)
-                    {
-                        binaryWriter.WriteVector3(Utility.Vector3String(strArray7[i + 1]));
-                    }
-                    else binaryWriter.WriteVector3(new Vector3(0f, 1.9f, 0.4f));
-                    binaryWriter.Write(lightType == 3 ? 0 : lightType);
-                    binaryWriter.Write(false);
-                    binaryWriter.Write(lightType == 3);
-                }
-            }
-        }
-
-        private static void SerializeEnvironment(string[] data, BinaryWriter binaryWriter, bool kankyo)
-        {
-            binaryWriter.Write(Plugin.EnvironmentManager.header);
-
-            string bgAsset = "Theater";
-
-            if (!int.TryParse(data[2], out _))
-            {
-                bgAsset = data[2].Replace(" ", "_");
-            }
-
-            binaryWriter.Write(bgAsset);
-
-            binaryWriter.WriteVector3(new Vector3(
-                float.Parse(data[6]), float.Parse(data[7]), float.Parse(data[8])
-            ));
-
-            binaryWriter.WriteQuaternion(Quaternion.Euler(
-                float.Parse(data[3]), float.Parse(data[4]), float.Parse(data[5])
-            ));
-
-            binaryWriter.WriteVector3(new Vector3(
-                float.Parse(data[9]), float.Parse(data[10]), float.Parse(data[11])
-            ));
-
-            binaryWriter.Write(kankyo);
-
-            Vector3 cameraTargetPos = new Vector3(0f, 0.9f, 0f);
-            float cameraDistance = 3f;
-            Quaternion cameraRotation = Quaternion.identity;
-
-            if (data.Length > 16)
-            {
-                cameraTargetPos = new Vector3(
-                    float.Parse(data[27]), float.Parse(data[28]), float.Parse(data[29])
-                );
-
-                cameraDistance = float.Parse(data[30]);
-
-                cameraRotation = Quaternion.Euler(
-                    float.Parse(data[31]), float.Parse(data[32]), float.Parse(data[33])
-                );
-            }
-
-            binaryWriter.WriteVector3(cameraTargetPos);
-
-            binaryWriter.Write(cameraDistance);
-
-            binaryWriter.WriteQuaternion(cameraRotation);
-        }
-
-        public static void SerializeAttachPoint(BinaryWriter binaryWriter)
-        {
-            binaryWriter.Write(0);
-            binaryWriter.Write(-1);
-        }
-
-        public static void SerializeDefaultLight(BinaryWriter binaryWriter)
-        {
-            SerializeLightProperty(binaryWriter, Quaternion.Euler(40f, 180f, 0f), Color.white);
-        }
-
-        public static void SerializeLightProperty(
-            BinaryWriter binaryWriter,
-            Quaternion rotation, Color colour, float intensity = 0.95f, float range = 50f,
-            float spotAngle = 50f, float shadowStrength = 0.1f
-        )
-        {
-            binaryWriter.WriteQuaternion(rotation);
-            binaryWriter.Write(intensity);
-            binaryWriter.Write(range);
-            binaryWriter.Write(spotAngle);
-            binaryWriter.Write(shadowStrength);
-            binaryWriter.WriteColour(colour);
-        }
-    }
-
-    public static class Utility
-    {
-        public static Quaternion EulerString(string euler)
-        {
-            string[] data = euler.Split(',');
-            return Quaternion.Euler(
-                float.Parse(data[0]), float.Parse(data[1]), float.Parse(data[2])
-            );
-        }
-
-        public static Vector3 Vector3String(string vector3)
-        {
-            string[] data = vector3.Split(',');
-            return new Vector3(
-                float.Parse(data[0]), float.Parse(data[1]), float.Parse(data[2])
-            );
-        }
-    }
-}

+ 40 - 0
src/MeidoPhotoStudio.Converter/UI.cs

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

+ 40 - 0
src/MeidoPhotoStudio.Converter/Utility/LZMA.cs

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

+ 60 - 0
src/MeidoPhotoStudio.Converter/Utility/PngUtility.cs

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

+ 56 - 18
src/MeidoPhotoStudio.Plugin/Meido/MeidoDragPointManager.cs

@@ -144,28 +144,66 @@ namespace MeidoPhotoStudio.Plugin
 
         public MeidoDragPointManager(Meido meido) => this.meido = meido;
 
-        public void Deserialize(BinaryReader binaryReader)
+        public void Deserialize(BinaryReader reader)
         {
-            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.ThighL, Bone.ThighR, Bone.CalfL, Bone.CalfR, Bone.MuneL, Bone.MuneR, Bone.MuneSubL, Bone.MuneSubR,
-                Bone.HandL, Bone.HandR, Bone.FootL, Bone.FootR
-            };
-            int localRotationIndex = Array.IndexOf(bones, Bone.CalfR);
-            for (Bone bone = Bone.Finger0L; bone <= Bone.Toe2NubR; ++bone)
-            {
-                BoneTransform[bone].localRotation = binaryReader.ReadQuaternion();
-            }
-            for (int i = 0; i < bones.Length; i++)
+            var sixtyFourFlag = reader.ReadBoolean();
+            var upperBone = sixtyFourFlag ? Bone.Finger4NubR : Bone.Toe2NubR;
+
+            // finger rotations. Toe rotations as well if sixtyFourFlag is false
+            for (var bone = Bone.Finger0L; bone <= upperBone; ++bone)
+                BoneTransform[bone].localRotation = reader.ReadQuaternion();
+
+            var bones = sixtyFourFlag ? new[]
+                {
+                    Bone.Pelvis, Bone.Spine, Bone.Spine0a, Bone.Spine1, Bone.Spine1a, Bone.Neck, Bone.UpperArmL,
+                    Bone.UpperArmR, Bone.ForearmL, Bone.ForearmR, Bone.ThighL, Bone.ThighR, Bone.CalfL, Bone.CalfR,
+                    Bone.HandL, Bone.HandR, Bone.FootL, Bone.FootR,
+                }
+                : new[]
+                {
+                    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.ThighL, Bone.ThighR, Bone.CalfL, Bone.CalfR, Bone.MuneL, Bone.MuneR, Bone.MuneSubL,
+                    Bone.MuneSubR, Bone.HandL, Bone.HandR, Bone.FootL, Bone.FootR,
+                };
+
+            var localRotationIndex = Array.IndexOf(bones, Bone.CalfR);
+
+            for (var i = 0; i < bones.Length; i++)
             {
-                Bone bone = bones[i];
-                Quaternion rotation = binaryReader.ReadQuaternion();
-                if (i > localRotationIndex) BoneTransform[bone].localRotation = rotation;
-                else BoneTransform[bone].rotation = rotation;
+                var bone = bones[i];
+
+                if (bone == Bone.ClavicleL)
+                {
+                    /*
+                     * Versions of MM possibly serialized ClavicleL improperly.
+                     * At least I think that's what happened otherwise why would they make this check at all.
+                     * https://git.coder.horse/meidomustard/modifiedMM/src/master/MultipleMaids/CM3D2/MultipleMaids/Plugin/MultipleMaids.Update.cs#L4355 
+                     *
+                     * Just look at the way MM serializes rotations.
+                     * https://git.coder.horse/meidomustard/modifiedMM/src/master/MultipleMaids/CM3D2/MultipleMaids/Plugin/MultipleMaids.Update.cs#L2364
+                     * It is most definitely possible MM dev missed a component.
+                     *
+                     * Also why is strArray9.Length == 2 acceptable? If the length were only 2,
+                     * float.Parse(strArray9[2]) would throw an index out of range exception???
+                     */
+                    if (!reader.ReadBoolean())
+                    {
+                        reader.ReadQuaternion();
+                        continue;
+                    }
+                }
+
+                var rotation = reader.ReadQuaternion();
+
+                if (sixtyFourFlag || i > localRotationIndex)
+                    BoneTransform[bone].localRotation = rotation;
+                else
+                    BoneTransform[bone].rotation = rotation;
             }
+
             // WHY????
-            GameMain.Instance.StartCoroutine(ApplyHipPosition(binaryReader.ReadVector3()));
+            GameMain.Instance.StartCoroutine(ApplyHipPosition(reader.ReadVector3()));
         }
 
         /*

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

@@ -172,8 +172,19 @@ namespace MeidoPhotoStudio.Plugin
 
             _ = reader.ReadVersion();
 
-            body.quaDefEyeL = reader.ReadQuaternion() * meido.DefaultEyeRotL;
-            body.quaDefEyeR = reader.ReadQuaternion() * meido.DefaultEyeRotR;
+            var mmConverted = metadata.MMConverted;
+
+            var eyeRotationL = reader.ReadQuaternion();
+            var eyeRotationR = reader.ReadQuaternion();
+
+            if (!mmConverted)
+            {
+                eyeRotationL *= meido.DefaultEyeRotL;
+                eyeRotationR *= meido.DefaultEyeRotR;
+            }
+
+            body.quaDefEyeL = eyeRotationL;
+            body.quaDefEyeR = eyeRotationR;
 
             var freeLook = meido.FreeLook = reader.ReadBoolean();
             var offsetLookTarget = reader.ReadVector3();

+ 1 - 1
src/MeidoPhotoStudio.Plugin/Utility.cs

@@ -135,7 +135,7 @@ namespace MeidoPhotoStudio.Plugin
         public static string HandItemToOdogu(string menu)
         {
             menu = menu.Substring(menu.IndexOf('_') + 1);
-            menu = menu.Substring(0, menu.IndexOf("_i_.menu"));
+            menu = menu.Substring(0, menu.IndexOf("_i_.menu", StringComparison.OrdinalIgnoreCase));
             menu = $"odogu_{menu}";
             return menu;
         }