Skip to content

Commit d9be49c

Browse files
authored
Fix Sea Emperor hatching sequence not completing in multiplayer (SubnauticaNitrox#2653)
1 parent fbd5caf commit d9be49c

3 files changed

Lines changed: 142 additions & 27 deletions

File tree

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
using System.Reflection;
2+
using UnityEngine;
3+
4+
namespace NitroxPatcher.Patches.Dynamic;
5+
6+
/// <summary>
7+
/// Handles cleanup of temporary babies created for animation on non-simulating players.
8+
/// Only the real networked baby (from simulating player) should call SwimToMother().
9+
/// Temporary babies are destroyed after the animation completes.
10+
/// </summary>
11+
public sealed partial class IncubatorEggAnimation_OnHatchAnimationEnd_Patch : NitroxPatch, IDynamicPatch
12+
{
13+
internal const string TEMPORARY_BABY_MARKER = "_NitroxTemporary";
14+
15+
private static readonly MethodInfo TARGET_METHOD = Reflect.Method((IncubatorEggAnimation t) => t.OnHatchAnimationEnd());
16+
17+
public static bool Prefix(IncubatorEggAnimation __instance)
18+
{
19+
// Safety check - ensure we have a baby reference
20+
if (!__instance.baby)
21+
{
22+
return true; // Let original method handle this case
23+
}
24+
25+
// Check if this is a temporary baby (created for animation only)
26+
if (__instance.baby.name.Contains(TEMPORARY_BABY_MARKER))
27+
{
28+
// For temporary babies, we only want to end the cinematic mode and cleanup
29+
// Don't call SwimToMother() as that should only happen for the real networked baby
30+
__instance.baby.cinematicController.SetCinematicMode(false);
31+
32+
// Set animationActive to false (field is publicized via BepInEx.AssemblyPublicizer)
33+
__instance.animationActive = false;
34+
35+
// Destroy the temporary baby - the real networked one will be spawned via server broadcast
36+
Object.Destroy(__instance.baby.gameObject);
37+
return false; // Skip the original method
38+
}
39+
40+
// For real networked babies, let the original method run (which calls SwimToMother)
41+
return true;
42+
}
43+
}
Lines changed: 45 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,70 @@
11
using System.Reflection;
22
using NitroxClient.GameLogic;
33
using NitroxClient.MonoBehaviours;
4-
using Nitrox.Model.Core;
54
using Nitrox.Model.DataStructures;
65
using Nitrox.Model.Subnautica.DataStructures.GameLogic.Entities;
76
using UnityEngine;
87

98
namespace NitroxPatcher.Patches.Dynamic;
109

10+
/// <summary>
11+
/// Handles the Sea Emperor baby hatching sequence in multiplayer.
12+
/// All players see the full hatching animation for better visual consistency.
13+
/// The simulating player spawns the real networked baby that will call SwimToMother().
14+
/// Non-simulating players create temporary babies just for the animation sequence.
15+
/// </summary>
1116
public sealed partial class IncubatorEgg_HatchNow_Patch : NitroxPatch, IDynamicPatch
1217
{
1318
private static readonly MethodInfo TARGET_METHOD = Reflect.Method((IncubatorEgg t) => t.HatchNow());
1419

1520
public static bool Prefix(IncubatorEgg __instance)
1621
{
17-
StartHatchingVisuals(__instance);
22+
// Play hatching visual/sound effects for all players
23+
__instance.fxControl.Play();
24+
Utils.PlayFMODAsset(__instance.hatchSound, __instance.transform, 30f);
1825

19-
SpawnBabiesIfSimulating(__instance);
20-
21-
return false;
22-
}
23-
24-
private static void StartHatchingVisuals(IncubatorEgg egg)
25-
{
26-
egg.fxControl.Play();
27-
Utils.PlayFMODAsset(egg.hatchSound, egg.transform, 30f);
26+
NitroxEntity serverKnownParent = __instance.GetComponentInParent<NitroxEntity>();
27+
if (!serverKnownParent)
28+
{
29+
Log.Error("Could not find a server known parent for incubator egg");
30+
return false;
31+
}
2832

29-
SafeAnimator.SetBool(egg.animationController.eggAnimator, egg.animParameter, true);
30-
}
33+
bool isSimulating = Resolve<SimulationOwnership>().HasAnyLockType(serverKnownParent.Id);
3134

32-
private static void SpawnBabiesIfSimulating(IncubatorEgg egg)
33-
{
34-
SimulationOwnership simulationOwnership = NitroxServiceLocator.LocateService<SimulationOwnership>();
35+
// Create baby GameObject for animation (networked for simulating player, temporary for others)
36+
GameObject baby = Object.Instantiate(__instance.seaEmperorBabyPrefab);
37+
baby.transform.SetParent(__instance.attachPoint);
38+
baby.transform.localPosition = Vector3.zero;
39+
baby.transform.localRotation = Quaternion.identity;
3540

36-
NitroxEntity serverKnownParent = egg.GetComponentInParent<NitroxEntity>();
37-
Validate.NotNull(serverKnownParent, "Could not find a server known parent for incubator egg");
38-
39-
// Only spawn the babies if we are simulating the main incubator platform.
40-
if (simulationOwnership.HasAnyLockType(serverKnownParent.Id))
41+
if (isSimulating)
4142
{
42-
GameObject baby = UnityEngine.Object.Instantiate<GameObject>(egg.seaEmperorBabyPrefab);
43-
baby.transform.position = egg.attachPoint.transform.position;
44-
baby.transform.localRotation = Quaternion.identity;
45-
43+
// Simulating player: make this baby networked and broadcast it
4644
NitroxId babyId = NitroxEntity.GenerateNewId(baby);
47-
48-
WorldEntity entity = new(baby.transform.position.ToDto(), baby.transform.rotation.ToDto(), baby.transform.localScale.ToDto(), TechType.SeaEmperorBaby.ToDto(), 3, "09883a6c-9e78-4bbf-9561-9fa6e49ce766", false, babyId, null);
45+
WorldEntity entity = new(
46+
baby.transform.position.ToDto(),
47+
baby.transform.rotation.ToDto(),
48+
baby.transform.localScale.ToDto(),
49+
TechType.SeaEmperorBaby.ToDto(),
50+
3,
51+
"09883a6c-9e78-4bbf-9561-9fa6e49ce766",
52+
false,
53+
babyId,
54+
null);
4955
Resolve<Entities>().BroadcastEntitySpawnedByClient(entity);
5056
}
57+
else
58+
{
59+
// Non-simulating player: mark baby as temporary (will be cleaned up after animation)
60+
baby.name += IncubatorEggAnimation_OnHatchAnimationEnd_Patch.TEMPORARY_BABY_MARKER;
61+
}
62+
63+
// All players run the full animation sequence
64+
__instance.babyGO = baby;
65+
__instance.animationController.StartHatchAnimation(__instance.babyIdentifier, __instance.animParameter, baby);
66+
__instance.Invoke("PlayFxOnBaby", 2f);
67+
68+
return false;
5169
}
5270
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
using System.Linq;
2+
using System.Reflection;
3+
4+
namespace NitroxPatcher.Patches.Dynamic;
5+
6+
/// <summary>
7+
/// When a Sea Emperor baby is spawned on a remote client (via server broadcast),
8+
/// it needs to swim to mother. The simulating player handles this via the egg animation
9+
/// callback, but remote players receive the baby as a standalone entity.
10+
///
11+
/// This patch ensures the baby swims to mother if:
12+
/// 1. SeaEmperor.main exists (we're in the prison aquarium)
13+
/// 2. The baby has no parent (not attached to an egg - means it's a remote spawn)
14+
/// 3. The baby doesn't already have a swim target (not already swimming)
15+
/// 4. The baby is not a temporary animation baby
16+
/// </summary>
17+
public sealed partial class SeaEmperorBaby_Start_Patch : NitroxPatch, IDynamicPatch
18+
{
19+
private static readonly MethodInfo TARGET_METHOD = Reflect.Method((SeaEmperorBaby t) => t.Start());
20+
21+
public static void Postfix(SeaEmperorBaby __instance)
22+
{
23+
// Only trigger swim to mother for remotely spawned babies
24+
if (!SeaEmperor.main)
25+
{
26+
return;
27+
}
28+
29+
// Skip temporary babies created for animation
30+
if (__instance.name.Contains(IncubatorEggAnimation_OnHatchAnimationEnd_Patch.TEMPORARY_BABY_MARKER))
31+
{
32+
return;
33+
}
34+
35+
// If the baby has a parent, it was spawned by the local hatching sequence
36+
// The animation system will handle calling SwimToMother() via OnHatchAnimationEnd()
37+
if (__instance.transform.parent != null)
38+
{
39+
return;
40+
}
41+
42+
// Check if this baby already has a swim target (already being handled)
43+
if (__instance.swimToTarget != null && __instance.swimToTarget.target != null)
44+
{
45+
return;
46+
}
47+
48+
// This is a remotely spawned baby - make it swim to mother
49+
// Assign a baby ID based on how many babies are already registered
50+
int babyId = SeaEmperor.main.GetBabies().Count();
51+
__instance.SetId(babyId);
52+
__instance.SwimToMother();
53+
}
54+
}

0 commit comments

Comments
 (0)