Browse Source

Merge remote-tracking branch 'origin/converter'

habeebweeb 3 năm trước cách đây
mục cha
commit
3922653e94

+ 30 - 0
MeidoPhotoStudio.Plugin.sln

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

+ 18 - 0
src/MeidoPhotoStudio.Converter/MeidoPhotoStudio.Converter.csproj

@@ -0,0 +1,18 @@
+<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>
+  </PropertyGroup>
+  <ItemGroup>
+    <Reference Include="..\..\lib\Assembly-CSharp.dll" />
+    <Reference Include="..\..\lib\Assembly-CSharp-firstpass.dll" />
+    <Reference Include="..\..\lib\UnityEngine.dll" />
+    <Reference Include="..\..\lib\Ionic.Zlib.dll" />
+    <Reference Include="..\..\lib\ExIni.dll" />
+    <Reference Include="..\..\lib\BepInEx.dll" />
+  </ItemGroup>
+  <ItemGroup>
+    <ProjectReference Include="..\MeidoPhotoStudio.Plugin\MeidoPhotoStudio.Plugin.csproj" />
+  </ItemGroup>
+</Project>

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

@@ -0,0 +1,806 @@
+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])
+            );
+        }
+    }
+}