Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
0a4a571
initial go at abstract stove classes
Lordfirespeed Mar 28, 2026
61442ea
remove unused imports
Lordfirespeed Mar 28, 2026
bb95997
remove impl-specific particle stuff
Lordfirespeed Mar 28, 2026
e5fc48c
implement `Clearable` in the abstract class
Lordfirespeed Mar 28, 2026
6b6ab1d
remove unused `getGrillingArea`
Lordfirespeed Mar 28, 2026
8fdf8f3
stove classes extend corresponding abstract classes
Lordfirespeed Mar 28, 2026
f65b83e
adjust default stove renderer to handle generic stove entity
Lordfirespeed Mar 28, 2026
4a5b0c9
fix: only burn living entities
Lordfirespeed Mar 28, 2026
9da3c01
add editorconfig rule matching java files which sets `indent_style` t…
Lordfirespeed Mar 30, 2026
6fd1054
style: standardise indents to use tabs instead of spaces
Lordfirespeed Mar 30, 2026
3b1f0a1
style: invert `if` condition and remove braces
Lordfirespeed Mar 30, 2026
eebdad6
style: invert `if` condition and remove braces
Lordfirespeed Mar 30, 2026
8ad5c96
fix: water bucket tag has moved
Lordfirespeed Apr 7, 2026
4adac6f
move suppress of deprecation warnings to `AbstractStoveBlock`
Lordfirespeed Apr 7, 2026
82620ed
move `addSmokeParticles` from `private` to `public`
Lordfirespeed Apr 7, 2026
e6368b1
remove unused field `recipeType`
Lordfirespeed Apr 7, 2026
b71082d
revert to using `ItemStackHandler`
Lordfirespeed Apr 7, 2026
9cba550
rewrite `tryToPlaceFoodItem` logic using early-return
Lordfirespeed Apr 7, 2026
ec70e57
fix: `addSmokeParticles` needed adjusting for `ItemStackHandler`
Lordfirespeed Apr 7, 2026
9a334a7
fix: use `SOUND_EXTINGUISH_FIRE` const instead of literal `1009`
Lordfirespeed Apr 13, 2026
a8d5704
fix: only trigger gameEvent once on extinguish
Lordfirespeed Apr 13, 2026
c0663f9
add `gameEvent` call when ignited
Lordfirespeed Apr 13, 2026
61851c2
add `ignite` method to abstract stove entity, for subtypes to override
Lordfirespeed Apr 13, 2026
43cb648
fix: standardise how sounds are played
Lordfirespeed Apr 13, 2026
9a226cd
replace `isCreative` checks with `instabuild` ability checks
Lordfirespeed Apr 13, 2026
3de71d0
ignition should probably also trigger a block update
Lordfirespeed Apr 13, 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
3 changes: 3 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
# http://editorconfig.org
root = true

[*.java]
indent_style = tab

[*.json]
indent_style = space
indent_size = 2
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import vectorwing.farmersdelight.client.particle.SteamParticle;
import vectorwing.farmersdelight.client.recipebook.RecipeCategories;
import vectorwing.farmersdelight.client.renderer.*;
import vectorwing.farmersdelight.common.block.entity.StoveBlockEntity;
import vectorwing.farmersdelight.common.registry.ModBlockEntityTypes;
import vectorwing.farmersdelight.common.registry.ModEntityTypes;
import vectorwing.farmersdelight.common.registry.ModParticleTypes;
Expand All @@ -40,7 +41,7 @@ public static void onEntityRendererRegister(EntityRenderersEvent.RegisterRendere

@SubscribeEvent
public static void onRegisterRenderers(EntityRenderersEvent.RegisterRenderers event) {
event.registerBlockEntityRenderer(ModBlockEntityTypes.STOVE.get(), StoveRenderer::new);
event.registerBlockEntityRenderer(ModBlockEntityTypes.STOVE.get(), DefaultStoveRenderer<StoveBlockEntity>::new);
event.registerBlockEntityRenderer(ModBlockEntityTypes.CUTTING_BOARD.get(), CuttingBoardRenderer::new);
event.registerBlockEntityRenderer(ModBlockEntityTypes.CANVAS_SIGN.get(), CanvasSignRenderer::new);
event.registerBlockEntityRenderer(ModBlockEntityTypes.HANGING_CANVAS_SIGN.get(), HangingCanvasSignRenderer::new);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package vectorwing.farmersdelight.client.renderer;

import com.mojang.blaze3d.vertex.PoseStack;
import com.mojang.math.Axis;
import net.minecraft.client.Minecraft;
import net.minecraft.client.renderer.LevelRenderer;
import net.minecraft.client.renderer.MultiBufferSource;
import net.minecraft.client.renderer.blockentity.BlockEntityRenderer;
import net.minecraft.client.renderer.blockentity.BlockEntityRendererProvider;
import net.minecraft.client.renderer.entity.ItemRenderer;
import net.minecraft.core.Direction;
import net.minecraft.core.NonNullList;
import net.minecraft.world.item.ItemDisplayContext;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.phys.Vec2;
import vectorwing.farmersdelight.common.block.StoveBlock;
import vectorwing.farmersdelight.common.block.entity.AbstractStoveBlockEntity;

public class DefaultStoveRenderer<T extends AbstractStoveBlockEntity> implements BlockEntityRenderer<T>
{
private static final float SIZE = 0.375F;
private final ItemRenderer itemRenderer;

public DefaultStoveRenderer(BlockEntityRendererProvider.Context context) {
this.itemRenderer = context.getItemRenderer();
}

@Override
public void render(T stove, float partialTicks, PoseStack poseStack, MultiBufferSource buffer, int packedLight, int packedOverlay) {
Direction direction = stove.getBlockState().getValue(StoveBlock.FACING).getOpposite();

var items = stove.getItems();
int posLong = (int) stove.getBlockPos().asLong();

for (int i = 0; i < items.getSlots(); ++i) {
ItemStack stoveStack = items.getStackInSlot(i);
if (stoveStack.isEmpty()) continue;

poseStack.pushPose();

// Center item above the stove
poseStack.translate(0.5D, 1.02D, 0.5D);

// Rotate item to face the stove's front side
float f = -direction.toYRot();
poseStack.mulPose(Axis.YP.rotationDegrees(f));

// Rotate item flat on the stove. Use X and Y from now on
poseStack.mulPose(Axis.XP.rotationDegrees(90.0F));

// Neatly align items according to their index
Vec2 itemOffset = stove.getStoveItemOffset(i);
poseStack.translate(itemOffset.x, itemOffset.y, 0.0D);

// Resize the items
poseStack.scale(SIZE, SIZE, SIZE);

itemRenderer.renderStatic(stoveStack, ItemDisplayContext.FIXED, LevelRenderer.getLightColor(stove.getLevel(), stove.getBlockPos().above()), packedOverlay, poseStack, buffer, stove.getLevel(), posLong + i);
poseStack.popPose();
}
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
package vectorwing.farmersdelight.common.block;

import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.sounds.SoundEvents;
import net.minecraft.sounds.SoundSource;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.InteractionResult;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.Mob;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.*;
import net.minecraft.world.item.context.BlockPlaceContext;
import net.minecraft.world.item.crafting.AbstractCookingRecipe;
import net.minecraft.world.item.enchantment.EnchantmentHelper;
import net.minecraft.world.level.BlockGetter;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.LevelAccessor;
import net.minecraft.world.level.block.*;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.entity.BlockEntityTicker;
import net.minecraft.world.level.block.entity.BlockEntityType;
import net.minecraft.world.level.block.state.BlockBehaviour;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.state.StateDefinition;
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
import net.minecraft.world.level.block.state.properties.BooleanProperty;
import net.minecraft.world.level.block.state.properties.DirectionProperty;
import net.minecraft.world.level.gameevent.GameEvent;
import net.minecraft.world.level.pathfinder.BlockPathTypes;
import net.minecraft.world.phys.BlockHitResult;
import net.minecraft.world.phys.shapes.BooleanOp;
import net.minecraft.world.phys.shapes.Shapes;
import net.minecraft.world.phys.shapes.VoxelShape;
import net.minecraftforge.common.ToolActions;
import vectorwing.farmersdelight.common.block.entity.AbstractStoveBlockEntity;
import vectorwing.farmersdelight.common.registry.ModDamageTypes;
import vectorwing.farmersdelight.common.tag.CommonTags;
import vectorwing.farmersdelight.common.utility.ItemUtils;
import vectorwing.farmersdelight.common.utility.MathUtils;

import javax.annotation.Nullable;
import java.util.Optional;

@SuppressWarnings("deprecation")
public abstract class AbstractStoveBlock extends BaseEntityBlock {
public static final DirectionProperty FACING = HorizontalDirectionalBlock.FACING;
public static final BooleanProperty LIT = BlockStateProperties.LIT;

private static final VoxelShape GRILLING_AREA = Block.box(3.0F, 0.0F, 3.0F, 13.0F, 1.0F, 13.0F);

public AbstractStoveBlock(BlockBehaviour.Properties properties) {
super(properties);
this.registerDefaultState(
this.stateDefinition.any()
.setValue(FACING, Direction.NORTH)
.setValue(LIT, false)
);
}

@Override
public InteractionResult use(BlockState state, Level level, BlockPos pos, Player player, InteractionHand hand, BlockHitResult hit) {
if (state.getValue(LIT)) {
var extinguishResult = tryToExtinguish(state, level, pos, player, hand, hit);
if (extinguishResult != InteractionResult.PASS) return extinguishResult;
} else {
var igniteResult = tryToIgnite(state, level, pos, player, hand, hit);
if (igniteResult != InteractionResult.PASS) return igniteResult;
}

return tryToPlaceFoodItem(state, level, pos, player, hand, hit);
}

protected InteractionResult tryToIgnite(BlockState state, Level level, BlockPos pos, Player player, InteractionHand hand, BlockHitResult hit) {
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Igniting currently lacks the gameEvent call, so it doesn't trigger sculk sensors. Might be worth adding here too.

ItemStack heldStack = player.getItemInHand(hand);
Item heldItem = heldStack.getItem();

if (heldItem instanceof FlintAndSteelItem) {
if (!level.isClientSide()) {
level.playSound(null, pos, SoundEvents.FLINTANDSTEEL_USE, SoundSource.BLOCKS, 1.0F, MathUtils.RAND.nextFloat() * 0.4F + 0.8F);
}
ignite(player, level, pos, state);
heldStack.hurtAndBreak(1, player, action -> action.broadcastBreakEvent(hand));
return InteractionResult.SUCCESS;
}

if (heldItem instanceof FireChargeItem) {
if (!level.isClientSide()) {
level.playSound(null, pos, SoundEvents.FIRECHARGE_USE, SoundSource.BLOCKS, 1.0F, (MathUtils.RAND.nextFloat() - MathUtils.RAND.nextFloat()) * 0.2F + 1.0F);
}
ignite(player, level, pos, state);
if (!player.getAbilities().instabuild) {
heldStack.shrink(1);
}
return InteractionResult.SUCCESS;
}

return InteractionResult.PASS;
}

protected InteractionResult tryToExtinguish(BlockState state, Level level, BlockPos pos, Player player, InteractionHand hand, BlockHitResult hit) {
ItemStack heldStack = player.getItemInHand(hand);

if (heldStack.canPerformAction(ToolActions.SHOVEL_DIG)) {
if (!level.isClientSide()) {
level.levelEvent(null, LevelEvent.SOUND_EXTINGUISH_FIRE, pos, 0);
}
extinguish(player, level, pos, state);
heldStack.hurtAndBreak(1, player, (action) -> action.broadcastBreakEvent(hand));
return InteractionResult.sidedSuccess(level.isClientSide());
}

if (heldStack.is(CommonTags.Items.BUCKETS_WATER)) {
if (!level.isClientSide()) {
level.playSound(null, pos, SoundEvents.GENERIC_EXTINGUISH_FIRE, SoundSource.BLOCKS, 1.0F, 1.0F);
}
extinguish(player, level, pos, state);
if (!player.getAbilities().instabuild) player.setItemInHand(hand, heldStack.getCraftingRemainingItem());
return InteractionResult.sidedSuccess(level.isClientSide());
}

return InteractionResult.PASS;
}

protected InteractionResult tryToPlaceFoodItem(BlockState state, Level level, BlockPos pos, Player player, InteractionHand hand, BlockHitResult hit) {
if (isStoveTopCovered(level, pos, state)) return InteractionResult.PASS;
if (!(level.getBlockEntity(pos) instanceof AbstractStoveBlockEntity stoveEntity)) return InteractionResult.PASS;

ItemStack heldStack = player.getItemInHand(hand);
Optional<? extends AbstractCookingRecipe> maybeRecipe = stoveEntity.getCookingRecipe(heldStack);
if (maybeRecipe.isEmpty()) return InteractionResult.PASS;
if (level.isClientSide) return InteractionResult.CONSUME;
boolean placeFoodSuccess = stoveEntity.placeFood(player, player.getAbilities().instabuild ? heldStack.copy() : heldStack, maybeRecipe.get().getCookingTime());
if (!placeFoodSuccess) return InteractionResult.CONSUME;
level.playSound(null, pos, SoundEvents.LANTERN_PLACE, SoundSource.BLOCKS, 0.5F, 1.0F);
return InteractionResult.SUCCESS;
}

public void ignite(@Nullable Entity entity, LevelAccessor level, BlockPos pos, BlockState state) {
if (level.getBlockEntity(pos) instanceof AbstractStoveBlockEntity stoveEntity) {
stoveEntity.ignite();
}
if (level.isClientSide()) return;

var newState = state.setValue(LIT, true);
level.setBlock(pos, newState, 11);
level.gameEvent(entity, GameEvent.BLOCK_CHANGE, pos);
}

public void extinguish(@Nullable Entity entity, LevelAccessor level, BlockPos pos, BlockState state) {
if (level.getBlockEntity(pos) instanceof AbstractStoveBlockEntity stoveEntity) {
stoveEntity.extinguish();
}
if (level.isClientSide()) return;

var newState = state.setValue(LIT, false);
level.setBlock(pos, newState, 11);
level.gameEvent(entity, GameEvent.BLOCK_CHANGE, pos);
}

@Override
public RenderShape getRenderShape(BlockState pState) { return RenderShape.MODEL; }

@Override
public BlockState getStateForPlacement(BlockPlaceContext context) {
return this.defaultBlockState()
.setValue(FACING, context.getHorizontalDirection().getOpposite())
.setValue(LIT, true);
}

@Override
public void stepOn(Level level, BlockPos pos, BlockState state, Entity entity) {
burnEntitySteppingOnStove(level, pos, state, entity);
super.stepOn(level, pos, state, entity);
}

protected void burnEntitySteppingOnStove(Level level, BlockPos pos, BlockState state, Entity entity) {
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if this method needs to be separate, as it contains almost all of the code that would go into stepOn.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the original version of the code, the base.stepOn() was invoked at the end of the method, but I wanted to use early-return pattern to make this method easier to read.

To keep the order of invocation the same and use early return pattern, I had to extract this method

if (!state.getValue(LIT)) return;
if (!entity.getBoundingBox().intersects(GRILLING_AREA.bounds().move(pos.above()))) return;
if (entity.isSteppingCarefully()) return;
if (entity.fireImmune()) return;
if (!(entity instanceof LivingEntity livingEntity)) return;
if (EnchantmentHelper.hasFrostWalker(livingEntity)) return;
entity.hurt(ModDamageTypes.getSimpleDamageSource(level, ModDamageTypes.STOVE_BURN), 1.0F);
}

@Override
public void onRemove(BlockState state, Level level, BlockPos pos, BlockState newState, boolean isMoving) {
if (state.is(newState.getBlock())) return;
if (level.getBlockEntity(pos) instanceof AbstractStoveBlockEntity stoveEntity) {
ItemUtils.dropItems(level, pos, stoveEntity.getItems());
}
super.onRemove(state, level, pos, newState, isMoving);
}

/**
* Checks if the state is a Stove, and if the grilling area is being obstructed by the block above.
*/
public static boolean isStoveTopCovered(Level level, BlockPos pos, BlockState stoveState) {
if (!(stoveState.getBlock() instanceof StoveBlock)) return false;
BlockPos abovePos = pos.above();
BlockState aboveState = level.getBlockState(abovePos);
return Shapes.joinIsNotEmpty(GRILLING_AREA, aboveState.getShape(level, abovePos), BooleanOp.AND);
}

@Override
protected void createBlockStateDefinition(final StateDefinition.Builder<Block, BlockState> builder) {
super.createBlockStateDefinition(builder);
builder.add(FACING, LIT);
}

@Nullable
protected static <T extends BlockEntity> BlockEntityTicker<T> createStoveTicker(Level level, BlockEntityType<T> serverType, BlockEntityType<? extends AbstractStoveBlockEntity> clientType) {
if (level.isClientSide) return null;
return createTickerHelper(serverType, clientType, AbstractStoveBlockEntity::serverTick);
}

@Nullable
@Override
public BlockPathTypes getBlockPathType(BlockState state, BlockGetter level, BlockPos pos, @Nullable Mob entity) {
return state.getValue(LIT) ? BlockPathTypes.DAMAGE_FIRE : null;
}

@Override
public BlockState rotate(BlockState state, Rotation rotation) {
return state.setValue(FACING, rotation.rotate(state.getValue(FACING)));
}

@Override
public BlockState mirror(BlockState state, Mirror mirror) {
return state.rotate(mirror.getRotation(state.getValue(FACING)));
}
}
Loading