Skip to content

Commit cee48c3

Browse files
committed
Merge crops-persistence PR #1 (SubnauticaNitrox#2137)
2 parents d18c74d + 76dbd05 commit cee48c3

27 files changed

+746
-26
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
using HarmonyLib;
2+
using NitroxTest.Patcher;
3+
4+
namespace NitroxPatcher.Patches.Dynamic;
5+
6+
[TestClass]
7+
public class GrowingPlant_SpawnGrownModelAsync_PatchTest
8+
{
9+
[TestMethod]
10+
public void Sanity()
11+
{
12+
IEnumerable<CodeInstruction> originalIl = PatchTestHelper.GetInstructionsFromMethod(GrowingPlant_SpawnGrownModelAsync_Patch.TARGET_METHOD);
13+
IEnumerable<CodeInstruction> transformedIl = GrowingPlant_SpawnGrownModelAsync_Patch.Transpiler(originalIl);
14+
transformedIl.Count().Should().Be(originalIl.Count() - 1);
15+
}
16+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
using HarmonyLib;
2+
using NitroxTest.Patcher;
3+
4+
namespace NitroxPatcher.Patches.Dynamic;
5+
6+
[TestClass]
7+
public class PickPrefab_AddToContainerAsync_PatchTest
8+
{
9+
[TestMethod]
10+
public void Sanity()
11+
{
12+
IEnumerable<CodeInstruction> originalIl = PatchTestHelper.GetInstructionsFromMethod(PickPrefab_AddToContainerAsync_Patch.TARGET_METHOD);
13+
IEnumerable<CodeInstruction> transformedIl = PickPrefab_AddToContainerAsync_Patch.Transpiler(originalIl);
14+
transformedIl.Count().Should().Be(originalIl.Count() + 4);
15+
}
16+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
using HarmonyLib;
2+
using NitroxTest.Patcher;
3+
4+
namespace NitroxPatcher.Patches.Dynamic;
5+
6+
[TestClass]
7+
public class Trashcan_Update_PatchTest
8+
{
9+
[TestMethod]
10+
public void Sanity()
11+
{
12+
IEnumerable<CodeInstruction> originalIl = PatchTestHelper.GetInstructionsFromMethod(Trashcan_Update_Patch.TARGET_METHOD);
13+
IEnumerable<CodeInstruction> transformedIl = Trashcan_Update_Patch.Transpiler(originalIl);
14+
transformedIl.Count().Should().Be(originalIl.Count() + 3);
15+
}
16+
}

Nitrox.Test/Server/Serialization/WorldPersistenceTest.cs

+6-1
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,12 @@ private static void EntityTest(Entity entity, Entity entityAfter)
208208
Assert.AreEqual(metadata.Duration, metadataAfter.Duration);
209209
break;
210210
case PlantableMetadata metadata when entityAfter.Metadata is PlantableMetadata metadataAfter:
211-
Assert.AreEqual(metadata.Progress, metadataAfter.Progress);
211+
Assert.AreEqual(metadata.TimeStartGrowth, metadataAfter.TimeStartGrowth);
212+
Assert.AreEqual(metadata.SlotID, metadataAfter.SlotID);
213+
break;
214+
case FruitPlantMetadata metadata when entityAfter.Metadata is FruitPlantMetadata metadataAfter:
215+
Assert.AreEqual(metadata.PickedStates, metadataAfter.PickedStates);
216+
Assert.AreEqual(metadata.TimeNextFruit, metadataAfter.TimeNextFruit);
212217
break;
213218
case CyclopsMetadata metadata when entityAfter.Metadata is CyclopsMetadata metadataAfter:
214219
Assert.AreEqual(metadata.SilentRunningOn, metadataAfter.SilentRunningOn);

NitroxClient/GameLogic/Entities.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ public Entities(IPacketSender packetSender, ThrottledPacketSender throttledPacke
5454
entitySpawnersByType[typeof(InstalledModuleEntity)] = new InstalledModuleEntitySpawner();
5555
entitySpawnersByType[typeof(InstalledBatteryEntity)] = new InstalledBatteryEntitySpawner();
5656
entitySpawnersByType[typeof(InventoryEntity)] = new InventoryEntitySpawner();
57-
entitySpawnersByType[typeof(InventoryItemEntity)] = new InventoryItemEntitySpawner();
57+
entitySpawnersByType[typeof(InventoryItemEntity)] = new InventoryItemEntitySpawner(entityMetadataManager);
5858
entitySpawnersByType[typeof(WorldEntity)] = new WorldEntitySpawner(entityMetadataManager, playerManager, localPlayer, this, simulationOwnership);
5959
entitySpawnersByType[typeof(PlaceholderGroupWorldEntity)] = entitySpawnersByType[typeof(WorldEntity)];
6060
entitySpawnersByType[typeof(PrefabPlaceholderEntity)] = entitySpawnersByType[typeof(WorldEntity)];

NitroxClient/GameLogic/SimulationOwnership.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ public SimulationOwnership(IMultiplayerSession muliplayerSession, IPacketSender
2424
}
2525
public bool PlayerHasMinLockType(NitroxId id, SimulationLockType lockType)
2626
{
27-
if (simulatedIdsByLockType.TryGetValue(id, out SimulationLockType playerLock))
27+
if (id != null && simulatedIdsByLockType.TryGetValue(id, out SimulationLockType playerLock))
2828
{
2929
return playerLock <= lockType;
3030
}

NitroxClient/GameLogic/Spawning/InventoryItemEntitySpawner.cs

+79-2
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,26 @@
1+
using System;
12
using System.Collections;
23
using NitroxClient.Communication;
34
using NitroxClient.GameLogic.Helper;
45
using NitroxClient.GameLogic.Spawning.Abstract;
6+
using NitroxClient.GameLogic.Spawning.Metadata;
57
using NitroxClient.GameLogic.Spawning.WorldEntities;
68
using NitroxClient.MonoBehaviours;
79
using NitroxClient.Unity.Helper;
810
using NitroxModel.DataStructures.GameLogic.Entities;
11+
using NitroxModel.DataStructures.GameLogic.Entities.Metadata;
912
using NitroxModel.DataStructures.Util;
1013
using NitroxModel.Packets;
1114
using NitroxModel_Subnautica.DataStructures;
1215
using UnityEngine;
16+
using UWE;
1317

1418
namespace NitroxClient.GameLogic.Spawning;
1519

16-
public class InventoryItemEntitySpawner : SyncEntitySpawner<InventoryItemEntity>
20+
public class InventoryItemEntitySpawner(EntityMetadataManager entityMetadataManager) : SyncEntitySpawner<InventoryItemEntity>
1721
{
22+
private readonly EntityMetadataManager entityMetadataManager = entityMetadataManager;
23+
1824
protected override IEnumerator SpawnAsync(InventoryItemEntity entity, TaskResult<Optional<GameObject>> result)
1925
{
2026
if (!CanSpawn(entity, out GameObject parentObject, out ItemsContainer container, out string errorLog))
@@ -87,15 +93,86 @@ private void SetupObject(InventoryItemEntity entity, GameObject gameObject, Game
8793
Pickupable pickupable = gameObject.RequireComponent<Pickupable>();
8894
pickupable.Initialize();
8995

96+
InventoryItem inventoryItem = new(pickupable);
97+
9098
// Items eventually get "secured" once a player gets into a SubRoot (or for other reasons) so we need to force this state by default
9199
// so that player don't risk their whole inventory if they reconnect in the water.
92100
pickupable.destroyOnDeath = false;
93101

102+
bool isPlanter = parentObject.TryGetComponent(out Planter planter);
103+
bool subscribedValue = false;
104+
if (isPlanter)
105+
{
106+
subscribedValue = planter.subscribed;
107+
planter.Subscribe(false);
108+
}
109+
94110
using (PacketSuppressor<EntityReparented>.Suppress())
95111
using (PacketSuppressor<PlayerQuickSlotsBindingChanged>.Suppress())
112+
using (PacketSuppressor<EntityMetadataUpdate>.Suppress())
96113
{
97-
container.UnsafeAdd(new InventoryItem(pickupable));
114+
container.UnsafeAdd(inventoryItem);
98115
Log.Debug($"Received: Added item {pickupable.GetTechType()} ({entity.Id}) to container {parentObject.GetFullHierarchyPath()}");
99116
}
117+
118+
if (isPlanter)
119+
{
120+
planter.Subscribe(subscribedValue);
121+
122+
if (entity.Metadata is PlantableMetadata metadata)
123+
{
124+
PostponeAddNotification(() => planter.subscribed, () => planter, true, () =>
125+
{
126+
// Adapted from Planter.AddItem(InventoryItem) to be able to call directly AddItem(Plantable, slotID) with our parameters
127+
Plantable plantable = pickupable.GetComponent<Plantable>();
128+
pickupable.SetTechTypeOverride(plantable.plantTechType, false);
129+
inventoryItem.isEnabled = false;
130+
planter.AddItem(plantable, metadata.SlotID);
131+
132+
// Reapply the plantable metadata after the GrowingPlant (or the GrownPlant) is spawned
133+
entityMetadataManager.ApplyMetadata(plantable.gameObject, metadata);
134+
135+
// Plant spawning occurs in multiple steps over frames:
136+
// spawning the item, adding it to the planter, having the GrowingPlant created, and eventually having it create a GrownPlant (when progress == 1)
137+
// therefore we give the metadata to the object so it can be used when required
138+
if (metadata.FruitPlantMetadata != null && plantable.growingPlant && plantable.growingPlant.GetProgress() == 1f)
139+
{
140+
MetadataHolder.AddMetadata(plantable.growingPlant.gameObject, metadata.FruitPlantMetadata);
141+
}
142+
});
143+
}
144+
}
145+
else if (parentObject.TryGetComponent(out Trashcan trashcan))
146+
{
147+
PostponeAddNotification(() => trashcan.subscribed, () => trashcan, false, () =>
148+
{
149+
trashcan.AddItem(inventoryItem);
150+
});
151+
}
152+
}
153+
154+
private static void PostponeAddNotification(Func<bool> subscribed, Func<bool> instanceValid, bool callbackIfAlreadySubscribed, Action callback)
155+
{
156+
IEnumerator PostponedAddCallback()
157+
{
158+
yield return new WaitUntil(() => subscribed() || !instanceValid());
159+
if (instanceValid())
160+
{
161+
using (PacketSuppressor<EntityReparented>.Suppress())
162+
using (PacketSuppressor<EntityMetadataUpdate>.Suppress())
163+
{
164+
callback();
165+
}
166+
}
167+
}
168+
169+
if (!subscribed())
170+
{
171+
CoroutineHost.StartCoroutine(PostponedAddCallback());
172+
}
173+
else if (callbackIfAlreadySubscribed)
174+
{
175+
callback();
176+
}
100177
}
101178
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
using System.Linq;
2+
using NitroxClient.GameLogic.Spawning.Metadata.Extractor.Abstract;
3+
using NitroxModel.DataStructures.GameLogic.Entities.Metadata;
4+
5+
namespace NitroxClient.GameLogic.Spawning.Metadata.Extractor;
6+
7+
public class FruitPlantMetadataExtractor : EntityMetadataExtractor<FruitPlant, FruitPlantMetadata>
8+
{
9+
public override FruitPlantMetadata Extract(FruitPlant fruitPlant)
10+
{
11+
bool[] prefabsPicked = fruitPlant.fruits.Select(prefab => prefab.pickedState).ToArray();
12+
13+
// If fruit spawn is disabled (certain plants like kelp don't regrow their fruits) and if none of the fruits were picked (all picked = false)
14+
// then we don't need to save this data because the plant is spawned like this by default
15+
if (!fruitPlant.fruitSpawnEnabled && prefabsPicked.All(b => !b))
16+
{
17+
return null;
18+
}
19+
20+
return new(prefabsPicked, fruitPlant.timeNextFruit);
21+
}
22+
}

NitroxClient/GameLogic/Spawning/Metadata/Extractor/PlantableMetadataExtractor.cs

+11-6
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,20 @@
33

44
namespace NitroxClient.GameLogic.Spawning.Metadata.Extractor;
55

6-
public class PlantableMetadataExtractor : EntityMetadataExtractor<Plantable, PlantableMetadata>
6+
public class PlantableMetadataExtractor(FruitPlantMetadataExtractor fruitPlantMetadataExtractor) : EntityMetadataExtractor<Plantable, PlantableMetadata>
77
{
8-
public override PlantableMetadata Extract(Plantable entity)
8+
private readonly FruitPlantMetadataExtractor fruitPlantMetadataExtractor = fruitPlantMetadataExtractor;
9+
10+
public override PlantableMetadata Extract(Plantable plantable)
911
{
10-
GrowingPlant growingPlant = entity.growingPlant;
12+
PlantableMetadata metadata = new(plantable.growingPlant ? plantable.growingPlant.timeStartGrowth : 0, plantable.GetSlotID());
1113

12-
// The growing plant will only spawn in the proper container. In other containers, just consider progress as 0.
13-
float progress = (growingPlant != null) ? growingPlant.GetProgress() : 0;
14+
// TODO: Refer to the TODO in PlantableMetadata
15+
if (plantable.linkedGrownPlant && plantable.linkedGrownPlant.TryGetComponent(out FruitPlant fruitPlant))
16+
{
17+
metadata.FruitPlantMetadata = fruitPlantMetadataExtractor.Extract(fruitPlant);
18+
}
1419

15-
return new(progress);
20+
return metadata;
1621
}
1722
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
using NitroxClient.Communication;
2+
using NitroxClient.GameLogic.Spawning.Metadata.Processor.Abstract;
3+
using NitroxModel.DataStructures.GameLogic.Entities.Metadata;
4+
using NitroxModel.Packets;
5+
using UnityEngine;
6+
7+
namespace NitroxClient.GameLogic.Spawning.Metadata.Processor;
8+
9+
public class FruitPlantMetadataProcessor : EntityMetadataProcessor<FruitPlantMetadata>
10+
{
11+
public override void ProcessMetadata(GameObject gameObject, FruitPlantMetadata metadata)
12+
{
13+
// Two cases:
14+
// 1. The entity with an id
15+
if (gameObject.TryGetComponent(out FruitPlant fruitPlant))
16+
{
17+
ProcessMetadata(fruitPlant, metadata);
18+
return;
19+
}
20+
21+
// 2. The entity with an id has a Plantable (located in the plot's storage),
22+
// we want to access the FruitPlant component which is on the spawned plant object
23+
if (gameObject.TryGetComponent(out Plantable plantable))
24+
{
25+
if (plantable.linkedGrownPlant && plantable.linkedGrownPlant.TryGetComponent(out fruitPlant))
26+
{
27+
ProcessMetadata(fruitPlant, metadata);
28+
}
29+
return;
30+
}
31+
32+
Log.Error($"[{nameof(FruitPlantMetadataProcessor)}] Could not find {nameof(FruitPlant)} related to {gameObject.name}");
33+
}
34+
35+
private static void ProcessMetadata(FruitPlant fruitPlant, FruitPlantMetadata metadata)
36+
{
37+
// Inspired by FruitPlant.Initialize
38+
fruitPlant.inactiveFruits.Clear();
39+
using (PacketSuppressor<EntityMetadataUpdate>.Suppress())
40+
{
41+
for (int i = 0; i < fruitPlant.fruits.Length; i++)
42+
{
43+
fruitPlant.fruits[i].SetPickedState(metadata.PickedStates[i]);
44+
if (metadata.PickedStates[i])
45+
{
46+
fruitPlant.inactiveFruits.Add(fruitPlant.fruits[i]);
47+
}
48+
}
49+
}
50+
51+
fruitPlant.timeNextFruit = metadata.TimeNextFruit;
52+
}
53+
}

NitroxClient/GameLogic/Spawning/Metadata/Processor/PlantableMetadataProcessor.cs

+22-5
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,33 @@
44

55
namespace NitroxClient.GameLogic.Spawning.Metadata.Processor;
66

7-
public class PlantableMetadataProcessor : EntityMetadataProcessor<PlantableMetadata>
7+
public class PlantableMetadataProcessor(FruitPlantMetadataProcessor fruitPlantMetadataProcessor) : EntityMetadataProcessor<PlantableMetadata>
88
{
9+
private readonly FruitPlantMetadataProcessor fruitPlantMetadataProcessor = fruitPlantMetadataProcessor;
10+
911
public override void ProcessMetadata(GameObject gameObject, PlantableMetadata metadata)
1012
{
11-
Plantable plantable = gameObject.GetComponent<Plantable>();
13+
if (gameObject.TryGetComponent(out Plantable plantable))
14+
{
15+
if (plantable.growingPlant)
16+
{
17+
plantable.growingPlant.timeStartGrowth = metadata.TimeStartGrowth;
18+
}
19+
else if (plantable.model.TryGetComponent(out GrowingPlant growingPlant))
20+
{
21+
// Calculation from GrowingPlant.GetProgress (reversed because we're looking for "progress" while we already know timeStartGrowth)
22+
plantable.plantAge = Mathf.Clamp((DayNightCycle.main.timePassedAsFloat - metadata.TimeStartGrowth) / growingPlant.GetGrowthDuration(), 0f, growingPlant.maxProgress);
23+
}
1224

13-
// Plantable will only have a growing plant when residing in the proper container.
14-
if (plantable && plantable.growingPlant)
25+
// TODO: Refer to the TODO in PlantableMetadata
26+
if (metadata.FruitPlantMetadata != null)
27+
{
28+
fruitPlantMetadataProcessor.ProcessMetadata(gameObject, metadata.FruitPlantMetadata);
29+
}
30+
}
31+
else
1532
{
16-
plantable.growingPlant.SetProgress(metadata.Progress);
33+
Log.Error($"[{nameof(PlantableMetadataProcessor)}] Could not find {nameof(Plantable)} on {gameObject.name}");
1734
}
1835
}
1936
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
using System.Collections;
2+
using NitroxModel.DataStructures.GameLogic.Entities;
3+
using NitroxModel.DataStructures.Util;
4+
using UnityEngine;
5+
6+
namespace NitroxClient.GameLogic.Spawning.WorldEntities;
7+
8+
public class CreepvineEntitySpawner(DefaultWorldEntitySpawner defaultWorldEntitySpawner) : IWorldEntitySpawner, IWorldEntitySyncSpawner
9+
{
10+
private readonly DefaultWorldEntitySpawner defaultWorldEntitySpawner = defaultWorldEntitySpawner;
11+
12+
public IEnumerator SpawnAsync(WorldEntity entity, Optional<GameObject> parent, EntityCell cellRoot, TaskResult<Optional<GameObject>> result)
13+
{
14+
yield return defaultWorldEntitySpawner.SpawnAsync(entity, parent, cellRoot, result);
15+
if (!result.value.HasValue)
16+
{
17+
yield break;
18+
}
19+
20+
SetupObject(result.value.Value);
21+
22+
// result is already set
23+
}
24+
25+
public bool SpawnsOwnChildren() => false;
26+
27+
public bool SpawnSync(WorldEntity entity, Optional<GameObject> parent, EntityCell cellRoot, TaskResult<Optional<GameObject>> result)
28+
{
29+
if (!defaultWorldEntitySpawner.SpawnSync(entity, parent, cellRoot, result))
30+
{
31+
return false;
32+
}
33+
34+
SetupObject(result.value.Value);
35+
36+
// result is already set
37+
return true;
38+
}
39+
40+
private static void SetupObject(GameObject gameObject)
41+
{
42+
if (gameObject.GetComponent<FruitPlant>())
43+
{
44+
return;
45+
}
46+
47+
FruitPlant fruitPlant = gameObject.AddComponent<FruitPlant>();
48+
fruitPlant.fruitSpawnEnabled = false;
49+
fruitPlant.timeNextFruit = -1;
50+
fruitPlant.fruits = gameObject.GetComponentsInChildren<PickPrefab>(true);
51+
}
52+
}

0 commit comments

Comments
 (0)