Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Water Park experience improvements #2132

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using HarmonyLib;
using NitroxTest.Patcher;

namespace NitroxPatcher.Patches.Dynamic;

[TestClass]
public class WaterParkCreature_BornAsync_PatchTest
{
[TestMethod]
public void Sanity()
{
IEnumerable<CodeInstruction> originalIl = PatchTestHelper.GetInstructionsFromMethod(WaterParkCreature_BornAsync_Patch.TARGET_METHOD);
IEnumerable<CodeInstruction> transformedIl = WaterParkCreature_BornAsync_Patch.Transpiler(originalIl);
transformedIl.Count().Should().Be(originalIl.Count() + 6);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using HarmonyLib;
using NitroxTest.Patcher;

namespace NitroxPatcher.Patches.Dynamic;

[TestClass]
public class WaterParkCreature_ManagedUpdate_PatchTest
{
[TestMethod]
public void Sanity()
{
IEnumerable<CodeInstruction> originalIl = PatchTestHelper.GetInstructionsFromMethod(WaterParkCreature_ManagedUpdate_Patch.TARGET_METHOD);
IEnumerable<CodeInstruction> transformedIl = WaterParkCreature_ManagedUpdate_Patch.Transpiler(originalIl);
transformedIl.Count().Should().Be(originalIl.Count() + 2);
}
}
20 changes: 19 additions & 1 deletion NitroxClient/Debuggers/Drawer/Nitrox/NitroxEntityDrawer.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
using System;
using System;
using System.Linq;
using NitroxClient.GameLogic;
using NitroxClient.MonoBehaviours;
using NitroxModel.Core;
using NitroxModel.DataStructures;
using UnityEngine;

Expand Down Expand Up @@ -47,5 +49,21 @@ private static void DrawNitroxId(NitroxId nitroxId)
NitroxGUILayout.Separator();
GUILayout.TextField(nitroxId == null ? "ID IS NULL!!!" : nitroxId.ToString());
}

GUILayout.Space(8);

using (new GUILayout.HorizontalScope())
{
GUILayout.Label("Simulating state", GUILayout.Width(LABEL_WIDTH));
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know we haven't got it on these so far but do we want to have the multi language support on them?

Copy link
Member

@Jannify Jannify Nov 14, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would definitely say no. These are debug tools intended only for us.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thinking that as well, just thought I would see people's appetite.

Also being a dev tool add and removing template strings would be a pain

NitroxGUILayout.Separator();
if (NitroxServiceLocator.Cache<SimulationOwnership>.Value.TryGetLockType(nitroxId, out SimulationLockType simulationLockType))
{
GUILayout.TextField(simulationLockType.ToString());
tornac1234 marked this conversation as resolved.
Show resolved Hide resolved
}
else
{
GUILayout.TextField("NONE");
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,9 @@ public override IEnumerator Process(InitialPlayerSync packet, WaitScreen.ManualW
}

Player.main.SetCurrentSub(subRoot, true);
if (subRoot.isBase)
if (subRoot.TryGetComponent(out Base @base))
{
SetupPlayerIfInWaterPark(@base);
// If the player's in a base, we don't need to wait for the world to load
Player.main.cinematicModeActive = false;
yield break;
Expand Down Expand Up @@ -100,4 +101,27 @@ private void AttachPlayerToEscapePod(NitroxId escapePodId)
Player.main.currentEscapePod = escapePod.GetComponent<EscapePod>();
}

private static void SetupPlayerIfInWaterPark(Base @base)
{
foreach (Transform baseChild in @base.transform)
{
if (baseChild.TryGetComponent(out WaterPark waterPark))
{
if (waterPark is LargeRoomWaterPark)
{
// LargeRoomWaterPark.VerifyPlayerWaterPark sets Player.main.currentWaterPark to the right value
waterPark.VerifyPlayerWaterPark(Player.main);
}
else if (waterPark.IsPointInside(Player.main.transform.position))
{
Player.main.currentWaterPark = waterPark;
}
}

if (Player.main.currentWaterPark)
{
return;
}
}
}
}
24 changes: 20 additions & 4 deletions NitroxClient/GameLogic/Items.cs
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ public void Dropped(GameObject gameObject, TechType? techType = null)

// If the item is dropped in a WaterPark we need to handle it differently
NitroxId parentId = null;
if (IsGlobalRootObject(gameObject) || (gameObject.GetComponent<Pickupable>() && TryGetCurrentWaterParkId(out parentId)))
if (IsGlobalRootObject(gameObject) || (gameObject.GetComponent<Pickupable>() && TryGetParentWaterParkId(gameObject.transform.parent, out parentId)))
{
// We cast it to an entity type that is always seeable by clients
// therefore, the packet will be redirected to everyone
Expand Down Expand Up @@ -232,13 +232,29 @@ private void RemoveAnyRemoteControl(GameObject gameObject)
UnityEngine.Object.Destroy(gameObject.GetComponent<RemotelyControlled>());
}

private bool TryGetCurrentWaterParkId(out NitroxId waterParkId)
/// <param name="parent">Parent of the GameObject to check</param>
public static bool TryGetParentWaterPark(Transform parent, out WaterPark waterPark)
{
if (Player.main && Player.main.currentWaterPark &&
Player.main.currentWaterPark.TryGetNitroxId(out waterParkId))
// NB: When dropped in a WaterPark, items are placed under WaterPark/items_root/
// So we need to search two steps higher to find the WaterPark
if (parent && parent.parent && parent.parent.TryGetComponent(out waterPark))
{
return true;
}

waterPark = null;
return false;
}


/// <inheritdoc cref="TryGetParentWaterPark" />
private static bool TryGetParentWaterParkId(Transform parent, out NitroxId waterParkId)
{
if (TryGetParentWaterPark(parent, out WaterPark waterPark) && waterPark.TryGetNitroxId(out waterParkId))
{
return true;
}

waterParkId = null;
return false;
}
Expand Down
9 changes: 6 additions & 3 deletions NitroxClient/GameLogic/LiveMixinManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,12 @@ public bool IsWhitelistedUpdateType(LiveMixin entity)

public bool ShouldBroadcastDeath(LiveMixin liveMixin)
{
return liveMixin.TryGetComponent(out UniqueIdentifier uniqueIdentifier) &&
!string.IsNullOrEmpty(uniqueIdentifier.classId) &&
broadcastDeathClassIdWhitelist.Contains(uniqueIdentifier.classId);
if (liveMixin.TryGetComponent(out UniqueIdentifier uniqueIdentifier) && !string.IsNullOrEmpty(uniqueIdentifier.classId))
{
return broadcastDeathClassIdWhitelist.Contains(uniqueIdentifier.classId);
}

return true;
}

public bool ShouldApplyNextHealthUpdate(LiveMixin receiver, GameObject dealer = null)
Expand Down
5 changes: 5 additions & 0 deletions NitroxClient/GameLogic/SimulationOwnership.cs
Original file line number Diff line number Diff line change
Expand Up @@ -111,5 +111,10 @@ public void TreatSimulatedEntity(SimulatedEntity simulatedEntity)
GameObject.Destroy(remotelyControlled);
}
}

public bool TryGetLockType(NitroxId nitroxId, out SimulationLockType simulationLockType)
{
return simulatedIdsByLockType.TryGetValue(nitroxId, out simulationLockType);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using NitroxClient.GameLogic.Spawning.Metadata.Extractor.Abstract;
using NitroxModel.DataStructures.GameLogic.Entities.Metadata;

namespace NitroxClient.GameLogic.Spawning.Metadata.Extractor;

public class EggMetadataExtractor : EntityMetadataExtractor<CreatureEgg, EggMetadata>
{
public override EggMetadata Extract(CreatureEgg creatureEgg)
{
// If the egg is not in a water park (when being picked up or dropped outside of one),
// we only need the exact progress value because progress only increases while inside a water park
if (Items.PickingUpObject == creatureEgg.gameObject || !creatureEgg.insideWaterPark)
{
return new(-1f, creatureEgg.progress);
}
return new(creatureEgg.timeStartHatching, creatureEgg.progress);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
using NitroxClient.GameLogic.Spawning.Metadata.Processor.Abstract;
using NitroxClient.MonoBehaviours;
using NitroxModel.DataStructures.GameLogic.Entities.Metadata;
using UnityEngine;

namespace NitroxClient.GameLogic.Spawning.Metadata.Processor;

public class EggMetadataProcessor : EntityMetadataProcessor<EggMetadata>
{
public override void ProcessMetadata(GameObject gameObject, EggMetadata metadata)
{
if (gameObject.TryGetComponent(out CreatureEgg creatureEgg))
{
if (metadata.TimeStartHatching == -1f)
{
// If the egg is not in a water park we only need its progress value
creatureEgg.progress = metadata.Progress;
}
else
{
// If the egg is in a water park we only need its time start hatching value
// the current progress will be automatically computed by UpdateProgress()
creatureEgg.timeStartHatching = metadata.TimeStartHatching;

// While being fully loaded, the base is inactive and coroutines shouldn't be started (they'll throw an exception)
// To avoid, that we postpone their execution to 1 more second which is enough because time is frozen during initial sync
if (Multiplayer.Main && !Multiplayer.Main.InitialSyncCompleted &&
creatureEgg.timeStartHatching + creatureEgg.GetHatchDuration() < DayNightCycle.main.timePassedAsFloat)
{
creatureEgg.timeStartHatching = DayNightCycle.main.timePassedAsFloat + 1 - creatureEgg.GetHatchDuration();
}

creatureEgg.UpdateProgress();
}
}
else
{
Log.Error($"[{nameof(EggMetadataProcessor)}] Could not find {nameof(CreatureEgg)} on {gameObject.name}");
}
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using NitroxClient.GameLogic.Spawning.Metadata.Processor.Abstract;
using NitroxClient.MonoBehaviours;
using NitroxModel.DataStructures.GameLogic.Entities.Metadata;
using UnityEngine;

Expand Down Expand Up @@ -26,9 +27,30 @@ public override void ProcessMetadata(GameObject gameObject, WaterParkCreatureMet
waterParkCreature.timeNextBreed = -1;
}

// Scaling according to WaterParkCreature.ManagedUpdate
waterParkCreature.transform.localScale = Mathf.Lerp(waterParkCreature.data.initialSize, waterParkCreature.data.maxSize, waterParkCreature.age) * Vector3.one;

waterParkCreature.isMature = waterParkCreature.age == 1f;
waterParkCreature.bornInside = metadata.BornInside;

// This field is not serialized but is always the exact same so it's supposedly recomputed but it would break with our system
// (calculation from WaterParkCreature.ManagedUpdate)
waterParkCreature.breedInterval = waterParkCreature.data.growingPeriod * 0.5f;

// While being fully loaded, the base is inactive and coroutines shouldn't be started (they'll throw an exception)
// To avoid, that we postpone their execution to 1 more second which is enough because time is frozen during initial sync
// This is the mating condition from WaterParkCreature.ManagedUpdate to postpone mating
if (Multiplayer.Main && !Multiplayer.Main.InitialSyncCompleted && waterParkCreature.currentWaterPark && waterParkCreature.isMature &&
waterParkCreature.GetCanBreed() && DayNightCycle.main.timePassedAsFloat > waterParkCreature.timeNextBreed)
{
waterParkCreature.timeNextBreed = DayNightCycle.main.timePassedAsFloat + 1;
}

waterParkCreature.OnProtoDeserialize(null);
}
else
{
Log.Error($"[{nameof(WaterParkCreatureMetadataProcessor)}] Could not find {nameof(WaterParkCreature)} on {gameObject.name}");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,12 @@ private void SetupObject(WorldEntity entity, Optional<GameObject> parent, GameOb

CrafterLogic.NotifyCraftEnd(gameObject, techType);

WaterPark parentWaterPark = parent.HasValue ? parent.Value.GetComponent<WaterPark>() : null;
WaterPark parentWaterPark = null;
if (parent.HasValue)
{
Items.TryGetParentWaterPark(parent.Value.transform.parent, out parentWaterPark);
}

if (!parentWaterPark)
{
if (parent.HasValue && !parent.Value.GetComponent<LargeWorldEntityCell>())
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
using System.Collections;
using NitroxClient.Communication;
using NitroxClient.GameLogic.Spawning.Abstract;
using NitroxClient.MonoBehaviours;
using NitroxClient.Unity.Helper;
using NitroxModel.DataStructures.GameLogic.Entities;
using NitroxModel.DataStructures.Util;
using NitroxModel.Packets;
using NitroxModel_Subnautica.DataStructures;
using UnityEngine;
using UWE;

namespace NitroxClient.GameLogic.Spawning.WorldEntities;

Expand Down Expand Up @@ -39,15 +42,68 @@ private void SetupObject(GlobalRootEntity entity, GameObject gameObject)
{
LargeWorldEntity largeWorldEntity = gameObject.EnsureComponent<LargeWorldEntity>();
largeWorldEntity.cellLevel = LargeWorldEntity.CellLevel.Global;

LargeWorld.main.streamer.cellManager.RegisterEntity(largeWorldEntity);
if (entity.ParentId != null && NitroxEntity.TryGetComponentFrom(entity.ParentId, out Transform parentTransform))
{
gameObject.transform.parent = parentTransform;
}
largeWorldEntity.Start();

gameObject.transform.localPosition = entity.Transform.LocalPosition.ToUnity();
gameObject.transform.localRotation = entity.Transform.LocalRotation.ToUnity();
gameObject.transform.localScale = entity.Transform.LocalScale.ToUnity();

if (entity.ParentId != null && NitroxEntity.TryGetComponentFrom(entity.ParentId, out Transform parentTransform))
tornac1234 marked this conversation as resolved.
Show resolved Hide resolved
{
// WaterParks have a child named "items_root" where the fish are put
if (parentTransform.TryGetComponent(out WaterPark waterPark))
{
gameObject.transform.SetParent(waterPark.itemsRoot, false);
using (PacketSuppressor<EntityMetadataUpdate>.Suppress())
{
waterPark.AddItem(gameObject.EnsureComponent<Pickupable>());

// While being fully loaded, the base is inactive so GameObject.SendMessage doesn't work and we need to execute their callbacks manually
tornac1234 marked this conversation as resolved.
Show resolved Hide resolved
if (Multiplayer.Main && !Multiplayer.Main.InitialSyncCompleted)
{
// Below are distinct incompatible cases
if (gameObject.TryGetComponent(out CreatureEgg creatureEgg) && !creatureEgg.insideWaterPark)
{
creatureEgg.OnAddToWaterPark();
}
else if (gameObject.TryGetComponent(out CuteFish cuteFish))
{
cuteFish.OnAddToWaterPark(null);
}
else if (gameObject.TryGetComponent(out CrabSnake crabSnake))
{
// This callback interacts with an animator, but this behaviour needs to be initialized (probably during Start) before it can be modified
IEnumerator PostponedCallback()
{
yield return new WaitUntil(() => !crabSnake || crabSnake.animationController.animator.isInitialized);
if (crabSnake)
{
crabSnake.OnAddToWaterPark();
}
}
CoroutineHost.StartCoroutine(PostponedCallback());
}
}
}

// TODO: When metadata is reworked (it'll be possible to give different metadatas to the same entity)
// this will no longer be needed because the entity metadata will set this to false accordingly

// If fishes are in a WaterPark, it means that they were once picked up
if (gameObject.TryGetComponent(out CreatureDeath creatureDeath))
{
// This is set to false when picking up a fish or when a fish is born in the WaterPark
creatureDeath.respawn = false;
}
}
else
{
gameObject.transform.SetParent(parentTransform, false);
}
}

if (gameObject.GetComponent<PlaceTool>())
{
PlacedWorldEntitySpawner.AdditionalSpawningSteps(gameObject);
Expand Down
Loading