Skip to content

Commit 76dbd05

Browse files
committed
Sync fruit growing and harvesting (from plants and objects like kelp)
1 parent 88eae5a commit 76dbd05

20 files changed

+512
-29
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+
}

Nitrox.Test/Server/Serialization/WorldPersistenceTest.cs

+5
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,11 @@ private static void EntityTest(Entity entity, Entity entityAfter)
227227
break;
228228
case PlantableMetadata metadata when entityAfter.Metadata is PlantableMetadata metadataAfter:
229229
Assert.AreEqual(metadata.TimeStartGrowth, metadataAfter.TimeStartGrowth);
230+
Assert.AreEqual(metadata.SlotID, metadataAfter.SlotID);
231+
break;
232+
case FruitPlantMetadata metadata when entityAfter.Metadata is FruitPlantMetadata metadataAfter:
233+
Assert.AreEqual(metadata.PickedStates, metadataAfter.PickedStates);
234+
Assert.AreEqual(metadata.TimeNextFruit, metadataAfter.TimeNextFruit);
230235
break;
231236
case CyclopsMetadata metadata when entityAfter.Metadata is CyclopsMetadata metadataAfter:
232237
Assert.AreEqual(metadata.SilentRunningOn, metadataAfter.SilentRunningOn);

NitroxClient/GameLogic/Entities.cs

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

NitroxClient/GameLogic/Spawning/InventoryItemEntitySpawner.cs

+22-8
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using NitroxClient.Communication;
44
using NitroxClient.GameLogic.Helper;
55
using NitroxClient.GameLogic.Spawning.Abstract;
6+
using NitroxClient.GameLogic.Spawning.Metadata;
67
using NitroxClient.GameLogic.Spawning.WorldEntities;
78
using NitroxClient.MonoBehaviours;
89
using NitroxClient.Unity.Helper;
@@ -16,8 +17,10 @@
1617

1718
namespace NitroxClient.GameLogic.Spawning;
1819

19-
public class InventoryItemEntitySpawner : SyncEntitySpawner<InventoryItemEntity>
20+
public class InventoryItemEntitySpawner(EntityMetadataManager entityMetadataManager) : SyncEntitySpawner<InventoryItemEntity>
2021
{
22+
private readonly EntityMetadataManager entityMetadataManager = entityMetadataManager;
23+
2124
protected override IEnumerator SpawnAsync(InventoryItemEntity entity, TaskResult<Optional<GameObject>> result)
2225
{
2326
if (!CanSpawn(entity, out GameObject parentObject, out ItemsContainer container, out string errorLog))
@@ -116,17 +119,28 @@ private void SetupObject(InventoryItemEntity entity, GameObject gameObject, Game
116119
{
117120
planter.Subscribe(subscribedValue);
118121

119-
PostponeAddNotification(() => planter.subscribed, () => planter, true, () =>
122+
if (entity.Metadata is PlantableMetadata metadata)
120123
{
121-
if (entity.Metadata is PlantableMetadata metadata)
124+
PostponeAddNotification(() => planter.subscribed, () => planter, true, () =>
122125
{
123126
// Adapted from Planter.AddItem(InventoryItem) to be able to call directly AddItem(Plantable, slotID) with our parameters
124-
Plantable component = pickupable.GetComponent<Plantable>();
125-
pickupable.SetTechTypeOverride(component.plantTechType, false);
127+
Plantable plantable = pickupable.GetComponent<Plantable>();
128+
pickupable.SetTechTypeOverride(plantable.plantTechType, false);
126129
inventoryItem.isEnabled = false;
127-
planter.AddItem(component, metadata.SlotID);
128-
}
129-
});
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+
}
130144
}
131145
else if (parentObject.TryGetComponent(out Trashcan trashcan))
132146
{
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

+12-2
Original file line numberDiff line numberDiff line change
@@ -3,10 +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+
private readonly FruitPlantMetadataExtractor fruitPlantMetadataExtractor = fruitPlantMetadataExtractor;
9+
810
public override PlantableMetadata Extract(Plantable plantable)
911
{
10-
return new(plantable.growingPlant ? plantable.growingPlant.timeStartGrowth : 0, plantable.GetSlotID());
12+
PlantableMetadata metadata = new(plantable.growingPlant ? plantable.growingPlant.timeStartGrowth : 0, plantable.GetSlotID());
13+
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+
}
19+
20+
return metadata;
1121
}
1222
}
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

+11-3
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@
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
{
1113
if (gameObject.TryGetComponent(out Plantable plantable))
@@ -16,8 +18,14 @@ public override void ProcessMetadata(GameObject gameObject, PlantableMetadata me
1618
}
1719
else if (plantable.model.TryGetComponent(out GrowingPlant growingPlant))
1820
{
19-
// Calculation from GrowingPlant.SetProgress (reversed because we're looking for "progress" while we already know timeStartGrowth)
20-
plantable.plantAge = (DayNightCycle.main.timePassedAsFloat - metadata.TimeStartGrowth) / growingPlant.GetGrowthDuration();
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+
}
24+
25+
// TODO: Refer to the TODO in PlantableMetadata
26+
if (metadata.FruitPlantMetadata != null)
27+
{
28+
fruitPlantMetadataProcessor.ProcessMetadata(gameObject, metadata.FruitPlantMetadata);
2129
}
2230
}
2331
else
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+
}

NitroxClient/GameLogic/Spawning/WorldEntities/WorldEntitySpawnerResolver.cs

+1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ public WorldEntitySpawnerResolver(EntityMetadataManager entityMetadataManager, P
2626
{
2727
customSpawnersByTechType[TechType.Crash] = new CrashEntitySpawner();
2828
customSpawnersByTechType[TechType.EscapePod] = new EscapePodWorldEntitySpawner(entityMetadataManager);
29+
customSpawnersByTechType[TechType.Creepvine] = new CreepvineEntitySpawner(defaultEntitySpawner);
2930

3031
vehicleWorldEntitySpawner = new(entities);
3132
prefabPlaceholderEntitySpawner = new(defaultEntitySpawner);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
using NitroxModel.DataStructures.GameLogic.Entities.Metadata;
2+
using UnityEngine;
3+
4+
namespace NitroxClient.MonoBehaviours;
5+
6+
public class MetadataHolder : MonoBehaviour
7+
{
8+
public EntityMetadata Metadata;
9+
10+
public EntityMetadata Consume()
11+
{
12+
Destroy(this);
13+
return Metadata;
14+
}
15+
16+
public static MetadataHolder AddMetadata(GameObject gameObject, EntityMetadata metadata)
17+
{
18+
MetadataHolder metadataHolder = gameObject.AddComponent<MetadataHolder>();
19+
metadataHolder.Metadata = metadata;
20+
return metadataHolder;
21+
}
22+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
using UnityEngine;
2+
3+
namespace NitroxClient.MonoBehaviours;
4+
5+
public class ReferenceHolder : MonoBehaviour
6+
{
7+
public object Reference;
8+
9+
public static ReferenceHolder EnsureReferenceAttached(Component component, object reference)
10+
{
11+
return EnsureReferenceAttached(component.gameObject, reference);
12+
}
13+
14+
public bool TryGetReference<T>(out T outReference)
15+
{
16+
if (Reference is T reference)
17+
{
18+
outReference = reference;
19+
return true;
20+
}
21+
22+
outReference = default;
23+
return false;
24+
}
25+
26+
public static ReferenceHolder EnsureReferenceAttached(GameObject gameObject, object reference)
27+
{
28+
ReferenceHolder referenceHolder = gameObject.EnsureComponent<ReferenceHolder>();
29+
referenceHolder.Reference = reference;
30+
return referenceHolder;
31+
}
32+
}

NitroxModel/DataStructures/GameLogic/Entities/Metadata/EntityMetadata.cs

+2
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ namespace NitroxModel.DataStructures.GameLogic.Entities.Metadata
3636
[ProtoInclude(76, typeof(RadiationMetadata))]
3737
[ProtoInclude(77, typeof(CrashHomeMetadata))]
3838
[ProtoInclude(78, typeof(EatableMetadata))]
39+
[ProtoInclude(79, typeof(PlantableMetadata))]
40+
[ProtoInclude(80, typeof(FruitPlantMetadata))]
3941
public abstract class EntityMetadata
4042
{
4143
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
using System;
2+
using System.Runtime.Serialization;
3+
using BinaryPack.Attributes;
4+
5+
namespace NitroxModel.DataStructures.GameLogic.Entities.Metadata;
6+
7+
[Serializable]
8+
[DataContract]
9+
public class FruitPlantMetadata : EntityMetadata
10+
{
11+
[DataMember(Order = 1)]
12+
public bool[] PickedStates { get; }
13+
14+
[DataMember(Order = 1)]
15+
public float TimeNextFruit { get; }
16+
17+
[IgnoreConstructor]
18+
protected FruitPlantMetadata()
19+
{
20+
// Constructor for serialization. Has to be "protected" for json serialization.
21+
}
22+
23+
public FruitPlantMetadata(bool[] pickedStates, float timeNextFruit)
24+
{
25+
PickedStates = pickedStates;
26+
TimeNextFruit = timeNextFruit;
27+
}
28+
29+
public override string ToString()
30+
{
31+
return $"[{nameof(FruitPlantMetadata)} PickedStates: [{string.Join(", ", PickedStates)}], TimeNextFruit: {TimeNextFruit}]";
32+
}
33+
}

0 commit comments

Comments
 (0)