diff --git a/Nitrox.Test/Patcher/Patches/Dynamic/GrowingPlant_SpawnGrownModelAsync_PatchTest.cs b/Nitrox.Test/Patcher/Patches/Dynamic/GrowingPlant_SpawnGrownModelAsync_PatchTest.cs new file mode 100644 index 0000000000..7f9f4ec568 --- /dev/null +++ b/Nitrox.Test/Patcher/Patches/Dynamic/GrowingPlant_SpawnGrownModelAsync_PatchTest.cs @@ -0,0 +1,16 @@ +using HarmonyLib; +using NitroxTest.Patcher; + +namespace NitroxPatcher.Patches.Dynamic; + +[TestClass] +public class GrowingPlant_SpawnGrownModelAsync_PatchTest +{ + [TestMethod] + public void Sanity() + { + IEnumerable originalIl = PatchTestHelper.GetInstructionsFromMethod(GrowingPlant_SpawnGrownModelAsync_Patch.TARGET_METHOD); + IEnumerable transformedIl = GrowingPlant_SpawnGrownModelAsync_Patch.Transpiler(originalIl); + transformedIl.Count().Should().Be(originalIl.Count() - 1); + } +} diff --git a/Nitrox.Test/Patcher/Patches/Dynamic/PickPrefab_AddToContainerAsync_PatchTest.cs b/Nitrox.Test/Patcher/Patches/Dynamic/PickPrefab_AddToContainerAsync_PatchTest.cs new file mode 100644 index 0000000000..23a478a061 --- /dev/null +++ b/Nitrox.Test/Patcher/Patches/Dynamic/PickPrefab_AddToContainerAsync_PatchTest.cs @@ -0,0 +1,16 @@ +using HarmonyLib; +using NitroxTest.Patcher; + +namespace NitroxPatcher.Patches.Dynamic; + +[TestClass] +public class PickPrefab_AddToContainerAsync_PatchTest +{ + [TestMethod] + public void Sanity() + { + IEnumerable originalIl = PatchTestHelper.GetInstructionsFromMethod(PickPrefab_AddToContainerAsync_Patch.TARGET_METHOD); + IEnumerable transformedIl = PickPrefab_AddToContainerAsync_Patch.Transpiler(originalIl); + transformedIl.Count().Should().Be(originalIl.Count() + 4); + } +} diff --git a/Nitrox.Test/Patcher/Patches/Dynamic/Trashcan_Update_PatchTest.cs b/Nitrox.Test/Patcher/Patches/Dynamic/Trashcan_Update_PatchTest.cs new file mode 100644 index 0000000000..5c204c1e96 --- /dev/null +++ b/Nitrox.Test/Patcher/Patches/Dynamic/Trashcan_Update_PatchTest.cs @@ -0,0 +1,16 @@ +using HarmonyLib; +using NitroxTest.Patcher; + +namespace NitroxPatcher.Patches.Dynamic; + +[TestClass] +public class Trashcan_Update_PatchTest +{ + [TestMethod] + public void Sanity() + { + IEnumerable originalIl = PatchTestHelper.GetInstructionsFromMethod(Trashcan_Update_Patch.TARGET_METHOD); + IEnumerable transformedIl = Trashcan_Update_Patch.Transpiler(originalIl); + transformedIl.Count().Should().Be(originalIl.Count() + 3); + } +} diff --git a/Nitrox.Test/Server/Serialization/WorldPersistenceTest.cs b/Nitrox.Test/Server/Serialization/WorldPersistenceTest.cs index 95cec71d69..1c835afcaa 100644 --- a/Nitrox.Test/Server/Serialization/WorldPersistenceTest.cs +++ b/Nitrox.Test/Server/Serialization/WorldPersistenceTest.cs @@ -71,9 +71,6 @@ private static void ItemDataTest(ItemData itemData, ItemData itemDataAfter) Assert.AreEqual(equippedItemData.Slot, equippedItemDataAfter.Slot); Assert.AreEqual(equippedItemData.TechType, equippedItemDataAfter.TechType); break; - case PlantableItemData plantableItemData when itemDataAfter is PlantableItemData plantableItemDataAfter: - Assert.AreEqual(plantableItemData.PlantedGameTime, plantableItemDataAfter.PlantedGameTime); - break; default: Assert.Fail($"Runtime types of {nameof(ItemData)} where not equal"); break; @@ -229,7 +226,12 @@ private static void EntityTest(Entity entity, Entity entityAfter) Assert.AreEqual(metadata.Duration, metadataAfter.Duration); break; case PlantableMetadata metadata when entityAfter.Metadata is PlantableMetadata metadataAfter: - Assert.AreEqual(metadata.Progress, metadataAfter.Progress); + Assert.AreEqual(metadata.TimeStartGrowth, metadataAfter.TimeStartGrowth); + Assert.AreEqual(metadata.SlotID, metadataAfter.SlotID); + break; + case FruitPlantMetadata metadata when entityAfter.Metadata is FruitPlantMetadata metadataAfter: + Assert.AreEqual(metadata.PickedStates, metadataAfter.PickedStates); + Assert.AreEqual(metadata.TimeNextFruit, metadataAfter.TimeNextFruit); break; case CyclopsMetadata metadata when entityAfter.Metadata is CyclopsMetadata metadataAfter: Assert.AreEqual(metadata.SilentRunningOn, metadataAfter.SilentRunningOn); diff --git a/NitroxClient/GameLogic/Containers/ContainerAddItemPostProcessor.cs b/NitroxClient/GameLogic/Containers/ContainerAddItemPostProcessor.cs deleted file mode 100644 index 2f72ce4514..0000000000 --- a/NitroxClient/GameLogic/Containers/ContainerAddItemPostProcessor.cs +++ /dev/null @@ -1,49 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using NitroxModel.DataStructures.GameLogic; -using UnityEngine; - - -namespace NitroxClient.GameLogic.Containers -{ - public abstract class ContainerAddItemPostProcessor - { - - private static readonly NoOpContainerAddItemPostProcessor noOpProcessor = new NoOpContainerAddItemPostProcessor(); - private static readonly IEnumerable processors; - - public abstract Type[] ApplicableComponents { get; } - - static ContainerAddItemPostProcessor() - { - processors = Assembly.GetExecutingAssembly() - .GetTypes() - .Where(t => typeof(ContainerAddItemPostProcessor).IsAssignableFrom(t) && - t.IsClass && !t.IsAbstract && t != typeof(NoOpContainerAddItemPostProcessor) - ) - .Select(Activator.CreateInstance) - .Cast(); - } - - public abstract void process(GameObject item, ItemData itemData); - - public static ContainerAddItemPostProcessor From(GameObject item) - { - foreach (ContainerAddItemPostProcessor processor in processors) - { - foreach (Type type in processor.ApplicableComponents) - { - if (item.GetComponent(type)) - { - Log.Info($"Found custom ContainerAddItemPostProcessor for {type}"); - return processor; - } - } - } - - return noOpProcessor; - } - } -} diff --git a/NitroxClient/GameLogic/Containers/NoOpContainerAddItemPostProcessor.cs b/NitroxClient/GameLogic/Containers/NoOpContainerAddItemPostProcessor.cs deleted file mode 100644 index 7608130bd2..0000000000 --- a/NitroxClient/GameLogic/Containers/NoOpContainerAddItemPostProcessor.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System; -using NitroxModel.DataStructures.GameLogic; -using UnityEngine; - -namespace NitroxClient.GameLogic.Containers -{ - class NoOpContainerAddItemPostProcessor : ContainerAddItemPostProcessor - { - public override Type[] ApplicableComponents { get; } = Type.EmptyTypes; - - public override void process(GameObject item, ItemData itemData) - { - // No-Op! - } - - } -} diff --git a/NitroxClient/GameLogic/Containers/PlantableContainerAddItemPostProcessor.cs b/NitroxClient/GameLogic/Containers/PlantableContainerAddItemPostProcessor.cs deleted file mode 100644 index 3586ddb8f8..0000000000 --- a/NitroxClient/GameLogic/Containers/PlantableContainerAddItemPostProcessor.cs +++ /dev/null @@ -1,80 +0,0 @@ -using System; -using NitroxModel.DataStructures.GameLogic; -using UnityEngine; - - -namespace NitroxClient.GameLogic.Containers -{ - /// - /// Restore projected Plant growth based on age of item in Planter - /// - class PlantableContainerAddItemPostProcessor : ContainerAddItemPostProcessor - { - public override Type[] ApplicableComponents { get; } = { typeof(Plantable) }; - - public override void process(GameObject item, ItemData itemData) - { - if (itemData is not PlantableItemData plantableData) - { - // nothing to do; false alarm - return; - } - Plantable plant = item.GetComponent(); - if (!plant) - { - Log.Error($"FixPlantGrowth: Item for Plantable {plantableData.ItemId} is not a Plantable!"); - return; - } - - GrowingPlant grower = GetGrowingPlant(plant); - if (!grower) - { - Log.Error($"FixPlantGrowth: Could not find GrowingPlant for Plantable {plantableData.ItemId}!"); - return; - } - - - // time in seconds - double elapsedGrowthTime = (DayNightCycle.main.timePassedAsDouble - plantableData.PlantedGameTime); - - if (elapsedGrowthTime > grower.growthDuration) - { - // should be ready - Log.Debug($"FixPlantGrowth: Finishing {item.name} {plantableData.ItemId} that has grown for {elapsedGrowthTime} seconds"); - grower.SetProgress(1.0f); - } - else - { - Log.Debug($"FixPlantGrowth: Growing {item.name} {plantableData.ItemId} that has grown for {elapsedGrowthTime} seconds"); - grower.SetProgress(Convert.ToSingle(elapsedGrowthTime / grower.growthDuration)); - } - } - - private static GrowingPlant GetGrowingPlant(Plantable plantable) - { - int slot = plantable.GetSlotID(); - - Planter planter = plantable.currentPlanter; - if (!planter) - { - Log.Error($"GetGrowingPlant: plant not inside a Planter!"); - return null; - } - - // int smallSlotCount = pp.slots.Length; - int bigSlotCount = planter.bigSlots.Length; - - // for all the planters I have seen, the logic is the same: Available slots are numbered starting with the big slots - if (slot < bigSlotCount) - { - // index 0 .. #big-1 - return planter.bigSlots[slot].GetComponentInChildren(); - } - else - { - // index #big .. #big+#small-1 - return planter.slots[slot - bigSlotCount].GetComponentInChildren(); - } - } - } -} diff --git a/NitroxClient/GameLogic/Entities.cs b/NitroxClient/GameLogic/Entities.cs index ffa21a4ef8..87752ef3f8 100644 --- a/NitroxClient/GameLogic/Entities.cs +++ b/NitroxClient/GameLogic/Entities.cs @@ -52,7 +52,7 @@ public Entities(IPacketSender packetSender, ThrottledPacketSender throttledPacke entitySpawnersByType[typeof(InstalledModuleEntity)] = new InstalledModuleEntitySpawner(); entitySpawnersByType[typeof(InstalledBatteryEntity)] = new InstalledBatteryEntitySpawner(); entitySpawnersByType[typeof(InventoryEntity)] = new InventoryEntitySpawner(); - entitySpawnersByType[typeof(InventoryItemEntity)] = new InventoryItemEntitySpawner(); + entitySpawnersByType[typeof(InventoryItemEntity)] = new InventoryItemEntitySpawner(entityMetadataManager); entitySpawnersByType[typeof(WorldEntity)] = new WorldEntitySpawner(entityMetadataManager, playerManager, localPlayer, this, simulationOwnership); entitySpawnersByType[typeof(PlaceholderGroupWorldEntity)] = entitySpawnersByType[typeof(WorldEntity)]; entitySpawnersByType[typeof(PrefabPlaceholderEntity)] = entitySpawnersByType[typeof(WorldEntity)]; diff --git a/NitroxClient/GameLogic/SimulationOwnership.cs b/NitroxClient/GameLogic/SimulationOwnership.cs index aadd9f3d34..96c506c7e1 100644 --- a/NitroxClient/GameLogic/SimulationOwnership.cs +++ b/NitroxClient/GameLogic/SimulationOwnership.cs @@ -22,7 +22,7 @@ public SimulationOwnership(IMultiplayerSession muliplayerSession, IPacketSender } public bool PlayerHasMinLockType(NitroxId id, SimulationLockType lockType) { - if (simulatedIdsByLockType.TryGetValue(id, out SimulationLockType playerLock)) + if (id != null && simulatedIdsByLockType.TryGetValue(id, out SimulationLockType playerLock)) { return playerLock <= lockType; } diff --git a/NitroxClient/GameLogic/Spawning/InventoryItemEntitySpawner.cs b/NitroxClient/GameLogic/Spawning/InventoryItemEntitySpawner.cs index b7b6046ce2..4c17a39f33 100644 --- a/NitroxClient/GameLogic/Spawning/InventoryItemEntitySpawner.cs +++ b/NitroxClient/GameLogic/Spawning/InventoryItemEntitySpawner.cs @@ -1,20 +1,26 @@ +using System; using System.Collections; using NitroxClient.Communication; using NitroxClient.GameLogic.Helper; using NitroxClient.GameLogic.Spawning.Abstract; +using NitroxClient.GameLogic.Spawning.Metadata; using NitroxClient.GameLogic.Spawning.WorldEntities; using NitroxClient.MonoBehaviours; using NitroxClient.Unity.Helper; using NitroxModel.DataStructures.GameLogic.Entities; +using NitroxModel.DataStructures.GameLogic.Entities.Metadata; using NitroxModel.DataStructures.Util; using NitroxModel.Packets; using NitroxModel_Subnautica.DataStructures; using UnityEngine; +using UWE; namespace NitroxClient.GameLogic.Spawning; -public class InventoryItemEntitySpawner : SyncEntitySpawner +public class InventoryItemEntitySpawner(EntityMetadataManager entityMetadataManager) : SyncEntitySpawner { + private readonly EntityMetadataManager entityMetadataManager = entityMetadataManager; + protected override IEnumerator SpawnAsync(InventoryItemEntity entity, TaskResult> result) { if (!CanSpawn(entity, out GameObject parentObject, out ItemsContainer container, out string errorLog)) @@ -87,15 +93,86 @@ private void SetupObject(InventoryItemEntity entity, GameObject gameObject, Game Pickupable pickupable = gameObject.RequireComponent(); pickupable.Initialize(); + InventoryItem inventoryItem = new(pickupable); + // Items eventually get "secured" once a player gets into a SubRoot (or for other reasons) so we need to force this state by default // so that player don't risk their whole inventory if they reconnect in the water. pickupable.destroyOnDeath = false; + bool isPlanter = parentObject.TryGetComponent(out Planter planter); + bool subscribedValue = false; + if (isPlanter) + { + subscribedValue = planter.subscribed; + planter.Subscribe(false); + } + using (PacketSuppressor.Suppress()) using (PacketSuppressor.Suppress()) + using (PacketSuppressor.Suppress()) { - container.UnsafeAdd(new InventoryItem(pickupable)); + container.UnsafeAdd(inventoryItem); Log.Debug($"Received: Added item {pickupable.GetTechType()} ({entity.Id}) to container {parentObject.GetFullHierarchyPath()}"); } + + if (isPlanter) + { + planter.Subscribe(subscribedValue); + + if (entity.Metadata is PlantableMetadata metadata) + { + PostponeAddNotification(() => planter.subscribed, () => planter, true, () => + { + // Adapted from Planter.AddItem(InventoryItem) to be able to call directly AddItem(Plantable, slotID) with our parameters + Plantable plantable = pickupable.GetComponent(); + pickupable.SetTechTypeOverride(plantable.plantTechType, false); + inventoryItem.isEnabled = false; + planter.AddItem(plantable, metadata.SlotID); + + // Reapply the plantable metadata after the GrowingPlant (or the GrownPlant) is spawned + entityMetadataManager.ApplyMetadata(plantable.gameObject, metadata); + + // Plant spawning occurs in multiple steps over frames: + // spawning the item, adding it to the planter, having the GrowingPlant created, and eventually having it create a GrownPlant (when progress == 1) + // therefore we give the metadata to the object so it can be used when required + if (metadata.FruitPlantMetadata != null && plantable.growingPlant && plantable.growingPlant.GetProgress() == 1f) + { + MetadataHolder.AddMetadata(plantable.growingPlant.gameObject, metadata.FruitPlantMetadata); + } + }); + } + } + else if (parentObject.TryGetComponent(out Trashcan trashcan)) + { + PostponeAddNotification(() => trashcan.subscribed, () => trashcan, false, () => + { + trashcan.AddItem(inventoryItem); + }); + } + } + + private static void PostponeAddNotification(Func subscribed, Func instanceValid, bool callbackIfAlreadySubscribed, Action callback) + { + IEnumerator PostponedAddCallback() + { + yield return new WaitUntil(() => subscribed() || !instanceValid()); + if (instanceValid()) + { + using (PacketSuppressor.Suppress()) + using (PacketSuppressor.Suppress()) + { + callback(); + } + } + } + + if (!subscribed()) + { + CoroutineHost.StartCoroutine(PostponedAddCallback()); + } + else if (callbackIfAlreadySubscribed) + { + callback(); + } } } diff --git a/NitroxClient/GameLogic/Spawning/Metadata/Extractor/FruitPlantMetadataExtractor.cs b/NitroxClient/GameLogic/Spawning/Metadata/Extractor/FruitPlantMetadataExtractor.cs new file mode 100644 index 0000000000..e3b536c3e9 --- /dev/null +++ b/NitroxClient/GameLogic/Spawning/Metadata/Extractor/FruitPlantMetadataExtractor.cs @@ -0,0 +1,22 @@ +using System.Linq; +using NitroxClient.GameLogic.Spawning.Metadata.Extractor.Abstract; +using NitroxModel.DataStructures.GameLogic.Entities.Metadata; + +namespace NitroxClient.GameLogic.Spawning.Metadata.Extractor; + +public class FruitPlantMetadataExtractor : EntityMetadataExtractor +{ + public override FruitPlantMetadata Extract(FruitPlant fruitPlant) + { + bool[] prefabsPicked = fruitPlant.fruits.Select(prefab => prefab.pickedState).ToArray(); + + // 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) + // then we don't need to save this data because the plant is spawned like this by default + if (!fruitPlant.fruitSpawnEnabled && prefabsPicked.All(b => !b)) + { + return null; + } + + return new(prefabsPicked, fruitPlant.timeNextFruit); + } +} diff --git a/NitroxClient/GameLogic/Spawning/Metadata/Extractor/PlantableMetadataExtractor.cs b/NitroxClient/GameLogic/Spawning/Metadata/Extractor/PlantableMetadataExtractor.cs index 9b3a7cbb07..817d800576 100644 --- a/NitroxClient/GameLogic/Spawning/Metadata/Extractor/PlantableMetadataExtractor.cs +++ b/NitroxClient/GameLogic/Spawning/Metadata/Extractor/PlantableMetadataExtractor.cs @@ -3,15 +3,20 @@ namespace NitroxClient.GameLogic.Spawning.Metadata.Extractor; -public class PlantableMetadataExtractor : EntityMetadataExtractor +public class PlantableMetadataExtractor(FruitPlantMetadataExtractor fruitPlantMetadataExtractor) : EntityMetadataExtractor { - public override PlantableMetadata Extract(Plantable entity) + private readonly FruitPlantMetadataExtractor fruitPlantMetadataExtractor = fruitPlantMetadataExtractor; + + public override PlantableMetadata Extract(Plantable plantable) { - GrowingPlant growingPlant = entity.growingPlant; + PlantableMetadata metadata = new(plantable.growingPlant ? plantable.growingPlant.timeStartGrowth : 0, plantable.GetSlotID()); - // The growing plant will only spawn in the proper container. In other containers, just consider progress as 0. - float progress = (growingPlant != null) ? growingPlant.GetProgress() : 0; + // TODO: Refer to the TODO in PlantableMetadata + if (plantable.linkedGrownPlant && plantable.linkedGrownPlant.TryGetComponent(out FruitPlant fruitPlant)) + { + metadata.FruitPlantMetadata = fruitPlantMetadataExtractor.Extract(fruitPlant); + } - return new(progress); + return metadata; } } diff --git a/NitroxClient/GameLogic/Spawning/Metadata/Processor/FruitPlantMetadataProcessor.cs b/NitroxClient/GameLogic/Spawning/Metadata/Processor/FruitPlantMetadataProcessor.cs new file mode 100644 index 0000000000..76bbcc081b --- /dev/null +++ b/NitroxClient/GameLogic/Spawning/Metadata/Processor/FruitPlantMetadataProcessor.cs @@ -0,0 +1,53 @@ +using NitroxClient.Communication; +using NitroxClient.GameLogic.Spawning.Metadata.Processor.Abstract; +using NitroxModel.DataStructures.GameLogic.Entities.Metadata; +using NitroxModel.Packets; +using UnityEngine; + +namespace NitroxClient.GameLogic.Spawning.Metadata.Processor; + +public class FruitPlantMetadataProcessor : EntityMetadataProcessor +{ + public override void ProcessMetadata(GameObject gameObject, FruitPlantMetadata metadata) + { + // Two cases: + // 1. The entity with an id + if (gameObject.TryGetComponent(out FruitPlant fruitPlant)) + { + ProcessMetadata(fruitPlant, metadata); + return; + } + + // 2. The entity with an id has a Plantable (located in the plot's storage), + // we want to access the FruitPlant component which is on the spawned plant object + if (gameObject.TryGetComponent(out Plantable plantable)) + { + if (plantable.linkedGrownPlant && plantable.linkedGrownPlant.TryGetComponent(out fruitPlant)) + { + ProcessMetadata(fruitPlant, metadata); + } + return; + } + + Log.Error($"[{nameof(FruitPlantMetadataProcessor)}] Could not find {nameof(FruitPlant)} related to {gameObject.name}"); + } + + private static void ProcessMetadata(FruitPlant fruitPlant, FruitPlantMetadata metadata) + { + // Inspired by FruitPlant.Initialize + fruitPlant.inactiveFruits.Clear(); + using (PacketSuppressor.Suppress()) + { + for (int i = 0; i < fruitPlant.fruits.Length; i++) + { + fruitPlant.fruits[i].SetPickedState(metadata.PickedStates[i]); + if (metadata.PickedStates[i]) + { + fruitPlant.inactiveFruits.Add(fruitPlant.fruits[i]); + } + } + } + + fruitPlant.timeNextFruit = metadata.TimeNextFruit; + } +} diff --git a/NitroxClient/GameLogic/Spawning/Metadata/Processor/PlantableMetadataProcessor.cs b/NitroxClient/GameLogic/Spawning/Metadata/Processor/PlantableMetadataProcessor.cs index cc2d5db71b..aaca2aad37 100644 --- a/NitroxClient/GameLogic/Spawning/Metadata/Processor/PlantableMetadataProcessor.cs +++ b/NitroxClient/GameLogic/Spawning/Metadata/Processor/PlantableMetadataProcessor.cs @@ -4,16 +4,33 @@ namespace NitroxClient.GameLogic.Spawning.Metadata.Processor; -public class PlantableMetadataProcessor : EntityMetadataProcessor +public class PlantableMetadataProcessor(FruitPlantMetadataProcessor fruitPlantMetadataProcessor) : EntityMetadataProcessor { + private readonly FruitPlantMetadataProcessor fruitPlantMetadataProcessor = fruitPlantMetadataProcessor; + public override void ProcessMetadata(GameObject gameObject, PlantableMetadata metadata) { - Plantable plantable = gameObject.GetComponent(); + if (gameObject.TryGetComponent(out Plantable plantable)) + { + if (plantable.growingPlant) + { + plantable.growingPlant.timeStartGrowth = metadata.TimeStartGrowth; + } + else if (plantable.model.TryGetComponent(out GrowingPlant growingPlant)) + { + // Calculation from GrowingPlant.GetProgress (reversed because we're looking for "progress" while we already know timeStartGrowth) + plantable.plantAge = Mathf.Clamp((DayNightCycle.main.timePassedAsFloat - metadata.TimeStartGrowth) / growingPlant.GetGrowthDuration(), 0f, growingPlant.maxProgress); + } - // Plantable will only have a growing plant when residing in the proper container. - if (plantable && plantable.growingPlant) + // TODO: Refer to the TODO in PlantableMetadata + if (metadata.FruitPlantMetadata != null) + { + fruitPlantMetadataProcessor.ProcessMetadata(gameObject, metadata.FruitPlantMetadata); + } + } + else { - plantable.growingPlant.SetProgress(metadata.Progress); + Log.Error($"[{nameof(PlantableMetadataProcessor)}] Could not find {nameof(Plantable)} on {gameObject.name}"); } } } diff --git a/NitroxClient/GameLogic/Spawning/WorldEntities/CreepvineEntitySpawner.cs b/NitroxClient/GameLogic/Spawning/WorldEntities/CreepvineEntitySpawner.cs new file mode 100644 index 0000000000..5cb1d2a4f2 --- /dev/null +++ b/NitroxClient/GameLogic/Spawning/WorldEntities/CreepvineEntitySpawner.cs @@ -0,0 +1,52 @@ +using System.Collections; +using NitroxModel.DataStructures.GameLogic.Entities; +using NitroxModel.DataStructures.Util; +using UnityEngine; + +namespace NitroxClient.GameLogic.Spawning.WorldEntities; + +public class CreepvineEntitySpawner(DefaultWorldEntitySpawner defaultWorldEntitySpawner) : IWorldEntitySpawner, IWorldEntitySyncSpawner +{ + private readonly DefaultWorldEntitySpawner defaultWorldEntitySpawner = defaultWorldEntitySpawner; + + public IEnumerator SpawnAsync(WorldEntity entity, Optional parent, EntityCell cellRoot, TaskResult> result) + { + yield return defaultWorldEntitySpawner.SpawnAsync(entity, parent, cellRoot, result); + if (!result.value.HasValue) + { + yield break; + } + + SetupObject(result.value.Value); + + // result is already set + } + + public bool SpawnsOwnChildren() => false; + + public bool SpawnSync(WorldEntity entity, Optional parent, EntityCell cellRoot, TaskResult> result) + { + if (!defaultWorldEntitySpawner.SpawnSync(entity, parent, cellRoot, result)) + { + return false; + } + + SetupObject(result.value.Value); + + // result is already set + return true; + } + + private static void SetupObject(GameObject gameObject) + { + if (gameObject.GetComponent()) + { + return; + } + + FruitPlant fruitPlant = gameObject.AddComponent(); + fruitPlant.fruitSpawnEnabled = false; + fruitPlant.timeNextFruit = -1; + fruitPlant.fruits = gameObject.GetComponentsInChildren(true); + } +} diff --git a/NitroxClient/GameLogic/Spawning/WorldEntities/WorldEntitySpawnerResolver.cs b/NitroxClient/GameLogic/Spawning/WorldEntities/WorldEntitySpawnerResolver.cs index fa251e7f84..0d0cd652b7 100644 --- a/NitroxClient/GameLogic/Spawning/WorldEntities/WorldEntitySpawnerResolver.cs +++ b/NitroxClient/GameLogic/Spawning/WorldEntities/WorldEntitySpawnerResolver.cs @@ -26,6 +26,7 @@ public WorldEntitySpawnerResolver(EntityMetadataManager entityMetadataManager, P { customSpawnersByTechType[TechType.Crash] = new CrashEntitySpawner(); customSpawnersByTechType[TechType.EscapePod] = new EscapePodWorldEntitySpawner(entityMetadataManager); + customSpawnersByTechType[TechType.Creepvine] = new CreepvineEntitySpawner(defaultEntitySpawner); vehicleWorldEntitySpawner = new(entities); prefabPlaceholderEntitySpawner = new(defaultEntitySpawner); diff --git a/NitroxClient/MonoBehaviours/MetadataHolder.cs b/NitroxClient/MonoBehaviours/MetadataHolder.cs new file mode 100644 index 0000000000..14914b97c0 --- /dev/null +++ b/NitroxClient/MonoBehaviours/MetadataHolder.cs @@ -0,0 +1,22 @@ +using NitroxModel.DataStructures.GameLogic.Entities.Metadata; +using UnityEngine; + +namespace NitroxClient.MonoBehaviours; + +public class MetadataHolder : MonoBehaviour +{ + public EntityMetadata Metadata; + + public EntityMetadata Consume() + { + Destroy(this); + return Metadata; + } + + public static MetadataHolder AddMetadata(GameObject gameObject, EntityMetadata metadata) + { + MetadataHolder metadataHolder = gameObject.AddComponent(); + metadataHolder.Metadata = metadata; + return metadataHolder; + } +} diff --git a/NitroxClient/MonoBehaviours/ReferenceHolder.cs b/NitroxClient/MonoBehaviours/ReferenceHolder.cs new file mode 100644 index 0000000000..3f36bad083 --- /dev/null +++ b/NitroxClient/MonoBehaviours/ReferenceHolder.cs @@ -0,0 +1,32 @@ +using UnityEngine; + +namespace NitroxClient.MonoBehaviours; + +public class ReferenceHolder : MonoBehaviour +{ + public object Reference; + + public static ReferenceHolder EnsureReferenceAttached(Component component, object reference) + { + return EnsureReferenceAttached(component.gameObject, reference); + } + + public bool TryGetReference(out T outReference) + { + if (Reference is T reference) + { + outReference = reference; + return true; + } + + outReference = default; + return false; + } + + public static ReferenceHolder EnsureReferenceAttached(GameObject gameObject, object reference) + { + ReferenceHolder referenceHolder = gameObject.EnsureComponent(); + referenceHolder.Reference = reference; + return referenceHolder; + } +} diff --git a/NitroxModel/DataStructures/GameLogic/Entities/Metadata/EntityMetadata.cs b/NitroxModel/DataStructures/GameLogic/Entities/Metadata/EntityMetadata.cs index ca3d3426db..d418ba30f0 100644 --- a/NitroxModel/DataStructures/GameLogic/Entities/Metadata/EntityMetadata.cs +++ b/NitroxModel/DataStructures/GameLogic/Entities/Metadata/EntityMetadata.cs @@ -36,6 +36,8 @@ namespace NitroxModel.DataStructures.GameLogic.Entities.Metadata [ProtoInclude(76, typeof(RadiationMetadata))] [ProtoInclude(77, typeof(CrashHomeMetadata))] [ProtoInclude(78, typeof(EatableMetadata))] + [ProtoInclude(79, typeof(PlantableMetadata))] + [ProtoInclude(80, typeof(FruitPlantMetadata))] public abstract class EntityMetadata { } diff --git a/NitroxModel/DataStructures/GameLogic/Entities/Metadata/FruitPlantMetadata.cs b/NitroxModel/DataStructures/GameLogic/Entities/Metadata/FruitPlantMetadata.cs new file mode 100644 index 0000000000..029bac3e4b --- /dev/null +++ b/NitroxModel/DataStructures/GameLogic/Entities/Metadata/FruitPlantMetadata.cs @@ -0,0 +1,33 @@ +using System; +using System.Runtime.Serialization; +using BinaryPack.Attributes; + +namespace NitroxModel.DataStructures.GameLogic.Entities.Metadata; + +[Serializable] +[DataContract] +public class FruitPlantMetadata : EntityMetadata +{ + [DataMember(Order = 1)] + public bool[] PickedStates { get; } + + [DataMember(Order = 1)] + public float TimeNextFruit { get; } + + [IgnoreConstructor] + protected FruitPlantMetadata() + { + // Constructor for serialization. Has to be "protected" for json serialization. + } + + public FruitPlantMetadata(bool[] pickedStates, float timeNextFruit) + { + PickedStates = pickedStates; + TimeNextFruit = timeNextFruit; + } + + public override string ToString() + { + return $"[{nameof(FruitPlantMetadata)} PickedStates: [{string.Join(", ", PickedStates)}], TimeNextFruit: {TimeNextFruit}]"; + } +} diff --git a/NitroxModel/DataStructures/GameLogic/Entities/Metadata/PlantableMetadata.cs b/NitroxModel/DataStructures/GameLogic/Entities/Metadata/PlantableMetadata.cs index e26f52f2d7..0d3398169e 100644 --- a/NitroxModel/DataStructures/GameLogic/Entities/Metadata/PlantableMetadata.cs +++ b/NitroxModel/DataStructures/GameLogic/Entities/Metadata/PlantableMetadata.cs @@ -9,7 +9,14 @@ namespace NitroxModel.DataStructures.GameLogic.Entities.Metadata; public class PlantableMetadata : EntityMetadata { [DataMember(Order = 1)] - public float Progress { get; } + public float TimeStartGrowth { get; set; } + + [DataMember(Order = 2)] + public int SlotID { get; set; } + + // TODO: When the metadata system is reworked and we can have multiple metadatas on one entity, this won't be required anymore + [DataMember(Order = 3)] + public FruitPlantMetadata FruitPlantMetadata { get; set; } [IgnoreConstructor] protected PlantableMetadata() @@ -17,13 +24,21 @@ protected PlantableMetadata() // Constructor for serialization. Has to be "protected" for json serialization. } - public PlantableMetadata(float progress) + public PlantableMetadata(float timeStartGrowth, int slotID) + { + TimeStartGrowth = timeStartGrowth; + SlotID = slotID; + } + + public PlantableMetadata(float timeStartGrowth, int slotID, FruitPlantMetadata fruitPlantMetadata) { - Progress = progress; + TimeStartGrowth = timeStartGrowth; + SlotID = slotID; + FruitPlantMetadata = fruitPlantMetadata; } public override string ToString() { - return $"[PlantableMetadata Time: {Progress}]"; + return $"[{nameof(PlantableMetadata)} TimeStartGrowth: {TimeStartGrowth}, SlotID: {SlotID}, FruitPlantMetadata: {FruitPlantMetadata}]"; } } diff --git a/NitroxModel/DataStructures/GameLogic/ItemData.cs b/NitroxModel/DataStructures/GameLogic/ItemData.cs index 3452465096..afe879499f 100644 --- a/NitroxModel/DataStructures/GameLogic/ItemData.cs +++ b/NitroxModel/DataStructures/GameLogic/ItemData.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Runtime.Serialization; using BinaryPack.Attributes; using ProtoBufNet; @@ -9,7 +9,6 @@ namespace NitroxModel.DataStructures.GameLogic [DataContract] [ProtoInclude(50, typeof(BasicItemData))] [ProtoInclude(51, typeof(EquippedItemData))] - [ProtoInclude(52, typeof(PlantableItemData))] public abstract class ItemData { [DataMember(Order = 1)] diff --git a/NitroxModel/DataStructures/GameLogic/PlantableItemData.cs b/NitroxModel/DataStructures/GameLogic/PlantableItemData.cs deleted file mode 100644 index 1c79eea4b4..0000000000 --- a/NitroxModel/DataStructures/GameLogic/PlantableItemData.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System; -using System.Runtime.Serialization; -using BinaryPack.Attributes; - -namespace NitroxModel.DataStructures.GameLogic -{ - [Serializable] - [DataContract] - public class PlantableItemData : ItemData - { - [DataMember(Order = 1)] - public double PlantedGameTime { get; } - - [IgnoreConstructor] - protected PlantableItemData() - { - // Constructor for serialization. Has to be "protected" for json serialization. - } - - /// - /// Extends the basic ItemData by adding the game time when the Plantable was added to its Planter container. - /// - /// Clients will use this to determine expected plant growth progress when connecting - public PlantableItemData(NitroxId containerId, NitroxId itemId, byte[] serializedData, double plantedGameTime) : base(containerId, itemId, serializedData) - { - PlantedGameTime = plantedGameTime; - } - - public override string ToString() - { - return $"[PlantedItemData ContainerId: {ContainerId} Id: {ItemId} Planted: {PlantedGameTime}"; - } - } -} diff --git a/NitroxPatcher/Patches/Dynamic/FruitPlant_Start_Patch.cs b/NitroxPatcher/Patches/Dynamic/FruitPlant_Start_Patch.cs new file mode 100644 index 0000000000..e606f78e75 --- /dev/null +++ b/NitroxPatcher/Patches/Dynamic/FruitPlant_Start_Patch.cs @@ -0,0 +1,21 @@ +using System.Reflection; +using NitroxClient.MonoBehaviours; +using NitroxModel.Helper; + +namespace NitroxPatcher.Patches.Dynamic; + +/// +/// Gives its own reference () to its children s +/// +public sealed partial class FruitPlant_Start_Patch : NitroxPatch, IDynamicPatch +{ + private static readonly MethodInfo TARGET_METHOD = Reflect.Method((FruitPlant t) => t.Start()); + + public static void Prefix(FruitPlant __instance) + { + foreach (PickPrefab pickPrefab in __instance.fruits) + { + ReferenceHolder.EnsureReferenceAttached(pickPrefab, __instance); + } + } +} diff --git a/NitroxPatcher/Patches/Dynamic/FruitPlant_Update_Patch.cs b/NitroxPatcher/Patches/Dynamic/FruitPlant_Update_Patch.cs new file mode 100644 index 0000000000..612cbb75c9 --- /dev/null +++ b/NitroxPatcher/Patches/Dynamic/FruitPlant_Update_Patch.cs @@ -0,0 +1,73 @@ +using System.Reflection; +using NitroxClient.Communication.Abstract; +using NitroxClient.GameLogic; +using NitroxClient.GameLogic.Spawning.Metadata.Extractor; +using NitroxModel.DataStructures; +using NitroxModel.DataStructures.GameLogic.Entities.Metadata; +using NitroxModel.Helper; +using NitroxModel.Packets; + +namespace NitroxPatcher.Patches.Dynamic; + +/// +/// Broadcasts fruit respawns only by the simulating player. +/// +public sealed partial class FruitPlant_Update_Patch : NitroxPatch, IDynamicPatch +{ + private static readonly MethodInfo TARGET_METHOD = Reflect.Method((FruitPlant t) => t.Update()); + + public static bool Prefix(FruitPlant __instance, out (float, NitroxId, Plantable) __state) + { + // To avoid too many iterations of the code below, we check the conditions of the Update's while loop + if (DayNightCycle.main.timePassedAsFloat < __instance.timeNextFruit || __instance.inactiveFruits.Count == 0) + { + __state = (__instance.timeNextFruit, null, null); + return false; + } + + // If the NitroxEntity is on the same object (e.g. Kelp) + if (__instance.TryGetNitroxId(out NitroxId entityId)) + { + __state = (__instance.timeNextFruit, entityId, null); + return Resolve().HasAnyLockType(entityId); + } + + // If the NitroxEntity is on the distant Plantable object (e.g. fruit tree in a plant pot) + if (PickPrefab_SetPickedUp_Patch.TryGetPlantable(__instance, out Plantable plantable) && + plantable.TryGetNitroxId(out entityId) && + plantable.currentPlanter && plantable.currentPlanter.TryGetNitroxId(out NitroxId planterId)) + { + __state = (__instance.timeNextFruit, entityId, plantable); + // In this precise case, we look for ownership over the planter and not the plant + // This simplifies a lot simulation ownership distribution + return Resolve().HasAnyLockType(planterId); + } + + __state = (__instance.timeNextFruit, null, null); + return true; + } + + public static void Postfix(FruitPlant __instance, (float, NitroxId, Plantable) __state) + { + // If no change was made + if (__state.Item1 == __instance.timeNextFruit) + { + return; + } + + // If the NitroxEntity is on the same object + if (!__state.Item3) + { + FruitPlantMetadata metadata = Resolve().Extract(__instance); + Resolve().Send(new EntityMetadataUpdate(__state.Item2, metadata)); + } + // If the NitroxEntity is on a distant Plantable object + else + { + // TODO: Refer to the TODO in PlantableMetadata. + // When TODO is done, change this to only update the FruitPlant metadata (like the above if) + PlantableMetadata metadata = Resolve().Extract(__state.Item3); + Resolve().Send(new EntityMetadataUpdate(__state.Item2, metadata)); + } + } +} diff --git a/NitroxPatcher/Patches/Dynamic/GrowingPlant_SpawnGrownModelAsync_Patch.cs b/NitroxPatcher/Patches/Dynamic/GrowingPlant_SpawnGrownModelAsync_Patch.cs new file mode 100644 index 0000000000..401d8fe63e --- /dev/null +++ b/NitroxPatcher/Patches/Dynamic/GrowingPlant_SpawnGrownModelAsync_Patch.cs @@ -0,0 +1,55 @@ +using System.Collections.Generic; +using System.Reflection; +using System.Reflection.Emit; +using HarmonyLib; +using NitroxClient.GameLogic.Spawning; +using NitroxClient.GameLogic.Spawning.Metadata.Processor; +using NitroxClient.MonoBehaviours; +using NitroxModel.Helper; +using UnityEngine; + +namespace NitroxPatcher.Patches.Dynamic; + +/// +/// Applies metadata to a spawned GrownPlant when it was provided by +/// +public sealed partial class GrowingPlant_SpawnGrownModelAsync_Patch : NitroxPatch, IDynamicPatch +{ + internal static readonly MethodInfo TARGET_METHOD = AccessTools.EnumeratorMoveNext(Reflect.Method((GrowingPlant t) => t.SpawnGrownModelAsync())); + + /* + * REPLACE: + * grownPlant.SendMessage("OnGrown", SendMessageOptions.DontRequireReceiver); + * BY: + * GrowingPlant_SpawnGrownModelAsync_Patch.OnGrown(grownPlant, this); + */ + public static IEnumerable Transpiler(IEnumerable instructions) + { + return new CodeMatcher(instructions).MatchStartForward([ + new CodeMatch(OpCodes.Ldstr, "OnGrown"), + new CodeMatch(OpCodes.Ldc_I4_1), + new CodeMatch(OpCodes.Callvirt) + ]) + .RemoveInstructions(3) // Remove the Ldstr, Ldc_I4_1 and callvirt + .Insert([ // GrownPlant component is already on stack + new CodeInstruction(OpCodes.Ldloc_1), // Ldloc_1 refers to this instance (GrowingPlant) + new CodeInstruction(OpCodes.Call, Reflect.Method(() => OnGrown(default, default))) + ]) + .InstructionEnumeration(); + } + + public static void OnGrown(GrownPlant grownPlant, GrowingPlant growingPlant) + { + if (!grownPlant.TryGetComponent(out FruitPlant fruitPlant) || !growingPlant.TryGetComponent(out MetadataHolder metadataHolder)) + { + // Original call if we don't need to apply anything + grownPlant.SendMessage("OnGrown", SendMessageOptions.DontRequireReceiver); + return; + } + + // Only useful stuff from FruitPlant.OnGrown + fruitPlant.Initialize(); + fruitPlant.fruitSpawnEnabled = true; + Resolve().ProcessMetadata(grownPlant.seed.gameObject, metadataHolder.Consume()); + } +} diff --git a/NitroxPatcher/Patches/Dynamic/ItemsContainer_NotifyAddItem_Patch.cs b/NitroxPatcher/Patches/Dynamic/ItemsContainer_NotifyAddItem_Patch.cs index d743199071..3aa7cc2f93 100644 --- a/NitroxPatcher/Patches/Dynamic/ItemsContainer_NotifyAddItem_Patch.cs +++ b/NitroxPatcher/Patches/Dynamic/ItemsContainer_NotifyAddItem_Patch.cs @@ -1,5 +1,6 @@ using System.Reflection; using NitroxClient.GameLogic; +using NitroxClient.MonoBehaviours; using NitroxModel.Helper; namespace NitroxPatcher.Patches.Dynamic; @@ -10,7 +11,7 @@ public sealed partial class ItemsContainer_NotifyAddItem_Patch : NitroxPatch, ID public static void Postfix(ItemsContainer __instance, InventoryItem item) { - if (item != null) + if (item != null && Multiplayer.Main && Multiplayer.Main.InitialSyncCompleted) { Resolve().BroadcastItemAdd(item.item, __instance.tr); } diff --git a/NitroxPatcher/Patches/Dynamic/PickPrefab_AddToContainerAsync_Patch.cs b/NitroxPatcher/Patches/Dynamic/PickPrefab_AddToContainerAsync_Patch.cs new file mode 100644 index 0000000000..7823a7298c --- /dev/null +++ b/NitroxPatcher/Patches/Dynamic/PickPrefab_AddToContainerAsync_Patch.cs @@ -0,0 +1,55 @@ +using System.Collections.Generic; +using System.Reflection; +using System.Reflection.Emit; +using HarmonyLib; +using NitroxClient.Communication.Abstract; +using NitroxModel.DataStructures; +using NitroxModel.Helper; +using NitroxModel.Packets; +using UnityEngine; + +namespace NitroxPatcher.Patches.Dynamic; + +/// +/// Sync destruction of giver object when required. +/// +public sealed partial class PickPrefab_AddToContainerAsync_Patch : NitroxPatch, IDynamicPatch +{ + internal static readonly MethodInfo TARGET_METHOD = AccessTools.EnumeratorMoveNext(Reflect.Method((PickPrefab t) => t.AddToContainerAsync(default, default))); + + /* + * 1st injection: + * if (!component) + * { + * UnityEngine.Object.Destroy(gameObject); + * BroadcastDeletion(this); <------- [INSERTED LINE] + * + * 2nd injection: + * else if (!container.HasRoomFor(component)) + * { + * UnityEngine.Object.Destroy(gameObject); + * BroadcastDeletion(this); <------- [INSERTED LINE] + */ + public static IEnumerable Transpiler(IEnumerable instructions) + { + // The 2 injections are similar (looking for a destroy instruction and adding our callback after it) + return new CodeMatcher(instructions).MatchEndForward(new CodeMatch(OpCodes.Call, Reflect.Method(() => Object.Destroy(default)))) + .Repeat(matcher => + { + matcher.Advance(1) + .InsertAndAdvance([ + new CodeInstruction(OpCodes.Ldarg_0), + new CodeInstruction(OpCodes.Call, Reflect.Method(() => BroadcastDeletion(default))) + ]); + }).InstructionEnumeration(); + } + + public static void BroadcastDeletion(PickPrefab pickPrefab) + { + if (pickPrefab.TryGetNitroxId(out NitroxId objectId) || + (pickPrefab.TryGetComponent(out GrownPlant grownPlant) && grownPlant.seed && grownPlant.seed.TryGetNitroxId(out objectId))) + { + Resolve().Send(new EntityDestroyed(objectId)); + } + } +} diff --git a/NitroxPatcher/Patches/Dynamic/PickPrefab_SetPickedState_Patch.cs b/NitroxPatcher/Patches/Dynamic/PickPrefab_SetPickedState_Patch.cs new file mode 100644 index 0000000000..3f510f0ff8 --- /dev/null +++ b/NitroxPatcher/Patches/Dynamic/PickPrefab_SetPickedState_Patch.cs @@ -0,0 +1,20 @@ +using System.Reflection; +using NitroxModel.Helper; + +namespace NitroxPatcher.Patches.Dynamic; + +/// +/// Sync destruction of giver object when required. +/// +public sealed partial class PickPrefab_SetPickedState_Patch : NitroxPatch, IDynamicPatch +{ + private static readonly MethodInfo TARGET_METHOD = Reflect.Method((PickPrefab t) => t.SetPickedState(default)); + + public static void Postfix(PickPrefab __instance, bool newPickedState) + { + if (newPickedState && __instance.destroyOnPicked) + { + PickPrefab_AddToContainerAsync_Patch.BroadcastDeletion(__instance); + } + } +} diff --git a/NitroxPatcher/Patches/Dynamic/PickPrefab_SetPickedUp_Patch.cs b/NitroxPatcher/Patches/Dynamic/PickPrefab_SetPickedUp_Patch.cs new file mode 100644 index 0000000000..d31f88e648 --- /dev/null +++ b/NitroxPatcher/Patches/Dynamic/PickPrefab_SetPickedUp_Patch.cs @@ -0,0 +1,57 @@ +using System.Reflection; +using NitroxClient.Communication.Abstract; +using NitroxClient.GameLogic.Spawning.Metadata.Extractor; +using NitroxClient.MonoBehaviours; +using NitroxModel.DataStructures; +using NitroxModel.DataStructures.GameLogic.Entities.Metadata; +using NitroxModel.Helper; +using NitroxModel.Packets; + +namespace NitroxPatcher.Patches.Dynamic; + +/// +/// Broadcasts fruit harvesting (metadata update) when is under a . +/// +public sealed partial class PickPrefab_SetPickedUp_Patch : NitroxPatch, IDynamicPatch +{ + private static readonly MethodInfo TARGET_METHOD = Reflect.Method((PickPrefab t) => t.SetPickedUp()); + + public static void Postfix(PickPrefab __instance) + { + if (!__instance.TryGetComponent(out ReferenceHolder referenceHolder) || + !referenceHolder.TryGetReference(out FruitPlant fruitPlant)) + { + return; + } + + // This broadcast doesn't require to be simulating the plant because harvesting a fruit is a local action + // therefore it needs to be known by other players + + // In case the FruitPlant is directly on the entity object (which has an id, just like kelp) + if (fruitPlant.TryGetNitroxId(out NitroxId fruitPlantId)) + { + FruitPlantMetadata metadata = Resolve().Extract(fruitPlant); + Resolve().Send(new EntityMetadataUpdate(fruitPlantId, metadata)); + } + // In case the FruitPlant is on the GrownPlant object (doesn't have the id on it) + else if (TryGetPlantable(fruitPlant, out Plantable plantable) && plantable.TryGetNitroxId(out NitroxId plantableId)) + { + // TODO: Refer to the TODO in PlantableMetadata. + // When TODO is done, change this to only update the FruitPlant metadata (like the above if) + PlantableMetadata metadata = Resolve().Extract(plantable); + Resolve().Send(new EntityMetadataUpdate(plantableId, metadata)); + } + } + + public static bool TryGetPlantable(FruitPlant fruitPlant, out Plantable plantable) + { + if (fruitPlant.TryGetComponent(out GrownPlant grownPlant) && grownPlant.seed) + { + plantable = grownPlant.seed; + return true; + } + + plantable = null; + return false; + } +} diff --git a/NitroxPatcher/Patches/Dynamic/Planter_AddItem_Patch.cs b/NitroxPatcher/Patches/Dynamic/Planter_AddItem_Patch.cs index 9f4461adf8..701057427c 100644 --- a/NitroxPatcher/Patches/Dynamic/Planter_AddItem_Patch.cs +++ b/NitroxPatcher/Patches/Dynamic/Planter_AddItem_Patch.cs @@ -7,15 +7,20 @@ namespace NitroxPatcher.Patches.Dynamic; public sealed partial class Planter_AddItem_Patch : NitroxPatch, IDynamicPatch { - public static readonly MethodInfo TARGET_METHOD = Reflect.Method((Planter p) => p.AddItem(default)); + public static readonly MethodInfo TARGET_METHOD = Reflect.Method((Planter p) => p.AddItem(default, default)); - public static void Prefix(InventoryItem item) + public static void Postfix(Plantable plantable, int slotID, Planter __instance) { - Pickupable pickupable = item.item; + Planter.PlantSlot slotByID = __instance.GetSlotByID(slotID); + + if (slotByID == null || slotByID.plantable != plantable) + { + return; + } + // When the planter accepts the new incoming seed, we want to send out metadata about what time the seed was planted. - if (pickupable && pickupable.TryGetIdOrWarn(out NitroxId id)) + if (plantable.TryGetNitroxId(out NitroxId id)) { - Plantable plantable = pickupable.GetComponent(); Resolve().EntityMetadataChanged(plantable, id); } } diff --git a/NitroxPatcher/Patches/Dynamic/Trashcan_Update_Patch.cs b/NitroxPatcher/Patches/Dynamic/Trashcan_Update_Patch.cs new file mode 100644 index 0000000000..39372bf0d1 --- /dev/null +++ b/NitroxPatcher/Patches/Dynamic/Trashcan_Update_Patch.cs @@ -0,0 +1,49 @@ +using System.Collections.Generic; +using System.Reflection; +using System.Reflection.Emit; +using HarmonyLib; +using NitroxClient.Communication.Abstract; +using NitroxClient.GameLogic; +using NitroxModel.DataStructures; +using NitroxModel.Helper; +using NitroxModel.Packets; +using UnityEngine; + +namespace NitroxPatcher.Patches.Dynamic; + +/// +/// Syncs item deletion in trash can (only if you have simulation ownership over the trash can) +/// +public sealed partial class Trashcan_Update_Patch : NitroxPatch, IDynamicPatch +{ + internal static readonly MethodInfo TARGET_METHOD = Reflect.Method((Trashcan t) => t.Update()); + + /* + * if (this.storageContainer.container.RemoveItem(item, true)) + * { + * BroadcastDeletion(item.gameObject); <------- [INSERTED LINE] + * UnityEngine.Object.Destroy(item.gameObject); + * } + */ + public static IEnumerable Transpiler(IEnumerable instructions) + { + return new CodeMatcher(instructions).End() + .Insert([ + new CodeInstruction(OpCodes.Ldarg_0), + new CodeInstruction(OpCodes.Ldloc_1), + new CodeInstruction(OpCodes.Callvirt, Reflect.Property((Component t) => t.gameObject).GetGetMethod()), + new CodeInstruction(OpCodes.Call, Reflect.Method(() => BroadcastDeletion(default, default))) + ]) + .InstructionEnumeration(); + } + + public static void BroadcastDeletion(Trashcan trashcan, GameObject gameObject) + { + if (trashcan.TryGetNitroxId(out NitroxId trashcanId) && + Resolve().HasAnyLockType(trashcanId) && + gameObject.TryGetNitroxId(out NitroxId objectId)) + { + Resolve().Send(new EntityDestroyed(objectId)); + } + } +}