Browse Source

Support KC and preset body swapping

In regards to preset body swapping, the method of identifying when the
body is changing needed to be changed. The new method notifies right at
the heart of maid prop processing.

The new method also removes the need for coroutines which is nice.

Some bugs were fixed as well like gravity control breaking when
switching body.
habeebweeb 2 years ago
parent
commit
dce7a836e4

+ 283 - 195
src/MeidoPhotoStudio.Plugin/Meido/Meido.cs

@@ -43,7 +43,7 @@ public class Meido
     private readonly FieldInfo m_eMaskMode = Utility.GetFieldInfo<TBody>("m_eMaskMode");
     private readonly FieldInfo m_eMaskMode = Utility.GetFieldInfo<TBody>("m_eMaskMode");
 #pragma warning restore SA1308
 #pragma warning restore SA1308
 
 
-    private bool initialized;
+    private bool faceNeedsInitialization = true;
     private float[] blendSetValueBackup;
     private float[] blendSetValueBackup;
     private bool freeLook;
     private bool freeLook;
 
 
@@ -128,21 +128,25 @@ public class Meido
 
 
     public bool HairGravityActive
     public bool HairGravityActive
     {
     {
-        get => HairGravityControl.Active;
+        get => HairGravityControl && HairGravityControl.Active;
         set
         set
         {
         {
-            if (HairGravityControl.Valid)
-                HairGravityControl.gameObject.SetActive(value);
+            if (!HairGravityControl || !HairGravityControl.Valid)
+                return;
+
+            HairGravityControl.gameObject.SetActive(value);
         }
         }
     }
     }
 
 
     public bool SkirtGravityActive
     public bool SkirtGravityActive
     {
     {
-        get => SkirtGravityControl.Active;
+        get => SkirtGravityControl && SkirtGravityControl.Active;
         set
         set
         {
         {
-            if (SkirtGravityControl.Valid)
-                SkirtGravityControl.gameObject.SetActive(value);
+            if (!SkirtGravityControl || !SkirtGravityControl.Valid)
+                return;
+
+            SkirtGravityControl.gameObject.SetActive(value);
         }
         }
     }
     }
 
 
@@ -326,16 +330,30 @@ public class Meido
 
 
         if (!Body.isLoadedBody)
         if (!Body.isLoadedBody)
         {
         {
+            AllProcPropSeqStartPatcher.SequenceEnded += LoadMaid;
+
             Maid.DutPropAll();
             Maid.DutPropAll();
             Maid.AllProcPropSeqStart();
             Maid.AllProcPropSeqStart();
-        }
 
 
-        StartLoad(OnBodyLoad);
+            void LoadMaid(object sender, ProcStartEventArgs e)
+            {
+                if (e.Maid.status.guid != Maid.status.guid)
+                    return;
+
+                OnBodyLoad();
+
+                AllProcPropSeqStartPatcher.SequenceEnded -= LoadMaid;
+            }
+        }
+        else
+        {
+            OnBodyLoad();
+        }
     }
     }
 
 
     public void Unload()
     public void Unload()
     {
     {
-        if (Body.isLoadedBody && Maid.Visible)
+        if (Maid.Visible)
         {
         {
             DetachAllMpnAttach();
             DetachAllMpnAttach();
 
 
@@ -363,7 +381,11 @@ public class Meido
             SetFaceBlendSet(DefaultFaceBlendSet);
             SetFaceBlendSet(DefaultFaceBlendSet);
         }
         }
 
 
-        AllProcPropSeqStartPatcher.SequenceStart -= ReinitializeBody;
+        AllProcPropSeqStartPatcher.SequenceStarting -= OnMaidPropChanging;
+
+#if COM25
+        SwapNewMaidPropPatcher.NewMaidPropSwapping -= OnNewBodySwapping;
+#endif
 
 
         MuneYureLEnabled = true;
         MuneYureLEnabled = true;
         MuneYureREnabled = true;
         MuneYureREnabled = true;
@@ -394,7 +416,6 @@ public class Meido
         Body.transform.localScale = Vector3.one;
         Body.transform.localScale = Vector3.one;
         Maid.ResetAll();
         Maid.ResetAll();
         Maid.MabatakiUpdateStop = false;
         Maid.MabatakiUpdateStop = false;
-        Maid.ActiveSlotNo = -1;
     }
     }
 
 
     public void SetPose(PoseInfo poseInfo)
     public void SetPose(PoseInfo poseInfo)
@@ -580,8 +601,133 @@ public class Meido
         return faceData;
         return faceData;
     }
     }
 
 
-    public void SetFaceBlendSet(string blendSet)
+    public void SetFaceBlendSet(string blendSet) =>
+        SetFaceBlendSet(blendSet, true);
+
+    public void SetFaceBlendValue(string faceKey, float value)
+    {
+        var morph = Body.Face.morph;
+        var hash = Utility.GP01FbFaceHash(morph, faceKey);
+
+        if (!morph.Contains(hash))
+            return;
+
+        var blendIndex = (int)morph.hash[hash];
+
+        if (faceKey is "nosefook")
+            Maid.boNoseFook = morph.boNoseFook = value > 0f;
+        else
+            morph.dicBlendSet[CurrentFaceBlendSet][blendIndex] = value;
+
+        morph.SetBlendValues(blendIndex, value);
+        morph.FixBlendValues_Face();
+    }
+
+    public float GetFaceBlendValue(string hash)
+    {
+        var morph = Body.Face.morph;
+
+        if (hash is "nosefook")
+            return (Maid.boNoseFook || morph.boNoseFook) ? 1f : 0f;
+
+        hash = Utility.GP01FbFaceHash(morph, hash);
+
+        return morph.dicBlendSet[CurrentFaceBlendSet][(int)morph.hash[hash]];
+    }
+
+    public void StopBlink()
+    {
+        Maid.MabatakiUpdateStop = true;
+        Body.Face.morph.EyeMabataki = 0f;
+        Utility.SetFieldValue(Maid, "MabatakiVal", 0f);
+    }
+
+    public void SetMaskMode(Mask maskMode) =>
+        SetMaskMode(maskMode is Mask.Nude ? MaskMode.Nude : (MaskMode)maskMode);
+
+    public void SetMaskMode(MaskMode maskMode)
+    {
+        var invisibleBody = !Body.GetMask(SlotID.body);
+
+        Body.SetMaskMode(maskMode);
+
+        if (invisibleBody)
+            SetBodyMask(false);
+    }
+
+    public void SetBodyMask(bool enabled)
+    {
+        var table = Utility.GetFieldValue<TBody, Hashtable>(Body, "m_hFoceHide");
+
+        foreach (var bodySlot in MaidDressingPane.BodySlots)
+            table[bodySlot] = enabled;
+
+        Body.FixMaskFlag();
+        Body.FixVisibleFlag(false);
+    }
+
+    public void SetCurling(Curl curling, bool enabled)
+    {
+        var name = curling is Curl.Shift
+            ? new[] { "panz", "mizugi" }
+            : new[] { "skirt", "onepiece" };
+
+        if (enabled)
+        {
+            var action = curling switch
+            {
+                Curl.Shift => "パンツずらし",
+                Curl.Front => "めくれスカート",
+                _ => "めくれスカート後ろ",
+            };
+
+            Maid.ItemChangeTemp(name[0], action);
+            Maid.ItemChangeTemp(name[1], action);
+        }
+        else
+        {
+            Maid.ResetProp(name[0]);
+            Maid.ResetProp(name[1]);
+        }
+
+        Maid.AllProcProp();
+        HairGravityControl.Control.OnChangeMekure();
+        SkirtGravityControl.Control.OnChangeMekure();
+    }
+
+    public void SetMpnProp(MpnAttachProp prop, bool detach)
+    {
+        if (detach)
+            Maid.ResetProp(prop.Tag, false);
+        else
+            Maid.SetProp(prop.Tag, prop.MenuFile, 0, true);
+
+        Maid.AllProcProp();
+    }
+
+    public void DetachAllMpnAttach()
+    {
+        if (!Body.isLoadedBody)
+            return;
+
+        Maid.ResetProp(MPN.kousoku_lower, false);
+        Maid.ResetProp(MPN.kousoku_upper, false);
+        Maid.AllProcProp();
+    }
+
+    public void ApplyGravity(Vector3 position, bool skirt = false)
     {
     {
+        var dragPoint = skirt ? SkirtGravityControl : HairGravityControl;
+
+        if (dragPoint && dragPoint.Valid)
+            dragPoint.Control.transform.localPosition = position;
+    }
+
+    private void SetFaceBlendSet(string blendSet, bool withNotify)
+    {
+        if (!Body.isLoadedBody)
+            return;
+
         if (blendSet.StartsWith(Constants.CustomFacePath))
         if (blendSet.StartsWith(Constants.CustomFacePath))
         {
         {
             var blendSetFileName = Path.GetFileNameWithoutExtension(blendSet);
             var blendSetFileName = Path.GetFileNameWithoutExtension(blendSet);
@@ -690,184 +836,87 @@ public class Meido
         }
         }
 
 
         StopBlink();
         StopBlink();
-        OnUpdateMeido();
-    }
-
-    public void SetFaceBlendValue(string faceKey, float value)
-    {
-        var morph = Body.Face.morph;
-        var hash = Utility.GP01FbFaceHash(morph, faceKey);
-
-        if (!morph.Contains(hash))
-            return;
-
-        var blendIndex = (int)morph.hash[hash];
-
-        if (faceKey is "nosefook")
-            Maid.boNoseFook = morph.boNoseFook = value > 0f;
-        else
-            morph.dicBlendSet[CurrentFaceBlendSet][blendIndex] = value;
-
-        morph.SetBlendValues(blendIndex, value);
-        morph.FixBlendValues();
-    }
-
-    public float GetFaceBlendValue(string hash)
-    {
-        var morph = Body.Face.morph;
-
-        if (hash is "nosefook")
-            return (Maid.boNoseFook || morph.boNoseFook) ? 1f : 0f;
-
-        hash = Utility.GP01FbFaceHash(morph, hash);
-
-        return morph.dicBlendSet[CurrentFaceBlendSet][(int)morph.hash[hash]];
-    }
-
-    public void StopBlink()
-    {
-        Maid.MabatakiUpdateStop = true;
-        Body.Face.morph.EyeMabataki = 0f;
-        Utility.SetFieldValue(Maid, "MabatakiVal", 0f);
-    }
-
-    public void SetMaskMode(Mask maskMode) =>
-        SetMaskMode(maskMode is Mask.Nude ? MaskMode.Nude : (MaskMode)maskMode);
-
-    public void SetMaskMode(MaskMode maskMode)
-    {
-        var invisibleBody = !Body.GetMask(SlotID.body);
-
-        Body.SetMaskMode(maskMode);
 
 
-        if (invisibleBody)
-            SetBodyMask(false);
+        if (withNotify)
+            OnUpdateMeido();
     }
     }
 
 
-    public void SetBodyMask(bool enabled)
+    private void OnBodyLoad()
     {
     {
-        var table = Utility.GetFieldValue<TBody, Hashtable>(Body, "m_hFoceHide");
+        InitializeFace();
 
 
-        foreach (var bodySlot in MaidDressingPane.BodySlots)
-            table[bodySlot] = enabled;
+        InitializeGravityControls();
 
 
-        Body.FixMaskFlag();
-        Body.FixVisibleFlag(false);
-    }
+        InitializeBody();
 
 
-    public void SetCurling(Curl curling, bool enabled)
-    {
-        var name = curling is Curl.Shift
-            ? new[] { "panz", "mizugi" }
-            : new[] { "skirt", "onepiece" };
-
-        if (enabled)
+        if (MeidoPhotoStudio.EditMode)
         {
         {
-            var action = curling switch
-            {
-                Curl.Shift => "パンツずらし",
-                Curl.Front => "めくれスカート",
-                _ => "めくれスカート後ろ",
-            };
+            AllProcPropSeqStartPatcher.SequenceStarting += OnMaidPropChanging;
 
 
-            Maid.ItemChangeTemp(name[0], action);
-            Maid.ItemChangeTemp(name[1], action);
-        }
-        else
-        {
-            Maid.ResetProp(name[0]);
-            Maid.ResetProp(name[1]);
+#if COM25
+            SwapNewMaidPropPatcher.NewMaidPropSwapping += OnNewBodySwapping;
+#endif
         }
         }
 
 
-        Maid.AllProcProp();
-        HairGravityControl.Control.OnChangeMekure();
-        SkirtGravityControl.Control.OnChangeMekure();
-    }
-
-    public void SetMpnProp(MpnAttachProp prop, bool detach)
-    {
-        if (detach)
-            Maid.ResetProp(prop.Tag, false);
-        else
-            Maid.SetProp(prop.Tag, prop.MenuFile, 0, true);
-
-        Maid.AllProcProp();
-    }
+        IK = true;
+        Stop = false;
+        Bone = false;
 
 
-    public void DetachAllMpnAttach()
-    {
-        Maid.ResetProp(MPN.kousoku_lower, false);
-        Maid.ResetProp(MPN.kousoku_upper, false);
-        Maid.AllProcProp();
+        Active = true;
     }
     }
 
 
-    public void ApplyGravity(Vector3 position, bool skirt = false)
+#if COM25
+    private void OnNewBodySwapping(object sender, ProcStartEventArgs args)
     {
     {
-        var dragPoint = skirt ? SkirtGravityControl : HairGravityControl;
-
-        if (dragPoint.Valid)
-            dragPoint.Control.transform.localPosition = position;
-    }
+        if (Loading || !Body.isLoadedBody)
+            return;
 
 
-    private void StartLoad(Action callback)
-    {
-        if (Loading)
+        if (args.Maid.status.guid != Maid.status.guid)
             return;
             return;
 
 
-        GameMain.Instance.StartCoroutine(Load(callback));
-    }
+        IKManager.Destroy();
 
 
-    private IEnumerator Load(Action callback)
-    {
-        Loading = true;
+        SetFaceBlendSet(DefaultFaceBlendSet);
 
 
-        while (Maid.IsBusy)
-            yield return null;
+        DestroyGravityControl(HairGravityControl);
+        DestroyGravityControl(SkirtGravityControl);
 
 
-        yield return new WaitForEndOfFrame();
+        // Prevent reinitializing again
+        AllProcPropSeqStartPatcher.SequenceStarting -= OnMaidPropChanging;
 
 
-        callback();
-        Loading = false;
-    }
+        AllProcPropSeqStartPatcher.SequenceEnded += OnSequenceEnded;
 
 
-    private void OnBodyLoad()
-    {
-        if (!initialized)
+        void OnSequenceEnded(object sender, ProcStartEventArgs e)
         {
         {
-            DefaultEyeRotL = Body.quaDefEyeL;
-            DefaultEyeRotR = Body.quaDefEyeR;
-
-            initialized = true;
-        }
+            if (e.Maid.status.guid != Maid.status.guid)
+                return;
 
 
-        if (blendSetValueBackup is null)
-            BackupBlendSetValues();
+            InitializeFace(true);
 
 
-        if (!HairGravityControl)
             InitializeGravityControls();
             InitializeGravityControls();
 
 
-        HairGravityControl.Move += OnGravityEvent;
-        SkirtGravityControl.Move += OnGravityEvent;
+            InitializeBody();
 
 
-        if (MeidoPhotoStudio.EditMode)
-            AllProcPropSeqStartPatcher.SequenceStart += ReinitializeBody;
+            if (!Maid.IsCrcBody)
+            {
+                // Maid animation needs to be set again for custom parts edit
+                var uiRoot = GameObject.Find("UI Root");
+                var customPartsWindow = UTY.GetChildObject(uiRoot, "Window/CustomPartsWindow")
+                    .GetComponent<SceneEditWindow.CustomPartsWindow>();
 
 
-#if COM25
-        // NOTE: This is required for IK to work in COM3D2.5
-        Body.motionBlendTime = 0f;
-#endif
-        IKManager.Initialize();
+                Utility.SetFieldValue(customPartsWindow, "animation", Maid.GetAnimation());
+            }
 
 
-        SetFaceBlendSet(DefaultFaceBlendSet);
+            OnUpdateMeido();
 
 
-        IK = true;
-        Stop = false;
-        Bone = false;
+            AllProcPropSeqStartPatcher.SequenceEnded -= OnSequenceEnded;
 
 
-        Active = true;
+            AllProcPropSeqStartPatcher.SequenceStarting += OnMaidPropChanging;
+        }
     }
     }
+#endif
 
 
-    private void ReinitializeBody(object sender, ProcStartEventArgs args)
+    private void OnMaidPropChanging(object sender, ProcStartEventArgs args)
     {
     {
         if (Loading || !Body.isLoadedBody)
         if (Loading || !Body.isLoadedBody)
             return;
             return;
@@ -878,76 +927,72 @@ public class Meido
         var gravityControlProps =
         var gravityControlProps =
             new[]
             new[]
             {
             {
-                MPN.skirt, MPN.onepiece, MPN.mizugi, MPN.panz, MPN.set_maidwear, MPN.set_mywear, MPN.set_underwear,
-                MPN.hairf, MPN.hairr, MPN.hairs, MPN.hairt,
+                // NOTE: body is checked because the gravity control is destroyed along with the body.
+                MPN.body, MPN.skirt, MPN.onepiece, MPN.mizugi, MPN.panz, MPN.set_maidwear, MPN.set_mywear,
+                MPN.set_underwear, MPN.hairf, MPN.hairr, MPN.hairs, MPN.hairt,
             };
             };
 
 
-        Action action = null;
+        Action reinitializationCallback = null;
 
 
         // Change body
         // Change body
         if (Maid.GetProp(MPN.body).boDut)
         if (Maid.GetProp(MPN.body).boDut)
         {
         {
             IKManager.Destroy();
             IKManager.Destroy();
-            action += ReinitializeBody;
+
+            reinitializationCallback += () =>
+            {
+                // Maid animation needs to be set again for custom parts edit
+                var uiRoot = GameObject.Find("UI Root");
+                var customPartsWindow = UTY.GetChildObject(uiRoot, "Window/CustomPartsWindow")
+                    .GetComponent<SceneEditWindow.CustomPartsWindow>();
+
+                Utility.SetFieldValue(customPartsWindow, "animation", Maid.GetAnimation());
+
+                InitializeBody();
+            };
         }
         }
 
 
         // Change face
         // Change face
         if (Maid.GetProp(MPN.head).boDut)
         if (Maid.GetProp(MPN.head).boDut)
         {
         {
             SetFaceBlendSet(DefaultFaceBlendSet);
             SetFaceBlendSet(DefaultFaceBlendSet);
-            action += ReinitializeFace;
+
+            reinitializationCallback += () =>
+                InitializeFace(true);
         }
         }
 
 
         // Gravity control clothing/hair change
         // Gravity control clothing/hair change
         if (gravityControlProps.Any(prop => Maid.GetProp(prop).boDut))
         if (gravityControlProps.Any(prop => Maid.GetProp(prop).boDut))
         {
         {
-            if (HairGravityControl)
-                Object.Destroy(HairGravityControl.gameObject);
-
-            if (SkirtGravityControl)
-                Object.Destroy(SkirtGravityControl.gameObject);
+            DestroyGravityControl(HairGravityControl);
+            DestroyGravityControl(SkirtGravityControl);
 
 
-            action += ReinitializeGravity;
+            reinitializationCallback += InitializeGravityControls;
         }
         }
 
 
         // Clothing/accessory changes
         // Clothing/accessory changes
         // Includes null_mpn too but any button click results in null_mpn bodut I think
         // Includes null_mpn too but any button click results in null_mpn bodut I think
-        action ??= Default;
+        reinitializationCallback += () =>
+            OnUpdateMeido();
 
 
-        StartLoad(action);
+        AllProcPropSeqStartPatcher.SequenceEnded += OnSequenceEnded;
 
 
-        void ReinitializeBody()
+        void OnSequenceEnded(object sender, ProcStartEventArgs e)
         {
         {
-            IKManager.Initialize();
-            Stop = false;
-
-            // Maid animation needs to be set again for custom parts edit
-            var uiRoot = GameObject.Find("UI Root");
-            var customPartsWindow = UTY.GetChildObject(uiRoot, "Window/CustomPartsWindow")
-                .GetComponent<SceneEditWindow.CustomPartsWindow>();
-
-            Utility.SetFieldValue(customPartsWindow, "animation", Maid.GetAnimation());
-        }
+            if (e.Maid.status.guid != Maid.status.guid)
+                return;
 
 
-        void ReinitializeFace()
-        {
-            DefaultEyeRotL = Body.quaDefEyeL;
-            DefaultEyeRotR = Body.quaDefEyeR;
-            BackupBlendSetValues();
-        }
+            AllProcPropSeqStartPatcher.SequenceEnded -= OnSequenceEnded;
 
 
-        void ReinitializeGravity()
-        {
-            InitializeGravityControls();
-            OnUpdateMeido();
+            reinitializationCallback?.Invoke();
         }
         }
-
-        void Default() =>
-            OnUpdateMeido();
     }
     }
 
 
     private void BackupBlendSetValues()
     private void BackupBlendSetValues()
     {
     {
+        if (!Body.isLoadedBody)
+            return;
+
         var values = Body.Face.morph.dicBlendSet[CurrentFaceBlendSet];
         var values = Body.Face.morph.dicBlendSet[CurrentFaceBlendSet];
 
 
         blendSetValueBackup = new float[values.Length];
         blendSetValueBackup = new float[values.Length];
@@ -982,10 +1027,44 @@ public class Meido
         return cache;
         return cache;
     }
     }
 
 
+    private void InitializeBody()
+    {
+        if (!Body.isLoadedBody)
+            return;
+
+#if COM25
+        // NOTE: This is required for IK to work in COM3D2.5
+        Body.motionBlendTime = 0f;
+#endif
+
+        IKManager.Initialize();
+    }
+
+    private void InitializeFace(bool force = false)
+    {
+        if (force || faceNeedsInitialization)
+        {
+            DefaultEyeRotL = Body.quaDefEyeL;
+            DefaultEyeRotR = Body.quaDefEyeR;
+
+            faceNeedsInitialization = false;
+        }
+
+        BackupBlendSetValues();
+
+        SetFaceBlendSet(DefaultFaceBlendSet, false);
+    }
+
     private void InitializeGravityControls()
     private void InitializeGravityControls()
     {
     {
-        HairGravityControl = MakeGravityControl(skirt: false);
-        SkirtGravityControl = MakeGravityControl(skirt: true);
+        if (!HairGravityControl)
+            HairGravityControl = MakeGravityControl(skirt: false);
+
+        if (!SkirtGravityControl)
+            SkirtGravityControl = MakeGravityControl(skirt: true);
+
+        HairGravityControl.Move += OnGravityEvent;
+        SkirtGravityControl.Move += OnGravityEvent;
     }
     }
 
 
     private DragPointGravity MakeGravityControl(bool skirt = false)
     private DragPointGravity MakeGravityControl(bool skirt = false)
@@ -1001,6 +1080,15 @@ public class Meido
         return gravityDragpoint;
         return gravityDragpoint;
     }
     }
 
 
+    private void DestroyGravityControl(DragPointGravity control)
+    {
+        if (!control)
+            return;
+
+        control.Move -= OnGravityEvent;
+        Object.Destroy(control.gameObject);
+    }
+
     private void OnUpdateMeido(MeidoUpdateEventArgs args = null) =>
     private void OnUpdateMeido(MeidoUpdateEventArgs args = null) =>
         UpdateMeido?.Invoke(this, args ?? MeidoUpdateEventArgs.Empty);
         UpdateMeido?.Invoke(this, args ?? MeidoUpdateEventArgs.Empty);
 
 

+ 25 - 5
src/MeidoPhotoStudio.Plugin/Patchers/AllProcPropSeqStartPatcher.cs

@@ -5,14 +5,34 @@ using HarmonyLib;
 
 
 namespace MeidoPhotoStudio.Plugin;
 namespace MeidoPhotoStudio.Plugin;
 
 
-// TODO: Extend this further to potentially reduce the need for coroutines that wait for maid proc state
 public static class AllProcPropSeqStartPatcher
 public static class AllProcPropSeqStartPatcher
 {
 {
-    public static event EventHandler<ProcStartEventArgs> SequenceStart;
+    public static event EventHandler<ProcStartEventArgs> SequenceStarting;
 
 
-    [HarmonyPatch(typeof(Maid), nameof(Maid.AllProcPropSeqStart))]
+    public static event EventHandler<ProcStartEventArgs> SequenceEnded;
+
+    [HarmonyPatch(typeof(Maid), "AllProcPropSeq")]
+    [HarmonyPrefix]
+    [SuppressMessage("StyleCop.Analyzers.NamingRules", "SA1313", Justification = "Harmony parameter")]
+    private static void NotifyAllProcPropStarting(Maid __instance)
+    {
+        // TODO: Consider sending a patch to EditBodyLoadFix rather than relying on this brittle hack.
+        // The check for boModelChg is needed because AllProcProp2Cnt gets reset to 0 before the next phase which causes
+        // a second false start
+        if (__instance.AllProcProp2Fase == 0 && __instance.AllProcProp2Cnt == 0 && !__instance.boModelChg)
+        {
+            SequenceStarting?.Invoke(null, new(__instance));
+            if (__instance.GetProp(MPN.head).boDut)
+                Utility.LogDebug("Face is being initialized");
+        }
+    }
+
+    [HarmonyPatch(typeof(Maid), "AllProcPropSeq")]
     [HarmonyPostfix]
     [HarmonyPostfix]
     [SuppressMessage("StyleCop.Analyzers.NamingRules", "SA1313", Justification = "Harmony parameter")]
     [SuppressMessage("StyleCop.Analyzers.NamingRules", "SA1313", Justification = "Harmony parameter")]
-    private static void NotifyProcStart(Maid __instance) =>
-        SequenceStart?.Invoke(null, new(__instance));
+    private static void NotifyAllProcPropEnded(Maid __instance)
+    {
+        if (__instance.AllProcProp2Fase == 5 && !__instance.IsAllProcPropBusy)
+            SequenceEnded?.Invoke(null, new(__instance));
+    }
 }
 }

+ 24 - 0
src/MeidoPhotoStudio.Plugin/Patchers/SwapNewMaidPropPatcher.cs

@@ -0,0 +1,24 @@
+#if COM25
+using System;
+using System.Diagnostics.CodeAnalysis;
+
+using HarmonyLib;
+
+namespace MeidoPhotoStudio.Plugin;
+
+public static class SwapNewMaidPropPatcher
+{
+    public static event EventHandler<ProcStartEventArgs> NewMaidPropSwapping;
+
+    [HarmonyPatch(typeof(Maid), nameof(Maid.SwapNewMaidProp))]
+    [HarmonyPrefix]
+    [SuppressMessage("StyleCop.Analyzers.NamingRules", "SA1313", Justification = "Harmony parameter")]
+    private static void NotifyNewMaidPropSwapping(Maid __instance, bool toNewBody)
+    {
+        if (__instance.IsCrcBody == toNewBody)
+            return;
+
+        NewMaidPropSwapping?.Invoke(null, new(__instance));
+    }
+}
+#endif