Browse Source

Rework prop related stuff

DragPointDogu -> DragPointProp and moved to separate file.
Prop attachment methods moved to DragPointProp.
Props that come from backgrounds like "Salon:65" are removed to simplify prop spawning.
ModelUtility now contains model loading methods.
PropInfo class introduced for prop copying and serialization.
habeebweeb 4 years ago
parent
commit
98e75a0282

+ 1 - 1
COM3D2.MeidoPhotoStudio.Plugin/MeidoPhotoStudio/Constants.cs

@@ -582,7 +582,7 @@ namespace COM3D2.MeidoPhotoStudio.Plugin
             DoguDict[customDoguCategories[DoguCategory.Mob]].AddRange(new[] {
                 "Mob_Man_Stand001", "Mob_Man_Stand002", "Mob_Man_Stand003", "Mob_Man_Sit001", "Mob_Man_Sit002",
                 "Mob_Man_Sit003", "Mob_Girl_Stand001", "Mob_Girl_Stand002", "Mob_Girl_Stand003", "Mob_Girl_Sit001",
-                "Mob_Girl_Sit002", "Mob_Girl_Sit003", "Salon:65", "Salon:63", "Salon:69"
+                "Mob_Girl_Sit002", "Mob_Girl_Sit003"
             });
 
             List<string> DoguList = DoguDict[customDoguCategories[DoguCategory.Other]];

+ 0 - 45
COM3D2.MeidoPhotoStudio.Plugin/MeidoPhotoStudio/DragPoint/DragPointOther.cs

@@ -44,49 +44,4 @@ namespace COM3D2.MeidoPhotoStudio.Plugin
             ApplyColours();
         }
     }
-
-    public class DragPointDogu : DragPointGeneral
-    {
-        private List<Renderer> meshRenderers;
-        public AttachPointInfo attachPointInfo = AttachPointInfo.Empty;
-        public string Name => MyGameObject.name;
-        public string assetName = string.Empty;
-        public bool ShadowCasting
-        {
-            get
-            {
-                if (meshRenderers.Count == 0) return false;
-                return meshRenderers[0].shadowCastingMode == ShadowCastingMode.On;
-            }
-            set
-            {
-                foreach (Renderer renderer in meshRenderers)
-                {
-                    renderer.shadowCastingMode = value ? ShadowCastingMode.On : ShadowCastingMode.Off;
-                }
-            }
-        }
-
-        public override void Set(Transform myObject)
-        {
-            base.Set(myObject);
-            DefaultRotation = MyObject.rotation;
-            DefaultPosition = MyObject.position;
-            meshRenderers = new List<Renderer>(MyObject.GetComponentsInChildren<SkinnedMeshRenderer>());
-            meshRenderers.AddRange(MyObject.GetComponentsInChildren<MeshRenderer>());
-        }
-
-        protected override void ApplyDragType()
-        {
-            bool active = (DragPointEnabled && Transforming) || Special;
-            ApplyProperties(active, active, GizmoEnabled && Rotating);
-            ApplyColours();
-        }
-
-        protected override void OnDestroy()
-        {
-            GameObject.Destroy(MyGameObject);
-            base.OnDestroy();
-        }
-    }
 }

+ 118 - 0
COM3D2.MeidoPhotoStudio.Plugin/MeidoPhotoStudio/DragPoint/DragPointProp.cs

@@ -0,0 +1,118 @@
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using HarmonyLib;
+using UnityEngine;
+using UnityEngine.Rendering;
+
+namespace COM3D2.MeidoPhotoStudio.Plugin
+{
+    public class DragPointProp : DragPointGeneral
+    {
+        private List<Renderer> renderers;
+        public AttachPointInfo AttachPointInfo { get; private set; } = AttachPointInfo.Empty;
+        public string Name => MyGameObject.name;
+        public string assetName = string.Empty;
+        public PropInfo Info { get; set; }
+
+        public bool ShadowCasting
+        {
+            get => renderers.Count != 0 && renderers.Any(r => r.shadowCastingMode == ShadowCastingMode.On);
+            set
+            {
+                foreach (var renderer in renderers)
+                    renderer.shadowCastingMode = value ? ShadowCastingMode.On : ShadowCastingMode.Off;
+            }
+        }
+
+        public override void Set(Transform myObject)
+        {
+            base.Set(myObject);
+            DefaultRotation = MyObject.rotation;
+            DefaultPosition = MyObject.position;
+            DefaultScale = MyObject.localScale;
+            renderers = new List<Renderer>(MyObject.GetComponentsInChildren<Renderer>());
+        }
+
+        public void AttachTo(Meido meido, AttachPoint point, bool keepWorldPosition = true)
+        {
+            var attachPoint = meido?.IKManager.GetAttachPointTransform(point);
+
+            AttachPointInfo = meido == null ? AttachPointInfo.Empty : new AttachPointInfo(point, meido);
+
+            var position = MyObject.position;
+            var rotation = MyObject.rotation;
+            var scale = MyObject.localScale;
+
+            MyObject.transform.SetParent(attachPoint, keepWorldPosition);
+
+            if (keepWorldPosition)
+            {
+                MyObject.position = position;
+                MyObject.rotation = rotation;
+            }
+            else
+            {
+                MyObject.localPosition = Vector3.zero;
+                MyObject.rotation = Quaternion.identity;
+            }
+
+            MyObject.localScale = scale;
+
+            if (attachPoint == null) Utility.FixGameObjectScale(MyGameObject);
+        }
+
+        public void DetachFrom(bool keepWorldPosition = true) => AttachTo(null, AttachPoint.None, keepWorldPosition);
+
+        public void DetachTemporary()
+        {
+            MyObject.transform.SetParent(null, true);
+            Utility.FixGameObjectScale(MyGameObject);
+        }
+
+        protected override void ApplyDragType()
+        {
+            var active = DragPointEnabled && Transforming || Special;
+            ApplyProperties(active, active, GizmoEnabled && Rotating);
+            ApplyColours();
+        }
+
+        protected override void OnDestroy()
+        {
+            Destroy(MyGameObject);
+            base.OnDestroy();
+        }
+    }
+
+    public class PropInfo
+    {
+        public enum PropType { Mod, MyRoom, Bg, Odogu }
+
+        public PropType Type { get; }
+        public string IconFile { get; set; }
+        public string Filename { get; set; }
+        public string SubFilename { get; set; }
+        public int MyRoomID { get; set; }
+
+        public PropInfo(PropType type) => Type = type;
+
+        public static PropInfo FromModItem(ModItem modItem)
+        {
+            var info = new PropInfo(PropType.Mod)
+            {
+                Filename = modItem.IsOfficialMod ? Path.GetFileName(modItem.MenuFile) : modItem.MenuFile
+            };
+            if (modItem.IsOfficialMod) info.SubFilename = modItem.BaseMenuFile;
+            return info;
+        }
+
+        public static PropInfo FromMyRoom(MyRoomItem myRoomItem) => new PropInfo(PropType.MyRoom)
+        {
+            MyRoomID = myRoomItem.ID, Filename = myRoomItem.PrefabName
+        };
+
+        public static PropInfo FromBg(string name) => new PropInfo(PropType.Bg) { Filename = name };
+
+        public static PropInfo FromGameProp(string name) => new PropInfo(PropType.Odogu) { Filename = name };
+    }
+}

+ 65 - 64
COM3D2.MeidoPhotoStudio.Plugin/MeidoPhotoStudio/GUI/Panes/BackgroundWindow2Panes/AttachPropPane.cs

@@ -9,7 +9,8 @@ namespace COM3D2.MeidoPhotoStudio.Plugin
     {
         private readonly PropManager propManager;
         private readonly MeidoManager meidoManager;
-        private readonly Dictionary<AttachPoint, Toggle> Toggles = new Dictionary<AttachPoint, Toggle>();
+        private readonly Dictionary<AttachPoint, Toggle> toggles = new Dictionary<AttachPoint, Toggle>();
+
         private static readonly Dictionary<AttachPoint, string> toggleTranslation =
             new Dictionary<AttachPoint, string>()
             {
@@ -35,13 +36,17 @@ namespace COM3D2.MeidoPhotoStudio.Plugin
                 [AttachPoint.Spine0a] = "spine0a",
                 [AttachPoint.Spine0] = "spine0"
             };
+
         private readonly Toggle keepWorldPositionToggle;
         private readonly Dropdown meidoDropdown;
+        private Toggle activeToggle;
         private bool meidoDropdownActive;
         private bool doguDropdownActive;
-        private bool PaneActive => meidoDropdownActive && doguDropdownActive;
         private string header;
-        private int selectedMaid;
+        private bool PaneActive => meidoDropdownActive && doguDropdownActive;
+        private Meido SelectedMeido => meidoManager.ActiveMeidoList[meidoDropdown.SelectedItemIndex];
+        private DragPointProp SelectedProp => propManager.CurrentProp;
+        private bool KeepWoldPosition => keepWorldPositionToggle.Value;
 
         public AttachPropPane(MeidoManager meidoManager, PropManager propManager)
         {
@@ -50,25 +55,27 @@ namespace COM3D2.MeidoPhotoStudio.Plugin
             this.meidoManager = meidoManager;
 
             this.meidoManager.EndCallMeidos += (s, a) => SetMeidoDropdown();
-            this.propManager.DoguSelectChange += (s, a) => SwitchDogu();
-            this.propManager.DoguListChange += (s, a) => doguDropdownActive = this.propManager.DoguCount > 0;
+            this.propManager.PropSelectionChange += (s, a) => UpdateToggles();
+
+            this.propManager.PropListChange += (s, a) =>
+            {
+                doguDropdownActive = this.propManager.PropCount > 0;
+                UpdateToggles();
+            };
 
             meidoDropdown = new Dropdown(new[] { Translation.Get("systemMessage", "noMaids") });
-            meidoDropdown.SelectionChange += (s, a) => SwitchMaid();
+            meidoDropdown.SelectionChange += (s, a) => UpdateToggles();
 
             keepWorldPositionToggle = new Toggle(Translation.Get("attachPropPane", "keepWorldPosition"));
 
             foreach (AttachPoint attachPoint in Enum.GetValues(typeof(AttachPoint)))
             {
                 if (attachPoint == AttachPoint.None) continue;
-                AttachPoint point = attachPoint;
+
+                var point = attachPoint;
                 var toggle = new Toggle(Translation.Get("attachPropPane", toggleTranslation[point]));
-                toggle.ControlEvent += (s, a) =>
-                {
-                    if (updating) return;
-                    ChangeAttachPoint(point);
-                };
-                Toggles[point] = toggle;
+                toggle.ControlEvent += (s, a) => OnToggleChange(point);
+                toggles[point] = toggle;
             }
         }
 
@@ -79,7 +86,8 @@ namespace COM3D2.MeidoPhotoStudio.Plugin
             foreach (AttachPoint attachPoint in Enum.GetValues(typeof(AttachPoint)))
             {
                 if (attachPoint == AttachPoint.None) continue;
-                Toggles[attachPoint].Label = Translation.Get("attachPropPane", toggleTranslation[attachPoint]);
+
+                toggles[attachPoint].Label = Translation.Get("attachPropPane", toggleTranslation[attachPoint]);
             }
         }
 
@@ -87,7 +95,8 @@ namespace COM3D2.MeidoPhotoStudio.Plugin
         {
             const float dropdownButtonHeight = 30;
             const float dropdownButtonWidth = 153f;
-            GUILayoutOption[] dropdownLayoutOptions = {
+            GUILayoutOption[] dropdownLayoutOptions =
+            {
                 GUILayout.Height(dropdownButtonHeight), GUILayout.Width(dropdownButtonWidth)
             };
 
@@ -104,8 +113,8 @@ namespace COM3D2.MeidoPhotoStudio.Plugin
             DrawToggleGroup(AttachPoint.UpperArmR, AttachPoint.Spine1a, AttachPoint.UpperArmL);
             DrawToggleGroup(AttachPoint.ForearmR, AttachPoint.Spine1, AttachPoint.ForearmL);
             DrawToggleGroup(AttachPoint.MuneR, AttachPoint.Spine0a, AttachPoint.MuneL);
-            DrawToggleGroup(AttachPoint.HandR, AttachPoint.Spine0,AttachPoint.HandL);
-            DrawToggleGroup(AttachPoint.ThighR,AttachPoint.Pelvis, AttachPoint.ThighL);
+            DrawToggleGroup(AttachPoint.HandR, AttachPoint.Spine0, AttachPoint.HandL);
+            DrawToggleGroup(AttachPoint.ThighR, AttachPoint.Pelvis, AttachPoint.ThighL);
             DrawToggleGroup(AttachPoint.CalfR, AttachPoint.CalfL);
             DrawToggleGroup(AttachPoint.FootR, AttachPoint.FootL);
 
@@ -116,72 +125,64 @@ namespace COM3D2.MeidoPhotoStudio.Plugin
         {
             GUILayout.BeginHorizontal();
             GUILayout.FlexibleSpace();
-            foreach (AttachPoint point in attachPoints) Toggles[point].Draw();
+            foreach (var point in attachPoints) toggles[point].Draw();
             GUILayout.FlexibleSpace();
             GUILayout.EndHorizontal();
         }
 
-        private void SetAttachPointToggle(AttachPoint point, bool value)
+        private void OnToggleChange(AttachPoint point)
         {
-            updating = true;
-            foreach (Toggle toggle in Toggles.Values) toggle.Value = false;
-            if (point != AttachPoint.None) Toggles[point].Value = value;
-            updating = false;
+            if (updating) return;
+
+            var toggle = toggles[point];
+            if (toggle.Value)
+            {
+                if (activeToggle != null)
+                {
+                    updating = true;
+                    activeToggle.Value = false;
+                    updating = false;
+                }
+
+                activeToggle = toggle;
+                SelectedProp.AttachTo(SelectedMeido, point, KeepWoldPosition);
+            }
+            else
+            {
+                SelectedProp.DetachFrom(KeepWoldPosition);
+                activeToggle = null;
+            }
         }
 
-        private void ChangeAttachPoint(AttachPoint point)
+        private void UpdateToggles()
         {
-            var toggleValue = point != AttachPoint.None && Toggles[point].Value;
-            SetAttachPointToggle(point, toggleValue);
-
-            Meido meido = null;
+            updating = true;
+            if (activeToggle != null) activeToggle.Value = false;
+            activeToggle = null;
+            updating = false;
 
-            if (point != AttachPoint.None)
-                meido = Toggles[point].Value
-                    ? meidoManager.ActiveMeidoList[meidoDropdown.SelectedItemIndex]
-                    : null;
+            if (!meidoManager.HasActiveMeido || propManager.PropCount == 0) return;
 
-            propManager.AttachProp(
-                propManager.CurrentDoguIndex, point, meido, keepWorldPositionToggle.Value
-            );
-        }
+            var info = SelectedProp.AttachPointInfo;
 
-        private void SwitchMaid()
-        {
-            if (updating || selectedMaid == meidoDropdown.SelectedItemIndex) return;
-            selectedMaid = meidoDropdown.SelectedItemIndex;
-            DragPointDogu dragDogu = propManager.CurrentDogu;
-            if (!dragDogu) return;
-            if (dragDogu.attachPointInfo.AttachPoint == AttachPoint.None) return;
-            ChangeAttachPoint(dragDogu.attachPointInfo.AttachPoint);
-        }
+            if (SelectedMeido.Maid.status.guid != info.MaidGuid) return;
 
-        private void SwitchDogu()
-        {
-            if (updating) return;
-            DragPointDogu dragDogu = propManager.CurrentDogu;
-            if (dragDogu) SetAttachPointToggle(dragDogu.attachPointInfo.AttachPoint, true);
+            updating = true;
+            var toggle = toggles[info.AttachPoint];
+            toggle.Value = true;
+            activeToggle = toggle;
+            updating = false;
         }
 
         private void SetMeidoDropdown()
         {
-            if (meidoManager.ActiveMeidoList.Count == 0)
-            {
-                SetAttachPointToggle(AttachPoint.Head, false);
-            }
-            var index = Mathf.Clamp(meidoDropdown.SelectedItemIndex, 0, meidoManager.ActiveMeidoList.Count);
-
+            meidoDropdownActive = meidoManager.HasActiveMeido;
             string[] dropdownList = meidoManager.ActiveMeidoList.Count == 0
                 ? new[] { Translation.Get("systemMessage", "noMaids") }
-                : meidoManager.ActiveMeidoList.Select(
-                    meido => $"{meido.Slot + 1}: {meido.FirstName} {meido.LastName}"
-                ).ToArray();
+                : meidoManager.ActiveMeidoList.Select(meido => $"{meido.Slot + 1}: {meido.FirstName} {meido.LastName}")
+                    .ToArray();
 
-            updating = true;
-            meidoDropdown.SetDropdownItems(dropdownList, index);
-            updating = false;
-
-            meidoDropdownActive = meidoManager.HasActiveMeido;
+            meidoDropdown.SetDropdownItems(dropdownList, 0);
         }
     }
 }

+ 1 - 1
COM3D2.MeidoPhotoStudio.Plugin/MeidoPhotoStudio/GUI/Panes/BackgroundWindow2Panes/ModPropsPane.cs

@@ -139,7 +139,7 @@ namespace COM3D2.MeidoPhotoStudio.Plugin
                     float x = modIndex % columns * buttonSize;
                     float y = modIndex / columns * buttonSize;
                     Rect iconRect = new Rect(x, y, buttonSize, buttonSize);
-                    if (GUI.Button(iconRect, "")) propManager.SpawnModItemProp(modItem);
+                    if (GUI.Button(iconRect, "")) propManager.AddModProp(modItem);
                     GUI.DrawTexture(iconRect, modItem.Icon);
                     modIndex++;
                 }

+ 1 - 2
COM3D2.MeidoPhotoStudio.Plugin/MeidoPhotoStudio/GUI/Panes/BackgroundWindow2Panes/MyRoomPropsPane.cs

@@ -3,7 +3,6 @@ using UnityEngine;
 
 namespace COM3D2.MeidoPhotoStudio.Plugin
 {
-    using static MenuFileUtility;
     public class MyRoomPropsPane : BasePane
     {
         private readonly PropManager propManager;
@@ -69,7 +68,7 @@ namespace COM3D2.MeidoPhotoStudio.Plugin
                 float y = i / columns * buttonSize;
                 MyRoomItem myRoomItem = myRoomPropList[i];
                 Rect iconRect = new Rect(x, y, buttonSize, buttonSize);
-                if (GUI.Button(iconRect, "")) propManager.SpawnMyRoomProp(myRoomItem);
+                if (GUI.Button(iconRect, "")) propManager.AddMyRoomProp(myRoomItem);
                 GUI.DrawTexture(iconRect, myRoomItem.Icon);
             }
 

+ 18 - 18
COM3D2.MeidoPhotoStudio.Plugin/MeidoPhotoStudio/GUI/Panes/BackgroundWindow2Panes/PropManagerPane.cs

@@ -15,18 +15,18 @@ namespace COM3D2.MeidoPhotoStudio.Plugin
         private readonly Button copyPropButton;
         private string propManagerHeader;
 
-        private int CurrentDoguIndex => propManager.CurrentDoguIndex;
+        private int CurrentDoguIndex => propManager.CurrentPropIndex;
 
         public PropManagerPane(PropManager propManager)
         {
             this.propManager = propManager;
-            this.propManager.DoguListChange += (s, a) =>
+            this.propManager.PropListChange += (s, a) =>
             {
                 UpdatePropList();
                 UpdateToggles();
             };
 
-            this.propManager.DoguSelectChange += (s, a) =>
+            this.propManager.FromPropSelect += (s, a) =>
             {
                 updating = true;
                 propDropdown.SelectedItemIndex = CurrentDoguIndex;
@@ -38,7 +38,7 @@ namespace COM3D2.MeidoPhotoStudio.Plugin
             propDropdown.SelectionChange += (s, a) =>
             {
                 if (updating) return;
-                this.propManager.SetCurrentDogu(propDropdown.SelectedItemIndex);
+                this.propManager.CurrentPropIndex = propDropdown.SelectedItemIndex;
                 UpdateToggles();
             };
 
@@ -51,29 +51,29 @@ namespace COM3D2.MeidoPhotoStudio.Plugin
             dragPointToggle = new Toggle(Translation.Get("propManagerPane", "dragPointToggle"));
             dragPointToggle.ControlEvent += (s, a) =>
             {
-                if (updating || this.propManager.DoguCount == 0) return;
-                this.propManager.CurrentDogu.DragPointEnabled = dragPointToggle.Value;
+                if (updating || this.propManager.PropCount == 0) return;
+                this.propManager.CurrentProp.DragPointEnabled = dragPointToggle.Value;
             };
 
             gizmoToggle = new Toggle(Translation.Get("propManagerPane", "gizmoToggle"));
             gizmoToggle.ControlEvent += (s, a) =>
             {
-                if (updating || this.propManager.DoguCount == 0) return;
-                this.propManager.CurrentDogu.GizmoEnabled = gizmoToggle.Value;
+                if (updating || this.propManager.PropCount == 0) return;
+                this.propManager.CurrentProp.GizmoEnabled = gizmoToggle.Value;
             };
 
             shadowCastingToggle = new Toggle(Translation.Get("propManagerPane", "shadowCastingToggle"));
             shadowCastingToggle.ControlEvent += (s, a) =>
             {
-                if (updating || this.propManager.DoguCount == 0) return;
-                this.propManager.CurrentDogu.ShadowCasting = shadowCastingToggle.Value;
+                if (updating || this.propManager.PropCount == 0) return;
+                this.propManager.CurrentProp.ShadowCasting = shadowCastingToggle.Value;
             };
 
             copyPropButton = new Button(Translation.Get("propManagerPane", "copyButton"));
-            copyPropButton.ControlEvent += (s, a) => this.propManager.CopyDogu(CurrentDoguIndex);
+            copyPropButton.ControlEvent += (s, a) => this.propManager.CopyProp(CurrentDoguIndex);
 
             deletePropButton = new Button(Translation.Get("propManagerPane", "deleteButton"));
-            deletePropButton.ControlEvent += (s, a) => this.propManager.RemoveDogu(CurrentDoguIndex);
+            deletePropButton.ControlEvent += (s, a) => this.propManager.RemoveProp(CurrentDoguIndex);
 
             propManagerHeader = Translation.Get("propManagerPane", "header");
         }
@@ -105,7 +105,7 @@ namespace COM3D2.MeidoPhotoStudio.Plugin
             MpsGui.Header(propManagerHeader);
             MpsGui.WhiteLine();
 
-            GUI.enabled = propManager.DoguCount > 0;
+            GUI.enabled = propManager.PropCount > 0;
 
             GUILayout.BeginHorizontal();
             propDropdown.Draw(dropdownLayoutOptions);
@@ -138,13 +138,13 @@ namespace COM3D2.MeidoPhotoStudio.Plugin
 
         private void UpdateToggles()
         {
-            DragPointDogu dogu = propManager.CurrentDogu;
-            if (dogu == null) return;
+            DragPointProp prop = propManager.CurrentProp;
+            if (prop == null) return;
 
             updating = true;
-            dragPointToggle.Value = dogu.DragPointEnabled;
-            gizmoToggle.Value = dogu.GizmoEnabled;
-            shadowCastingToggle.Value = dogu.ShadowCasting;
+            dragPointToggle.Value = prop.DragPointEnabled;
+            gizmoToggle.Value = prop.GizmoEnabled;
+            shadowCastingToggle.Value = prop.ShadowCasting;
             updating = false;
         }
     }

+ 2 - 2
COM3D2.MeidoPhotoStudio.Plugin/MeidoPhotoStudio/GUI/Panes/BackgroundWindow2Panes/PropsPane.cs

@@ -157,11 +157,11 @@ namespace COM3D2.MeidoPhotoStudio.Plugin
             string assetName = Constants.DoguDict[SelectedCategory][doguDropdown.SelectedItemIndex];
             if (SelectedCategory == Constants.customDoguCategories[Constants.DoguCategory.BGSmall])
             {
-                propManager.SpawnBG(assetName);
+                propManager.AddBgProp(assetName);
             }
             else
             {
-                propManager.SpawnObject(assetName);
+                propManager.AddGameProp(assetName);
             }
         }
     }

+ 1 - 1
COM3D2.MeidoPhotoStudio.Plugin/MeidoPhotoStudio/GUI/Panes/MainWindowPanes/BG2WindowPane.cs

@@ -16,7 +16,7 @@ namespace COM3D2.MeidoPhotoStudio.Plugin
         {
             this.meidoManager = meidoManager;
             this.propManager = propManager;
-            this.propManager.DoguSelectChange += (s, a) => propTabs.SelectedItemIndex = 0;
+            this.propManager.FromPropSelect += (s, a) => propTabs.SelectedItemIndex = 0;
 
             // should be added in this order
             AddPane(new PropsPane(propManager));

+ 1 - 1
COM3D2.MeidoPhotoStudio.Plugin/MeidoPhotoStudio/GUI/Windows/MainWindow.cs

@@ -39,7 +39,7 @@ namespace COM3D2.MeidoPhotoStudio.Plugin
             this.meidoManager.UpdateMeido += UpdateMeido;
 
             this.propManager = propManager;
-            this.propManager.DoguSelectChange += (s, a) => ChangeWindow(Constants.Window.BG2);
+            this.propManager.FromPropSelect += (s, a) => ChangeWindow(Constants.Window.BG2);
 
             this.lightManager = lightManager;
             this.lightManager.Select += (s, a) => ChangeWindow(Constants.Window.BG);

+ 187 - 439
COM3D2.MeidoPhotoStudio.Plugin/MeidoPhotoStudio/Managers/PropManager.cs

@@ -1,22 +1,41 @@
-using System;
-using System.IO;
+using System;
 using System.Collections.Generic;
+using System.IO;
 using System.Linq;
-using UnityEngine;
-using UnityEngine.Rendering;
 using BepInEx.Configuration;
+using UnityEngine;
+using Object = UnityEngine.Object;
 
 namespace COM3D2.MeidoPhotoStudio.Plugin
 {
-    using static MenuFileUtility;
+    using static ModelUtility;
+
     public class PropManager : IManager, ISerializable
     {
         public const string header = "PROP";
-        public const int propDataVersion = 1000;
         private static readonly ConfigEntry<bool> modItemsOnly;
-        public static bool ModItemsOnly => modItemsOnly.Value;
-        private readonly MeidoManager meidoManager;
         private static bool cubeActive = true;
+        private static Dictionary<string, string> modFileToFullPath;
+
+        private static Dictionary<string, string> ModFileToFullPath
+        {
+            get
+            {
+                if (modFileToFullPath != null) return modFileToFullPath;
+
+                string[] modFiles = Menu.GetModFiles();
+                modFileToFullPath = new Dictionary<string, string>(modFiles.Length, StringComparer.OrdinalIgnoreCase);
+
+                foreach (var mod in modFiles)
+                {
+                    var key = Path.GetFileName(mod);
+                    if (!modFileToFullPath.ContainsKey(key)) modFileToFullPath[key] = mod;
+                }
+
+                return modFileToFullPath;
+            }
+        }
+
         public static bool CubeActive
         {
             get => cubeActive;
@@ -29,7 +48,9 @@ namespace COM3D2.MeidoPhotoStudio.Plugin
                 }
             }
         }
+
         private static bool cubeSmall;
+
         public static bool CubeSmall
         {
             get => cubeSmall;
@@ -42,17 +63,44 @@ namespace COM3D2.MeidoPhotoStudio.Plugin
                 }
             }
         }
+
         private static event EventHandler CubeActiveChange;
         private static event EventHandler CubeSmallChange;
-        private readonly List<DragPointDogu> doguList = new List<DragPointDogu>();
-        public int DoguCount => doguList.Count;
-        public event EventHandler DoguListChange;
-        public event EventHandler DoguSelectChange;
-        public string[] PropNameList => doguList.Count == 0
+        public static bool ModItemsOnly => modItemsOnly.Value;
+        private readonly List<DragPointProp> propList = new List<DragPointProp>();
+
+        public string[] PropNameList => propList.Count == 0
             ? new[] { Translation.Get("systemMessage", "noProps") }
-            : doguList.Select(dogu => dogu.Name).ToArray();
-        public int CurrentDoguIndex { get; private set; }
-        public DragPointDogu CurrentDogu => DoguCount == 0 ? null : doguList[CurrentDoguIndex];
+            : propList.Select(prop => prop.Name).ToArray();
+
+        public int PropCount => propList.Count;
+        private int currentPropIndex;
+        private MeidoManager meidoManager;
+
+        public int CurrentPropIndex
+        {
+            get => currentPropIndex;
+            set
+            {
+                if (PropCount == 0)
+                {
+                    currentPropIndex = 0;
+                    return;
+                }
+
+                if ((uint) value >= (uint) PropCount) throw new ArgumentOutOfRangeException(nameof(value));
+
+                if (currentPropIndex == value) return;
+
+                currentPropIndex = value;
+                PropSelectionChange?.Invoke(this, EventArgs.Empty);
+            }
+        }
+
+        public DragPointProp CurrentProp => PropCount == 0 ? null : propList[CurrentPropIndex];
+        public event EventHandler PropSelectionChange;
+        public event EventHandler FromPropSelect;
+        public event EventHandler PropListChange;
 
         static PropManager() => modItemsOnly = Configuration.Config.Bind(
             "Prop", "ModItemsOnly",
@@ -63,509 +111,209 @@ namespace COM3D2.MeidoPhotoStudio.Plugin
         public PropManager(MeidoManager meidoManager)
         {
             this.meidoManager = meidoManager;
-            this.meidoManager.BeginCallMeidos += DetachProps;
-            this.meidoManager.EndCallMeidos += OnEndCall;
+            meidoManager.BeginCallMeidos += OnBeginCallMeidos;
+            meidoManager.EndCallMeidos += OnEndCallMeidos;
             Activate();
         }
 
-        public void Serialize(BinaryWriter binaryWriter)
+        public void AddFromPropInfo(PropInfo propInfo)
         {
-            binaryWriter.Write(header);
-            binaryWriter.Write(propDataVersion);
-            binaryWriter.Write(doguList.Count);
-            foreach (DragPointDogu dogu in doguList)
+            switch (propInfo.Type)
             {
-                binaryWriter.WriteVector3(dogu.MyObject.position);
-                binaryWriter.WriteQuaternion(dogu.MyObject.rotation);
-                binaryWriter.WriteVector3(dogu.MyObject.localScale);
-                dogu.attachPointInfo.Serialize(binaryWriter);
-                binaryWriter.Write(dogu.ShadowCasting);
-
-                binaryWriter.Write(dogu.assetName);
+                case PropInfo.PropType.Mod:
+                    ModItem modItem;
+                    if (!string.IsNullOrEmpty(propInfo.SubFilename))
+                    {
+                        modItem = ModItem.OfficialMod(ModFileToFullPath[propInfo.Filename]);
+                        modItem.BaseMenuFile = propInfo.SubFilename;
+                    }
+                    else
+                        modItem = ModItem.Mod(propInfo.Filename);
+
+                    AddModProp(modItem);
+                    break;
+                case PropInfo.PropType.MyRoom:
+                    AddMyRoomProp(new MyRoomItem { ID = propInfo.MyRoomID, PrefabName = propInfo.Filename });
+                    break;
+                case PropInfo.PropType.Bg:
+                    AddBgProp(propInfo.Filename);
+                    break;
+                case PropInfo.PropType.Odogu:
+                    AddGameProp(propInfo.Filename);
+                    break;
+                default: throw new ArgumentOutOfRangeException();
             }
         }
 
-        public void Deserialize(BinaryReader binaryReader)
+        public bool AddModProp(ModItem modItem)
         {
-            Dictionary<string, string> modToModPath = null;
-            ClearDogu();
+            var model = LoadMenuModel(modItem);
+            if (!model) return false;
 
-            int version = binaryReader.ReadInt32();
+            var name = modItem.MenuFile;
+            if (modItem.IsOfficialMod) name = Path.GetFileName(name) + ".menu"; // add '.menu' for partsedit support
+            model.name = name;
 
-            int numberOfProps = binaryReader.ReadInt32();
+            var dragPoint = AttachDragPoint(model);
+            dragPoint.Info = PropInfo.FromModItem(modItem);
 
-            int doguIndex = 0;
+            AddProp(dragPoint);
 
-            for (int i = 0; i < numberOfProps; i++)
-            {
-                Vector3 position = binaryReader.ReadVector3();
-                Quaternion rotation = binaryReader.ReadQuaternion();
-                Vector3 scale = binaryReader.ReadVector3();
-
-                AttachPointInfo info = AttachPointInfo.Deserialize(binaryReader);
-
-                bool shadowCasting = binaryReader.ReadBoolean();
-
-                string assetName = binaryReader.ReadString();
-
-                if (assetName.EndsWith(".menu") && assetName.Contains('#') && modToModPath == null)
-                {
-                    modToModPath = new Dictionary<string, string>(StringComparer.InvariantCultureIgnoreCase);
-                    foreach (string mod in Menu.GetModFiles()) modToModPath.Add(Path.GetFileName(mod), mod);
-                }
-
-                if (SpawnFromAssetString(assetName, modToModPath))
-                {
-                    DragPointDogu dogu = doguList[doguIndex++];
-                    Transform obj = dogu.MyObject;
-                    obj.position = position;
-                    obj.rotation = rotation;
-                    obj.localScale = scale;
-                    dogu.attachPointInfo = info;
-                    dogu.ShadowCasting = shadowCasting;
-                }
-            }
-            CurrentDoguIndex = 0;
-            GameMain.Instance.StartCoroutine(DeserializeAttach());
+            return true;
         }
 
-        private System.Collections.IEnumerator DeserializeAttach()
+        public bool AddMyRoomProp(MyRoomItem myRoomItem)
         {
-            yield return new WaitForEndOfFrame();
-
-            foreach (DragPointDogu dogu in doguList)
-            {
-                AttachPointInfo info = dogu.attachPointInfo;
-                if (info.AttachPoint != AttachPoint.None)
-                {
-                    Meido parent = meidoManager.GetMeido(info.MaidIndex);
-                    if (parent != null)
-                    {
-                        Transform obj = dogu.MyObject;
-                        Vector3 position = obj.position;
-                        Vector3 scale = obj.localScale;
-                        Quaternion rotation = obj.rotation;
-
-                        Transform point = parent.IKManager.GetAttachPointTransform(info.AttachPoint);
-                        dogu.MyObject.SetParent(point, true);
-                        info = new AttachPointInfo(
-                            info.AttachPoint,
-                            parent.Maid.status.guid,
-                            parent.Slot
-                        );
-                        dogu.attachPointInfo = info;
-
-                        obj.position = position;
-                        obj.localScale = scale;
-                        obj.rotation = rotation;
-                    }
-                }
-            }
-        }
+            var model = LoadMyRoomModel(myRoomItem);
+            if (!model) return false;
 
-        public void Activate()
-        {
-            CubeSmallChange += OnCubeSmall;
-            CubeActiveChange += OnCubeActive;
-        }
+            model.name = Translation.Get("myRoomPropNames", myRoomItem.PrefabName);
 
-        public void Deactivate()
-        {
-            ClearDogu();
-            CubeSmallChange -= OnCubeSmall;
-            CubeActiveChange -= OnCubeActive;
-        }
+            var dragPoint = AttachDragPoint(model);
+            dragPoint.Info = PropInfo.FromMyRoom(myRoomItem);
 
-        public void Update() { }
+            AddProp(dragPoint);
 
-        private GameObject GetDeploymentObject()
-        {
-            return GameObject.Find("Deployment Object Parent")
-                ?? new GameObject("Deployment Object Parent");
+            return true;
         }
 
-        public bool SpawnModItemProp(ModItem modItem)
+        public bool AddBgProp(string assetName)
         {
-            GameObject dogu = LoadModel(modItem);
-            string name = modItem.MenuFile;
-            if (modItem.IsOfficialMod) name = Path.GetFileName(name) + ".menu"; // Add '.menu' for partsedit support
-            if (dogu != null) AttachDragPoint(dogu, modItem.ToString(), name, new Vector3(0f, 0f, 0.5f));
-            return dogu != null;
-        }
+            var model = LoadBgModel(assetName);
+            if (!model) return false;
 
-        public bool SpawnMyRoomProp(MyRoomItem item)
-        {
-            MyRoomCustom.PlacementData.Data data = MyRoomCustom.PlacementData.GetData(item.ID);
-            GameObject dogu = GameObject.Instantiate(data.GetPrefab());
-            string name = Translation.Get("myRoomPropNames", item.PrefabName);
-            if (dogu != null)
-            {
-                GameObject finalDogu = new GameObject();
-                dogu.transform.SetParent(finalDogu.transform, true);
-                finalDogu.transform.SetParent(GetDeploymentObject().transform, false);
-                AttachDragPoint(finalDogu, item.ToString(), name, new Vector3(0f, 0f, 0.5f));
-            }
-            else Utility.LogInfo($"Could not load MyRoomCreative prop '{item.PrefabName}'");
-            return dogu != null;
-        }
+            model.name = Translation.Get("bgNames", assetName);
 
-        public bool SpawnBG(string assetName)
-        {
-            if (assetName.StartsWith("BG_")) assetName = assetName.Substring(3);
-            GameObject obj = GameMain.Instance.BgMgr.CreateAssetBundle(assetName)
-                ?? Resources.Load<GameObject>("BG/" + assetName)
-                ?? Resources.Load<GameObject>("BG/2_0/" + assetName);
+            var dragPoint = AttachDragPoint(model);
+            dragPoint.Info = PropInfo.FromBg(assetName);
 
-            if (obj != null)
-            {
-                GameObject dogu = GameObject.Instantiate(obj);
-                string name = Translation.Get("bgNames", assetName);
-                dogu.transform.localScale = Vector3.one * 0.1f;
-                AttachDragPoint(dogu, $"BG_{assetName}", name, new Vector3(0f, 0f, 0.5f));
-            }
-            return obj != null;
+            AddProp(dragPoint);
+
+            return true;
         }
 
-        public bool SpawnObject(string assetName)
+        public bool AddGameProp(string assetName)
         {
-            // TODO: Add a couple more things to ignore list
-            GameObject dogu = null;
-            string doguName = Translation.Get("propNames", assetName, false);
-            Vector3 doguPosition = new Vector3(0f, 0f, 0.5f);
+            var isMenu = assetName.EndsWith(".menu");
+            var model = isMenu ? LoadMenuModel(assetName) : LoadGameModel(assetName);
+            if (!model) return false;
 
-            if (assetName.EndsWith(".menu"))
-            {
-                dogu = LoadModel(assetName);
-                string handItem = Utility.HandItemToOdogu(assetName);
-                if (Translation.Has("propNames", handItem)) doguName = Translation.Get("propNames", handItem);
-            }
-            else if (assetName.StartsWith("mirror"))
-            {
-                Material mirrorMaterial = new Material(Shader.Find("Mirror"));
-                dogu = GameObject.CreatePrimitive(PrimitiveType.Plane);
-                Renderer mirrorRenderer = dogu.GetComponent<Renderer>();
-                mirrorRenderer.material = mirrorMaterial;
-                mirrorRenderer.enabled = true;
-                MirrorReflection2 mirrorReflection = dogu.AddComponent<MirrorReflection2>();
-                mirrorReflection.m_TextureSize = 2048;
-
-                Vector3 localPosition = new Vector3(0f, 0.96f, 0f);
-                dogu.transform.Rotate(dogu.transform.right, 90f);
-                dogu.transform.localPosition = localPosition;
-
-                switch (assetName)
-                {
-                    case "mirror1":
-                        dogu.transform.localScale = new Vector3(0.2f, 0.4f, 0.2f);
-                        break;
-                    case "mirror2":
-                        dogu.transform.localScale = new Vector3(0.1f, 0.4f, 0.2f);
-                        break;
-                    case "mirror3":
-                        localPosition.y = 0.85f;
-                        dogu.transform.localScale = new Vector3(0.03f, 0.18f, 0.124f);
-                        break;
-                    default:
-                        GameObject.Destroy(dogu);
-                        dogu = null;
-                        break;
-                }
-            }
-            else if (assetName.IndexOf(':') >= 0)
-            {
-                string[] assetParts = assetName.Split(':');
-                GameObject obj = GameMain.Instance.BgMgr.CreateAssetBundle(assetParts[0])
-                    ?? Resources.Load<GameObject>("BG/" + assetParts[0]);
-                try
-                {
-                    GameObject bg = GameObject.Instantiate(obj);
-                    int num = int.Parse(assetParts[1]);
-                    dogu = bg.transform.GetChild(num).gameObject;
-                    dogu.transform.SetParent(null);
-                    GameObject.Destroy(bg);
-                }
-                catch { }
-            }
-            else
-            {
-                GameObject obj = GameMain.Instance.BgMgr.CreateAssetBundle(assetName)
-                    ?? Resources.Load<GameObject>("Prefab/" + assetName)
-                    ?? Resources.Load<GameObject>("BG/" + assetName);
-                try
-                {
-                    dogu = GameObject.Instantiate<GameObject>(obj);
-                    dogu.transform.localPosition = Vector3.zero;
-
-                    MeshRenderer[] meshRenderers = dogu.GetComponentsInChildren<MeshRenderer>();
-                    for (int i = 0; i < meshRenderers.Length; i++)
-                    {
-                        if (meshRenderers[i])
-                        {
-                            string name = meshRenderers[i].gameObject.name;
-                            if (name.IndexOf("castshadow", StringComparison.OrdinalIgnoreCase) < 0)
-                            {
-                                meshRenderers[i].shadowCastingMode = ShadowCastingMode.Off;
-                            }
-                        }
-                    }
-
-                    Collider collider = dogu.transform.GetComponent<Collider>();
-                    if (collider != null) collider.enabled = false;
-                    foreach (Transform transform in dogu.transform)
-                    {
-                        collider = transform.GetComponent<Collider>();
-                        if (collider != null)
-                        {
-                            collider.enabled = false;
-                        }
-                    }
-                }
-                catch { }
-                #region particle system experiment
-                // if (asset.StartsWith("Particle/"))
-                // {
-                //     ParticleSystem particleSystem = go.GetComponent<ParticleSystem>();
-                //     if (particleSystem != null)
-                //     {
-                //         ParticleSystem.MainModule main;
-                //         main = particleSystem.main;
-                //         main.loop = true;
-                //         main.duration = Mathf.Infinity;
-
-                //         ParticleSystem[] particleSystems = particleSystem.GetComponents<ParticleSystem>();
-                //         foreach (ParticleSystem part in particleSystems)
-                //         {
-                //             ParticleSystem.EmissionModule emissionModule = part.emission;
-                //             ParticleSystem.Burst[] bursts = new ParticleSystem.Burst[emissionModule.burstCount];
-                //             emissionModule.GetBursts(bursts);
-                //             for (int i = 0; i < bursts.Length; i++)
-                //             {
-                //                 bursts[i].cycleCount = Int32.MaxValue;
-                //             }
-                //             emissionModule.SetBursts(bursts);
-                //             main = part.main;
-                //             main.loop = true;
-                //             main.duration = Mathf.Infinity;
-                //         }
-                //     }
-                // }
-                #endregion particle system experiment
-            }
+            model.name = Translation.Get("propNames", isMenu ? Utility.HandItemToOdogu(assetName) : assetName, !isMenu);
 
-            if (dogu != null)
-            {
-                AttachDragPoint(dogu, assetName, doguName, doguPosition);
-                return true;
-            }
+            var dragPoint = AttachDragPoint(model);
+            dragPoint.Info = PropInfo.FromGameProp(assetName);
 
-            Utility.LogInfo($"Could not spawn object '{assetName}'");
+            AddProp(dragPoint);
 
-            return false;
+            return true;
         }
 
-        private bool SpawnFromAssetString(string assetName, Dictionary<string, string> modDict = null)
+        public void CopyProp(int propIndex)
         {
-            bool result;
-            if (assetName.EndsWith(".menu"))
-            {
-                if (assetName.Contains('#'))
-                {
-                    string[] assetParts = assetName.Split('#');
-                    string menuFile = modDict == null ? Menu.GetModPathFileName(assetParts[0]) : modDict[assetParts[0]];
+            if ((uint) propIndex >= (uint) PropCount) throw new ArgumentOutOfRangeException(nameof(propIndex));
 
-                    ModItem item = ModItem.OfficialMod(menuFile);
-                    item.BaseMenuFile = assetParts[1];
-                    result = SpawnModItemProp(item);
-                }
-                else if (assetName.StartsWith("handitem")) result = SpawnObject(assetName);
-                else result = SpawnModItemProp(ModItem.Mod(assetName));
-            }
-            else if (assetName.StartsWith("MYR_"))
-            {
-                string[] assetParts = assetName.Split('#');
-                int id = int.Parse(assetParts[0].Substring(4));
-                string prefabName;
-                if (assetParts.Length == 2 && !string.IsNullOrEmpty(assetParts[1])) prefabName = assetParts[1];
-                else
-                {
-                    // deserialize modifiedMM and maybe MM 23.0+.
-                    MyRoomCustom.PlacementData.Data data = MyRoomCustom.PlacementData.GetData(id);
-                    prefabName = !string.IsNullOrEmpty(data.resourceName) ? data.resourceName : data.assetName;
-                }
-                result = SpawnMyRoomProp(new MyRoomItem() { ID = id, PrefabName = prefabName });
-            }
-            else if (assetName.StartsWith("BG_")) result = SpawnBG(assetName);
-            else result = SpawnObject(assetName);
-
-            return result;
+            AddFromPropInfo(propList[propIndex].Info);
         }
 
-        private void AttachDragPoint(GameObject dogu, string assetName, string name, Vector3 position)
+        public void DeleteAllProps()
         {
-            // TODO: Figure out why some props aren't centred properly
-            // Doesn't happen in MM but even after copy pasting the code, it doesn't work :/
-            dogu.name = name;
-            dogu.transform.position = position;
-
-            DragPointDogu dragDogu = DragPoint.Make<DragPointDogu>(PrimitiveType.Cube, Vector3.one * 0.12f);
-            dragDogu.Initialize(() => dogu.transform.position, () => Vector3.zero);
-            dragDogu.Set(dogu.transform);
-            dragDogu.AddGizmo(scale: 0.45f, mode: CustomGizmo.GizmoMode.World);
-            dragDogu.ConstantScale = true;
-            dragDogu.Delete += DeleteDogu;
-            dragDogu.Select += SelectDogu;
-            dragDogu.DragPointScale = CubeSmall ? DragPointGeneral.smallCube : 1f;
-            dragDogu.assetName = assetName;
-
-            doguList.Add(dragDogu);
-            OnDoguListChange();
+            foreach (var prop in propList) DestroyProp(prop);
+            propList.Clear();
+            CurrentPropIndex = 0;
+            EmitPropListChange();
         }
 
-        public void SetCurrentDogu(int doguIndex)
+        public void RemoveProp(int index)
         {
-            if (doguIndex >= 0 && doguIndex < DoguCount)
-            {
-                CurrentDoguIndex = doguIndex;
-                DoguSelectChange?.Invoke(this, EventArgs.Empty);
-            }
-        }
+            if ((uint) index >= (uint) PropCount) throw new ArgumentOutOfRangeException(nameof(index));
 
-        public void RemoveDogu(int doguIndex)
-        {
-            if (doguIndex >= 0 && doguIndex < DoguCount)
-            {
-                DestroyDogu(doguList[doguIndex]);
-                doguList.RemoveAt(doguIndex);
-                CurrentDoguIndex = Utility.Bound(CurrentDoguIndex, 0, DoguCount - 1);
-                OnDoguListChange();
-            }
+            DestroyProp(propList[index]);
+            propList.RemoveAt(index);
+            CurrentPropIndex = Utility.Bound(CurrentPropIndex, 0, PropCount - 1);
+            EmitPropListChange();
         }
 
-        public void CopyDogu(int doguIndex)
+        private DragPointProp AttachDragPoint(GameObject model)
         {
-            if (doguIndex >= 0 && doguIndex < DoguCount)
-            {
-                SpawnFromAssetString(doguList[doguIndex].assetName);
-            }
+            var dragPoint = DragPoint.Make<DragPointProp>(PrimitiveType.Cube, Vector3.one * 0.12f);
+            dragPoint.Initialize(() => model.transform.position, () => Vector3.zero);
+            dragPoint.Set(model.transform);
+            dragPoint.AddGizmo(0.45f, CustomGizmo.GizmoMode.World);
+            dragPoint.ConstantScale = true;
+            dragPoint.DragPointScale = CubeSmall ? DragPointGeneral.smallCube : 1f;
+            dragPoint.Delete += OnDeleteProp;
+            dragPoint.Select += OnSelectProp;
+            return dragPoint;
         }
 
-        public void AttachProp(
-            int doguIndex, AttachPoint attachPoint, Meido meido, bool worldPositionStays = true
-        )
+        private void AddProp(DragPointProp dragPoint)
         {
-            if (doguList.Count == 0 || doguIndex >= doguList.Count || doguIndex < 0) return;
-            AttachProp(doguList[doguIndex], attachPoint, meido, worldPositionStays);
+            propList.Add(dragPoint);
+            EmitPropListChange();
         }
 
-        private void AttachProp(
-            DragPointDogu dragDogu, AttachPoint attachPoint, Meido meido, bool worldPositionStays = true
-        )
+        private void DestroyProp(DragPointProp prop)
         {
-            GameObject dogu = dragDogu.MyGameObject;
-
-            Transform attachPointTransform = meido?.IKManager.GetAttachPointTransform(attachPoint);
-            // ?? GetDeploymentObject().transform;
-
-            dragDogu.attachPointInfo = new AttachPointInfo(
-                attachPoint: meido == null ? AttachPoint.None : attachPoint,
-                maidGuid: meido == null ? string.Empty : meido.Maid.status.guid,
-                maidIndex: meido == null ? -1 : meido.Slot
-            );
+            if (!prop) return;
 
-            Vector3 position = dogu.transform.position;
-            Quaternion rotation = dogu.transform.rotation;
-            Vector3 scale = dogu.transform.localScale;
-
-            dogu.transform.SetParent(attachPointTransform, worldPositionStays);
-
-            if (worldPositionStays)
-            {
-                dogu.transform.position = position;
-                dogu.transform.rotation = rotation;
-            }
-            else
-            {
-                dogu.transform.localPosition = Vector3.zero;
-                dogu.transform.rotation = Quaternion.identity;
-            }
-
-            dogu.transform.localScale = scale;
-
-            if (meido == null) Utility.FixGameObjectScale(dogu);
+            prop.Delete -= OnDeleteProp;
+            prop.Select -= OnSelectProp;
+            Object.Destroy(prop.gameObject);
         }
 
-        private void DetachProps(object sender, EventArgs args)
+        private void EmitPropListChange() => PropListChange?.Invoke(this, EventArgs.Empty);
+
+        private void OnBeginCallMeidos(object sender, EventArgs args)
         {
-            foreach (DragPointDogu dogu in doguList)
-            {
-                if (dogu.attachPointInfo.AttachPoint != AttachPoint.None)
-                {
-                    dogu.MyObject.SetParent(null, /*GetDeploymentObject().transform*/ true);
-                }
-            }
+            foreach (var prop in propList.Where(p => p.AttachPointInfo.AttachPoint != AttachPoint.None))
+                prop.DetachTemporary();
         }
 
-        private void ClearDogu()
+        private void OnEndCallMeidos(object sender, EventArgs args)
         {
-            for (int i = DoguCount - 1; i >= 0; i--)
+            foreach (var prop in propList.Where(p => p.AttachPointInfo.AttachPoint != AttachPoint.None))
             {
-                DestroyDogu(doguList[i]);
+                var info = prop.AttachPointInfo;
+                var meido = meidoManager.GetMeido(info.MaidGuid);
+                prop.AttachTo(meido, info.AttachPoint, meido == null);
             }
-            doguList.Clear();
-            CurrentDoguIndex = 0;
         }
 
-        private void OnEndCall(object sender, EventArgs args) => ReattachProps(useGuid: true);
+        private void OnDeleteProp(object sender, EventArgs args)
+            => RemoveProp(propList.IndexOf((DragPointProp) sender));
 
-        private void ReattachProps(bool useGuid, bool forceStay = false)
+        private void OnSelectProp(object sender, EventArgs args)
         {
-            foreach (DragPointDogu dragDogu in doguList)
-            {
-                AttachPointInfo info = dragDogu.attachPointInfo;
-                Meido meido = useGuid
-                    ? meidoManager.GetMeido(info.MaidGuid)
-                    : meidoManager.GetMeido(info.MaidIndex);
-                bool worldPositionStays = forceStay || meido == null;
-                AttachProp(dragDogu, dragDogu.attachPointInfo.AttachPoint, meido, worldPositionStays);
-            }
+            CurrentPropIndex = propList.IndexOf((DragPointProp) sender);
+            FromPropSelect?.Invoke(this, EventArgs.Empty);
         }
 
-        private void DeleteDogu(object sender, EventArgs args)
+        private void OnCubeSmall(object sender, EventArgs args)
         {
-            DragPointDogu dogu = (DragPointDogu)sender;
-            RemoveDogu(doguList.FindIndex(dragDogu => dragDogu == dogu));
+            foreach (var dragPoint in propList) dragPoint.DragPointScale = CubeSmall ? DragPointGeneral.smallCube : 1f;
         }
 
-        private void DestroyDogu(DragPointDogu dogu)
+        private void OnCubeActive(object sender, EventArgs args)
         {
-            if (dogu == null) return;
-            dogu.Delete -= DeleteDogu;
-            dogu.Select -= SelectDogu;
-            GameObject.Destroy(dogu.gameObject);
+            foreach (var dragPoint in propList) dragPoint.gameObject.SetActive(CubeActive);
         }
 
-        private void SelectDogu(object sender, EventArgs args)
-        {
-            DragPointDogu dogu = (DragPointDogu)sender;
-            SetCurrentDogu(doguList.IndexOf(dogu));
-        }
+        public void Update() { }
 
-        private void OnCubeSmall(object sender, EventArgs args)
+        public void Activate()
         {
-            foreach (DragPointDogu dogu in doguList)
-            {
-                dogu.DragPointScale = CubeSmall ? DragPointGeneral.smallCube : 1f;
-            }
+            CubeSmallChange += OnCubeSmall;
+            CubeActiveChange += OnCubeActive;
         }
 
-        private void OnCubeActive(object sender, EventArgs args)
+        public void Deactivate()
         {
-            foreach (DragPointDogu dragPoint in doguList)
-            {
-                dragPoint.gameObject.SetActive(CubeActive);
-            }
+            DeleteAllProps();
+            CubeSmallChange -= OnCubeSmall;
+            CubeActiveChange -= OnCubeActive;
         }
 
-        private void OnDoguListChange() => DoguListChange?.Invoke(this, EventArgs.Empty);
+        public void Serialize(BinaryWriter binaryWriter) => Utility.LogMessage("no prop serialization :(");
+        public void Deserialize(BinaryReader binaryReader) => Utility.LogMessage("no prop deserialization :(");
     }
 }

+ 7 - 0
COM3D2.MeidoPhotoStudio.Plugin/MeidoPhotoStudio/Meido/MeidoDragPointManager.cs

@@ -681,6 +681,13 @@ namespace COM3D2.MeidoPhotoStudio.Plugin
         private static readonly AttachPointInfo empty = new AttachPointInfo(AttachPoint.None, string.Empty, -1);
         public static ref readonly AttachPointInfo Empty => ref empty;
 
+        public AttachPointInfo(AttachPoint attachPoint, Meido meido)
+        {
+            AttachPoint = attachPoint;
+            MaidGuid = meido.Maid.status.guid;
+            MaidIndex = meido.Slot;
+        }
+
         public AttachPointInfo(AttachPoint attachPoint, string maidGuid, int maidIndex)
         {
             AttachPoint = attachPoint;

+ 3 - 353
COM3D2.MeidoPhotoStudio.Plugin/MeidoPhotoStudio/MenuFileUtility.cs

@@ -3,13 +3,12 @@ using System.Text;
 using System.IO;
 using System.Collections;
 using System.Collections.Generic;
-using System.Globalization;
 using System.Linq;
 using UnityEngine;
 
 namespace COM3D2.MeidoPhotoStudio.Plugin
 {
-    public static partial class MenuFileUtility
+    public static class MenuFileUtility
     {
         private static byte[] fileBuffer;
         public const string noCategory = "noCategory";
@@ -23,13 +22,6 @@ namespace COM3D2.MeidoPhotoStudio.Plugin
 
         private static readonly HashSet<string> accMpn = new HashSet<string>(StringComparer.InvariantCultureIgnoreCase);
 
-        private enum IMode
-        {
-            None,
-            ItemChange,
-            TexChange
-        }
-
         public static event EventHandler MenuFilesReadyChange;
         public static bool MenuFilesReady { get; private set; }
 
@@ -55,7 +47,7 @@ namespace COM3D2.MeidoPhotoStudio.Plugin
             return ref fileBuffer;
         }
 
-        private static byte[] ReadAFileBase(string filename)
+        public static byte[] ReadAFileBase(string filename)
         {
             using AFileBase aFileBase = GameUty.FileOpen(filename);
 
@@ -72,7 +64,7 @@ namespace COM3D2.MeidoPhotoStudio.Plugin
             return null;
         }
 
-        private static byte[] ReadOfficialMod(string filename)
+        public static byte[] ReadOfficialMod(string filename)
         {
             using var fileStream = new FileStream(filename, FileMode.Open);
 
@@ -89,258 +81,6 @@ namespace COM3D2.MeidoPhotoStudio.Plugin
             return null;
         }
 
-        private static IEnumerable<Renderer> GetRenderers(GameObject gameObject)
-            => gameObject.transform.GetComponentsInChildren<Transform>(true)
-                .Select(transform => transform.GetComponent<Renderer>())
-                .Where(renderer => renderer && renderer.material).ToList();
-
-        private static bool ProcScriptBin(byte[] menuBuf, out ModelInfo modelInfo)
-        {
-            modelInfo = null;
-            using var binaryReader = new BinaryReader(new MemoryStream(menuBuf), Encoding.UTF8);
-
-            if (binaryReader.ReadString() != "CM3D2_MENU") return false;
-
-            modelInfo = new ModelInfo();
-
-            binaryReader.ReadInt32(); // file version
-            binaryReader.ReadString(); // txt path
-            binaryReader.ReadString(); // name
-            binaryReader.ReadString(); // category
-            binaryReader.ReadString(); // description
-            binaryReader.ReadInt32(); // idk (as long)
-
-            try
-            {
-                while (true)
-                {
-                    int numberOfProps = binaryReader.ReadByte();
-                    var menuPropString = string.Empty;
-
-                    if (numberOfProps != 0)
-                    {
-                        for (var i = 0; i < numberOfProps; i++)
-                        {
-                            menuPropString = $"{menuPropString}\"{binaryReader.ReadString()}\"";
-                        }
-
-                        if (menuPropString != string.Empty)
-                        {
-                            var header = UTY.GetStringCom(menuPropString);
-                            string[] menuProps = UTY.GetStringList(menuPropString);
-
-                            if (header == "end") break;
-
-                            switch (header)
-                            {
-                                case "マテリアル変更":
-                                {
-                                    var matNo = int.Parse(menuProps[2]);
-                                    var materialFile = menuProps[3];
-                                    modelInfo.MaterialChanges.Add(new MaterialChange(matNo, materialFile));
-                                    break;
-                                }
-                                case "additem":
-                                    modelInfo.ModelFile = menuProps[1];
-                                    break;
-                            }
-                        }
-                    }
-                    else break;
-                }
-            }
-            catch { return false; }
-
-            return true;
-        }
-
-        private static void ProcModScriptBin(byte[] cd, GameObject go)
-        {
-            var matDict = new Dictionary<string, byte[]>();
-            string modData;
-
-            using (var binaryReader = new BinaryReader(new MemoryStream(cd), Encoding.UTF8))
-            {
-                if (binaryReader.ReadString() != "CM3D2_MOD") return;
-                binaryReader.ReadInt32();
-                binaryReader.ReadString();
-                binaryReader.ReadString();
-                binaryReader.ReadString();
-                binaryReader.ReadString();
-                binaryReader.ReadString();
-                var mpnValue = binaryReader.ReadString();
-                var mpn = MPN.null_mpn;
-                try { mpn = (MPN) Enum.Parse(typeof(MPN), mpnValue, true); }
-                catch { /* ignored */ }
-
-                if (mpn != MPN.null_mpn) binaryReader.ReadString();
-
-                modData = binaryReader.ReadString();
-                var entryCount = binaryReader.ReadInt32();
-                for (var i = 0; i < entryCount; i++)
-                {
-                    var key = binaryReader.ReadString();
-                    var count = binaryReader.ReadInt32();
-                    byte[] value = binaryReader.ReadBytes(count);
-                    matDict.Add(key, value);
-                }
-            }
-
-            var mode = IMode.None;
-            var materialChange = false;
-
-            Material material = null;
-            var materialIndex = 0;
-
-            using var stringReader = new StringReader(modData);
-
-            string line;
-
-            List<Renderer> renderers = null;
-
-            while ((line = stringReader.ReadLine()) != null)
-            {
-                string[] data = line.Split(new[] {'\t', ' '}, StringSplitOptions.RemoveEmptyEntries);
-
-                switch (data[0])
-                {
-                    case "アイテム変更":
-                    case "マテリアル変更":
-                        mode = IMode.ItemChange;
-                        break;
-                    case "テクスチャ変更":
-                        mode = IMode.TexChange;
-                        break;
-                }
-
-                switch (mode)
-                {
-                    case IMode.ItemChange:
-                    {
-                        if (data[0] == "スロット名") materialChange = true;
-
-                        if (materialChange)
-                        {
-                            if (data[0] == "マテリアル番号")
-                            {
-                                materialIndex = int.Parse(data[1]);
-
-                                renderers ??= GetRenderers(go).ToList();
-
-                                foreach (Renderer renderer in renderers)
-                                {
-                                    if (materialIndex < renderer.materials.Length)
-                                        material = renderer.materials[materialIndex];
-                                }
-                            }
-
-                            if (!material) continue;
-
-                            switch (data[0])
-                            {
-                                case "テクスチャ設定":
-                                    ChangeTex(materialIndex, data[1], data[2].ToLower());
-                                    break;
-                                case "色設定":
-                                    material.SetColor(data[1],
-                                        new Color(
-                                            float.Parse(data[2]) / 255f, float.Parse(data[3]) / 255f,
-                                            float.Parse(data[4]) / 255f, float.Parse(data[5]) / 255f
-                                        )
-                                    );
-                                    break;
-                                case "数値設定":
-                                    material.SetFloat(data[1], float.Parse(data[2]));
-                                    break;
-                            }
-                        }
-
-                        break;
-                    }
-                    case IMode.TexChange:
-                    {
-                        var matno = int.Parse(data[2]);
-                        ChangeTex(matno, data[3], data[4].ToLower());
-                        break;
-                    }
-                    case IMode.None:
-                        break;
-                    default:
-                        throw new ArgumentOutOfRangeException();
-                }
-            }
-
-            void ChangeTex(int matno, string prop, string filename)
-            {
-                byte[] buf = matDict[filename.ToLowerInvariant()];
-                var textureResource = new TextureResource(2, 2, TextureFormat.ARGB32, null, buf);
-                renderers ??= GetRenderers(go).ToList();
-                foreach (Renderer r in renderers)
-                {
-                    r.materials[matno].SetTexture(prop, null);
-                    Texture2D texture2D = textureResource.CreateTexture2D();
-                    texture2D.name = filename;
-                    r.materials[matno].SetTexture(prop, texture2D);
-                }
-            }
-        }
-
-        public static GameObject LoadModel(string menuFile) => LoadModel(new ModItem(menuFile));
-
-        public static GameObject LoadModel(ModItem modItem)
-        {
-            var menu = modItem.IsOfficialMod ? modItem.BaseMenuFile : modItem.MenuFile;
-
-            byte[] modelBuffer;
-
-            try { modelBuffer = ReadAFileBase(menu); }
-            catch (Exception e)
-            {
-                Utility.LogError($"Could not read menu file '{menu}' because {e.Message}\n{e.StackTrace}");
-                return null;
-            }
-
-            if (ProcScriptBin(modelBuffer, out ModelInfo modelInfo))
-            {
-                if (InstantiateModel(modelInfo.ModelFile, out GameObject finalModel))
-                {
-                    IEnumerable<Renderer> renderers = GetRenderers(finalModel).ToList();
-
-                    foreach (MaterialChange matChange in modelInfo.MaterialChanges)
-                    {
-                        foreach (Renderer renderer in renderers)
-                        {
-                            if (matChange.MaterialIndex < renderer.materials.Length)
-                            {
-                                renderer.materials[matChange.MaterialIndex] = ImportCM.LoadMaterial(
-                                    matChange.MaterialFile, null, renderer.materials[matChange.MaterialIndex]
-                                );
-                            }
-                        }
-                    }
-
-                    if (!modItem.IsOfficialMod) return finalModel;
-
-                    try { modelBuffer = ReadOfficialMod(modItem.MenuFile); }
-                    catch (Exception e)
-                    {
-                        Utility.LogError(
-                            $"Could not read mod menu file '{modItem.MenuFile}' because {e.Message}\n{e.StackTrace}"
-                        );
-                        return null;
-                    }
-
-                    ProcModScriptBin(modelBuffer, finalModel);
-
-                    return finalModel;
-                }
-            }
-
-            Utility.LogMessage($"Could not load model '{modItem.MenuFile}'");
-
-            return null;
-        }
-
         public static bool ParseNativeMenuFile(int menuIndex, ModItem modItem)
         {
             MenuDataBase menuDataBase = GameMain.Instance.MenuDataBase;
@@ -489,95 +229,5 @@ namespace COM3D2.MeidoPhotoStudio.Plugin
             return !(menu.EndsWith("_del") || menu.Contains("zurashi") || menu.Contains("mekure")
                 || menu.Contains("porori") || menu.Contains("moza") || menu.Contains("folder"));
         }
-
-        public abstract class MenuItem
-        {
-            public string IconFile { get; set; }
-            public Texture2D Icon { get; set; }
-        }
-
-        public class ModItem : MenuItem
-        {
-            public string MenuFile { get; set; }
-            public string BaseMenuFile { get; set; }
-            public string Name { get; set; }
-            public string Category { get; set; }
-            public float Priority { get; set; }
-            public bool IsMod { get; private set; }
-            public bool IsOfficialMod { get; private set; }
-
-            public static ModItem OfficialMod(string menuFile) => new ModItem()
-            {
-                MenuFile = menuFile,
-                IsMod = true,
-                IsOfficialMod = true,
-                Priority = 1000f
-            };
-
-            public static ModItem Mod(string menuFile) => new ModItem()
-            {
-                MenuFile = menuFile,
-                IsMod = true
-            };
-
-            public ModItem() { }
-
-            public ModItem(string menuFile) => MenuFile = menuFile;
-
-            public override string ToString()
-                => IsOfficialMod ? $"{Path.GetFileName(MenuFile)}#{BaseMenuFile}" : MenuFile;
-
-            public static ModItem Deserialize(BinaryReader binaryReader)
-                => new ModItem()
-                {
-                    MenuFile = binaryReader.ReadNullableString(),
-                    BaseMenuFile = binaryReader.ReadNullableString(),
-                    IconFile = binaryReader.ReadNullableString(),
-                    Name = binaryReader.ReadNullableString(),
-                    Category = binaryReader.ReadNullableString(),
-                    Priority = float.Parse(binaryReader.ReadNullableString()),
-                    IsMod = binaryReader.ReadBoolean(),
-                    IsOfficialMod = binaryReader.ReadBoolean()
-                };
-
-            public void Serialize(BinaryWriter binaryWriter)
-            {
-                if (IsOfficialMod) return;
-                binaryWriter.WriteNullableString(MenuFile);
-                binaryWriter.WriteNullableString(BaseMenuFile);
-                binaryWriter.WriteNullableString(IconFile);
-                binaryWriter.WriteNullableString(Name);
-                binaryWriter.WriteNullableString(Category);
-                binaryWriter.WriteNullableString(Priority.ToString(CultureInfo.InvariantCulture));
-                binaryWriter.Write(IsMod);
-                binaryWriter.Write(IsOfficialMod);
-            }
-        }
-
-        public class MyRoomItem : MenuItem
-        {
-            public int ID { get; set; }
-            public string PrefabName { get; set; }
-
-            public override string ToString() => $"MYR_{ID}#{PrefabName}";
-        }
-
-        private class ModelInfo
-        {
-            public List<MaterialChange> MaterialChanges { get; } = new List<MaterialChange>();
-            public string ModelFile { get; set; }
-        }
-
-        private readonly struct MaterialChange
-        {
-            public int MaterialIndex { get; }
-            public string MaterialFile { get; }
-
-            public MaterialChange(int materialIndex, string materialFile)
-            {
-                MaterialIndex = materialIndex;
-                MaterialFile = materialFile;
-            }
-        }
     }
 }

+ 70 - 0
COM3D2.MeidoPhotoStudio.Plugin/MeidoPhotoStudio/MenuItem.cs

@@ -0,0 +1,70 @@
+using System.Globalization;
+using System.IO;
+using UnityEngine;
+
+namespace COM3D2.MeidoPhotoStudio.Plugin
+{
+    public abstract class MenuItem
+    {
+        public string IconFile { get; set; }
+        public Texture2D Icon { get; set; }
+    }
+
+    public class ModItem : MenuItem
+    {
+        public string MenuFile { get; set; }
+        public string BaseMenuFile { get; set; }
+        public string Name { get; set; }
+        public string Category { get; set; }
+        public float Priority { get; set; }
+        public bool IsMod { get; private set; }
+        public bool IsOfficialMod { get; private set; }
+
+        public static ModItem OfficialMod(string menuFile) => new ModItem()
+        {
+            MenuFile = menuFile, IsMod = true, IsOfficialMod = true, Priority = 1000f
+        };
+
+        public static ModItem Mod(string menuFile) => new ModItem() { MenuFile = menuFile, IsMod = true };
+
+        public ModItem() { }
+
+        public ModItem(string menuFile) => MenuFile = menuFile;
+
+        public override string ToString() => IsOfficialMod ? $"{Path.GetFileName(MenuFile)}#{BaseMenuFile}" : MenuFile;
+
+        public static ModItem Deserialize(BinaryReader binaryReader) => new ModItem()
+        {
+            MenuFile = binaryReader.ReadNullableString(),
+            BaseMenuFile = binaryReader.ReadNullableString(),
+            IconFile = binaryReader.ReadNullableString(),
+            Name = binaryReader.ReadNullableString(),
+            Category = binaryReader.ReadNullableString(),
+            Priority = float.Parse(binaryReader.ReadNullableString()),
+            IsMod = binaryReader.ReadBoolean(),
+            IsOfficialMod = binaryReader.ReadBoolean()
+        };
+
+        public void Serialize(BinaryWriter binaryWriter)
+        {
+            if (IsOfficialMod) return;
+
+            binaryWriter.WriteNullableString(MenuFile);
+            binaryWriter.WriteNullableString(BaseMenuFile);
+            binaryWriter.WriteNullableString(IconFile);
+            binaryWriter.WriteNullableString(Name);
+            binaryWriter.WriteNullableString(Category);
+            binaryWriter.WriteNullableString(Priority.ToString(CultureInfo.InvariantCulture));
+            binaryWriter.Write(IsMod);
+            binaryWriter.Write(IsOfficialMod);
+        }
+    }
+
+    public class MyRoomItem : MenuItem
+    {
+        public int ID { get; set; }
+        public string PrefabName { get; set; }
+
+        public override string ToString() => $"MYR_{ID}#{PrefabName}";
+    }
+}

+ 376 - 12
COM3D2.MeidoPhotoStudio.Plugin/MeidoPhotoStudio/ModelUtility.cs

@@ -9,8 +9,158 @@ using Object = UnityEngine.Object;
 
 namespace COM3D2.MeidoPhotoStudio.Plugin
 {
-    public static partial class MenuFileUtility
+    using static MenuFileUtility;
+
+    public static class ModelUtility
     {
+        private enum IMode { None, ItemChange, TexChange }
+
+        private static GameObject deploymentObject;
+
+        private static GameObject GetDeploymentObject()
+        {
+            if (deploymentObject) return deploymentObject;
+
+            if (!(deploymentObject = GameObject.Find("Deployment Object Parent")))
+                deploymentObject = new GameObject("Deployment Object Parent");
+            return deploymentObject;
+        }
+
+        public static GameObject LoadMyRoomModel(MyRoomItem item)
+        {
+            var data = MyRoomCustom.PlacementData.GetData(item.ID);
+            var gameObject = Object.Instantiate(data.GetPrefab());
+            if (gameObject)
+            {
+                var final = new GameObject();
+                gameObject.transform.SetParent(final.transform, true);
+                final.transform.SetParent(GetDeploymentObject().transform, false);
+                return final;
+            }
+
+            Utility.LogMessage($"Could not load MyRoomCreative model '{item.PrefabName}'");
+
+            return null;
+        }
+
+        public static GameObject LoadBgModel(string bgName)
+        {
+            var gameObject = GameMain.Instance.BgMgr.CreateAssetBundle(bgName);
+            if (!gameObject) gameObject = Resources.Load<GameObject>("BG/" + bgName);
+            if (!gameObject) gameObject = Resources.Load<GameObject>("BG/2_0/" + bgName);
+
+            if (gameObject)
+            {
+                var final = Object.Instantiate(gameObject);
+                final.transform.localScale = Vector3.one * 0.1f;
+                return final;
+            }
+
+            Utility.LogMessage($"Could not load BG model '{bgName}'");
+
+            return null;
+        }
+
+        public static GameObject LoadGameModel(string assetName)
+        {
+            var gameObject = GameMain.Instance.BgMgr.CreateAssetBundle(assetName);
+            if (!gameObject) gameObject = Resources.Load<GameObject>("Prefab/" + assetName);
+            if (!gameObject) gameObject = Resources.Load<GameObject>("BG/" + assetName);
+
+            if (gameObject)
+            {
+                var final = Object.Instantiate(gameObject);
+                final.transform.localPosition = Vector3.zero;
+
+                Renderer[] renderers = final.GetComponentsInChildren<Renderer>();
+                foreach (var renderer in renderers)
+                {
+                    if (renderer && renderer.gameObject.name.Contains("castshadow"))
+                        renderer.shadowCastingMode = ShadowCastingMode.Off;
+                }
+
+                Collider[] colliders = final.GetComponentsInChildren<Collider>();
+                foreach (var collider in colliders)
+                {
+                    if (collider) collider.enabled = false;
+                }
+
+                if (final.transform.localScale != Vector3.one)
+                {
+                    var parent = new GameObject();
+                    final.transform.SetParent(parent.transform, true);
+                    return parent;
+                }
+
+                return final;
+            }
+
+            Utility.LogMessage($"Could not load game model '{assetName}'");
+
+            return null;
+        }
+
+        public static GameObject LoadMenuModel(string menuFile) => LoadMenuModel(new ModItem(menuFile));
+
+        public static GameObject LoadMenuModel(ModItem modItem)
+        {
+            var menu = modItem.IsOfficialMod ? modItem.BaseMenuFile : modItem.MenuFile;
+
+            byte[] modelBuffer;
+
+            try { modelBuffer = ReadAFileBase(menu); }
+            catch (Exception e)
+            {
+                Utility.LogError($"Could not read menu file '{menu}' because {e.Message}\n{e.StackTrace}");
+                return null;
+            }
+
+            if (ProcScriptBin(modelBuffer, out ModelInfo modelInfo))
+            {
+                if (InstantiateModel(modelInfo.ModelFile, out var finalModel))
+                {
+                    IEnumerable<Renderer> renderers = GetRenderers(finalModel).ToList();
+
+                    foreach (MaterialChange matChange in modelInfo.MaterialChanges)
+                    {
+                        foreach (Renderer renderer in renderers)
+                        {
+                            if (matChange.MaterialIndex < renderer.materials.Length)
+                            {
+                                renderer.materials[matChange.MaterialIndex] = ImportCM.LoadMaterial(
+                                    matChange.MaterialFile, null, renderer.materials[matChange.MaterialIndex]
+                                );
+                            }
+                        }
+                    }
+
+                    if (!modItem.IsOfficialMod) return finalModel;
+
+                    try { modelBuffer = ReadOfficialMod(modItem.MenuFile); }
+                    catch (Exception e)
+                    {
+                        Utility.LogError(
+                            $"Could not read mod menu file '{modItem.MenuFile}' because {e.Message}\n{e.StackTrace}"
+                        );
+                        return null;
+                    }
+
+                    ProcModScriptBin(modelBuffer, finalModel);
+
+                    return finalModel;
+                }
+            }
+
+            Utility.LogMessage($"Could not load menu model '{modItem.MenuFile}'");
+
+            return null;
+        }
+
+        private static IEnumerable<Renderer> GetRenderers(GameObject gameObject) => gameObject.transform
+            .GetComponentsInChildren<Transform>(true)
+            .Select(transform => transform.GetComponent<Renderer>())
+            .Where(renderer => renderer && renderer.material).ToList();
+
         private static GameObject CreateSeed() => Object.Instantiate(Resources.Load<GameObject>("seed"));
 
         private static bool InstantiateModel(string modelFilename, out GameObject modelParent)
@@ -25,7 +175,7 @@ namespace COM3D2.MeidoPhotoStudio.Plugin
                 Utility.LogError($"Could not load model file '{modelFilename}'");
                 return false;
             }
-            
+
             using var binaryReader = new BinaryReader(new MemoryStream(buffer), Encoding.UTF8);
 
             if (binaryReader.ReadString() != "CM3D2_MESH")
@@ -36,11 +186,11 @@ namespace COM3D2.MeidoPhotoStudio.Plugin
 
             var modelVersion = binaryReader.ReadInt32();
             var modelName = binaryReader.ReadString();
-            
+
             modelParent = CreateSeed();
             modelParent.layer = 1;
             modelParent.name = "_SM_" + modelName;
-            
+
             var rootName = binaryReader.ReadString();
             var boneCount = binaryReader.ReadInt32();
 
@@ -75,8 +225,8 @@ namespace COM3D2.MeidoPhotoStudio.Plugin
                 for (var i = 0; i < boneCount; i++)
                 {
                     var parentIndex = binaryReader.ReadInt32();
-                    boneList[i].transform.parent = parentIndex >= 0 
-                        ? boneList[parentIndex].transform 
+                    boneList[i].transform.parent = parentIndex >= 0
+                        ? boneList[parentIndex].transform
                         : modelParent.transform;
                 }
 
@@ -88,7 +238,7 @@ namespace COM3D2.MeidoPhotoStudio.Plugin
                     if (modelVersion >= 2001 && binaryReader.ReadBoolean())
                         transform.localScale = binaryReader.ReadVector3();
                 }
-                
+
                 // read mesh data
                 var meshRenderer = rootBone.AddComponent<SkinnedMeshRenderer>();
                 meshRenderer.updateWhenOffscreen = true;
@@ -96,9 +246,9 @@ namespace COM3D2.MeidoPhotoStudio.Plugin
                 meshRenderer.lightProbeUsage = LightProbeUsage.Off;
                 meshRenderer.reflectionProbeUsage = ReflectionProbeUsage.Off;
                 meshRenderer.motionVectorGenerationMode = MotionVectorGenerationMode.ForceNoMotion;
-                
+
                 Mesh sharedMesh = meshRenderer.sharedMesh = new Mesh();
-                
+
                 var vertCount = binaryReader.ReadInt32();
                 var subMeshCount = binaryReader.ReadInt32();
                 var meshBoneCount = binaryReader.ReadInt32();
@@ -107,8 +257,9 @@ namespace COM3D2.MeidoPhotoStudio.Plugin
                 for (var i = 0; i < meshBoneCount; i++)
                 {
                     var boneName = binaryReader.ReadString();
-                    
-                    if (!boneDict.ContainsKey(boneName)) Debug.LogError("nullbone= " + boneName);
+
+                    if (!boneDict.ContainsKey(boneName))
+                        Debug.LogError("nullbone= " + boneName);
                     else
                     {
                         var keyName = boneName + "$_SCL_";
@@ -121,7 +272,7 @@ namespace COM3D2.MeidoPhotoStudio.Plugin
 
                 var bindPoses = new Matrix4x4[meshBoneCount];
                 for (var i = 0; i < meshBoneCount; i++) bindPoses[i] = binaryReader.ReadMatrix4x4();
-                
+
                 sharedMesh.bindposes = bindPoses;
 
                 var vertices = new Vector3[vertCount];
@@ -197,5 +348,218 @@ namespace COM3D2.MeidoPhotoStudio.Plugin
                 return false;
             }
         }
+
+        private static bool ProcScriptBin(byte[] menuBuf, out ModelInfo modelInfo)
+        {
+            modelInfo = null;
+            using var binaryReader = new BinaryReader(new MemoryStream(menuBuf), Encoding.UTF8);
+
+            if (binaryReader.ReadString() != "CM3D2_MENU") return false;
+
+            modelInfo = new ModelInfo();
+
+            binaryReader.ReadInt32(); // file version
+            binaryReader.ReadString(); // txt path
+            binaryReader.ReadString(); // name
+            binaryReader.ReadString(); // category
+            binaryReader.ReadString(); // description
+            binaryReader.ReadInt32(); // idk (as long)
+
+            try
+            {
+                while (true)
+                {
+                    int numberOfProps = binaryReader.ReadByte();
+                    var menuPropString = string.Empty;
+
+                    if (numberOfProps != 0)
+                    {
+                        for (var i = 0; i < numberOfProps; i++)
+                        {
+                            menuPropString = $"{menuPropString}\"{binaryReader.ReadString()}\"";
+                        }
+
+                        if (menuPropString != string.Empty)
+                        {
+                            var header = UTY.GetStringCom(menuPropString);
+                            string[] menuProps = UTY.GetStringList(menuPropString);
+
+                            if (header == "end") break;
+
+                            switch (header)
+                            {
+                                case "マテリアル変更":
+                                {
+                                    var matNo = int.Parse(menuProps[2]);
+                                    var materialFile = menuProps[3];
+                                    modelInfo.MaterialChanges.Add(new MaterialChange(matNo, materialFile));
+                                    break;
+                                }
+                                case "additem":
+                                    modelInfo.ModelFile = menuProps[1];
+                                    break;
+                            }
+                        }
+                    }
+                    else
+                        break;
+                }
+            }
+            catch { return false; }
+
+            return true;
+        }
+
+        private static void ProcModScriptBin(byte[] cd, GameObject go)
+        {
+            var matDict = new Dictionary<string, byte[]>();
+            string modData;
+
+            using (var binaryReader = new BinaryReader(new MemoryStream(cd), Encoding.UTF8))
+            {
+                if (binaryReader.ReadString() != "CM3D2_MOD") return;
+
+                binaryReader.ReadInt32();
+                binaryReader.ReadString();
+                binaryReader.ReadString();
+                binaryReader.ReadString();
+                binaryReader.ReadString();
+                binaryReader.ReadString();
+                var mpnValue = binaryReader.ReadString();
+                var mpn = MPN.null_mpn;
+                try { mpn = (MPN) Enum.Parse(typeof(MPN), mpnValue, true); }
+                catch
+                {
+                    /* ignored */
+                }
+
+                if (mpn != MPN.null_mpn) binaryReader.ReadString();
+
+                modData = binaryReader.ReadString();
+                var entryCount = binaryReader.ReadInt32();
+                for (var i = 0; i < entryCount; i++)
+                {
+                    var key = binaryReader.ReadString();
+                    var count = binaryReader.ReadInt32();
+                    byte[] value = binaryReader.ReadBytes(count);
+                    matDict.Add(key, value);
+                }
+            }
+
+            var mode = IMode.None;
+            var materialChange = false;
+
+            Material material = null;
+            var materialIndex = 0;
+
+            using var stringReader = new StringReader(modData);
+
+            string line;
+
+            List<Renderer> renderers = null;
+
+            while ((line = stringReader.ReadLine()) != null)
+            {
+                string[] data = line.Split(new[] { '\t', ' ' }, StringSplitOptions.RemoveEmptyEntries);
+
+                switch (data[0])
+                {
+                    case "アイテム変更":
+                    case "マテリアル変更":
+                        mode = IMode.ItemChange;
+                        break;
+                    case "テクスチャ変更":
+                        mode = IMode.TexChange;
+                        break;
+                }
+
+                switch (mode)
+                {
+                    case IMode.ItemChange:
+                    {
+                        if (data[0] == "スロット名") materialChange = true;
+
+                        if (materialChange)
+                        {
+                            if (data[0] == "マテリアル番号")
+                            {
+                                materialIndex = int.Parse(data[1]);
+
+                                renderers ??= GetRenderers(go).ToList();
+
+                                foreach (Renderer renderer in renderers)
+                                {
+                                    if (materialIndex < renderer.materials.Length)
+                                        material = renderer.materials[materialIndex];
+                                }
+                            }
+
+                            if (!material) continue;
+
+                            switch (data[0])
+                            {
+                                case "テクスチャ設定":
+                                    ChangeTex(materialIndex, data[1], data[2].ToLower());
+                                    break;
+                                case "色設定":
+                                    material.SetColor(
+                                        data[1],
+                                        new Color(
+                                            float.Parse(data[2]) / 255f, float.Parse(data[3]) / 255f,
+                                            float.Parse(data[4]) / 255f, float.Parse(data[5]) / 255f
+                                        )
+                                    );
+                                    break;
+                                case "数値設定":
+                                    material.SetFloat(data[1], float.Parse(data[2]));
+                                    break;
+                            }
+                        }
+
+                        break;
+                    }
+                    case IMode.TexChange:
+                    {
+                        var matno = int.Parse(data[2]);
+                        ChangeTex(matno, data[3], data[4].ToLower());
+                        break;
+                    }
+                    case IMode.None: break;
+                    default: throw new ArgumentOutOfRangeException();
+                }
+            }
+
+            void ChangeTex(int matno, string prop, string filename)
+            {
+                byte[] buf = matDict[filename.ToLowerInvariant()];
+                var textureResource = new TextureResource(2, 2, TextureFormat.ARGB32, null, buf);
+                renderers ??= GetRenderers(go).ToList();
+                foreach (Renderer r in renderers)
+                {
+                    r.materials[matno].SetTexture(prop, null);
+                    Texture2D texture2D = textureResource.CreateTexture2D();
+                    texture2D.name = filename;
+                    r.materials[matno].SetTexture(prop, texture2D);
+                }
+            }
+        }
+
+        private class ModelInfo
+        {
+            public List<MaterialChange> MaterialChanges { get; } = new List<MaterialChange>();
+            public string ModelFile { get; set; }
+        }
+
+        private readonly struct MaterialChange
+        {
+            public int MaterialIndex { get; }
+            public string MaterialFile { get; }
+
+            public MaterialChange(int materialIndex, string materialFile)
+            {
+                MaterialIndex = materialIndex;
+                MaterialFile = materialFile;
+            }
+        }
     }
 }