Explorar o código

Complete converter

oof
habeebweeb %!s(int64=3) %!d(string=hai) anos
pai
achega
77d371eb99

+ 1 - 1
src/MeidoPhotoStudio.Converter/Converters/IConverter.cs

@@ -2,6 +2,6 @@
 {
     public interface IConverter
     {
-        void Convert();
+        void Convert(string workingDirectory);
     }
 }

+ 43 - 21
src/MeidoPhotoStudio.Converter/Converters/MMConverter.cs

@@ -9,41 +9,63 @@ namespace MeidoPhotoStudio.Converter.Converters
 {
     public class MMConverter : IConverter
     {
-        private readonly string workingDirectory;
-
-        public MMConverter(string directory) => workingDirectory = directory;
-
-        public void Convert()
+        public void Convert(string workingDirectory)
         {
-            if (!Directory.Exists(workingDirectory))
-                Directory.CreateDirectory(workingDirectory);
+            var baseDirectory = Path.Combine(workingDirectory, MPSSceneSerializer.FormatDate(DateTime.Now));
 
-            foreach (var section in GetSceneSections(workingDirectory))
+            foreach (var iniFilePath in GetIniFiles(workingDirectory))
             {
-                foreach (var key in section.Keys.Where(
-                    key => !key.Key.StartsWith("ss") && !string.IsNullOrEmpty(key.Value)
-                ))
+                var section = GetSceneSection(iniFilePath);
+
+                if (section is null)
+                    continue;
+
+                var outputDirectoryName = Path.GetFileNameWithoutExtension(iniFilePath);
+                var outputDirectory = Path.Combine(baseDirectory, outputDirectoryName);
+
+                Directory.CreateDirectory(outputDirectory);
+
+                var keys = section.Keys.Where(key => !key.Key.StartsWith("ss") && !string.IsNullOrEmpty(key.Value));
+
+                foreach (var key in keys)
                 {
-                    var data = key.Value;
-                    var screenshotKey = $"s{key.Key}";
+                    var background = int.Parse(key.Key.Substring(1)) >= 10000;
+
+                    var convertedData = MMSceneConverter.Convert(key.Value, background);
+                    var sceneMetadata = MMSceneConverter.GetSceneMetadata(key.Value, background);
+
+                    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;
 
-                    var convertedData = MMSceneConverter.Convert(data);
-                    
+                    var filename = GenerateFilename(iniFilePath, key);
+                    var fullPath = Path.Combine(outputDirectory, filename);
+
+                    MPSSceneSerializer.SaveToFile(fullPath, sceneMetadata, convertedData, screenshotBase64);
                 }
             }
         }
 
-        private static void Convert(MMScene scene) { }
+        private static IEnumerable<string> GetIniFiles(string workingDirectory) =>
+            Directory.GetFiles(workingDirectory, "*.ini", SearchOption.AllDirectories);
+
+        private static string GenerateFilename(string iniFilePath, IniKey sceneKey)
+        {
+            var background = int.Parse(sceneKey.Key.Substring(1)) >= 10000;
+
+            var iniFilename = Path.GetFileNameWithoutExtension(iniFilePath);
 
-        private static IEnumerable<IniSection> GetSceneSections(string directory) =>
-            Directory.GetFiles(directory, "*.ini", SearchOption.AllDirectories)
-                .Select(GetSceneSection)
-                .Where(section => section is not null)
-                .Select(section => section!);
+            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)
         {

+ 21 - 4
src/MeidoPhotoStudio.Converter/MPSSceneSerializer.cs

@@ -2,10 +2,11 @@
 using System.IO;
 using System.Text;
 using MeidoPhotoStudio.Plugin;
+using Ionic.Zlib;
 
 namespace MeidoPhotoStudio.Converter
 {
-    internal class MPSSceneSerializer
+    public static class MPSSceneSerializer
     {
         private const string NoThumbBase64 =
             "iVBORw0KGgoAAAANSUhEUgAAADIAAAAyCAIAAACRXR/mAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7D"
@@ -18,14 +19,30 @@ namespace MeidoPhotoStudio.Converter
         private static byte[]? noThumb;
         public static byte[] NoThumb => noThumb ??= Convert.FromBase64String(NoThumbBase64);
 
-        public void SaveToFile(string filename, byte[] rawSceneData, string? thumbnail)
+        public static void SaveToFile(string filename, SceneMetadata metadata, byte[] rawSceneData, string? thumbnail)
         {
+            if (Path.GetExtension(filename) != ".png")
+                filename += ".png";
+
             using var fileStream = File.Create(filename);
 
-            var rawThumbnail = thumbnail is null ? NoThumb : Convert.FromBase64String(thumbnail);
+            var rawThumbnail = string.IsNullOrEmpty(thumbnail) ? NoThumb : Convert.FromBase64String(thumbnail);
 
             fileStream.Write(rawThumbnail, 0, rawThumbnail.Length);
-            fileStream.Write(rawSceneData, 0, rawSceneData.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");
     }
 }

+ 686 - 25
src/MeidoPhotoStudio.Converter/MultipleMaids/MMSceneConverter.cs

@@ -1,18 +1,48 @@
-using System.IO;
+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 static readonly int[] bodyRotations =
+        private const int ClavicleLIndex = 68;
+        private static readonly int[] BodyRotationIndices =
         {
-            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,
+            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 SimpleSerializer<PoseInfo> PoseInfoSerializer => Serialization.GetSimple<PoseInfo>();
-        private static SimpleSerializer<TransformDTO> TransformDtoSerializer => Serialization.GetSimple<TransformDTO>();
+        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)
         {
@@ -38,20 +68,35 @@ namespace MeidoPhotoStudio.Converter.MultipleMaids
             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 gravityEnabled = false;
-
-            var transformSerializer = TransformDtoSerializer;
+            var transformSerializer = Serialization.GetSimple<TransformDTO>();
 
             foreach (var rawData in strArray2)
             {
@@ -68,27 +113,43 @@ namespace MeidoPhotoStudio.Converter.MultipleMaids
                         Position = ConversionUtility.ParseVector3(maidData[59]),
                         Rotation = ConversionUtility.ParseEulerAngle(maidData[58]),
                         LocalScale = ConversionUtility.ParseVector3(maidData[60]),
-                    }, writer
+                    }, 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
-                writer.Write(Quaternion.identity);
-                writer.Write(Quaternion.identity);
+                // 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 (maidData.Length == 64)
+                if (sixtyFourFlag)
                 {
                     writer.Write(false);
                     writer.Write(new Vector3(0f, 1f, 0f));
@@ -107,7 +168,8 @@ namespace MeidoPhotoStudio.Converter.MultipleMaids
                     writer.Write(offsetTarget);
                 }
 
-                // HeadEulerAngle is unknown and also ignored for converted scenes
+                // 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)
@@ -118,41 +180,640 @@ namespace MeidoPhotoStudio.Converter.MultipleMaids
                 var faceValues = maidData[63].Split(',');
                 writer.Write(faceValues.Length);
 
-                for (var i = 0; i < MMConstants.FaceKeys.Length - 1; i++)
+                for (var i = 0; i < faceValues.Length; i++)
                 {
                     writer.Write(MMConstants.FaceKeys[i]);
                     writer.Write(float.Parse(faceValues[i]));
                 }
-
-                // nosefook
-                if (faceValues.Length > 35)
-                    writer.Write(float.Parse(faceValues[35]));
             }
 
             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 ConvertMessage(string[] data, BinaryWriter writer) { }
+        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(',');
 
-        private static void ConvertCamera(string[] data, BinaryWriter writer) { }
+            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);
 
-        private static void ConvertLight(string[] data, BinaryWriter writer) { }
+            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);
 
-        private static void ConvertEffect(string[] data, BinaryWriter writer) { }
+                for (var j = 0; j < 3; j++)
+                    lightPropertySerializer.Serialize(j == lightType ? lightProperty : DefaultLightProperty, writer);
 
-        private static void ConvertEnvironment(string[] data, BinaryWriter writer) { }
+                var lightPosition = strArray7 is null
+                    ? LightProperty.DefaultPosition
+                    : ConversionUtility.ParseVector3(strArray7[i + 1]);
 
-        private static void ConvertProps(string[] data, BinaryWriter writer) { }
+                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(' ', '_');
+        }
     }
 }

+ 17 - 11
src/MeidoPhotoStudio.Converter/Plugin.cs

@@ -1,30 +1,26 @@
 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.0";
+        public const string PluginVersion = "0.0.1";
 
-        private readonly PluginCore pluginCore;
-        private readonly UI ui;
+        private PluginCore pluginCore;
+        private UI ui;
 
         public static Plugin? Instance { get; private set; }
         public new ManualLogSource Logger { get; private set; }
 
-        public Plugin()
-        {
-            pluginCore = new(); // Path.Combine(Paths.ConfigPath, PluginName)
-            ui = new();
-        }
-
         private void Awake()
         {
             DontDestroyOnLoad(this);
@@ -32,9 +28,19 @@ namespace MeidoPhotoStudio.Converter
             Instance = this;
             Logger = base.Logger;
 
-            SceneManager.sceneLoaded += (scene, _) => ui.Visible = scene.buildIndex is 3 or 9;
+            var workingDirectory = Path.Combine(Paths.ConfigPath, PluginName);
+
+            if (!Directory.Exists(workingDirectory))
+                Directory.CreateDirectory(workingDirectory);
+
+            pluginCore = new(workingDirectory, new MMConverter());
+            ui = new(pluginCore);
+
+            SceneManager.sceneLoaded += (scene, _) =>
+                ui.Visible = scene.buildIndex is 3 or 9;
         }
 
-        private void OnGUI() => ui.Draw();
+        private void OnGUI() =>
+            ui.Draw();
     }
 }

+ 34 - 16
src/MeidoPhotoStudio.Converter/PluginCore.cs

@@ -1,21 +1,39 @@
-namespace MeidoPhotoStudio.Converter
+using System;
+using System.IO;
+using MeidoPhotoStudio.Converter.Converters;
+
+namespace MeidoPhotoStudio.Converter
 {
     public class PluginCore
     {
-        // private readonly string workingDirectory;
-        // public Core(string directory)
-        // {
-        //     workingDirectory = directory;
-        // }
-        //
-        // public void ProcessMMConfigurations()
-        // {
-        //     if (!Directory.Exists(workingDirectory))
-        //         Directory.CreateDirectory(workingDirectory);
-        //
-        //     var mmScenes = GetMMScenes(workingDirectory);
-        //
-        //     foreach (var scene in mmScenes) { }
-        // }
+        private readonly IConverter[] converters;
+        public string WorkingDirectory { get; set; }
+
+        public PluginCore(string workingDirectory, params IConverter[] converters)
+        {
+            WorkingDirectory = workingDirectory;
+            this.converters = converters;
+        }
+
+        public void Convert()
+        {
+            if (!Directory.Exists(WorkingDirectory))
+                Directory.CreateDirectory(WorkingDirectory);
+
+            try
+            {
+                foreach (var converter in converters)
+                    converter.Convert(WorkingDirectory);
+            }
+            catch (Exception e)
+            {
+                if (Plugin.Instance is not null)
+                {
+                    var logger = Plugin.Instance.Logger;
+                    logger.LogWarning($"Could not convert data because {e.Message}");
+                    logger.LogMessage(e.StackTrace);
+                }
+            }
+        }
     }
 }

+ 25 - 6
src/MeidoPhotoStudio.Converter/UI.cs

@@ -1,19 +1,38 @@
-namespace MeidoPhotoStudio.Converter
+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;
-            
-            
+            if (!Visible)
+                return;
+
+            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(WindowID, windowRect, GUIFunc, WindowTitle);
         }
 
         private void GUIFunc(int windowId)
         {
-            
+            GUILayout.FlexibleSpace();
+
+            if (GUILayout.Button("Convert"))
+                core.Convert();
         }
     }
 }