MMSceneConverter.cs 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819
  1. using System;
  2. using System.IO;
  3. using System.Linq;
  4. using System.Text;
  5. using MeidoPhotoStudio.Plugin;
  6. using MyRoomCustom;
  7. using UnityEngine;
  8. namespace MeidoPhotoStudio.Converter.MultipleMaids
  9. {
  10. public static class MMSceneConverter
  11. {
  12. private const int ClavicleLIndex = 68;
  13. private static readonly int[] BodyRotationIndices =
  14. {
  15. 71, // Hip
  16. 44, // Pelvis
  17. 40, // Spine
  18. 41, // Spine0a
  19. 42, // Spine1
  20. 43, // Spine1a
  21. 57, // Neck
  22. ClavicleLIndex, // Clavicle L
  23. 69, // Clavicle R
  24. 46, // UpperArm L
  25. 49, // UpperArm R
  26. 47, // ForeArm L
  27. 50, // ForeArm R
  28. 52, // Thigh L
  29. 55, // Thigh R
  30. 53, // Calf L
  31. 56, // Calf R
  32. 92, // Mune L
  33. 94, // Mune R
  34. 93, // MuneSub L
  35. 95, // MuneSub R
  36. 45, // Hand L
  37. 48, // Hand R
  38. 51, // Foot L
  39. 54, // Foot R
  40. };
  41. private static readonly int[] BodyRotationIndices64 =
  42. BodyRotationIndices.Where(rotation => rotation < 64).ToArray();
  43. private static readonly CameraInfo DefaultCameraInfo = new();
  44. private static readonly LightProperty DefaultLightProperty = new();
  45. public static byte[] Convert(string data, bool environment = false)
  46. {
  47. var dataSegments = data.Split('_');
  48. using var memoryStream = new MemoryStream();
  49. using var dataWriter = new BinaryWriter(memoryStream, Encoding.UTF8);
  50. if (!environment)
  51. {
  52. ConvertMeido(dataSegments, dataWriter);
  53. ConvertMessage(dataSegments, dataWriter);
  54. ConvertCamera(dataSegments, dataWriter);
  55. }
  56. ConvertLight(dataSegments, dataWriter);
  57. ConvertEffect(dataSegments, dataWriter);
  58. ConvertEnvironment(dataSegments, dataWriter);
  59. ConvertProps(dataSegments, dataWriter);
  60. dataWriter.Write("END");
  61. return memoryStream.ToArray();
  62. }
  63. public static SceneMetadata GetSceneMetadata(string data, bool environment = false)
  64. {
  65. var dataSegments = data.Split('_');
  66. var strArray2 = dataSegments[1].Split(';');
  67. var meidoCount = environment ? MeidoPhotoStudio.Plugin.MeidoPhotoStudio.kankyoMagic : strArray2.Length;
  68. return new()
  69. {
  70. Version = 1,
  71. Environment = environment,
  72. MaidCount = meidoCount,
  73. MMConverted = true,
  74. };
  75. }
  76. private static void ConvertMeido(string[] data, BinaryWriter writer)
  77. {
  78. var strArray2 = data[1].Split(';');
  79. writer.Write(MeidoManager.header);
  80. // MeidoManagerSerializer version
  81. writer.WriteVersion(1);
  82. var meidoCount = strArray2.Length;
  83. writer.Write(meidoCount);
  84. var transformSerializer = Serialization.GetSimple<TransformDTO>();
  85. foreach (var rawData in strArray2)
  86. {
  87. using var memoryStream = new MemoryStream();
  88. using var tempWriter = new BinaryWriter(memoryStream, Encoding.UTF8);
  89. var maidData = rawData.Split(':');
  90. tempWriter.WriteVersion(1);
  91. transformSerializer.Serialize(
  92. new()
  93. {
  94. Position = ConversionUtility.ParseVector3(maidData[59]),
  95. Rotation = ConversionUtility.ParseEulerAngle(maidData[58]),
  96. LocalScale = ConversionUtility.ParseVector3(maidData[60]),
  97. }, tempWriter
  98. );
  99. ConvertHead(maidData, tempWriter);
  100. ConvertBody(maidData, tempWriter);
  101. ConvertClothing(maidData, tempWriter);
  102. writer.Write(memoryStream.Length);
  103. writer.Write(memoryStream.ToArray());
  104. }
  105. ConvertGravity(data[0].Split(','), writer);
  106. static void ConvertHead(string[] maidData, BinaryWriter writer)
  107. {
  108. // MeidoSerializer -> Head version
  109. writer.WriteVersion(1);
  110. var sixtyFourFlag = maidData.Length == 64;
  111. // eye direction
  112. // MM saves eye rotation directly which is garbage data for meido that don't use the same face model.
  113. // A lot of users associate scenes with specific meido though so keeping the data is desirable.
  114. var eyeRotationL = Quaternion.identity;
  115. var eyeRotationR = Quaternion.identity;
  116. if (!sixtyFourFlag)
  117. {
  118. eyeRotationL = ConversionUtility.ParseEulerAngle(maidData[90]);
  119. eyeRotationR = ConversionUtility.ParseEulerAngle(maidData[91]);
  120. }
  121. writer.Write(eyeRotationL);
  122. writer.Write(eyeRotationR);
  123. // free look
  124. if (sixtyFourFlag)
  125. {
  126. writer.Write(false);
  127. writer.Write(new Vector3(0f, 1f, 0f));
  128. }
  129. else
  130. {
  131. var freeLookData = maidData[64].Split(',');
  132. var isFreeLook = int.Parse(freeLookData[0]) == 1;
  133. writer.Write(isFreeLook);
  134. var offsetTarget = isFreeLook
  135. ? new(float.Parse(freeLookData[2]), 1f, float.Parse(freeLookData[1]))
  136. : new Vector3(0f, 1f, 0f);
  137. writer.Write(offsetTarget);
  138. }
  139. // HeadEulerAngle is used to save the head's facing rotation
  140. // MM does not have this data.
  141. writer.Write(Vector3.zero);
  142. // head/eye to camera (Not changed by MM so always true)
  143. writer.Write(true);
  144. writer.Write(true);
  145. // face
  146. var faceValues = maidData[63].Split(',');
  147. writer.Write(faceValues.Length);
  148. for (var i = 0; i < faceValues.Length; i++)
  149. {
  150. writer.Write(MMConstants.FaceKeys[i]);
  151. writer.Write(float.Parse(faceValues[i]));
  152. }
  153. }
  154. static void ConvertBody(string[] maidData, BinaryWriter writer)
  155. {
  156. // MeidoSerializer -> Body version
  157. writer.WriteVersion(1);
  158. var sixtyFourFlag = maidData.Length == 64;
  159. writer.Write(sixtyFourFlag);
  160. // finger rotations
  161. for (var i = 0; i < 40; i++)
  162. writer.Write(ConversionUtility.ParseEulerAngle(maidData[i]));
  163. if (!sixtyFourFlag)
  164. {
  165. // toe rotations
  166. for (var i = 0; i < 2; i++)
  167. for (var j = 72 + i; j < 90; j += 2)
  168. writer.Write(ConversionUtility.ParseEulerAngle(maidData[j]));
  169. }
  170. var rotationIndices = sixtyFourFlag ? BodyRotationIndices64 : BodyRotationIndices;
  171. // body rotations
  172. foreach (var index in rotationIndices)
  173. {
  174. var rotation = Quaternion.identity;
  175. var data = maidData[index];
  176. // check special case for ClavicleL
  177. if (index == ClavicleLIndex)
  178. {
  179. /*
  180. * Versions of MM possibly serialized ClavicleL improperly.
  181. * At least I think that's what happened otherwise why would they make this check at all.
  182. * https://git.coder.horse/meidomustard/modifiedMM/src/master/MultipleMaids/CM3D2/MultipleMaids/Plugin/MultipleMaids.Update.cs#L4355
  183. *
  184. * Look at the way MM serializes rotations.
  185. * https://git.coder.horse/meidomustard/modifiedMM/src/master/MultipleMaids/CM3D2/MultipleMaids/Plugin/MultipleMaids.Update.cs#L2364
  186. * It is most definitely possible MM dev missed a component.
  187. *
  188. * Also why is strArray9.Length == 2 acceptable? If the length were only 2,
  189. * float.Parse(strArray9[2]) would throw an index out of range exception???
  190. */
  191. writer.Write(ConversionUtility.TryParseEulerAngle(data, out rotation));
  192. }
  193. else
  194. rotation = ConversionUtility.ParseEulerAngle(data);
  195. writer.Write(rotation);
  196. }
  197. // hip position
  198. writer.Write(sixtyFourFlag ? Vector3.zero : ConversionUtility.ParseVector3(maidData[96]));
  199. Serialization.GetSimple<PoseInfo>().Serialize(PoseInfo.DefaultPose, writer);
  200. }
  201. static void ConvertClothing(string[] maidData, BinaryWriter writer)
  202. {
  203. // MeidoSerializer -> Clothing version
  204. writer.WriteVersion(1);
  205. // MM does not serialize body visibility
  206. writer.Write(true);
  207. // MM does not serialize clothing visibility
  208. for (var i = 0; i < MaidDressingPane.ClothingSlots.Length; i++)
  209. writer.Write(true);
  210. // MM does not serialize curling/shift
  211. writer.Write(false);
  212. writer.Write(false);
  213. writer.Write(false);
  214. // MPN attach props
  215. var kousokuUpperMenu = string.Empty;
  216. var kousokuLowerMenu = string.Empty;
  217. var sixtyFourFlag = maidData.Length == 64;
  218. if (!sixtyFourFlag)
  219. {
  220. var mpnIndex = int.Parse(maidData[65].Split(',')[0]);
  221. if (mpnIndex >= 9 && mpnIndex <= 16)
  222. {
  223. var actualIndex = mpnIndex - 9;
  224. if (mpnIndex == 12)
  225. {
  226. kousokuUpperMenu = MMConstants.MpnAttachProps[actualIndex];
  227. kousokuLowerMenu = MMConstants.MpnAttachProps[actualIndex - 1];
  228. }
  229. else if (mpnIndex == 13)
  230. {
  231. kousokuUpperMenu = MMConstants.MpnAttachProps[actualIndex + 1];
  232. kousokuLowerMenu = MMConstants.MpnAttachProps[actualIndex];
  233. }
  234. else
  235. {
  236. if (mpnIndex > 13) actualIndex++;
  237. var kousokuMenu = MMConstants.MpnAttachProps[actualIndex];
  238. if (MMConstants.MpnAttachProps[actualIndex][7] == 'u') kousokuUpperMenu = kousokuMenu;
  239. else kousokuLowerMenu = kousokuMenu;
  240. }
  241. }
  242. }
  243. writer.Write(!string.IsNullOrEmpty(kousokuUpperMenu));
  244. writer.Write(kousokuUpperMenu);
  245. writer.Write(!string.IsNullOrEmpty(kousokuLowerMenu));
  246. writer.Write(kousokuLowerMenu);
  247. // hair/skirt gravity
  248. // If gravity is enabled at all in MM, it affects all maids.
  249. // So it's like global gravity is enabled which overrides individual maid gravity settings.
  250. writer.Write(false);
  251. writer.Write(Vector3.zero);
  252. writer.Write(false);
  253. writer.Write(Vector3.zero);
  254. }
  255. static void ConvertGravity(string[] data, BinaryWriter writer)
  256. {
  257. var softG = new Vector3(
  258. float.Parse(data[12]), float.Parse(data[13]), float.Parse(data[14])
  259. );
  260. var hairGravityActive = softG != MMConstants.DefaultSoftG;
  261. writer.Write(hairGravityActive);
  262. // an approximation for hair gravity position
  263. writer.Write(softG * 90f);
  264. // MM does not serialize skirt gravity
  265. writer.Write(Vector3.zero);
  266. }
  267. }
  268. private static void ConvertMessage(string[] data, BinaryWriter writer)
  269. {
  270. const string newLine = "&kaigyo";
  271. writer.Write(MessageWindowManager.header);
  272. // MessageWindowManagerSerializer version
  273. writer.WriteVersion(1);
  274. var showingMessage = false;
  275. var name = string.Empty;
  276. var message = string.Empty;
  277. var strArray3 = data[0].Split(',');
  278. if (strArray3.Length > 16)
  279. {
  280. showingMessage = int.Parse(strArray3[34]) == 1;
  281. name = strArray3[35];
  282. message = strArray3[36].Replace(newLine, "\n");
  283. // MM does not serialize message font size
  284. }
  285. writer.Write(showingMessage);
  286. writer.Write((int)MessageWindowManager.fontBounds.Left);
  287. writer.Write(name);
  288. writer.Write(message);
  289. }
  290. private static void ConvertCamera(string[] data, BinaryWriter writer)
  291. {
  292. writer.Write(CameraManager.header);
  293. // CameraManagerSerializer version
  294. writer.WriteVersion(1);
  295. // MM only has one camera
  296. // current camera index
  297. writer.Write(0);
  298. // number of camera slots
  299. writer.Write(1);
  300. var strArray3 = data[0].Split(',');
  301. var cameraTargetPos = DefaultCameraInfo.TargetPos;
  302. var cameraDistance = DefaultCameraInfo.Distance;
  303. var cameraRotation = DefaultCameraInfo.Angle;
  304. if (strArray3.Length > 16)
  305. {
  306. cameraTargetPos = new(
  307. float.Parse(strArray3[27]), float.Parse(strArray3[28]), float.Parse(strArray3[29])
  308. );
  309. cameraDistance = float.Parse(strArray3[30]);
  310. cameraRotation = Quaternion.Euler(
  311. float.Parse(strArray3[31]), float.Parse(strArray3[32]), float.Parse(strArray3[33])
  312. );
  313. }
  314. Serialization.Get<CameraInfo>().Serialize(
  315. new()
  316. {
  317. TargetPos = cameraTargetPos,
  318. Angle = cameraRotation,
  319. Distance = cameraDistance,
  320. }, writer
  321. );
  322. }
  323. private static void ConvertLight(string[] data, BinaryWriter writer)
  324. {
  325. writer.Write(LightManager.header);
  326. // LightManagerSerializer version
  327. writer.WriteVersion(1);
  328. var strArray3 = data[0].Split(',');
  329. var greaterThan5 = data.Length >= 5;
  330. var strArray4 = greaterThan5 ? data[2].Split(',') : null;
  331. var strArray5 = greaterThan5 ? data[3].Split(';') : null;
  332. var strArray7 = data.Length >= 6 ? data[5].Split(';') : null;
  333. var numberOfLights = 1 + (strArray5?.Length - 1 ?? 0);
  334. writer.Write(numberOfLights);
  335. var lightPropertySerializer = Serialization.Get<LightProperty>();
  336. /* Light Types
  337. 0 = Directional
  338. 1 = Spot
  339. 2 = Point
  340. 3 = Directional (Colour Mode)
  341. */
  342. if (strArray3.Length > 16)
  343. {
  344. // Main Light
  345. var spotAngle = float.Parse(strArray3[25]);
  346. var lightProperty = new LightProperty
  347. {
  348. Rotation = Quaternion.Euler(
  349. float.Parse(strArray3[21]), float.Parse(strArray3[22]), float.Parse(strArray3[23])
  350. ),
  351. Intensity = float.Parse(strArray3[24]),
  352. // MM uses spotAngle for both range and spotAngle based on which light type is used
  353. SpotAngle = spotAngle,
  354. Range = spotAngle / 5f,
  355. ShadowStrength = strArray4 is null ? 0.098f : float.Parse(strArray4[0]),
  356. LightColour = new(
  357. float.Parse(strArray3[18]), float.Parse(strArray3[19]), float.Parse(strArray3[20]), 1f
  358. ),
  359. };
  360. var lightType = int.Parse(strArray3[17]);
  361. // DragPointLightSerializer version
  362. writer.WriteVersion(1);
  363. for (var i = 0; i < 3; i++)
  364. {
  365. if (i == lightType || i == 0 && lightType == 3)
  366. lightPropertySerializer.Serialize(lightProperty, writer);
  367. else
  368. lightPropertySerializer.Serialize(DefaultLightProperty, writer);
  369. }
  370. var lightPosition = strArray7 is null
  371. ? LightProperty.DefaultPosition
  372. : ConversionUtility.ParseVector3(strArray7[0]);
  373. writer.Write(lightPosition);
  374. // light type. 3 is colour mode which uses directional light type.
  375. writer.Write(lightType == 3 ? 0 : lightType);
  376. // colour mode
  377. writer.Write(lightType == 3);
  378. // MM lights cannot be disabled
  379. writer.Write(false);
  380. }
  381. else
  382. {
  383. // Just write defaults if missing
  384. // DragPointLightSerializer version
  385. writer.WriteVersion(1);
  386. for (var i = 0; i < 3; i++)
  387. lightPropertySerializer.Serialize(DefaultLightProperty, writer);
  388. writer.Write(LightProperty.DefaultPosition);
  389. writer.Write(0);
  390. writer.Write(false);
  391. writer.Write(false);
  392. }
  393. if (strArray5 is null)
  394. return;
  395. for (var i = 0; i < strArray5.Length - 1; i++)
  396. {
  397. var lightProperties = strArray5[i].Split(',');
  398. var spotAngle = float.Parse(lightProperties[7]);
  399. var lightProperty = new LightProperty
  400. {
  401. Rotation = Quaternion.Euler(
  402. float.Parse(lightProperties[4]), float.Parse(lightProperties[5]), 18f
  403. ),
  404. Intensity = float.Parse(lightProperties[6]),
  405. SpotAngle = spotAngle,
  406. Range = spotAngle / 5f,
  407. // MM does not save shadow strength for other lights
  408. ShadowStrength = 0.098f,
  409. LightColour = new(
  410. float.Parse(lightProperties[1]), float.Parse(lightProperties[2]),
  411. float.Parse(lightProperties[3]), 1f
  412. ),
  413. };
  414. var lightType = int.Parse(lightProperties[0]);
  415. // DragPointLightSerializer version
  416. writer.WriteVersion(1);
  417. for (var j = 0; j < 3; j++)
  418. lightPropertySerializer.Serialize(j == lightType ? lightProperty : DefaultLightProperty, writer);
  419. var lightPosition = strArray7 is null
  420. ? LightProperty.DefaultPosition
  421. : ConversionUtility.ParseVector3(strArray7[i + 1]);
  422. writer.Write(lightPosition);
  423. // light type. 3 is colour mode which uses directional light type.
  424. writer.Write(lightType == 3 ? 0 : lightType);
  425. // colour mode only applies to the main light
  426. writer.Write(false);
  427. // MM lights cannot be disabled
  428. writer.Write(false);
  429. }
  430. }
  431. private static void ConvertEffect(string[] data, BinaryWriter writer)
  432. {
  433. if (data.Length < 5) return;
  434. writer.Write(EffectManager.header);
  435. // EffectManagerSerializer version
  436. writer.WriteVersion(1);
  437. var effectData = data[2].Split(',');
  438. // bloom
  439. writer.Write(BloomEffectManager.header);
  440. writer.WriteVersion(1);
  441. writer.Write(int.Parse(effectData[1]) == 1); // active
  442. writer.Write(float.Parse(effectData[2]) / 5.7f * 100f); // intensity
  443. writer.Write((int)float.Parse(effectData[3])); // blur iterations
  444. writer.WriteColour(
  445. new(
  446. 1f - float.Parse(effectData[4]), 1f - float.Parse(effectData[5]), 1f - float.Parse(effectData[6]),
  447. 1f
  448. )
  449. ); // bloom threshold colour
  450. writer.Write(int.Parse(effectData[7]) == 1); // hdr
  451. // vignetting
  452. writer.Write(VignetteEffectManager.header);
  453. writer.WriteVersion(1);
  454. writer.Write(int.Parse(effectData[8]) == 1); // active
  455. writer.Write(float.Parse(effectData[9])); // intensity
  456. writer.Write(float.Parse(effectData[10])); // blur
  457. writer.Write(float.Parse(effectData[11])); // blur spread
  458. writer.Write(float.Parse(effectData[12])); // chromatic aberration
  459. // blur
  460. writer.Write(BlurEffectManager.header);
  461. writer.WriteVersion(1);
  462. var blurSize = float.Parse(effectData[13]);
  463. writer.Write(blurSize > 0f); // active
  464. writer.Write(blurSize); // blur size
  465. // Sepia Tone
  466. writer.Write(SepiaToneEffectManger.header);
  467. writer.WriteVersion(1);
  468. writer.Write(int.Parse(effectData[29]) == 1);
  469. if (effectData.Length > 15)
  470. {
  471. // depth of field
  472. writer.Write(DepthOfFieldEffectManager.header);
  473. writer.WriteVersion(1);
  474. writer.Write(int.Parse(effectData[15]) == 1); // active
  475. writer.Write(float.Parse(effectData[16])); // focal length
  476. writer.Write(float.Parse(effectData[17])); // focal size
  477. writer.Write(float.Parse(effectData[18])); // aperture
  478. writer.Write(float.Parse(effectData[19])); // max blur size
  479. writer.Write(int.Parse(effectData[20]) == 1); // visualize focus
  480. // fog
  481. writer.Write(FogEffectManager.header);
  482. writer.WriteVersion(1);
  483. writer.Write(int.Parse(effectData[21]) == 1); // active
  484. writer.Write(float.Parse(effectData[22])); // fog distance
  485. writer.Write(float.Parse(effectData[23])); // density
  486. writer.Write(float.Parse(effectData[24])); // height scale
  487. writer.Write(float.Parse(effectData[25])); // height
  488. // fog colour
  489. writer.WriteColour(
  490. new(
  491. float.Parse(effectData[26]), float.Parse(effectData[27]), float.Parse(effectData[28]), 1f
  492. )
  493. );
  494. }
  495. writer.Write(EffectManager.footer);
  496. }
  497. private static void ConvertEnvironment(string[] data, BinaryWriter writer)
  498. {
  499. writer.Write(EnvironmentManager.header);
  500. // EnvironmentManagerSerializer version
  501. writer.WriteVersion(1);
  502. var environmentData = data[0].Split(',');
  503. var bgAsset = EnvironmentManager.defaultBg;
  504. if (!int.TryParse(environmentData[2], out _))
  505. bgAsset = environmentData[2].Replace(' ', '_');
  506. writer.Write(bgAsset);
  507. Serialization.GetSimple<TransformDTO>()
  508. .Serialize(
  509. new()
  510. {
  511. Position = new(
  512. float.Parse(environmentData[6]), float.Parse(environmentData[7]),
  513. float.Parse(environmentData[8])
  514. ),
  515. Rotation = Quaternion.Euler(
  516. float.Parse(environmentData[3]), float.Parse(environmentData[4]),
  517. float.Parse(environmentData[5])
  518. ),
  519. LocalScale = new(
  520. float.Parse(environmentData[9]), float.Parse(environmentData[10]),
  521. float.Parse(environmentData[11])
  522. ),
  523. }, writer
  524. );
  525. }
  526. private static void ConvertProps(string[] data, BinaryWriter writer)
  527. {
  528. var strArray3 = data[0].Split(',');
  529. var strArray6 = data.Length >= 5 ? data[4].Split(';') : null;
  530. var hasWProp = strArray3.Length > 37 && !string.IsNullOrEmpty(strArray3[37]);
  531. var propCount = strArray6?.Length - 1 ?? 0;
  532. propCount += hasWProp ? 1 : 0;
  533. writer.Write(PropManager.header);
  534. // PropManagerSerializer version
  535. writer.WriteVersion(1);
  536. writer.Write(propCount);
  537. var propSerializer = Serialization.GetSimple<DragPointPropDTO>();
  538. if (hasWProp)
  539. {
  540. // Props that are spawned by pushing (shift +) W.
  541. writer.WriteVersion(1);
  542. var propDto = new DragPointPropDTO
  543. {
  544. TransformDTO = new()
  545. {
  546. Position = new(
  547. float.Parse(strArray3[41]), float.Parse(strArray3[42]), float.Parse(strArray3[43])
  548. ),
  549. Rotation = Quaternion.Euler(
  550. float.Parse(strArray3[38]), float.Parse(strArray3[39]), float.Parse(strArray3[40])
  551. ),
  552. LocalScale =
  553. new(float.Parse(strArray3[44]), float.Parse(strArray3[45]), float.Parse(strArray3[46])),
  554. },
  555. AttachPointInfo = AttachPointInfo.Empty,
  556. PropInfo = AssetToPropInfo(strArray3[37]),
  557. ShadowCasting = false,
  558. };
  559. propSerializer.Serialize(propDto, writer);
  560. }
  561. if (strArray6 is null)
  562. return;
  563. for (var i = 0; i < strArray6.Length - 1; i++)
  564. {
  565. var prop = strArray6[i];
  566. var assetParts = prop.Split(',');
  567. var propInfo = AssetToPropInfo(assetParts[0]);
  568. var propDto = new DragPointPropDTO
  569. {
  570. PropInfo = propInfo,
  571. TransformDTO = new()
  572. {
  573. Position = new(
  574. float.Parse(assetParts[4]), float.Parse(assetParts[5]), float.Parse(assetParts[6])
  575. ),
  576. Rotation = Quaternion.Euler(
  577. float.Parse(assetParts[1]), float.Parse(assetParts[2]), float.Parse(assetParts[3])
  578. ),
  579. LocalScale =
  580. new(float.Parse(assetParts[7]), float.Parse(assetParts[8]), float.Parse(assetParts[9])),
  581. },
  582. AttachPointInfo = AttachPointInfo.Empty,
  583. ShadowCasting = propInfo.Type == PropInfo.PropType.Mod,
  584. };
  585. propSerializer.Serialize(propDto, writer);
  586. }
  587. static PropInfo AssetToPropInfo(string asset)
  588. {
  589. const string mmMyRoomPrefix = "creative_";
  590. const string mm23MyRoomPrefix = "MYR_";
  591. const string bgOdoguPrefix = "BGodogu";
  592. const string bgAsPropPrefix = "BG_";
  593. asset = ConvertSpaces(asset);
  594. if (asset.StartsWith(mmMyRoomPrefix))
  595. {
  596. // modifiedMM my room creative prop
  597. // modifiedMM serializes the prefabName rather than the ID.
  598. // Kinda dumb tbh who's idea was this anyway?
  599. asset = asset.Replace(mmMyRoomPrefix, string.Empty);
  600. return new(PropInfo.PropType.MyRoom)
  601. {
  602. MyRoomID = MMConstants.MyrAssetNameToData[asset].ID,
  603. Filename = asset,
  604. };
  605. }
  606. if (asset.StartsWith(mm23MyRoomPrefix))
  607. {
  608. // MM 23.0+ my room creative prop
  609. var assetID = int.Parse(asset.Replace(mm23MyRoomPrefix, string.Empty));
  610. var placementData = PlacementData.GetData(assetID);
  611. var filename = string.IsNullOrEmpty(placementData.assetName)
  612. ? placementData.resourceName
  613. : placementData.assetName;
  614. return new(PropInfo.PropType.MyRoom)
  615. {
  616. MyRoomID = assetID,
  617. Filename = filename,
  618. };
  619. }
  620. if (asset.Contains('#'))
  621. {
  622. if (!asset.Contains(".menu"))
  623. {
  624. // MM's dumb way of using one data structure to store both a human readable name and asset name
  625. // ex. 'Pancakes                    #odogu_pancake'
  626. return new(PropInfo.PropType.Odogu) { Filename = asset.Split('#')[1] };
  627. }
  628. // modifiedMM official COM3D2 mod prop
  629. var modComponents = asset.Split('#');
  630. var baseMenuFile = ConvertSpaces(modComponents[0]);
  631. var modMenuFile = ConvertSpaces(modComponents[1]);
  632. return new(PropInfo.PropType.Mod)
  633. {
  634. Filename = modMenuFile,
  635. SubFilename = baseMenuFile,
  636. };
  637. }
  638. if (asset.EndsWith(".menu"))
  639. {
  640. var propType = PropInfo.PropType.Mod;
  641. // hand items are treated as game props (Odogu) in MPS
  642. if (asset.StartsWith("handitem", StringComparison.OrdinalIgnoreCase)
  643. || asset.StartsWith("kousoku", StringComparison.OrdinalIgnoreCase)
  644. ) propType = PropInfo.PropType.Odogu;
  645. return new(propType) { Filename = asset };
  646. }
  647. if (asset.StartsWith(bgOdoguPrefix, StringComparison.OrdinalIgnoreCase))
  648. {
  649. // MM prepends BG to certain prop asset names. Don't know why.
  650. return new(PropInfo.PropType.Odogu) { Filename = asset.Substring(2) };
  651. }
  652. if (asset.StartsWith(bgAsPropPrefix))
  653. {
  654. // game bg as prop
  655. return new(PropInfo.PropType.Bg) { Filename = asset.Substring(3) };
  656. }
  657. return new(PropInfo.PropType.Odogu) { Filename = asset };
  658. }
  659. // MM uses '_' as a separator for different parts of serialized data so it converts all '_' to spaces
  660. static string ConvertSpaces(string @string) => @string.Replace(' ', '_');
  661. }
  662. }
  663. }