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(); 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().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().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(); /* 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() .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(); 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(' ', '_'); } } }