Skip to content

Commit

Permalink
[1.21] Fix IShearable (#1433)
Browse files Browse the repository at this point in the history
  • Loading branch information
Shadows-of-Fire committed Aug 9, 2024
1 parent 37d266e commit a69634b
Show file tree
Hide file tree
Showing 7 changed files with 123 additions and 49 deletions.
4 changes: 3 additions & 1 deletion patches/net/minecraft/world/entity/Entity.java.patch
Original file line number Diff line number Diff line change
Expand Up @@ -452,7 +452,7 @@
}

public void checkDespawn() {
@@ -3449,6 +_,126 @@
@@ -3449,6 +_,128 @@

public boolean mayInteract(Level p_146843_, BlockPos p_146844_) {
return true;
Expand All @@ -466,11 +466,13 @@
+ @Nullable
+ private java.util.Collection<ItemEntity> captureDrops = null;
+
+ @Nullable
+ @Override
+ public java.util.Collection<ItemEntity> captureDrops() {
+ return captureDrops;
+ }
+
+ @Nullable
+ @Override
+ public java.util.Collection<ItemEntity> captureDrops(@Nullable java.util.Collection<ItemEntity> value) {
+ java.util.Collection<ItemEntity> ret = captureDrops;
Expand Down
21 changes: 17 additions & 4 deletions patches/net/minecraft/world/entity/animal/MushroomCow.java.patch
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
--- a/net/minecraft/world/entity/animal/MushroomCow.java
+++ b/net/minecraft/world/entity/animal/MushroomCow.java
@@ -108,7 +_,7 @@

this.playSound(soundevent, 1.0F, 1.0F);
return InteractionResult.sidedSuccess(this.level().isClientSide);
- } else if (itemstack.is(Items.SHEARS) && this.readyForShearing()) {
+ } else if (false && itemstack.is(Items.SHEARS) && this.readyForShearing()) { // Neo: Shear logic is handled by IShearable
this.shear(SoundSource.PLAYERS);
this.gameEvent(GameEvent.SHEAR, p_28941_);
if (!this.level().isClientSide) {
@@ -165,8 +_,10 @@
public void shear(SoundSource p_28924_) {
this.level().playSound(null, this, SoundEvents.MOOSHROOM_SHEAR, p_28924_, 1.0F, 1.0F);
Expand All @@ -11,17 +20,21 @@
((ServerLevel)this.level()).sendParticles(ParticleTypes.EXPLOSION, this.getX(), this.getY(0.5), this.getZ(), 1, 0.0, 0.0, 0.0, 0.0);
this.discard();
cow.moveTo(this.getX(), this.getY(), this.getZ(), this.getYRot(), this.getXRot());
@@ -185,10 +_,9 @@
@@ -185,10 +_,13 @@
this.level().addFreshEntity(cow);

for (int i = 0; i < 5; i++) {
- this.level()
- .addFreshEntity(
- new ItemEntity(this.level(), this.getX(), this.getY(1.0), this.getZ(), new ItemStack(this.getVariant().blockState.getBlock()))
- );
+ //Neo: Change from addFreshEntity to spawnAtLocation to ensure captureDrops can capture this, we also need to unset the default pickup delay from the item
+ ItemEntity item = spawnAtLocation(new ItemStack(this.getVariant().blockState.getBlock()), getBbHeight());
+ if (item != null) item.setNoPickUpDelay();
+ // Neo: Change from addFreshEntity to spawnAtLocation to ensure captureDrops can capture this, we also need to unset the default pickup delay from the item
+ // Vanilla uses this.getY(1.0) for the y-level, which is this.getY() + this.getBbHeight() * 1.0, so we pass the BB height as the Y-offset.
+ ItemEntity item = spawnAtLocation(new ItemStack(this.getVariant().blockState.getBlock()), this.getBbHeight());
+ if (item != null) {
+ // addFreshEntity does not incur a pickup delay, while spawnAtLocation sets the default pickup delay.
+ item.setNoPickUpDelay();
+ }
}
}
}
11 changes: 11 additions & 0 deletions patches/net/minecraft/world/entity/animal/Sheep.java.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
--- a/net/minecraft/world/entity/animal/Sheep.java
+++ b/net/minecraft/world/entity/animal/Sheep.java
@@ -212,7 +_,7 @@
@Override
public InteractionResult mobInteract(Player p_29853_, InteractionHand p_29854_) {
ItemStack itemstack = p_29853_.getItemInHand(p_29854_);
- if (itemstack.is(Items.SHEARS)) {
+ if (false && itemstack.is(Items.SHEARS)) { // Neo: Shear logic is handled by IShearable
if (!this.level().isClientSide && this.readyForShearing()) {
this.shear(SoundSource.PLAYERS);
this.gameEvent(GameEvent.SHEAR, p_29853_);
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,12 @@
return;
}

@@ -127,7 +_,7 @@
@Override
protected InteractionResult mobInteract(Player p_29920_, InteractionHand p_29921_) {
ItemStack itemstack = p_29920_.getItemInHand(p_29921_);
- if (itemstack.is(Items.SHEARS) && this.readyForShearing()) {
+ if (false && itemstack.is(Items.SHEARS) && this.readyForShearing()) { // Neo: Shear logic is handled by IShearable
this.shear(SoundSource.PLAYERS);
this.gameEvent(GameEvent.SHEAR, p_29920_);
if (!this.level().isClientSide) {
11 changes: 11 additions & 0 deletions patches/net/minecraft/world/entity/monster/Bogged.java.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
--- a/net/minecraft/world/entity/monster/Bogged.java
+++ b/net/minecraft/world/entity/monster/Bogged.java
@@ -74,7 +_,7 @@
@Override
protected InteractionResult mobInteract(Player p_330736_, InteractionHand p_331786_) {
ItemStack itemstack = p_330736_.getItemInHand(p_331786_);
- if (itemstack.is(Items.SHEARS) && this.readyForShearing()) {
+ if (false && itemstack.is(Items.SHEARS) && this.readyForShearing()) { // Neo: Shear logic is handled by IShearable
this.shear(SoundSource.PLAYERS);
this.gameEvent(GameEvent.SHEAR, p_330736_);
if (!this.level().isClientSide) {
34 changes: 26 additions & 8 deletions patches/net/minecraft/world/item/ShearsItem.java.patch
Original file line number Diff line number Diff line change
@@ -1,20 +1,38 @@
--- a/net/minecraft/world/item/ShearsItem.java
+++ b/net/minecraft/world/item/ShearsItem.java
@@ -55,19 +_,39 @@
@@ -54,20 +_,57 @@
|| p_43080_.is(BlockTags.WOOL);
}

@Override
+ /**
+ * Neo: Migrate shear behavior into {@link ShearsItem#interactLivingEntity} to call into IShearable instead of relying on {@link net.minecraft.world.entity.Mob#mobInteract}
+ * <p>
+ * To preserve vanilla behavior, this method retains the original flow shared by the various mobInteract overrides.
+ */
+ @Override
+ public InteractionResult interactLivingEntity(ItemStack stack, Player player, LivingEntity entity, net.minecraft.world.InteractionHand hand) {
+ if (entity instanceof net.neoforged.neoforge.common.IShearable target) {
+ if (entity.level().isClientSide) return InteractionResult.CONSUME;
+ BlockPos pos = entity.blockPosition();
+ boolean isClient = entity.level().isClientSide();
+ // Check isShearable on both sides (mirrors vanilla readyForShearing())
+ if (target.isShearable(player, stack, entity.level(), pos)) {
+ target.onSheared(player, stack, entity.level(), pos)
+ .forEach(drop -> target.spawnShearedDrop(entity.level(), pos, drop));
+ // Call onSheared on both sides (mirrors vanilla shear())
+ List<ItemStack> drops = target.onSheared(player, stack, entity.level(), pos);
+ // Spawn drops on the server side using spawnShearedDrop to retain vanilla mob-specific behavior
+ if (!isClient) {
+ for(ItemStack drop : drops) {
+ target.spawnShearedDrop(entity.level(), pos, drop);
+ }
+ }
+ // Call GameEvent.SHEAR on both sides
+ entity.gameEvent(GameEvent.SHEAR, player);
+ stack.hurtAndBreak(1, player, hand == net.minecraft.world.InteractionHand.MAIN_HAND ? net.minecraft.world.entity.EquipmentSlot.MAINHAND : net.minecraft.world.entity.EquipmentSlot.OFFHAND);
+ // Damage the shear item stack by 1 on the server side
+ if (!isClient) {
+ stack.hurtAndBreak(1, player, LivingEntity.getSlotForHand(hand));
+ }
+ // Return sided success if the entity was shearable
+ return InteractionResult.sidedSuccess(isClient);
+ }
+ return InteractionResult.SUCCESS;
+ }
+ return InteractionResult.PASS;
+ }
Expand All @@ -24,7 +42,7 @@
+ return net.neoforged.neoforge.common.ItemAbilities.DEFAULT_SHEARS_ACTIONS.contains(itemAbility);
+ }
+
+ @Override
@Override
public InteractionResult useOn(UseOnContext p_186371_) {
Level level = p_186371_.getLevel();
BlockPos blockpos = p_186371_.getClickedPos();
Expand Down
82 changes: 46 additions & 36 deletions src/main/java/net/neoforged/neoforge/common/IShearable.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
import java.util.List;
import net.minecraft.core.BlockPos;
import net.minecraft.sounds.SoundSource;
import net.minecraft.util.RandomSource;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.Shearable;
import net.minecraft.world.entity.animal.MushroomCow;
Expand All @@ -19,51 +21,51 @@
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.phys.Vec3;
import org.jetbrains.annotations.Nullable;

/**
* This interfaces allows shears (modded & vanilla) and {@linkplain Entity entities} (modded & vanilla) to cooperate
* without needing advance knowledge of each other.
* <p>
* In the future, this system may function for implementations on {@link Block}s as well.
*
* This allows for mods to create there own Shear-like items
* and have them interact with Blocks/Entities without extra work.
* Also, if your block/entity supports the Shears, this allows you
* to support mod-shears as well.
*
* TODO: reconsider this system, currently it is implemented but not checked for, for blocks.
* TODO: Implement support for {@link Block} or remove default implementations from vanilla block classes.
*/
public interface IShearable {
/**
* Checks if the object is currently shearable
* Example: Sheep return false when they have no wool
* Checks if this object can be sheared.
* <p>
* For example, Sheep return false when they have no wool.
*
* @param item The ItemStack that is being used, may be empty.
* @param item The shear tool that is being used, may be empty.
* @param level The current level.
* @param pos Block's position in level.
* @return If this is shearable, and onSheared should be called.
* @param pos The block position of this object (if this is an entity, the value of {@link Entity#blockPosition()}.
* @return If this is shearable, and {@link #onSheared} may be called.
*/
default boolean isShearable(@Nullable Player player, ItemStack item, Level level, BlockPos pos) {
//Default to checking readyForShearing if we are the vanilla shearable interface, and if we aren't assume a default of true
// Default to checking readyForShearing if we are the vanilla shearable interface, and if we aren't assume a default of true
return !(this instanceof Shearable shearable) || shearable.readyForShearing();
}

/**
* Performs the shear function on this object.
* This is called for both client, and server.
* The object should perform all actions related to being sheared,
* except for dropping of the items, and removal of the block.
* As those are handled by ItemShears itself.
* Shears this object. This function is called on both sides, and is responsible for performing any and all actions that happen when sheared, except spawning drops.
* <p>
* Drops that are spawned as a result of being sheared should be returned from this method, and will be spawned on the server using {@link #spawnShearedDrop}.
* <p>
* For entities, they should trust their internal location information over the values passed into this function.
* {@linkplain Entity Entities} may respect their internal position values instead of relying on the {@code pos} parameter.
*
* @param item The ItemStack that is being used, may be empty.
* @param item The shear tool that is being used, may be empty.
* @param level The current level.
* @param pos If this is a block, the block's position in level.
* @return A List containing all items that resulted from the shearing process. May be empty.
* @param pos The block position of this object (if this is an entity, the value of {@link Entity#blockPosition()}.
* @return A list holding all dropped items resulting from the shear operation, or an empty list if nothing dropped.
*/
@SuppressWarnings("deprecation") // Expected call to deprecated vanilla Shearable#shear
default List<ItemStack> onSheared(@Nullable Player player, ItemStack item, Level level, BlockPos pos) {
if (this instanceof LivingEntity entity && this instanceof Shearable shearable) {
if (!level.isClientSide) {
List<ItemEntity> drops = new ArrayList<>();
entity.captureDrops(drops);
entity.captureDrops(new ArrayList<>());
shearable.shear(player == null ? SoundSource.BLOCKS : SoundSource.PLAYERS);
return entity.captureDrops(null).stream().map(ItemEntity::getItem).toList();
}
Expand All @@ -74,31 +76,39 @@ default List<ItemStack> onSheared(@Nullable Player player, ItemStack item, Level
/**
* Performs the logic used to drop a shear result into the world at the correct position and with the proper movement.
* <br>
* For entities, they should trust their internal location information over the values passed into this function.
* {@linkplain Entity Entities} may respect their internal position values instead of relying on the {@code pos} parameter.
*
* @param level The current level.
* @param pos If this is a block, the block's position in level.
* @param drop The ItemStack to drop.
* @param pos The block position of this object (if this is an entity, the value of {@link Entity#blockPosition()}.
* @param drop The stack to drop, from the list of drops returned by {@link #onSheared}.
*/
default void spawnShearedDrop(Level level, BlockPos pos, ItemStack drop) {
if (this instanceof SnowGolem golem) {
golem.spawnAtLocation(drop, 1.7F);
// SnowGolem#shear uses spawnAtLocation(..., this.getEyeHeight());
golem.spawnAtLocation(drop, golem.getEyeHeight());
} else if (this instanceof Bogged bogged) {
bogged.spawnAtLocation(drop);
// Bogged#spawnShearedMushrooms uses spawnAtLocation(..., this.getBbHeight());
bogged.spawnAtLocation(drop, bogged.getBbHeight());
} else if (this instanceof MushroomCow cow) {
// Note: Vanilla uses addFreshEntity instead of spawnAtLocation for spawning mooshrooms drops
// In case a mod is capturing drops for the entity we instead do it the same way we patch in MushroomCow#shear
// We patch Mooshrooms from using addFreshEntity to spawnAtLocation to spawnAtLocation to capture the drops.
// In case a mod is also capturing drops, we also replicate that logic here.
ItemEntity itemEntity = cow.spawnAtLocation(drop, cow.getBbHeight());
if (itemEntity != null) itemEntity.setNoPickUpDelay();
} else if (this instanceof LivingEntity entity) {
if (itemEntity != null) {
itemEntity.setNoPickUpDelay();
}
} else if (this instanceof Entity entity) {
// Everything else uses the "default" rules invented by Sheep#shear, which uses a y-offset of 1 and these random delta movement values.
ItemEntity itemEntity = entity.spawnAtLocation(drop, 1);
if (itemEntity != null) {
itemEntity.setDeltaMovement(itemEntity.getDeltaMovement().add(
((entity.getRandom().nextFloat() - entity.getRandom().nextFloat()) * 0.1F),
(entity.getRandom().nextFloat() * 0.05F),
((entity.getRandom().nextFloat() - entity.getRandom().nextFloat()) * 0.1F)));
RandomSource rand = entity.getRandom();
Vec3 newDelta = itemEntity.getDeltaMovement().add(
(rand.nextFloat() - rand.nextFloat()) * 0.1F,
rand.nextFloat() * 0.05F,
(rand.nextFloat() - rand.nextFloat()) * 0.1F);
itemEntity.setDeltaMovement(newDelta);
}
} else {
// If we aren't an entity, fallback to spawning the item at the given position.
level.addFreshEntity(new ItemEntity(level, pos.getX(), pos.getY(), pos.getZ(), drop));
}
}
Expand Down

0 comments on commit a69634b

Please sign in to comment.