MMSceneConverter.cs 28 KB

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