diff --git a/src/generated/resources/data/tconstruct/tinkering/modifiers/glowing.json b/src/generated/resources/data/tconstruct/tinkering/modifiers/glowing.json index afd3fd8f629..8fda73802f1 100644 --- a/src/generated/resources/data/tconstruct/tinkering/modifiers/glowing.json +++ b/src/generated/resources/data/tconstruct/tinkering/modifiers/glowing.json @@ -26,6 +26,15 @@ }, { "type": "tconstruct:show_interaction_source" + }, + { + "type": "tconstruct:block_item_provider", + "item": "tconstruct:glow", + "tool": { + "type": "mantle:tag", + "tag": "tconstruct:modifiable/held" + }, + "tool_damage": 5 } ], "tooltip_display": "always" diff --git a/src/main/java/slimeknights/tconstruct/common/data/tags/BlockTagProvider.java b/src/main/java/slimeknights/tconstruct/common/data/tags/BlockTagProvider.java index 5f946164abc..3fa680215c7 100644 --- a/src/main/java/slimeknights/tconstruct/common/data/tags/BlockTagProvider.java +++ b/src/main/java/slimeknights/tconstruct/common/data/tags/BlockTagProvider.java @@ -371,7 +371,7 @@ private void addSmeltery() { .addTag(TinkerTags.Blocks.SCORCHED_TANKS); // blocks to ignore like air - this.tag(TinkerTags.Blocks.STRUCTURE_AIR).add(Blocks.LIGHT, TinkerCommons.glow.get()); + this.tag(TinkerTags.Blocks.STRUCTURE_AIR).add(Blocks.LIGHT, TinkerCommons.glowBlock.get()); // smeltery blocks // floor allows any basic seared blocks and all IO blocks diff --git a/src/main/java/slimeknights/tconstruct/gadgets/entity/GlowballEntity.java b/src/main/java/slimeknights/tconstruct/gadgets/entity/GlowballEntity.java index 8659fb4e847..817442fe7a5 100644 --- a/src/main/java/slimeknights/tconstruct/gadgets/entity/GlowballEntity.java +++ b/src/main/java/slimeknights/tconstruct/gadgets/entity/GlowballEntity.java @@ -59,7 +59,7 @@ protected void onHit(HitResult result) { } if (position != null) { - TinkerCommons.glow.get().addGlow(level, position, direction); + TinkerCommons.glowBlock.get().addGlow(level, position, direction); } } diff --git a/src/main/java/slimeknights/tconstruct/library/json/TinkerLoadables.java b/src/main/java/slimeknights/tconstruct/library/json/TinkerLoadables.java index 797c5d5dabf..7defd809716 100644 --- a/src/main/java/slimeknights/tconstruct/library/json/TinkerLoadables.java +++ b/src/main/java/slimeknights/tconstruct/library/json/TinkerLoadables.java @@ -7,6 +7,7 @@ import net.minecraft.world.entity.EquipmentSlot; import net.minecraft.world.entity.ai.attributes.AttributeModifier.Operation; import net.minecraft.world.item.ArmorItem; +import net.minecraft.world.item.BlockItem; import net.minecraft.world.item.Tier; import net.minecraft.world.item.crafting.RecipeType; import net.minecraft.world.level.LightLayer; @@ -57,6 +58,7 @@ public class TinkerLoadables { public static final StringLoadable MODIFIABLE_ITEM = instance(Loadables.ITEM, IModifiable.class, "Expected item to be instance of IModifiable"); public static final StringLoadable TOOL_PART_ITEM = instance(Loadables.ITEM, IToolPart.class, "Expected item to be instance of IToolPart"); public static final StringLoadable SIMPLE_PARTICLE = instance(Loadables.PARTICLE_TYPE, SimpleParticleType.class, "Expected particle type to be instance of SimpleParticleType"); + public static final StringLoadable BLOCK_ITEM = instance(Loadables.ITEM, BlockItem.class, "Expected item to be instance of BlockItem"); /** Tier loadable from the forge tier sorting registry */ public static final StringLoadable TIER = Loadables.RESOURCE_LOCATION.xmap((id, error) -> { diff --git a/src/main/java/slimeknights/tconstruct/library/modifiers/ModifierHooks.java b/src/main/java/slimeknights/tconstruct/library/modifiers/ModifierHooks.java index 73d0a2150e5..273100f888a 100644 --- a/src/main/java/slimeknights/tconstruct/library/modifiers/ModifierHooks.java +++ b/src/main/java/slimeknights/tconstruct/library/modifiers/ModifierHooks.java @@ -8,6 +8,7 @@ import net.minecraft.world.entity.LivingEntity; import net.minecraft.world.entity.projectile.AbstractArrow; import net.minecraft.world.entity.projectile.Projectile; +import net.minecraft.world.item.BlockItem; import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.enchantment.Enchantment; import net.minecraftforge.event.entity.player.PlayerEvent.BreakSpeed; @@ -75,6 +76,7 @@ import slimeknights.tconstruct.library.modifiers.hook.special.sling.SlingForceModifierHook; import slimeknights.tconstruct.library.modifiers.hook.special.sling.SlingLaunchModifierHook; import slimeknights.tconstruct.library.module.ModuleHook; +import slimeknights.tconstruct.library.tools.capability.ToolBlockItemProviderHook; import slimeknights.tconstruct.library.tools.nbt.IToolStackView; import slimeknights.tconstruct.library.tools.nbt.ModDataNBT; import slimeknights.tconstruct.library.utils.RestrictedCompoundTag; @@ -127,6 +129,19 @@ public float getRepairAmount(IToolStackView tool, ModifierEntry modifier, Materi /** Hook running while the tool is in the inventory */ public static final ModuleHook INVENTORY_TICK = register("inventory_tick", InventoryTickModifierHook.class, InventoryTickModifierHook.AllMerger::new, (tool, modifier, world, holder, itemSlot, isSelected, isCorrectSlot, stack) -> {}); + /** Hook for providing a BlockItem via the {@link slimeknights.tconstruct.library.tools.capability.BlockItemProviderCapability}*/ + public static final ModuleHook BLOCK_ITEM_PROVIDER = register("block_item_provider", ToolBlockItemProviderHook.class, new ToolBlockItemProviderHook() { + @Nullable + @Override + public ItemStack getBlockItemStack(IToolStackView tool, ModifierEntry modifier, @Nullable LivingEntity entity) { + return ItemStack.EMPTY; + } + + @Override + public boolean consumeBlockItem(IToolStackView tool, ItemStack toolStack, ModifierEntry modifier, ItemStack backingStack, @Nullable LivingEntity entity) { + return false; + } + }); /* Technical */ /** Hook for working with capacity bars, mainly used for durability bars */ diff --git a/src/main/java/slimeknights/tconstruct/library/modifiers/fluid/block/PlaceBlockFluidEffect.java b/src/main/java/slimeknights/tconstruct/library/modifiers/fluid/block/PlaceBlockFluidEffect.java index f215812c454..acf63e04255 100644 --- a/src/main/java/slimeknights/tconstruct/library/modifiers/fluid/block/PlaceBlockFluidEffect.java +++ b/src/main/java/slimeknights/tconstruct/library/modifiers/fluid/block/PlaceBlockFluidEffect.java @@ -29,10 +29,13 @@ import slimeknights.tconstruct.library.modifiers.fluid.EffectLevel; import slimeknights.tconstruct.library.modifiers.fluid.FluidEffect; import slimeknights.tconstruct.library.modifiers.fluid.FluidEffectContext; +import slimeknights.tconstruct.library.tools.capability.BlockItemProviderCapability; import javax.annotation.Nullable; import java.util.Objects; +import static slimeknights.tconstruct.library.tools.capability.BlockItemProviderCapability.getBlockProvider; + /** Effect to place a block in using logic similar to block item placement. */ public record PlaceBlockFluidEffect(@Nullable Block block, @Nullable SoundEvent sound) implements FluidEffect { public static final RecordLoadable LOADER = RecordLoadable.create( @@ -55,104 +58,159 @@ public float apply(FluidStack fluid, EffectLevel level, FluidEffectContext.Block // if we have no block, then use the block held by the player // its a bit magic, but eh, some fluids are magic Block block = this.block; - ItemStack stack = ItemStack.EMPTY; InteractionHand useHand = InteractionHand.MAIN_HAND; - if (block != null) { - stack = new ItemStack(block); - } else { - LivingEntity entity = context.getEntity(); - if (entity != null) { - // either hand is fine, allows using the tool from offhand or mainhand - for (InteractionHand hand : InteractionHand.values()) { - ItemStack held = entity.getItemInHand(hand); - if (!held.isEmpty() && held.getItem() instanceof BlockItem blockItem) { - block = blockItem.getBlock(); - stack = held; - useHand = hand; - break; - } - } - } else if (context.getStack().getItem() instanceof BlockItem blockItem) { - block = blockItem.getBlock(); - stack = context.getStack(); - } - } - // no block was found, means we either lack an entity or are holding nothing - if (block == null || context.placeRestricted(stack)) { - return 0; - } - // build the context + LivingEntity entity = context.getEntity(); Player player = context.getPlayer(); Level world = context.getLevel(); - BlockPlaceContext placeContext = new BlockPlaceContext(world, player, useHand, stack, context.getHitResult()); - BlockPos clicked = placeContext.getClickedPos(); - if (placeContext.canPlace()) { - // if we have a blockitem, we can offload a lot of the logic to it - if (block.asItem() instanceof BlockItem blockItem) { - if (action.execute()) { - if (blockItem.place(placeContext).consumesAction()) { - if (player instanceof ServerPlayer serverPlayer) { - BlockState placed = world.getBlockState(clicked); - SoundType soundType = placed.getSoundType(world, clicked, player); - serverPlayer.connection.send(new ClientboundSoundPacket( - BuiltInRegistries.SOUND_EVENT.wrapAsHolder(Objects.requireNonNullElse(sound, soundType.getPlaceSound())), - SoundSource.BLOCKS, clicked.getX(), clicked.getY(), clicked.getZ(), (soundType.getVolume() + 1.0F) / 2.0F, soundType.getPitch() * 0.8F, TConstruct.RANDOM.nextLong())); - } - return 1; - } - return 0; - } - // simulating is trickier but the methods exist - placeContext = blockItem.updatePlacementContext(placeContext); - if (placeContext == null) { - return 0; - } - } - // following code is based on block item, with notably differences of not calling block item methods (as if we had one we'd use it above) - // we do notably call this logic in simulation as we need to stop the block item logic early, differences are noted in comments with their vanilla impacts - // simulate note: we don't ask the block item for its state for placement as that method is protected, this notably affects signs/banners (unlikely need) - BlockState state = block.getStateForPlacement(placeContext); - if (state == null) { + // we have four paths to go here. + // 1. there is a block provided and it has a block item + // 2. there is a block probided but it has no block item + // 3. there is an item being held that provides a block item + // 4. there is an item in the context that provides a block item + + if (block != null) { + ItemStack stack = new ItemStack(block); + if (player == null || player.mayBuild()) { + BlockPlaceContext placeContext = new BlockPlaceContext(world, player, useHand, stack, context.getHitResult()); + if (stack.getItem() instanceof BlockItem blockItem) { + // path 1: there is a block provided, and it has a block item + return placeBlockItem(blockItem, context, action, block, placeContext); + } else { + // path 2: there is a block provided, but it has no block item + return placeNonBlockItem(context, action, block, placeContext); + } + } else { return 0; } - // simulate note: we don't call BlockItem#canPlace as its protected, though never overridden in vanilla - if (!state.canSurvive(world, clicked) || !world.isUnobstructed(state, clicked, player == null ? CollisionContext.empty() : CollisionContext.of(player))) { - return 0; + } + + if (entity != null) { + // either hand is fine, allows using the tool from offhand or mainhand + // iterate in reverse order so that we prefer the offhand + for (InteractionHand hand : new InteractionHand[]{InteractionHand.OFF_HAND, InteractionHand.MAIN_HAND}) { + // path 3: check if there is an item being held that provides a block item + ItemStack held = entity.getItemInHand(hand); + Integer result = maybePlaceFrom(context, action, held, useHand); + if (result != null) return result; } - // at this point the only check we are missing on simulate is actually placing the block failing - if (action.execute()) { - // actually place the block - if (!world.setBlock(clicked, state, Block.UPDATE_ALL_IMMEDIATE)) { - return 0; - } - // if its the expected block, run some criteria stuffs - BlockState placed = world.getBlockState(clicked); - if (placed.is(block)) { - // difference from BlockItem: do not update block state or block entity from tag as we have no tag - // it might however be worth passing in a set of properties to set here as part of JSON - // setPlacedBy only matters when placing from held item - block.setPlacedBy(world, clicked, placed, player, stack); - if (player instanceof ServerPlayer serverPlayer) { - CriteriaTriggers.PLACED_BLOCK.trigger(serverPlayer, clicked, stack); - } - } - // resulting events - LivingEntity placer = context.getEntity(); // possible that living is nonnull when player is null - world.gameEvent(GameEvent.BLOCK_PLACE, clicked, GameEvent.Context.of(placer, placed)); - SoundType sound = placed.getSoundType(world, clicked, placer); - world.playSound(null, clicked, Objects.requireNonNullElse(this.sound, sound.getPlaceSound()), SoundSource.BLOCKS, (sound.getVolume() + 1.0F) / 2.0F, sound.getPitch() * 0.8F); + } else if (!context.placeRestricted(context.getStack())) { + // path 4: there is an item in the context that provides a block item + ItemStack held = context.getStack(); + Integer result = maybePlaceFrom(context, action, held, useHand); + return result == null ? 0 : result; + } + } + return 0; + } - // stack might be empty if we failed to find an item form; only matters in null block form anyways - if ((player == null || !player.getAbilities().instabuild) && !stack.isEmpty()) { - stack.shrink(1); - } + /** + * Attempt to place from an item that may or may not provide a {@link BlockItem} via {@link BlockItemProviderCapability} + * @return {@code null} if the item couldn't provide a {@link BlockItem} to place. {@code 0} if placement failed, {@code 1} if it succeeded. + */ + @Nullable + private Integer maybePlaceFrom(FluidEffectContext.Block context, FluidAction action, ItemStack held, InteractionHand useHand) { + LivingEntity entity = context.getEntity(); + BlockItemProviderCapability cap = getBlockProvider(held); + if (cap == null) return null; + ItemStack backingStack = cap.getBlockItemStack(held, entity); + if (backingStack.isEmpty()) return null; + + BlockItem blockItem = BlockItemProviderCapability.verifyBlockItem(backingStack, cap); + if (blockItem == null) return null; + // immediately do a defensive copy of the stack. + ItemStack stack = backingStack.copyWithCount(1); + if (stack.isEmpty()) stack = new ItemStack(blockItem); + + BlockPlaceContext placeContext = new BlockPlaceContext(context.getLevel(), context.getPlayer(), useHand, stack, context.getHitResult()); + + int result = placeBlockItem(blockItem, context, action, blockItem.getBlock(), placeContext); + if (stack.isEmpty()) { + cap.consume(held, backingStack, entity); + } + return result; + } + + private int placeBlockItem(BlockItem blockItem, FluidEffectContext.Block context, FluidAction action, Block block, BlockPlaceContext placeContext) { + Level world = context.getLevel(); + BlockPos clicked = placeContext.getClickedPos(); + Player player = context.getPlayer(); + + if (context.placeRestricted(placeContext.getItemInHand())) return 0; + + if (action.execute()) { + if (blockItem.place(placeContext).consumesAction()) { + if (player instanceof ServerPlayer serverPlayer) { + BlockState placed = world.getBlockState(clicked); + SoundType soundType = placed.getSoundType(world, clicked, player); + serverPlayer.connection.send(new ClientboundSoundPacket( + BuiltInRegistries.SOUND_EVENT.wrapAsHolder(Objects.requireNonNullElse(sound, soundType.getPlaceSound())), + SoundSource.BLOCKS, clicked.getX(), clicked.getY(), clicked.getZ(), (soundType.getVolume() + 1.0F) / 2.0F, soundType.getPitch() * 0.8F, TConstruct.RANDOM.nextLong())); } return 1; } + return 0; + } else { + // simulating is trickier but the methods exist + placeContext = blockItem.updatePlacementContext(placeContext); + if (placeContext == null) { + return 0; + } + // we cannot simulate anything more with a BlockItem, so delegate to the same way as regular blocks + return placeNonBlockItem(context, action, block, placeContext); } - return 0; + } + + private int placeNonBlockItem(FluidEffectContext.Block context, FluidAction action, Block block, BlockPlaceContext placeContext) { + + // following code is based on block item, with notably differences of not calling block item methods (as if we had one we'd use it above) + // we do notably call this logic in simulation as we need to stop the block item logic early, differences are noted in comments with their vanilla impacts + + // simulate note: we don't ask the block item for its state for placement as that method is protected, this notably affects signs/banners (unlikely need) + BlockState state = block.getStateForPlacement(placeContext); + if (state == null) { + return 0; + } + + Level world = context.getLevel(); + BlockPos clicked = placeContext.getClickedPos(); + Player player = context.getPlayer(); + // simulate note: we don't call BlockItem#canPlace as its protected, though never overridden in vanilla + if (!state.canSurvive(world, clicked) || !world.isUnobstructed(state, clicked, player == null ? CollisionContext.empty() : CollisionContext.of(player))) { + return 0; + } + // at this point the only check we are missing on simulate is actually placing the block failing + if (action.execute()) { + // actually place the block + if (!world.setBlock(clicked, state, Block.UPDATE_ALL_IMMEDIATE)) { + return 0; + } + // if its the expected block, run some criteria stuffs + BlockState placed = world.getBlockState(clicked); + ItemStack stack = placeContext.getItemInHand(); + if (placed.is(block)) { + // difference from BlockItem: do not update block state or block entity from tag as we have no tag + // it might however be worth passing in a set of properties to set here as part of JSON + // setPlacedBy only matters when placing from held item + block.setPlacedBy(world, clicked, placed, player, stack); + if (player instanceof ServerPlayer serverPlayer) { + CriteriaTriggers.PLACED_BLOCK.trigger(serverPlayer, clicked, stack); + } + } + + // resulting events + LivingEntity placer = context.getEntity(); // possible that living is nonnull when player is null + world.gameEvent(GameEvent.BLOCK_PLACE, clicked, GameEvent.Context.of(placer, placed)); + SoundType sound = placed.getSoundType(world, clicked, placer); + world.playSound(null, clicked, Objects.requireNonNullElse(this.sound, sound.getPlaceSound()), SoundSource.BLOCKS, (sound.getVolume() + 1.0F) / 2.0F, sound.getPitch() * 0.8F); + + // stack might be empty if we failed to find an item form; only matters in null block form anyways + if ((player == null || !player.getAbilities().instabuild) && !stack.isEmpty()) { + stack.shrink(1); + } + } + return 1; } @Override diff --git a/src/main/java/slimeknights/tconstruct/library/modifiers/modules/behavior/BlockItemProviderModule.java b/src/main/java/slimeknights/tconstruct/library/modifiers/modules/behavior/BlockItemProviderModule.java new file mode 100644 index 00000000000..70983dfa8db --- /dev/null +++ b/src/main/java/slimeknights/tconstruct/library/modifiers/modules/behavior/BlockItemProviderModule.java @@ -0,0 +1,74 @@ +package slimeknights.tconstruct.library.modifiers.modules.behavior; + +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.world.entity.EquipmentSlot; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.item.BlockItem; +import net.minecraft.world.item.ItemStack; +import slimeknights.mantle.data.loadable.primitive.IntLoadable; +import slimeknights.mantle.data.loadable.record.RecordLoadable; +import slimeknights.tconstruct.library.json.TinkerLoadables; +import slimeknights.tconstruct.library.modifiers.ModifierEntry; +import slimeknights.tconstruct.library.modifiers.ModifierHooks; +import slimeknights.tconstruct.library.modifiers.modules.ModifierModule; +import slimeknights.tconstruct.library.modifiers.modules.util.ModifierCondition; +import slimeknights.tconstruct.library.module.ModuleHook; +import slimeknights.tconstruct.library.tools.capability.ToolBlockItemProviderHook; +import slimeknights.tconstruct.library.tools.helper.ToolDamageUtil; +import slimeknights.tconstruct.library.tools.nbt.IToolStackView; + +import javax.annotation.Nullable; +import java.util.List; + +/** + * A module that uses {@link slimeknights.tconstruct.library.tools.capability.BlockItemProviderCapability BlockItemProviderCapability} via {@link ToolBlockItemProviderHook} to provide BlockItems to modifiers like exchanging at the cost of durability. + * Note this does not let the tool place blocks, it only exposes this capability. See {@link slimeknights.tconstruct.tools.modules.interaction.PlaceGlowModule PlaceGlowModule} for an example of a custom module that lets the tool place blocks. + * @param item The BlockItem to provide, wrapped in an ItemStack + * @param damage The amount of damage it takes to provide one block (can be 0) + * @param condition Other conditions that you might want to condition the providing on, such as only happening on certain tool types. + */ +public record BlockItemProviderModule(ItemStack item, int damage, ModifierCondition condition) implements ModifierModule, ToolBlockItemProviderHook, ModifierCondition.ConditionalModule { + private static final List> DEFAULT_HOOKS = List.of(ModifierHooks.BLOCK_ITEM_PROVIDER); + public static final RecordLoadable LOADER = RecordLoadable.create( + TinkerLoadables.BLOCK_ITEM.flatComap(ItemStack::new, (i, e) -> { + if (i.getItem() instanceof BlockItem item) + return item; + throw e.create(String.format("Expected item %s to be instance of BlockItem, but was %s instead", BuiltInRegistries.ITEM.getKey(i.getItem()), i.getItem().getClass().getName())); + }).requiredField("item", BlockItemProviderModule::item), + IntLoadable.FROM_ZERO.defaultField("tool_damage", 1, BlockItemProviderModule::damage), + ModifierCondition.TOOL_FIELD, + BlockItemProviderModule::new); + + @Override + public RecordLoadable getLoader() { + return LOADER; + } + + @Override + public List> getDefaultHooks() { + return DEFAULT_HOOKS; + } + + @Nullable + @Override + public ItemStack getBlockItemStack(IToolStackView tool, ModifierEntry modifier, @Nullable LivingEntity entity) { + return !tool.isBroken() && condition.matches(tool, modifier) ? item : ItemStack.EMPTY; + } + + @Override + public boolean consumeBlockItem(IToolStackView tool, ItemStack toolStack, ModifierEntry modifier, ItemStack backingStack, @Nullable LivingEntity entity) { + // if this is not our item, then we did not provide it so we should avoid consuming + if (item != backingStack) return false; + + // we did provide it, so damage and show animation if possible + if (ToolDamageUtil.damage(tool, damage, entity, toolStack) && entity != null) { + for (EquipmentSlot slot : EquipmentSlot.values()) { + if (entity.getItemBySlot(slot) == toolStack) { + entity.broadcastBreakEvent(slot); + break; + } + } + } + return true; + } +} diff --git a/src/main/java/slimeknights/tconstruct/library/tools/capability/BlockItemProviderCapability.java b/src/main/java/slimeknights/tconstruct/library/tools/capability/BlockItemProviderCapability.java new file mode 100644 index 00000000000..c82c9c0e0ef --- /dev/null +++ b/src/main/java/slimeknights/tconstruct/library/tools/capability/BlockItemProviderCapability.java @@ -0,0 +1,127 @@ +package slimeknights.tconstruct.library.tools.capability; + +import net.minecraft.core.Direction; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.BlockItem; +import net.minecraft.world.item.ItemStack; +import net.minecraftforge.common.MinecraftForge; +import net.minecraftforge.common.capabilities.Capability; +import net.minecraftforge.common.capabilities.CapabilityManager; +import net.minecraftforge.common.capabilities.CapabilityToken; +import net.minecraftforge.common.capabilities.ICapabilityProvider; +import net.minecraftforge.common.capabilities.RegisterCapabilitiesEvent; +import net.minecraftforge.common.util.LazyOptional; +import net.minecraftforge.event.AttachCapabilitiesEvent; +import net.minecraftforge.eventbus.api.EventPriority; +import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext; +import org.jetbrains.annotations.ApiStatus; +import slimeknights.mantle.util.LogicHelper; +import slimeknights.tconstruct.TConstruct; + +import javax.annotation.Nullable; + +/** + * A capability that provides block items to things that place blocks, such as the Exchanging modifier or some place block fluid effects like Ichor. + * Providers of this capability are encouraged to use a single instance for all objects that use the same logic, as the stack and more context are provided in the relevant methods. + */ +public interface BlockItemProviderCapability { + + /** Capability ID */ + ResourceLocation ID = TConstruct.getResource("block_provider"); + /** Capability type */ + Capability CAPABILITY = CapabilityManager.get(new CapabilityToken<>() {}); + + /** Registers this capability */ + @ApiStatus.Internal + static void register() { + FMLJavaModLoadingContext.get().getModEventBus().addListener(EventPriority.NORMAL, false, RegisterCapabilitiesEvent.class, BlockItemProviderCapability::register); + // receive the attach event on low priority, so that our default implementations do not override other mods. + MinecraftForge.EVENT_BUS.addGenericListener(ItemStack.class, EventPriority.LOW, BlockItemProviderCapability::attachCapability); + } + + /** Registers the capability with the event bus */ + private static void register(RegisterCapabilitiesEvent event) { + event.register(BlockItemProviderCapability.class); + } + + /** Event listener to attach default implementation(s) of the capability */ + private static void attachCapability(AttachCapabilitiesEvent event) { + if (event.getObject().getItem() instanceof BlockItem) { + event.addCapability(SimpleBlockItem.ID, SimpleBlockItem.INSTANCE); + } + } + + /** + * Utility to fetch a BlockProvider or null from a given stack. + * @return The block provider for this stack, or null if this stack cannot provide block items. + */ + @Nullable + static BlockItemProviderCapability getBlockProvider(ItemStack stack) { + return LogicHelper.orElseNull(stack.getCapability(CAPABILITY)); + } + + /** + * Utility to verify that a given stack does indeed contain a BlockItem + * @param stack The stack to check + * @param blockProvider The provider that provided this item, used in case it fails as debugging information + * @return the contained BlockItem, or null if it was not a BlockItem + */ + @Nullable + static BlockItem verifyBlockItem(ItemStack stack, BlockItemProviderCapability blockProvider) { + if (stack.getItem() instanceof BlockItem bItem) { + return bItem; + } else { + TConstruct.LOG.warn("BlockItemProviderCapability implementation tried to return a non-empty, non-blockitem stack! Cap: {}, Cap Class: {}, Provided Item: {}", blockProvider, blockProvider.getClass().getName(), BuiltInRegistries.ITEM.getId(stack.getItem())); + return null; + } + } + + /** + * Get a {@link BlockItem} to provide, wrapped as an ItemStack with any required placement NBT data. Can be randomised, if desired. + *
+ *
+ * The returned stack must have {@link ItemStack#getItem} return an instance of {@link BlockItem}, or be {@link ItemStack#EMPTY}! + * @param stack The {@link ItemStack} that this capability was attached to. + * @param entity The {@link LivingEntity} (usually a {@link Player}) that is requesting a block. + * @return the {@link ItemStack} that this provides, or {@link ItemStack#EMPTY} if this cannot provide more block items (for example if the stack has been depleted) + */ + ItemStack getBlockItemStack(ItemStack stack, @Nullable LivingEntity entity); + + /** + * Consume one item from this provider. + * @param stack The {@link ItemStack} that this capability was attached to. + * @param backingStack The stack returned by {@link #getBlockItemStack} that was placed and is now being consumed. It is unmodified and the same instance so can use == for comparisons. + * @param entity The {@link LivingEntity} (usually a {@link Player}) that has just consumed a block. + * Consume a block from this provider. For example may decrease a contained stacks size or remove fluid from the stack's tank. + */ + void consume(ItemStack stack, ItemStack backingStack, @Nullable LivingEntity entity); + + /** + * A simple implementation of {@link BlockItemProviderCapability} that provides from an ItemStack holding a BlockItem + */ + final class SimpleBlockItem implements BlockItemProviderCapability, ICapabilityProvider { + public static final SimpleBlockItem INSTANCE = new SimpleBlockItem(); + private static final ResourceLocation ID = TConstruct.getResource("block_item_provider"); + + private final LazyOptional lazy = LazyOptional.of(() -> this); + + @Override + public ItemStack getBlockItemStack(ItemStack capStack, @Nullable LivingEntity entity) { + return capStack.isEmpty() ? ItemStack.EMPTY : capStack; + } + + @Override + public void consume(ItemStack capStack, ItemStack backingStack, @Nullable LivingEntity entity) { + capStack.shrink(1); + } + + // Because this is an incredibly simple capability it acts as provider and as the actual capability implementation. + @Override + public LazyOptional getCapability(Capability cap, @Nullable Direction dir) { + return CAPABILITY.orEmpty(cap, lazy); + } + } +} diff --git a/src/main/java/slimeknights/tconstruct/library/tools/capability/ToolBlockItemProviderHook.java b/src/main/java/slimeknights/tconstruct/library/tools/capability/ToolBlockItemProviderHook.java new file mode 100644 index 00000000000..c8c67c6b8c4 --- /dev/null +++ b/src/main/java/slimeknights/tconstruct/library/tools/capability/ToolBlockItemProviderHook.java @@ -0,0 +1,81 @@ +package slimeknights.tconstruct.library.tools.capability; + +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.item.BlockItem; +import net.minecraft.world.item.ItemStack; +import net.minecraftforge.common.capabilities.Capability; +import net.minecraftforge.common.util.LazyOptional; +import slimeknights.tconstruct.TConstruct; +import slimeknights.tconstruct.library.modifiers.ModifierEntry; +import slimeknights.tconstruct.library.modifiers.ModifierHooks; +import slimeknights.tconstruct.library.tools.nbt.IToolStackView; + +import javax.annotation.Nullable; +import java.util.function.Supplier; + +/** A hook used to provide BlockItems through the {@link BlockItemProviderCapability}, for modifiers such as exchanging */ +public interface ToolBlockItemProviderHook { + /** + * Get a {@link BlockItem} to provide, wrapped as an ItemStack with any required placement NBT data. Can be randomised, if desired. + *
+ *
+ * The returned stack must have {@link ItemStack#getItem} return an instance of {@link BlockItem}, or be {@link ItemStack#EMPTY}! + * @param tool The tool that this hook is attached to, as a tool stack view + * @param modifier The modifier that provided this hook + * @param entity The entity holding this tool. May be null if there is no entity + * @return the {@link BlockItem} that this provides, or {@code null} if this cannot provide more block items (for example if the stack has been depleted) + */ + ItemStack getBlockItemStack(IToolStackView tool, ModifierEntry modifier, @Nullable LivingEntity entity); + + /** + * Consume a block from this provider. For example may decrease a contained stacks size or remove fluid from the stack's tank. + * @param tool The tool that this hook is attached to, as a tool stack view + * @param toolStack The tool that this hook is attached to + * @param modifier The modifier that provided this hook + * @param entity The entity holding this tool. May be null if there is no entity + * @return {@code true} if this hook consumed, otherwise {@code false} indicating that another modifier needs + */ + boolean consumeBlockItem(IToolStackView tool, ItemStack toolStack, ModifierEntry modifier, ItemStack backingStack, @Nullable LivingEntity entity); + + record CapabilityImpl(IToolStackView tool) implements BlockItemProviderCapability { + + @Override + public ItemStack getBlockItemStack(ItemStack capStack, @Nullable LivingEntity entity) { + for (ModifierEntry entry : tool.getModifiers()) { + ToolBlockItemProviderHook hook = entry.getHook(ModifierHooks.BLOCK_ITEM_PROVIDER); + ItemStack item = hook.getBlockItemStack(tool, entry, entity); + if (!item.isEmpty()) { + if (!(item.getItem() instanceof BlockItem)) { + TConstruct.LOG.warn("ToolBlockItemProviderHook implementation tried to return a non-empty, non-blockitem stack! Hook: {}, Hook Class: {}, Provided Item: {}", hook, hook.getClass().getName(), BuiltInRegistries.ITEM.getId(item.getItem())); + } + return item; + } + } + return ItemStack.EMPTY; + } + + @Override + public void consume(ItemStack capStack, ItemStack backingStack, @Nullable LivingEntity entity) { + for (ModifierEntry entry : tool.getModifiers()) { + ToolBlockItemProviderHook provider = entry.getModifier().getHooks().getOrNull(ModifierHooks.BLOCK_ITEM_PROVIDER); + if (provider != null && provider.consumeBlockItem(tool, capStack, entry, backingStack, entity)) { + return; + } + } + TConstruct.LOG.warn("Could not find a modifier to consume {} from after providing it from ToolBlockItemProviderHook. This is likely causing a duplication glitch! Stack nbt: {}", BuiltInRegistries.ITEM.getKey(backingStack.getItem()), backingStack.getTag()); + } + } + + class Provider implements ToolCapabilityProvider.IToolCapabilityProvider { + private final LazyOptional lazy; + public Provider(Supplier tool) { + lazy = LazyOptional.of(() -> new CapabilityImpl(tool.get())); + } + + @Override + public LazyOptional getCapability(IToolStackView tool, Capability cap) { + return BlockItemProviderCapability.CAPABILITY.orEmpty(cap, lazy); + } + } +} diff --git a/src/main/java/slimeknights/tconstruct/shared/TinkerCommons.java b/src/main/java/slimeknights/tconstruct/shared/TinkerCommons.java index d28c792e3d8..0441c69ec18 100644 --- a/src/main/java/slimeknights/tconstruct/shared/TinkerCommons.java +++ b/src/main/java/slimeknights/tconstruct/shared/TinkerCommons.java @@ -30,6 +30,7 @@ import net.minecraftforge.data.event.GatherDataEvent; import net.minecraftforge.eventbus.api.SubscribeEvent; import net.minecraftforge.fml.event.lifecycle.FMLCommonSetupEvent; +import net.minecraftforge.registries.ForgeRegistries; import net.minecraftforge.registries.RegisterEvent; import net.minecraftforge.registries.RegistryObject; import slimeknights.mantle.data.predicate.block.BlockPredicate; @@ -104,7 +105,12 @@ public final class TinkerCommons extends TinkerModule { /* * Blocks */ - public static final RegistryObject glow = BLOCKS.registerNoItem("glow", () -> new GlowBlock(builder(MapColor.NONE, SoundType.WOOL).noCollission().pushReaction(PushReaction.DESTROY).replaceable().strength(0.0F).lightLevel(s -> 14).noOcclusion())); + public static final ItemObject glowBlock = BLOCKS.register("glow", () -> new GlowBlock(builder(MapColor.NONE, SoundType.WOOL).noCollission().pushReaction(PushReaction.DESTROY).replaceable().strength(0.0F).lightLevel(s -> 14).noOcclusion()), BLOCK_ITEM); + /** + * @deprecated Use {@link #glowBlock} + */ + @Deprecated(forRemoval = true) + public static final RegistryObject glow = RegistryObject.create(glowBlock.getId(), ForgeRegistries.BLOCKS); // glass public static final ItemObject clearGlass = BLOCKS.register("clear_glass", () -> new GlassBlock(glassBuilder(MapColor.NONE)), BLOCK_ITEM); public static final ItemObject clearTintedGlass = BLOCKS.register("clear_tinted_glass", () -> new TintedGlassBlock(glassBuilder(MapColor.COLOR_GRAY).noOcclusion().isValidSpawn(Blocks::never).isRedstoneConductor(Blocks::never).isSuffocating(Blocks::never).isViewBlocking(Blocks::never)), BLOCK_ITEM); diff --git a/src/main/java/slimeknights/tconstruct/shared/block/GlowBlock.java b/src/main/java/slimeknights/tconstruct/shared/block/GlowBlock.java index b062f3d35b9..8063b32e31d 100644 --- a/src/main/java/slimeknights/tconstruct/shared/block/GlowBlock.java +++ b/src/main/java/slimeknights/tconstruct/shared/block/GlowBlock.java @@ -5,6 +5,7 @@ import net.minecraft.world.item.context.BlockPlaceContext; import net.minecraft.world.level.BlockGetter; import net.minecraft.world.level.Level; +import net.minecraft.world.level.LevelReader; import net.minecraft.world.level.block.Block; import net.minecraft.world.level.block.LiquidBlock; import net.minecraft.world.level.block.Mirror; @@ -62,14 +63,18 @@ public BlockState getStateForPlacement(BlockPlaceContext context) { // direction of the glow to place Direction direction = context.getClickedFace().getOpposite(); BlockPos pos = context.getClickedPos(); + BlockState state = this.defaultBlockState().setValue(FACING, direction); // if the direction is valid, place it there - if (canBlockStay(level, pos, direction)) { - return this.defaultBlockState().setValue(FACING, direction); + if (this.canSurvive(state, level, pos)) { + return state; } // try all other directions for (Direction other : Direction.values()) { - if (other != direction && canBlockStay(level, pos, other)) { - return this.defaultBlockState().setValue(FACING, other); + if (other == direction) continue; + + state = this.defaultBlockState().setValue(FACING, other); + if (canSurvive(state, level, pos)) { + return state; } } // can't place @@ -96,25 +101,21 @@ protected void createBlockStateDefinition(StateDefinition.Builder pierceEffect = TinkerEffects.pierce; - + // cooldown public static final RegistryObject teleportCooldownEffect = MOB_EFFECTS.register("teleport_cooldown", () -> new NoMilkEffect(MobEffectCategory.HARMFUL, 0xCC00FA, true)); public static final RegistryObject fireballCooldownEffect = MOB_EFFECTS.register("fireball_cooldown", () -> new NoMilkEffect(MobEffectCategory.HARMFUL, 0xFC9600, true)); @@ -912,6 +914,7 @@ void registerSerializers(RegisterEvent event) { ModifierModule.LOADER.register(getResource("craft_count"), CraftCountModule.LOADER); ModifierModule.LOADER.register(getResource("tipped"), TippedModule.LOADER); ModifierModule.LOADER.register(getResource("projectile_bounce"), ProjectileBounceModule.LOADER); + ModifierModule.LOADER.register(getResource("block_item_provider"), BlockItemProviderModule.LOADER); ModifierModule.LOADER.register(getResource("tool_damage_range"), ToolDamageRangeModule.LOADER); // interaction ModifierModule.LOADER.register(getResource("brush"), BrushModule.LOADER); @@ -1044,6 +1047,7 @@ void commonSetup(final FMLCommonSetupEvent event) { TinkerDataCapability.register(); PersistentDataCapability.register(); EntityModifierCapability.register(); + BlockItemProviderCapability.register(); // by default, we support modifying projectiles (arrows or fireworks mainly, but maybe other stuff). other entities may come in the future EntityModifierCapability.registerEntityPredicate(entity -> entity instanceof Projectile); } diff --git a/src/main/java/slimeknights/tconstruct/tools/TinkerTools.java b/src/main/java/slimeknights/tconstruct/tools/TinkerTools.java index 5d4816bf65f..5b8cc55e2da 100644 --- a/src/main/java/slimeknights/tconstruct/tools/TinkerTools.java +++ b/src/main/java/slimeknights/tconstruct/tools/TinkerTools.java @@ -59,6 +59,7 @@ import slimeknights.tconstruct.library.recipe.ingredient.ToolHookIngredient; import slimeknights.tconstruct.library.tools.IndestructibleItemEntity; import slimeknights.tconstruct.library.tools.SlotType; +import slimeknights.tconstruct.library.tools.capability.ToolBlockItemProviderHook; import slimeknights.tconstruct.library.tools.capability.ToolCapabilityProvider; import slimeknights.tconstruct.library.tools.capability.ToolEnergyCapability; import slimeknights.tconstruct.library.tools.capability.fluid.ToolFluidCapability; @@ -283,6 +284,7 @@ void commonSetup(FMLCommonSetupEvent event) { ToolCapabilityProvider.register(ToolFluidCapability.Provider::new); ToolCapabilityProvider.register(ToolInventoryCapability.Provider::new); ToolCapabilityProvider.register((stack, tool) -> new ToolEnergyCapability.Provider(tool)); + ToolCapabilityProvider.register((stack, tool) -> new ToolBlockItemProviderHook.Provider(tool)); for (ConfigurableAction action : Config.COMMON.toolTweaks) { event.enqueueWork(action); } diff --git a/src/main/java/slimeknights/tconstruct/tools/data/FluidEffectProvider.java b/src/main/java/slimeknights/tconstruct/tools/data/FluidEffectProvider.java index b52d503f409..e3569f4b0df 100644 --- a/src/main/java/slimeknights/tconstruct/tools/data/FluidEffectProvider.java +++ b/src/main/java/slimeknights/tconstruct/tools/data/FluidEffectProvider.java @@ -108,7 +108,7 @@ protected void addFluids() { .fireDamage(2f) .addEntityEffect(new FireFluidEffect(TimeAction.ADD, 2)) .addEntityEffects(FluidMobEffect.builder().effect(MobEffects.GLOWING, 20*5).buildEntity(TimeAction.ADD)) - .addBlockEffect(new PlaceBlockFluidEffect(TinkerCommons.glow.get())); + .addBlockEffect(new PlaceBlockFluidEffect(TinkerCommons.glowBlock.get())); // milk addFluid(Tags.Fluids.MILK, FluidValues.SIP * 2) @@ -268,7 +268,7 @@ protected void addFluids() { // tinkers end addMetal(TinkerFluids.moltenKnightmetal).spikeDamage(3).addEffect(FluidMobEffect.builder().effect(TinkerEffects.pierce.get(), 20 * 5, 2), TimeAction.SET); // thermal compat - compatFluid("glowstone", FluidValues.GEM).addEffect(FluidMobEffect.builder().effect(MobEffects.GLOWING, 20 * 10), TimeAction.ADD).addBlockEffect(new PlaceBlockFluidEffect(TinkerCommons.glow.get())); + compatFluid("glowstone", FluidValues.GEM).addEffect(FluidMobEffect.builder().effect(MobEffects.GLOWING, 20 * 10), TimeAction.ADD).addBlockEffect(new PlaceBlockFluidEffect(TinkerCommons.glowBlock.get())); compatFluid("redstone", FluidValues.GEM).addEffect(ExplosionFluidEffect.radius(1, 0.5f).knockback(LevelingValue.eachLevel(-2)).ignoreBlocks().build()); compatMetal(TinkerFluids.moltenSignalum).addEffect(ExplosionFluidEffect.radius(1, 1).damage(LevelingValue.eachLevel(2)).knockback(LevelingValue.flat(-2)).ignoreBlocks().build()); compatMetal(TinkerFluids.moltenLumium).magicDamage(4).addEffect(FluidMobEffect.builder().effect(MobEffects.GLOWING, 20 * 5, 1).effect(MobEffects.MOVEMENT_SPEED, 20 * 5, 1).effect(MobEffects.JUMP, 20 * 5, 1), TimeAction.SET); diff --git a/src/main/java/slimeknights/tconstruct/tools/data/ModifierProvider.java b/src/main/java/slimeknights/tconstruct/tools/data/ModifierProvider.java index 088c6ba83cb..33b3059510b 100644 --- a/src/main/java/slimeknights/tconstruct/tools/data/ModifierProvider.java +++ b/src/main/java/slimeknights/tconstruct/tools/data/ModifierProvider.java @@ -13,6 +13,7 @@ import net.minecraft.world.entity.MobType; import net.minecraft.world.entity.ai.attributes.AttributeModifier.Operation; import net.minecraft.world.entity.ai.attributes.Attributes; +import net.minecraft.world.item.BlockItem; import net.minecraft.world.item.Item; import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.Items; @@ -176,6 +177,7 @@ import slimeknights.tconstruct.tools.entity.ThrownTool; import slimeknights.tconstruct.tools.item.CrystalshotItem; import slimeknights.tconstruct.tools.logic.ModifierEvents; +import slimeknights.tconstruct.library.modifiers.modules.behavior.BlockItemProviderModule; import slimeknights.tconstruct.tools.modules.AutosmeltModule; import slimeknights.tconstruct.tools.modules.ClearEffectOnUnequipModule; import slimeknights.tconstruct.tools.modules.CraftCountModule; @@ -372,7 +374,8 @@ protected void addModifiers() { .addModule(new PlaceGlowModule(5)) .addModule(new GlowWalkerModule(new LevelingValue(2, 1), 3, 5)) .addModule(new ProjectilePlaceGlowModule(5, true, true)) - .addModule(ShowOffhandModule.DISALLOW_BROKEN).addModule(ShowInteractionSourceModule.INSTANCE); + .addModule(ShowOffhandModule.DISALLOW_BROKEN).addModule(ShowInteractionSourceModule.INSTANCE) + .addModule(new BlockItemProviderModule(new ItemStack(TinkerCommons.glowBlock.asItem()), 5, ModifierCondition.ANY_TOOL.with(ToolStackPredicate.tag(TinkerTags.Items.HELD)))); buildModifier(ModifierIds.firestarter) .levelDisplay(ModifierLevelDisplay.NO_LEVELS) .addModule(PlaceFireModule.INSTANCE) diff --git a/src/main/java/slimeknights/tconstruct/tools/modifiers/ability/tool/ExchangingModifier.java b/src/main/java/slimeknights/tconstruct/tools/modifiers/ability/tool/ExchangingModifier.java index 19e57cfa6d5..f45246bc195 100644 --- a/src/main/java/slimeknights/tconstruct/tools/modifiers/ability/tool/ExchangingModifier.java +++ b/src/main/java/slimeknights/tconstruct/tools/modifiers/ability/tool/ExchangingModifier.java @@ -1,8 +1,10 @@ package slimeknights.tconstruct.tools.modifiers.ability.tool; import net.minecraft.core.BlockPos; +import net.minecraft.core.registries.BuiltInRegistries; import net.minecraft.world.InteractionHand; import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.LivingEntity; import net.minecraft.world.entity.player.Player; import net.minecraft.world.item.BlockItem; import net.minecraft.world.item.ItemStack; @@ -10,6 +12,7 @@ import net.minecraft.world.level.Level; import net.minecraft.world.level.block.Block; import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.state.pattern.BlockInWorld; import slimeknights.tconstruct.common.network.TinkerNetwork; import slimeknights.tconstruct.common.network.UpdateNeighborsPacket; import slimeknights.tconstruct.library.modifiers.ModifierEntry; @@ -17,6 +20,8 @@ import slimeknights.tconstruct.library.modifiers.hook.mining.RemoveBlockModifierHook; import slimeknights.tconstruct.library.modifiers.impl.NoLevelsModifier; import slimeknights.tconstruct.library.module.ModuleHookMap.Builder; +import slimeknights.tconstruct.library.tools.capability.BlockItemProviderCapability; +import slimeknights.tconstruct.library.tools.capability.ToolBlockItemProviderHook; import slimeknights.tconstruct.library.tools.context.ToolHarvestContext; import slimeknights.tconstruct.library.tools.nbt.IToolStackView; import slimeknights.tconstruct.library.utils.Util; @@ -39,12 +44,42 @@ public int getPriority() { @Nullable @Override public Boolean removeBlock(IToolStackView tool, ModifierEntry modifier, ToolHarvestContext context) { - // must have blocks in the offhand - ItemStack offhand = context.getLiving().getOffhandItem(); + // We check the offhand first + ItemStack item = context.getLiving().getOffhandItem(); BlockState state = context.getState(); Level world = context.getWorld(); BlockPos pos = context.getPos(); - if (offhand.isEmpty() || !(offhand.getItem() instanceof BlockItem blockItem)) { + LivingEntity entity = context.getLiving(); + if (item.isEmpty()) return null; + + BlockItemProviderCapability blockProvider = BlockItemProviderCapability.getBlockProvider(item); + ItemStack backingStack = blockProvider == null ? ItemStack.EMPTY : blockProvider.getBlockItemStack(item, entity); + BlockItem blockItem = null; + + // blockProvider != null is technically always true, but this makes the static analysis happy as it doesn't know that ItemStack.EMPTY.getItem() instanceof BlockItem is always false. + if (blockProvider != null) { + blockItem = BlockItemProviderCapability.verifyBlockItem(backingStack, blockProvider); + } + + // if the thing in our offhand cannot provide at all or cannot currently provide then check + // the mainhand next (this tool), in case we have glowing or a similar modifier to provide blocks. + if (blockItem == null) { + item = context.getLiving().getMainHandItem(); + // skip forges cap system and go to the tinkers hook because we know this is a tinkers tool + blockProvider = new ToolBlockItemProviderHook.CapabilityImpl(tool); + backingStack = blockProvider.getBlockItemStack(item, entity); + blockItem = BlockItemProviderCapability.verifyBlockItem(backingStack, blockProvider); + + // nothing could provide + if (blockItem == null) return null; + } + + // immediately do a defensive copy of the stack. + ItemStack fakeStack = backingStack.copyWithCount(1); + + // if we are an adventure mode player, check if we are allowed to place it. + // Note that we check the mined position as the block we are placing 'against', which could be considered variance against vanilla but it is the block that make the most sense here. + if (entity instanceof Player player && !player.mayBuild() && !fakeStack.hasAdventureModePlaceTagForBlock(BuiltInRegistries.BLOCK, new BlockInWorld(world, pos, false))) { return null; } @@ -69,11 +104,17 @@ public Boolean removeBlock(IToolStackView tool, ModifierEntry modifier, ToolHarv // generate placing context // use opposite side for hit as that produces better slab placement - BlockPlaceContext blockUseContext = new BlockPlaceContext(world, player, InteractionHand.OFF_HAND, offhand, Util.createTraceResult(pos, context.getSideHit().getOpposite(), true)); + BlockPlaceContext blockUseContext = new BlockPlaceContext(world, player, InteractionHand.OFF_HAND, fakeStack, Util.createTraceResult(pos, context.getSideHit().getOpposite(), true)); blockUseContext.replaceClicked = true; // force replacement, even if the position is not replacable (as it most always will be) // swap the block, it never goes to air so things like torches will remain InteractionResult success = blockItem.place(blockUseContext); + + // If our fake stack is now empty then it got placed (or otherwise consumed), so consume an item from the provider. + if (fakeStack.isEmpty()) { + blockProvider.consume(item, backingStack, entity); + } + if (success.consumesAction()) { if (!context.isAOE() && player != null) { TinkerNetwork.getInstance().sendTo(new UpdateNeighborsPacket(state, pos), player); @@ -91,4 +132,5 @@ public Boolean removeBlock(IToolStackView tool, ModifierEntry modifier, ToolHarv return world.setBlock(pos, fluidState, 3); } } + } diff --git a/src/main/java/slimeknights/tconstruct/tools/modules/armor/GlowWalkerModule.java b/src/main/java/slimeknights/tconstruct/tools/modules/armor/GlowWalkerModule.java index 6ac2b95da13..836eb9e4698 100644 --- a/src/main/java/slimeknights/tconstruct/tools/modules/armor/GlowWalkerModule.java +++ b/src/main/java/slimeknights/tconstruct/tools/modules/armor/GlowWalkerModule.java @@ -40,7 +40,7 @@ public float getRadius(IToolStackView tool, ModifierEntry modifier) { @Override public boolean walkOn(IToolStackView tool, ModifierEntry entry, LivingEntity living, Level world, BlockPos target, MutableBlockPos mutable, Void context) { if (world.isEmptyBlock(target) && world.getBrightness(LightLayer.BLOCK, target) < minLight) { - if (TinkerCommons.glow.get().addGlow(world, target, Direction.DOWN)) { + if (TinkerCommons.glowBlock.get().addGlow(world, target, Direction.DOWN)) { world.playSound(null, target, world.getBlockState(target).getSoundType(world, target, living).getPlaceSound(), SoundSource.BLOCKS, 1.0f, 1.0f); ToolDamageUtil.damageAnimated(tool, damage, living, EquipmentSlot.FEET, entry.getId()); // only run a single success, gives the lighting engine time to update before we place a ton of unneeded glows diff --git a/src/main/java/slimeknights/tconstruct/tools/modules/interaction/PlaceGlowModule.java b/src/main/java/slimeknights/tconstruct/tools/modules/interaction/PlaceGlowModule.java index 2e51d28e09e..fe7ba786bdb 100644 --- a/src/main/java/slimeknights/tconstruct/tools/modules/interaction/PlaceGlowModule.java +++ b/src/main/java/slimeknights/tconstruct/tools/modules/interaction/PlaceGlowModule.java @@ -52,7 +52,7 @@ public InteractionResult afterBlockUse(IToolStackView tool, ModifierEntry modifi Level world = context.getLevel(); Direction face = context.getClickedFace(); BlockPos pos = context.getClickedPos().relative(face); - if (TinkerCommons.glow.get().addGlow(world, pos, face.getOpposite())) { + if (TinkerCommons.glowBlock.get().addGlow(world, pos, face.getOpposite())) { // damage the tool, showing animation if relevant if (damage > 0 && ToolDamageUtil.damage(tool, damage, player, context.getItemInHand(), modifier.getId()) && player != null) { player.broadcastBreakEvent(source.getSlot(context.getHand())); @@ -70,7 +70,7 @@ public InteractionResult afterBlockUse(IToolStackView tool, ModifierEntry modifi public Boolean removeBlock(IToolStackView tool, ModifierEntry modifier, ToolHarvestContext context) { // if we have left click modifiers active, ensure we don't break the block on left click // otherwise our newly placed block is immediately removed - if (context.getState().is(TinkerCommons.glow.get()) && tool.hasTag(TinkerTags.Items.INTERACTABLE_LEFT) && tool.getHook(ToolHooks.INTERACTION).canInteract(tool, modifier.getId(), InteractionSource.LEFT_CLICK)) { + if (context.getState().is(TinkerCommons.glowBlock.get()) && tool.hasTag(TinkerTags.Items.INTERACTABLE_LEFT) && tool.getHook(ToolHooks.INTERACTION).canInteract(tool, modifier.getId(), InteractionSource.LEFT_CLICK)) { return false; } return null; diff --git a/src/main/java/slimeknights/tconstruct/tools/modules/ranged/common/ProjectilePlaceGlowModule.java b/src/main/java/slimeknights/tconstruct/tools/modules/ranged/common/ProjectilePlaceGlowModule.java index d150531c6e7..58ae2f6312f 100644 --- a/src/main/java/slimeknights/tconstruct/tools/modules/ranged/common/ProjectilePlaceGlowModule.java +++ b/src/main/java/slimeknights/tconstruct/tools/modules/ranged/common/ProjectilePlaceGlowModule.java @@ -63,7 +63,7 @@ public void onProjectileLaunch(IToolStackView tool, ModifierEntry modifier, Livi @Override public boolean onProjectileHitEntity(ModifierNBT modifiers, ModDataNBT persistentData, ModifierEntry modifier, Projectile projectile, EntityHitResult hit, @Nullable LivingEntity attacker, @Nullable LivingEntity target) { if (entities) { - TinkerCommons.glow.get().addGlow(projectile.level(), hit.getEntity().blockPosition(), Direction.DOWN); + TinkerCommons.glowBlock.get().addGlow(projectile.level(), hit.getEntity().blockPosition(), Direction.DOWN); if (damage > 0) { ModifierUtil.updateFishingRod(projectile, damage, false, modifier.getId()); } @@ -75,7 +75,7 @@ public boolean onProjectileHitEntity(ModifierNBT modifiers, ModDataNBT persisten public boolean onProjectileHitsBlock(ModifierNBT modifiers, ModDataNBT persistentData, ModifierEntry modifier, Projectile projectile, BlockHitResult hit, @Nullable LivingEntity owner) { if (blocks) { Direction direction = hit.getDirection(); - if (TinkerCommons.glow.get().addGlow(projectile.level(), hit.getBlockPos().relative(direction), direction.getOpposite())) { + if (TinkerCommons.glowBlock.get().addGlow(projectile.level(), hit.getBlockPos().relative(direction), direction.getOpposite())) { ModifierUtil.updateFishingRod(projectile, damage, true, modifier.getId()); ReusableProjectile.discard(projectile); } diff --git a/src/main/resources/assets/tconstruct/book/encyclopedia/en_us/abilities/interact/tconstruct_glowing.json b/src/main/resources/assets/tconstruct/book/encyclopedia/en_us/abilities/interact/tconstruct_glowing.json index 7bb040dc33b..d6a6ead9132 100644 --- a/src/main/resources/assets/tconstruct/book/encyclopedia/en_us/abilities/interact/tconstruct_glowing.json +++ b/src/main/resources/assets/tconstruct/book/encyclopedia/en_us/abilities/interact/tconstruct_glowing.json @@ -2,10 +2,10 @@ "modifier_id": "tconstruct:glowing", "text": [ { - "text": "Causes the side of the targeted block to glow, giving off light. Placing a glow consumes 5 durability." + "text": "Places glows at the cost of 5 durability, which give off light until broken. Glows can be placed on interaction or by launching a projectile, which will be consumed on hit." }, { - "text": "On bows, will also place a glow at blocks targeted by the arrow, consuming the arrow in the process." + "text": "Can also be used in place of blocks for exchanging or fluid effects, which will place glows when this is in the offhand or the offhand is empty.", "paragraph": true } ], "more_text_space": true, diff --git a/src/main/resources/assets/tconstruct/models/item/glow.json b/src/main/resources/assets/tconstruct/models/item/glow.json index 91542867e31..56fb0127dd5 100644 --- a/src/main/resources/assets/tconstruct/models/item/glow.json +++ b/src/main/resources/assets/tconstruct/models/item/glow.json @@ -1,3 +1,6 @@ { - "parent": "tconstruct:block/glow" -} + "parent": "item/generated", + "textures": { + "layer0": "tconstruct:item/gadgets/glowball" + } +} \ No newline at end of file