From 30d0a630d6fb81494132c49e476a8a7fc940d3a8 Mon Sep 17 00:00:00 2001 From: mgaray99 Date: Sat, 15 Jul 2023 12:37:15 -0700 Subject: [PATCH] Added LivingMakeBrainEvent, to allow a consistent way for modders to manipulate entity Brains. #9292 --- .../world/entity/LivingEntity.java.patch | 26 ++- .../world/entity/ai/Brain.java.patch | 31 ++++ .../net/minecraftforge/common/ForgeHooks.java | 12 ++ .../common/util/BrainBuilder.java | 162 ++++++++++++++++++ .../entity/living/LivingMakeBrainEvent.java | 48 ++++++ .../living/LivingMakeBrainEventTest.java | 42 +++++ 6 files changed, 317 insertions(+), 4 deletions(-) create mode 100644 patches/minecraft/net/minecraft/world/entity/ai/Brain.java.patch create mode 100644 src/main/java/net/minecraftforge/common/util/BrainBuilder.java create mode 100644 src/main/java/net/minecraftforge/event/entity/living/LivingMakeBrainEvent.java create mode 100644 src/test/java/net/minecraftforge/debug/entity/living/LivingMakeBrainEventTest.java diff --git a/patches/minecraft/net/minecraft/world/entity/LivingEntity.java.patch b/patches/minecraft/net/minecraft/world/entity/LivingEntity.java.patch index 7f7a0a7856..419b29eaab 100644 --- a/patches/minecraft/net/minecraft/world/entity/LivingEntity.java.patch +++ b/patches/minecraft/net/minecraft/world/entity/LivingEntity.java.patch @@ -16,6 +16,16 @@ public static final int f_147166_ = 2; public static final int f_147167_ = 4; public static final int f_147168_ = 98; +@@ -239,7 +_,8 @@ + this.f_20885_ = this.m_146908_(); + this.m_274367_(0.6F); + NbtOps nbtops = NbtOps.f_128958_; +- this.f_20939_ = this.m_8075_(new Dynamic<>(nbtops, nbtops.createMap(ImmutableMap.of(nbtops.createString("memories"), nbtops.emptyMap())))); ++ var dyn = new Dynamic<>(nbtops, nbtops.createMap(ImmutableMap.of(nbtops.createString("memories"), nbtops.emptyMap()))); ++ this.f_20939_ = net.minecraftforge.common.ForgeHooks.onLivingMakeBrain(this, this.m_8075_(dyn), dyn); + } + + public Brain m_6274_() { @@ -273,7 +_,7 @@ } @@ -61,6 +71,16 @@ this.m_252836_(); } +@@ -734,7 +_,8 @@ + } + + if (p_21096_.m_128425_("Brain", 10)) { +- this.f_20939_ = this.m_8075_(new Dynamic<>(NbtOps.f_128958_, p_21096_.m_128423_("Brain"))); ++ var dyn = new Dynamic<>(NbtOps.f_128958_, p_21096_.m_128423_("Brain")); ++ this.f_20939_ = net.minecraftforge.common.ForgeHooks.onLivingMakeBrain(this, this.m_8075_(dyn), dyn); + } + + } @@ -749,7 +_,7 @@ if (!mobeffectinstance.m_19552_(this, () -> { this.m_141973_(mobeffectinstance, true, (Entity)null); @@ -370,14 +390,12 @@ if (!this.f_20911_ || this.f_20913_ >= this.m_21304_() / 2 || this.f_20913_ < 0) { this.f_20913_ = -1; this.f_20911_ = true; -@@ -1760,9 +_,11 @@ - } +@@ -1761,8 +_,10 @@ private void m_21312_() { -- ItemStack itemstack = this.m_6844_(EquipmentSlot.OFFHAND); + ItemStack itemstack = this.m_6844_(EquipmentSlot.OFFHAND); - this.m_8061_(EquipmentSlot.OFFHAND, this.m_6844_(EquipmentSlot.MAINHAND)); - this.m_8061_(EquipmentSlot.MAINHAND, itemstack); -+ ItemStack itemstack = this.m_6844_(EquipmentSlot.OFFHAND); + var event = net.minecraftforge.common.ForgeHooks.onLivingSwapHandItems(this); + if (event.isCanceled()) return; + this.m_8061_(EquipmentSlot.OFFHAND, event.getItemSwappedToOffHand()); diff --git a/patches/minecraft/net/minecraft/world/entity/ai/Brain.java.patch b/patches/minecraft/net/minecraft/world/entity/ai/Brain.java.patch new file mode 100644 index 0000000000..66f2f7cbe2 --- /dev/null +++ b/patches/minecraft/net/minecraft/world/entity/ai/Brain.java.patch @@ -0,0 +1,31 @@ +--- a/net/minecraft/world/entity/ai/Brain.java ++++ b/net/minecraft/world/entity/ai/Brain.java +@@ -478,6 +_,28 @@ + return builder.build(); + } + ++ public net.minecraftforge.common.util.BrainBuilder createBuilder(){ ++ var builder = new net.minecraftforge.common.util.BrainBuilder<>(this); ++ builder.getMemoryTypes().addAll(this.f_21843_.keySet()); ++ builder.getSensorTypes().addAll(this.f_21844_.keySet()); ++ builder.addAvailableBehaviorsByPriorityFrom(this.f_21845_); ++ builder.setSchedule(this.m_21932_()); ++ builder.addActivityRequirementsFrom(this.f_21847_); ++ builder.addActivityMemoriesToEraseWhenStoppedFrom(this.f_21848_); ++ builder.getCoreActivities().addAll(this.f_21849_); ++ builder.setDefaultActivity(this.f_21851_); ++ return builder; ++ } ++ ++ public void copyFromBuilder(net.minecraftforge.common.util.BrainBuilder builder){ ++ builder.addAvailableBehaviorsByPriorityTo(this.f_21845_); ++ this.m_21912_(builder.getSchedule()); ++ builder.addActivityRequirementsTo(this.f_21847_); ++ builder.addActivityMemoriesToEraseWhenStoppedTo(this.f_21848_); ++ this.m_21930_(builder.getCoreActivities()); ++ this.m_21944_(builder.getDefaultActivity()); ++ } ++ + static final class MemoryValue { + private final MemoryModuleType f_22030_; + private final Optional> f_22031_; diff --git a/src/main/java/net/minecraftforge/common/ForgeHooks.java b/src/main/java/net/minecraftforge/common/ForgeHooks.java index de57728712..0c8d7c5fdb 100644 --- a/src/main/java/net/minecraftforge/common/ForgeHooks.java +++ b/src/main/java/net/minecraftforge/common/ForgeHooks.java @@ -35,6 +35,7 @@ import com.google.gson.Gson; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonParseException; +import com.mojang.serialization.Dynamic; import com.mojang.serialization.Lifecycle; import it.unimi.dsi.fastutil.objects.ObjectArrayList; @@ -77,6 +78,7 @@ import net.minecraft.world.entity.EntityType; import net.minecraft.world.entity.EquipmentSlot; import net.minecraft.world.entity.ExperienceOrb; import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.ai.Brain; import net.minecraft.world.entity.ai.attributes.Attribute; import net.minecraft.world.entity.ai.attributes.AttributeModifier; import net.minecraft.world.entity.ai.attributes.AttributeSupplier; @@ -128,6 +130,7 @@ import net.minecraftforge.common.loot.IGlobalLootModifier; import net.minecraftforge.common.loot.LootModifierManager; import net.minecraftforge.common.loot.LootTableIdCondition; import net.minecraftforge.common.util.BlockSnapshot; +import net.minecraftforge.common.util.BrainBuilder; import net.minecraftforge.common.util.Lazy; import net.minecraftforge.common.util.MavenVersionStringHelper; import net.minecraftforge.event.AnvilUpdateEvent; @@ -160,6 +163,7 @@ import net.minecraftforge.event.entity.living.LivingChangeTargetEvent.ILivingTar import net.minecraftforge.event.entity.living.LivingFallEvent; import net.minecraftforge.event.entity.living.LivingHurtEvent; import net.minecraftforge.event.entity.living.LivingKnockBackEvent; +import net.minecraftforge.event.entity.living.LivingMakeBrainEvent; import net.minecraftforge.event.entity.living.LivingSwapItemsEvent; import net.minecraftforge.event.entity.living.LivingUseTotemEvent; import net.minecraftforge.event.entity.living.LootingLevelEvent; @@ -244,6 +248,14 @@ public class ForgeHooks return event; } + public static Brain onLivingMakeBrain(LivingEntity entity, Brain originalBrain, Dynamic dynamic) + { + BrainBuilder brainBuilder = originalBrain.createBuilder(); + LivingMakeBrainEvent event = new LivingMakeBrainEvent(entity, brainBuilder); + MinecraftForge.EVENT_BUS.post(event); + return brainBuilder.makeBrain(dynamic); + } + public static boolean onLivingTick(LivingEntity entity) { return MinecraftForge.EVENT_BUS.post(new LivingTickEvent(entity)); diff --git a/src/main/java/net/minecraftforge/common/util/BrainBuilder.java b/src/main/java/net/minecraftforge/common/util/BrainBuilder.java new file mode 100644 index 0000000000..7de0dcc33c --- /dev/null +++ b/src/main/java/net/minecraftforge/common/util/BrainBuilder.java @@ -0,0 +1,162 @@ +/* + * Copyright (c) Forge Development LLC and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.minecraftforge.common.util; + +import com.google.common.collect.Maps; +import com.google.common.collect.Sets; +import com.mojang.datafixers.util.Pair; +import com.mojang.serialization.Dynamic; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.ai.Brain; +import net.minecraft.world.entity.ai.behavior.BehaviorControl; +import net.minecraft.world.entity.ai.memory.MemoryModuleType; +import net.minecraft.world.entity.ai.memory.MemoryStatus; +import net.minecraft.world.entity.ai.sensing.Sensor; +import net.minecraft.world.entity.ai.sensing.SensorType; +import net.minecraft.world.entity.schedule.Activity; +import net.minecraft.world.entity.schedule.Schedule; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import org.jetbrains.annotations.ApiStatus; + +/** + * This object is used to encapsulate state found inside a {@link Brain} instance, + * to make it easily accessible for modders to manipulate during {@link net.minecraftforge.event.entity.living.LivingMakeBrainEvent}.
+ *
+ * Provided are a variety of getter/setter methods to access and manipulate the encapsulated state.
+ *
+ * Methods marked with "INTENDED FOR INTERNAL USE" are only meant to be used inside: + *
    + *
  • {@link net.minecraftforge.common.ForgeHooks#onLivingMakeBrain(LivingEntity, Brain, Dynamic)} + *
  • {@link Brain#createBuilder()} + *
  • {@link Brain#copyFromBuilder(BrainBuilder)} + *
+ * Of course, nothing egregious will happen should a modder choose to use them for their own purposes.
+ */ +public class BrainBuilder { + private final Collection> memoryTypes = new HashSet<>(); + private final Collection>> sensorTypes = new HashSet<>(); + private final Map>>> availableBehaviorsByPriority = Maps.newTreeMap(); + private Schedule schedule = Schedule.EMPTY; + private final Map, MemoryStatus>>> activityRequirements = Maps.newHashMap(); + private final Map>> activityMemoriesToEraseWhenStopped = Maps.newHashMap(); + private final Set coreActivities = Sets.newHashSet(); + private Activity defaultActivity = Activity.IDLE; + + public BrainBuilder(Brain ignoredBrain) {} + + public Brain.Provider provider() { + return Brain.provider(this.memoryTypes, this.sensorTypes); + } + + public Collection> getMemoryTypes() { + return this.memoryTypes; + } + + public Collection>> getSensorTypes() { + return this.sensorTypes; + } + + public Map>>> getAvailableBehaviorsByPriority() { + return this.availableBehaviorsByPriority; + } + + public Schedule getSchedule() { + return this.schedule; + } + + public void setSchedule(Schedule schedule) { + this.schedule = schedule; + } + + public Map, MemoryStatus>>> getActivityRequirements() { + return this.activityRequirements; + } + + public Map>> getActivityMemoriesToEraseWhenStopped() { + return this.activityMemoriesToEraseWhenStopped; + } + + public Set getCoreActivities() { + return this.coreActivities; + } + + public Activity getDefaultActivity() { + return this.defaultActivity; + } + + public void setDefaultActivity(Activity defaultActivity) { + this.defaultActivity = defaultActivity; + } + + /** You may use this as a helper method for adding a behavior to an Activity by priority to an entity's brain. */ + public void addBehaviorToActivityByPriority(Integer priority, Activity activity, BehaviorControl behaviorControl) { + this.availableBehaviorsByPriority.computeIfAbsent(priority, (i) -> Maps.newHashMap()).computeIfAbsent(activity, (a) -> Sets.newLinkedHashSet()).add(behaviorControl); + } + + /** You may use this as a helper method for adding memory requirements for an Activity to an entity's brain. */ + public void addRequirementsToActivity(Activity activity, Collection, MemoryStatus>> requirements) { + addRequirementsToActivityInternal(this.activityRequirements, activity, requirements); + } + + /** You may use this as a helper method for adding a collection of memories to erase when an Activity is stopped to entity's brain. */ + public void addMemoriesToEraseWhenActivityStopped(Activity activity, Collection> memories) { + addMemoriesToEraseWhenActivityStoppedInternal(this.activityMemoriesToEraseWhenStopped, activity, memories); + } + + //====================================================================================================================// + // Everything below this is just to implement Brain.createBuilder and Brain.copyFromBuilder, hence marked as internal // + //====================================================================================================================// + + @ApiStatus.Internal + public void addAvailableBehaviorsByPriorityFrom(Map>>> addFrom) { + addFrom.forEach(((priority, activitySetMap) -> activitySetMap.forEach(((activity, behaviorControls) -> this.availableBehaviorsByPriority.computeIfAbsent(priority, (p) -> Maps.newHashMap()).computeIfAbsent(activity, (a) -> Sets.newLinkedHashSet()).addAll(behaviorControls))))); + } + + @ApiStatus.Internal + public void addAvailableBehaviorsByPriorityTo(Map>>> addTo){ + this.availableBehaviorsByPriority.forEach(((priority, activitySetMap) -> activitySetMap.forEach(((activity, behaviorControls) -> addTo.computeIfAbsent(priority, (p) -> Maps.newHashMap()).computeIfAbsent(activity, (a) -> Sets.newLinkedHashSet()).addAll(behaviorControls))))); + } + + @ApiStatus.Internal + public void addActivityRequirementsFrom(Map, MemoryStatus>>> addFrom) { + addFrom.forEach(this::addRequirementsToActivity); + } + + @ApiStatus.Internal + public void addActivityRequirementsTo(Map, MemoryStatus>>> addTo) { + this.activityRequirements.forEach(((activity, requirements) -> addRequirementsToActivityInternal(addTo, activity, requirements))); + } + + @ApiStatus.Internal + public void addActivityMemoriesToEraseWhenStoppedFrom(Map>> addFrom) { + addFrom.forEach(this::addMemoriesToEraseWhenActivityStopped); + } + + @ApiStatus.Internal + public void addActivityMemoriesToEraseWhenStoppedTo(Map>> addTo) { + this.activityMemoriesToEraseWhenStopped.forEach(((activity, memories) -> addMemoriesToEraseWhenActivityStoppedInternal(addTo, activity, memories))); + } + + private static void addMemoriesToEraseWhenActivityStoppedInternal(Map>> activityMemoriesToEraseWhenStopped, Activity activity, Collection> memories) { + activityMemoriesToEraseWhenStopped.computeIfAbsent(activity, (a) -> Sets.newHashSet()).addAll(memories); + } + + private static void addRequirementsToActivityInternal(Map, MemoryStatus>>> activityRequirements, Activity activity, Collection, MemoryStatus>> requirements) { + activityRequirements.computeIfAbsent(activity, (a) -> Sets.newHashSet()).addAll(requirements); + } + + @ApiStatus.Internal + public Brain makeBrain(Dynamic dynamic) { + Brain brain = Brain.provider(this.memoryTypes, this.sensorTypes).makeBrain(dynamic); + brain.copyFromBuilder(this); + return brain; + } +} diff --git a/src/main/java/net/minecraftforge/event/entity/living/LivingMakeBrainEvent.java b/src/main/java/net/minecraftforge/event/entity/living/LivingMakeBrainEvent.java new file mode 100644 index 0000000000..b826547f51 --- /dev/null +++ b/src/main/java/net/minecraftforge/event/entity/living/LivingMakeBrainEvent.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) Forge Development LLC and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.minecraftforge.event.entity.living; + +import com.mojang.serialization.Dynamic; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.ai.Brain; +import net.minecraftforge.common.ForgeHooks; +import net.minecraftforge.common.MinecraftForge; +import net.minecraftforge.common.util.BrainBuilder; +import net.minecraftforge.eventbus.api.Cancelable; + +/** + * LivingMakeBrainEvent is fired whenever a new {@link net.minecraft.world.entity.ai.Brain} instance is created using {@link LivingEntity#makeBrain(Dynamic)}.
+ *
+ * To access the internal {@link BrainBuilder}, call {@link LivingMakeBrainEvent#getTypedBrainBuilder(LivingEntity)} using the downcasted {@link LivingEntity} obtained from {@link LivingEvent#getEntity()}.
+ *
+ * The BrainBuilder will initially contain all the state found in the original Brain instance.
+ *
+ * After this event is posted, a fresh Brain instance will be created using the encapsulated state found in the BrainBuilder + * and replace the previously created Brain instance for the entity.
+ *
+ * This event is fired via the {@link ForgeHooks#onLivingMakeBrain(LivingEntity, Brain, Dynamic)}.
+ *
+ * This event is not {@link Cancelable}.
+ *
+ * This event does not have a result. {@link HasResult}
+ *
+ * This event is fired on the {@link MinecraftForge#EVENT_BUS}. + **/ +public class LivingMakeBrainEvent extends LivingEvent +{ + private final BrainBuilder brainBuilder; + + public LivingMakeBrainEvent(LivingEntity entity, BrainBuilder brainBuilder) + { + super(entity); + this.brainBuilder = brainBuilder; + } + + public BrainBuilder getTypedBrainBuilder(E ignoredEntity) + { + return (BrainBuilder) this.brainBuilder; + } +} diff --git a/src/test/java/net/minecraftforge/debug/entity/living/LivingMakeBrainEventTest.java b/src/test/java/net/minecraftforge/debug/entity/living/LivingMakeBrainEventTest.java new file mode 100644 index 0000000000..2373614c2a --- /dev/null +++ b/src/test/java/net/minecraftforge/debug/entity/living/LivingMakeBrainEventTest.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) Forge Development LLC and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.minecraftforge.debug.entity.living; + +import net.minecraft.world.entity.ai.behavior.Swim; +import net.minecraft.world.entity.monster.piglin.Piglin; +import net.minecraft.world.entity.schedule.Activity; +import net.minecraftforge.common.MinecraftForge; +import net.minecraftforge.common.util.BrainBuilder; +import net.minecraftforge.event.entity.living.LivingMakeBrainEvent; +import net.minecraftforge.eventtest.internal.EventTest; +import net.minecraftforge.eventtest.internal.TestHolder; + +@TestHolder("LivingMakeBrain") +public class LivingMakeBrainEventTest extends EventTest +{ + public static final boolean ENABLE = true; + + @Override + public void registerEvents() { + if(ENABLE) + { + MinecraftForge.EVENT_BUS.addListener(this::livingMakeBrainEventListener); + } + } + + public void livingMakeBrainEventListener(LivingMakeBrainEvent event) + { + if(event.getEntity() instanceof Piglin piglin) + { + // lets us test in the Overworld + piglin.setImmuneToZombification(true); + // add the ability to swim to Piglins + BrainBuilder brainBuilder = event.getTypedBrainBuilder(piglin); + brainBuilder.addBehaviorToActivityByPriority(0, Activity.CORE, new Swim(0.8F)); + this.pass(); + } + } +}