Skip to content
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
7c9ffb6
Initial BlockItemProviderCapability implementation
ChiefArug Jan 2, 2026
1d147d4
Missing Nullable annotation
ChiefArug Jan 12, 2026
158022c
Actually register capability and event handlers
ChiefArug Jan 12, 2026
6a04a7d
Shuffle place block fluid effect to use the new capability
ChiefArug Jan 12, 2026
5de4601
Fix up docs
ChiefArug Jan 12, 2026
6207caa
Move the cap to providing context to the methods so that singletons c…
ChiefArug Jan 12, 2026
94f694d
Move BlockItemProviderCapability to the main tool cap package, makes …
ChiefArug Jan 30, 2026
d64d6c0
Make Glowing provide a new item (glow) to BlockItemProvider for use w…
ChiefArug Jan 31, 2026
aac8df7
Fix priority of offhand/mainhand and update encyclopedia
ChiefArug Jan 31, 2026
6bbace3
Move BlockItemProviderModule to library modules package and add javadoc
ChiefArug Feb 2, 2026
df2b1f2
Reintroduce `glow` field and deprecate it, moving to glowBlock field
ChiefArug Feb 2, 2026
e917051
Make it clearer that simulating is separate to executing
ChiefArug Feb 2, 2026
dd417c9
Fix up most of the misc requested changes.
ChiefArug Feb 2, 2026
3d34ef8
Merge branch 'source/1.20.1' into 1.20.1
ChiefArug Feb 3, 2026
cbd58c6
Make keeping block entity data and adventure mode work by being able …
ChiefArug Feb 3, 2026
a823c33
Run datagen again
ChiefArug Feb 3, 2026
4388047
Add backing stack and add a bit more context to some cap methods
ChiefArug Feb 3, 2026
997e2a5
Merge branch 'source/1.20.1' into 1.20.1
ChiefArug Feb 3, 2026
2b65aba
Add canSurvive method to GlowBlock
ChiefArug Feb 3, 2026
6bcfe4e
Make getBackingStack default
ChiefArug Feb 4, 2026
ce5bdb7
Use getId instead of reconstructing a resource location
ChiefArug Feb 4, 2026
57b91fa
Fix consuming from the correct modifier by iterating modifiers on eve…
ChiefArug Feb 4, 2026
c2e774a
Another doc i forgot to commit
ChiefArug Feb 5, 2026
981dd03
Simplify obtaining the BlockItem
ChiefArug Feb 5, 2026
39ca6f8
Merge getBlockItem and getBackingStack into one method.
ChiefArug Feb 5, 2026
a9faad4
Merge branch 'refs/heads/source/1.20.1' into 1.20.1
ChiefArug Feb 5, 2026
0f80209
Merge branch '1.20.1' into blockItemProvider
KnightMiner Feb 17, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -370,7 +370,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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -57,6 +58,7 @@ public class TinkerLoadables {
public static final StringLoadable<IModifiable> MODIFIABLE_ITEM = instance(Loadables.ITEM, IModifiable.class, "Expected item to be instance of IModifiable");
public static final StringLoadable<IToolPart> TOOL_PART_ITEM = instance(Loadables.ITEM, IToolPart.class, "Expected item to be instance of IToolPart");
public static final StringLoadable<SimpleParticleType> SIMPLE_PARTICLE = instance(Loadables.PARTICLE_TYPE, SimpleParticleType.class, "Expected particle type to be instance of SimpleParticleType");
public static final StringLoadable<BlockItem> 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> TIER = Loadables.RESOURCE_LOCATION.xmap((id, error) -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -127,6 +129,22 @@ public float getRepairAmount(IToolStackView tool, ModifierEntry modifier, Materi
/** Hook running while the tool is in the inventory */
public static final ModuleHook<InventoryTickModifierHook> 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<ToolBlockItemProviderHook> BLOCK_ITEM_PROVIDER = register("block_item_provider", ToolBlockItemProviderHook.class, new ToolBlockItemProviderHook() {
@Nullable
@Override
public BlockItem getBlockItem(IToolStackView tool, ItemStack toolStack, ModifierEntry modifier, @Nullable LivingEntity entity) {
return null;
}

@Override
public void consumeBlockItem(IToolStackView tool, ItemStack toolStack, ModifierEntry modifier, ItemStack backingStack, @Nullable LivingEntity entity) {}

@Override
public ItemStack getBackingStack(IToolStackView tool, ItemStack toolStack, ModifierEntry modifier, BlockItem item, @Nullable LivingEntity entity) {
return ItemStack.EMPTY;
}
});
/* Technical */

/** Hook for working with capacity bars, mainly used for durability bars */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<FluidEffectContext.Block> {
public static final RecordLoadable<PlaceBlockFluidEffect> LOADER = RecordLoadable.create(
Expand All @@ -55,104 +58,157 @@ 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.
*/
private @Nullable Integer maybePlaceFrom(FluidEffectContext.Block context, FluidAction action, ItemStack held, InteractionHand useHand) {
LivingEntity entity = context.getEntity();
BlockItemProviderCapability cap = getBlockProvider(held);
if (cap == null) return null;
BlockItem blockItem = cap.getBlockItem(held, entity);
if (blockItem == null) return null;

ItemStack backingStack = cap.getBackingStack(held, blockItem, entity);
// 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, blockItem, 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
Expand Down
Loading