Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import dev.jsinco.brewery.bukkit.event.*;
import dev.jsinco.brewery.bukkit.ingredient.BukkitIngredientManager;
import dev.jsinco.brewery.bukkit.integration.IntegrationManagerImpl;
import dev.jsinco.brewery.bukkit.task.CauldronHoverTask;
import dev.jsinco.brewery.bukkit.migration.Migrations;
import dev.jsinco.brewery.bukkit.recipe.BukkitRecipeResultReader;
import dev.jsinco.brewery.bukkit.recipe.DefaultRecipeReader;
Expand Down Expand Up @@ -111,6 +112,7 @@ public class TheBrewingProject extends JavaPlugin implements TheBrewingProjectAp
private PlayerWalkListener playerWalkListener;
@Getter
private ModifierManager modifierManager = new ModifierManagerImpl();
private CauldronHoverTask cauldronHoverTask;
private BreweryTranslator translator;
private boolean successfullLoad = false;

Expand Down Expand Up @@ -314,6 +316,11 @@ public void onEnable() {
} else {
pluginManager.registerEvents(new LegacyPlayerJoinListener(), this);
}

// Start cauldron hover task for action bar display
this.cauldronHoverTask = new CauldronHoverTask(this.breweryRegistry);
this.cauldronHoverTask.start();

Bukkit.getGlobalRegionScheduler().runAtFixedRate(this, this::updateStructures, 1, 1);
Bukkit.getGlobalRegionScheduler().runAtFixedRate(this, this::otherTicking, 1, 1);
RecipeReader<ItemStack> recipeReader = new RecipeReader<>(this.getDataFolder(), new BukkitRecipeResultReader(), BukkitIngredientManager.INSTANCE);
Expand All @@ -329,6 +336,10 @@ public void onEnable() {

@Override
public void onDisable() {
// Cancel cauldron hover task
if (cauldronHoverTask != null) {
cauldronHoverTask.cancel();
}
closeDatabase();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,38 @@ public class BukkitCauldron implements Cauldron {
private boolean brewExtracted = false;
private Color particleColor = Color.AQUA;
private @Nullable Recipe<ItemStack> recipe;
// Track ingredient additions for removal feature
private final LinkedList<IngredientAddition> ingredientHistory = new LinkedList<>();

// Helper class to track ingredient additions
private static class IngredientAddition {
final Ingredient ingredient;
final long timestamp;
final ItemStack originalItem;

IngredientAddition(Ingredient ingredient, long timestamp, ItemStack originalItem) {
this.ingredient = ingredient;
this.timestamp = timestamp;
this.originalItem = originalItem.clone();
}
}
Comment on lines +60 to +70

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Could make this a record :^)


/**
* Checks if the cauldron has any ingredients in the current brewing step.
* @return true if there are ingredients, false if empty
*/
public boolean hasIngredients() {
if (brew.getCompletedSteps().isEmpty()) {
return false;
}
BrewingStep lastStep = brew.lastStep();
if (lastStep instanceof BrewingStep.Cook cook) {
return !cook.ingredients().isEmpty();
} else if (lastStep instanceof BrewingStep.Mix mix) {
return !mix.ingredients().isEmpty();
}
return false;
}


public BukkitCauldron(BreweryLocation location, boolean hot) {
Expand Down Expand Up @@ -83,6 +115,13 @@ public void tick() {
ListenerUtil.removeActiveSinglePositionStructure(this, TheBrewingProject.getInstance().getBreweryRegistry(), TheBrewingProject.getInstance().getDatabase());
return;
}

// Check if cauldron still has ingredients, if not remove it
if (!hasIngredients()) {
ListenerUtil.removeActiveSinglePositionStructure(this, TheBrewingProject.getInstance().getBreweryRegistry(), TheBrewingProject.getInstance().getDatabase());
return;
}

this.hot = isHeatSource(getBlock().getRelative(BlockFace.DOWN));
recalculateBrewTime();
if (getBrewTime() % Config.config().cauldrons().cookingMinuteTicks() == 0) {
Expand Down Expand Up @@ -137,6 +176,138 @@ public void remove() {
ListenerUtil.removeActiveSinglePositionStructure(this, TheBrewingProject.getInstance().getBreweryRegistry(), TheBrewingProject.getInstance().getDatabase());
}

/**
* Attempts to remove the last added ingredient from the cauldron.
* Can only remove ingredients within the configured time window.
* If all ingredients are removed, the cauldron reverts to a normal water cauldron.
*
* @param player The player attempting to remove the ingredient
* @return The removed ingredient item, or null if removal failed
*/
public @Nullable ItemStack removeLastIngredient(Player player) {
if (!player.hasPermission("brewery.cauldron.access")) {
MessageUtil.message(player, "tbp.cauldron.access-denied");
return null;
}

if (ingredientHistory.isEmpty()) {
return null;
}

// Check if brew has been extracted
if (brewExtracted) {
return null;
}
Comment on lines +193 to +200

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
if (ingredientHistory.isEmpty()) {
return null;
}
// Check if brew has been extracted
if (brewExtracted) {
return null;
}
if (ingredientHistory.isEmpty() || brewExtracted) {
return null;
}


long currentTime = TheBrewingProject.getInstance().getTime();
long timeWindow = Config.config().cauldrons().ingredientRemovalTimeWindow();
IngredientAddition lastAddition = ingredientHistory.getLast();

// Check if the ingredient is still within the removal time window
if (currentTime - lastAddition.timestamp > timeWindow) {
return null;
}

// Remove from history
ingredientHistory.removeLast();

// Remove from brew
Ingredient ingredientToRemove = lastAddition.ingredient;
boolean removed = false;
boolean isEmpty = false;

if (brew.lastStep() instanceof BrewingStep.Cook cook) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Try to unwrap these if possible. Use return statements, or maybe make a separate method for the cook and mix case. I think you already know this, but if you don't here's an example:

if(statment1) {
    if(statement2) {
        // Code 1
    } else {
        // Code 2
    }
} else {
    // Code 3
}

Becomes this:

if(!statement1) {
    // Code 3
    return;
}
if(!statement2) {
    // Code 2
    return;
}
// Code 1

Up to you, though. I have also written similar code in this project, for what I could guess at least. Makes life somewhat easier for me to review it though.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Also the mix and cook part here are extremely similar to each other, it should be possible to remove some code duplication.

Map<Ingredient, Integer> updatedIngredients = new HashMap<>(cook.ingredients());
Integer count = updatedIngredients.get(ingredientToRemove);
if (count != null && count > 0) {
if (count == 1) {
updatedIngredients.remove(ingredientToRemove);
} else {
updatedIngredients.put(ingredientToRemove, count - 1);
}

// Check if this was the last ingredient in the cauldron
if (updatedIngredients.isEmpty()) {
isEmpty = true;
// Revert to normal water cauldron - remove from active structures
ListenerUtil.removeActiveSinglePositionStructure(this,
TheBrewingProject.getInstance().getBreweryRegistry(),
TheBrewingProject.getInstance().getDatabase());
} else {
// Update brew with remaining ingredients
BrewingStep.Cook newCook = new CookStepImpl(cook.time(), updatedIngredients, cook.cauldronType());
List<BrewingStep> newSteps = new ArrayList<>(brew.getCompletedSteps());
if (!newSteps.isEmpty()) {
newSteps.set(newSteps.size() - 1, newCook);
brew = new BrewImpl(newSteps);
} else {
brew = new BrewImpl(List.of(newCook));
}
}
removed = true;
}
} else if (brew.lastStep() instanceof BrewingStep.Mix mix) {
Map<Ingredient, Integer> updatedIngredients = new HashMap<>(mix.ingredients());
Integer count = updatedIngredients.get(ingredientToRemove);
if (count != null && count > 0) {
if (count == 1) {
updatedIngredients.remove(ingredientToRemove);
} else {
updatedIngredients.put(ingredientToRemove, count - 1);
}

// Check if this was the last ingredient in the cauldron
if (updatedIngredients.isEmpty()) {
isEmpty = true;
// Revert to normal water cauldron - remove from active structures
ListenerUtil.removeActiveSinglePositionStructure(this,
TheBrewingProject.getInstance().getBreweryRegistry(),
TheBrewingProject.getInstance().getDatabase());
} else {
// Update brew with remaining ingredients
BrewingStep.Mix newMix = new MixStepImpl(mix.time(), updatedIngredients);
List<BrewingStep> newSteps = new ArrayList<>(brew.getCompletedSteps());
if (!newSteps.isEmpty()) {
newSteps.set(newSteps.size() - 1, newMix);
brew = new BrewImpl(newSteps);
} else {
brew = new BrewImpl(List.of(newMix));
}
}
removed = true;
}
}

if (removed) {
// Recalculate recipe only if there are still ingredients
if (!isEmpty) {
this.recipe = brew.closestRecipe(TheBrewingProject.getInstance().getRecipeRegistry())
.orElse(null);
} else {
this.recipe = null;
}

// Play a sound effect
final boolean finalIsEmpty = isEmpty;
BukkitAdapter.toLocation(this.location)
.ifPresent(loc -> {
World world = loc.getWorld();
if (finalIsEmpty) {
// Play water splash sound when reverting to normal cauldron
world.playSound(loc, org.bukkit.Sound.ENTITY_PLAYER_SPLASH, 0.7f, 1.0f);
world.spawnParticle(Particle.SPLASH, loc.add(0.5, 0.7, 0.5), 20, 0.2, 0.1, 0.2, 0.5);
} else {
// Play item pickup sound for normal removal
world.playSound(loc, org.bukkit.Sound.ENTITY_ITEM_PICKUP, 0.5f, 1.2f);
}
});

return lastAddition.originalItem;
}
Comment on lines +281 to +306

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Probably make this a method instead, I don't see why not


return null;
}

public boolean addIngredient(@NotNull ItemStack item, Player player) {
// TODO: Add API event
if (!player.hasPermission("brewery.cauldron.access")) {
Expand All @@ -149,6 +320,15 @@ public boolean addIngredient(@NotNull ItemStack item, Player player) {
this.hot = isHeatSource(getBlock().getRelative(BlockFace.DOWN));
long time = TheBrewingProject.getInstance().getTime();
Ingredient ingredient = BukkitIngredientManager.INSTANCE.getIngredient(item);

// Track this ingredient addition for potential removal
ingredientHistory.add(new IngredientAddition(ingredient, time, item));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The item added here has to have 1 in amount, I got a dupe otherwise by adding items when holding an item stack with more than 1 items in it.


// Keep only the configured amount of history
int maxHistory = Config.config().cauldrons().maxRemovableIngredients();
while (ingredientHistory.size() > maxHistory) {
ingredientHistory.removeFirst();
}
if (hot) {
brew = brew.withLastStep(BrewingStep.Cook.class,
cook -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,66 @@ public PlayerEventListener(PlacedStructureRegistryImpl placedStructureRegistry,
}


// Handle shift-click ingredient removal with higher priority
@EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true) //test and validate if that is correct
public void onPlayerShiftClickCauldron(PlayerInteractEvent event) {
// Check if feature is enabled
if (!Config.config().cauldrons().enableIngredientRemoval()) {
return;
}

if (event.getAction() != Action.RIGHT_CLICK_BLOCK || !event.getPlayer().isSneaking() || event.getHand() != EquipmentSlot.HAND) {
return;
}

Block block = event.getClickedBlock();
if (block == null || !Tag.CAULDRONS.isTagged(block.getType())) {
return;
}

ItemStack itemStack = event.getItem();
if (itemStack != null && itemStack.getType() != Material.AIR) {
return; // Only handle empty hand
}

Optional<BukkitCauldron> cauldronOptional = breweryRegistry.getActiveSinglePositionStructure(BukkitAdapter.toBreweryLocation(block))
.filter(BukkitCauldron.class::isInstance)
.map(BukkitCauldron.class::cast);

if (cauldronOptional.isPresent()) {
BukkitCauldron cauldron = cauldronOptional.get();
boolean hadIngredients = cauldron.hasIngredients();
ItemStack removedItem = cauldron.removeLastIngredient(event.getPlayer());

if (removedItem != null) {
event.getPlayer().getWorld().dropItem(event.getPlayer().getLocation(), removedItem);

// Check if cauldron was emptied (cauldron will be removed from registry if empty)
boolean isEmpty = !hadIngredients || !cauldron.hasIngredients();

if (isEmpty) {
event.getPlayer().sendActionBar(Component.text("Cauldron emptied - returned to water", net.kyori.adventure.text.format.NamedTextColor.AQUA));
} else {
event.getPlayer().sendActionBar(Component.text("Removed ingredient", net.kyori.adventure.text.format.NamedTextColor.YELLOW));
}

// Only update database if cauldron still has ingredients
if (!isEmpty) {
try {
database.updateValue(BukkitCauldronDataType.INSTANCE, cauldron);
} catch (PersistenceException e) {
Logger.logErr(e);
}
}

event.setUseInteractedBlock(Event.Result.DENY);
event.setUseItemInHand(Event.Result.DENY);
} else {
event.getPlayer().sendActionBar(Component.text("No ingredient to remove or time expired", net.kyori.adventure.text.format.NamedTextColor.RED));
}
}
}

@EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true)
public void onPlayerInteractStructure(PlayerInteractEvent playerInteractEvent) {
if (playerInteractEvent.getAction() != Action.RIGHT_CLICK_BLOCK || playerInteractEvent.getPlayer().isSneaking() || playerInteractEvent.getHand() != EquipmentSlot.HAND) {
Expand Down Expand Up @@ -201,6 +261,7 @@ private void handleCauldron(PlayerInteractEvent event, @NotNull Block block) {
.filter(BukkitCauldron.class::isInstance)
.map(BukkitCauldron.class::cast);
ItemStack itemStack = event.getItem();

if (itemStack == null) {
return;
}
Expand All @@ -225,11 +286,21 @@ private void handleCauldron(PlayerInteractEvent event, @NotNull Block block) {
cauldronOptional
.filter(cauldron -> itemStack.getType() == Material.CLOCK)
.filter(cauldron -> event.getPlayer().hasPermission("brewery.cauldron.time"))
.ifPresent(cauldron -> event.getPlayer().sendMessage(
Component.translatable("tbp.cauldron.clock-message", Argument.tagResolver(
Placeholder.parsed("time", TimeFormatter.format(cauldron.getTime(), TimeFormat.CLOCK_MECHANIC, TimeModifier.COOKING))
))
));
.ifPresent(cauldron -> {
Component timeMessage = Component.translatable("tbp.cauldron.clock-message", Argument.tagResolver(
Placeholder.parsed("time", TimeFormatter.format(cauldron.getTime(), TimeFormat.CLOCK_MECHANIC, TimeModifier.COOKING))
));

// Send to chat if enabled
if (Config.config().cauldrons().clockTimeInChat()) {
event.getPlayer().sendMessage(timeMessage);
}

// Send to action bar if click mode is enabled
if (Config.config().cauldrons().clockTimeActionBar() == dev.jsinco.brewery.configuration.CauldronSection.ClockActionBarMode.CLICK) {
event.getPlayer().sendActionBar(timeMessage);
}
});

cauldronOptional.ifPresent(ignored -> {
event.setUseInteractedBlock(Event.Result.DENY);
Expand Down
Loading