Skip to content
Merged
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 @@ -4,16 +4,47 @@
import com.dre.brewery.Brew;
import com.dre.brewery.api.events.IngedientAddEvent;
import com.dre.brewery.api.events.brew.BrewModifyEvent;
import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.NamespacedKey;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.inventory.InventoryAction;
import org.bukkit.event.inventory.InventoryClickEvent;
import org.bukkit.event.inventory.InventoryType;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import org.bukkit.persistence.PersistentDataContainer;
import org.bukkit.persistence.PersistentDataType;
import org.bukkit.plugin.java.JavaPlugin;
import net.tfminecraft.activitytf.managers.ActivityManager;

import java.util.EnumSet;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ThreadLocalRandom;

public class BreweryListener implements Listener {

static final NamespacedKey DISTILLED = new NamespacedKey("activity", "brew_distilled");

// Clicks that take the clicked item out of its slot.
static final Set<InventoryAction> TAKES = EnumSet.of(
InventoryAction.PICKUP_ALL, InventoryAction.PICKUP_SOME, InventoryAction.PICKUP_HALF,
InventoryAction.PICKUP_ONE, InventoryAction.MOVE_TO_OTHER_INVENTORY, InventoryAction.HOTBAR_SWAP,
InventoryAction.SWAP_WITH_CURSOR, InventoryAction.DROP_ALL_SLOT, InventoryAction.DROP_ONE_SLOT,
InventoryAction.PICKUP_ALL_INTO_BUNDLE, InventoryAction.PICKUP_SOME_INTO_BUNDLE);

private final ActivityManager manager;

// Nonce of every tag written by a take that has not been confirmed yet, and who made it.
private final Map<Long, UUID> pending = new HashMap<>();

public BreweryListener(ActivityManager manager) {
this.manager = manager;
}
Expand Down Expand Up @@ -47,4 +78,81 @@ private static int cookedMinutes(Brew brew) {
BIngredients ingredients = brew == null ? null : brew.getIngredients();
return ingredients == null ? 0 : ingredients.getCookedTime();
}

// BreweryX distils on a timer with no player attached, so the credit goes to whoever
// takes a distilled brew out of a brewing stand's bottle slots. The tag written onto
// the brew keeps it from counting again when it is put back and taken out.
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void onContainerTake(InventoryClickEvent event) {
if (!(event.getWhoClicked() instanceof Player player) || !TAKES.contains(event.getAction())) {
return;
}
Inventory clicked = event.getClickedInventory();
ItemStack item = event.getCurrentItem();
if (clicked == null || clicked != event.getView().getTopInventory()
|| item == null || item.getType() != Material.POTION) {
return;
}
if (clicked.getType() == InventoryType.BREWING && event.getSlot() < 3) {
Brew brew = Brew.get(item);
if (brew != null && brew.getDistillRuns() > 0) {
creditOnce(player, clicked, event.getSlot(), item, DISTILLED, "brew_distill");
}
}
}

private void creditOnce(Player player, Inventory inventory, int slot, ItemStack item,
NamespacedKey key, String activityId) {
ItemMeta meta = item.getItemMeta();
if (meta == null) {
return;
}
PersistentDataContainer data = meta.getPersistentDataContainer();
Long previous = data.has(key, PersistentDataType.LONG) ? data.get(key, PersistentDataType.LONG) : null;
if (previous != null && pending.remove(previous) != null) {
// Someone else's take of this brew is still unconfirmed: this click replaces it.
data.remove(key);
}
long nonce = ThreadLocalRandom.current().nextLong();
if (!markOnce(data, key, nonce)) {
return;
}
// The clicked item mirrors the slot, so the tag travels with the brew.
item.setItemMeta(meta);
pending.put(nonce, player.getUniqueId());
// The click fires before the move happens, and a quick-move into a full inventory
// leaves the brew where it was. Credit only once it has left the slot; otherwise
// take the tag back off so a later take can still count.
Bukkit.getScheduler().runTask(JavaPlugin.getProvidingPlugin(BreweryListener.class), () -> {
UUID taker = pending.remove(nonce);
if (taker == null) {
return; // replaced by a later take
}
ItemStack left = inventory.getItem(slot);
ItemMeta leftMeta = left == null ? null : left.getItemMeta();
if (leftMeta != null && unmarkIfStill(leftMeta.getPersistentDataContainer(), key, nonce)) {
Comment thread
coderabbitai[bot] marked this conversation as resolved.
left.setItemMeta(leftMeta);
inventory.setItem(slot, left);
return;
}
manager.recordAction(taker, activityId, 1);
});
}

static boolean markOnce(PersistentDataContainer data, NamespacedKey key, long nonce) {
if (data.has(key)) {
return false;
}
data.set(key, PersistentDataType.LONG, nonce);
return true;
}

// True, and the tag removed, when this is still the brew marked with this nonce.
static boolean unmarkIfStill(PersistentDataContainer data, NamespacedKey key, long nonce) {
if (!data.has(key, PersistentDataType.LONG) || data.get(key, PersistentDataType.LONG) != nonce) {
return false;
}
data.remove(key);
return true;
}
}
15 changes: 15 additions & 0 deletions src/main/resources/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,21 @@ activities:
every: 3
points: 1
daily-cap: 2
# BreweryX: distilled brews taken out of a brewing stand, each brew once. Where
# TFMCCore binds brewing stands to a station on right-click, only a shift-right-click
# with an empty hand opens the vanilla stand BreweryX distils in.
# Disabled for now: uncomment to offer distilling as an activity (the listener is in place).
#brew_distill:
# display: "#e6ca40&lDistill Brews"
# description:
# - "&7Take &a%every% &7distilled brews out of a brewing stand"
# - "&7(shift-right-click it with an empty hand)."
# - "&7Earn &a%points% &7activity points per completion."
# - "&7"
# material: BREWING_STAND
# every: 3
# points: 1
# daily-cap: 1
# RPCharacters: profession upgrades purchased
profession_upgrade:
display: "#e6ca40&lProfession Upgrade"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,23 @@
import net.tfminecraft.activitytf.models.ActivityDef;
import org.bukkit.Material;
import org.bukkit.entity.Player;
import org.bukkit.event.inventory.InventoryAction;
import org.bukkit.event.player.PlayerEvent;
import org.bukkit.persistence.PersistentDataContainer;
import org.junit.jupiter.api.Test;
import org.objenesis.ObjenesisStd;

import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;

import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;

class BreweryListenerTest {

Expand Down Expand Up @@ -107,4 +113,46 @@ void onlyCookedBottlesAreCredited() {
listener.recordBottle(stubPlayer(uuid), 12);
assertEquals(2, manager.tasks(uuid).count("brew_bottle"));
}

private static PersistentDataContainer stubData() {
Map<Object, Object> values = new HashMap<>();
InvocationHandler handler = (proxy, method, args) -> switch (method.getName()) {
case "has" -> values.containsKey(args[0]);
case "get" -> values.get(args[0]);
case "set" -> values.put(args[0], args[2]);
case "remove" -> values.remove(args[0]);
default -> throw new UnsupportedOperationException(
"unexpected call to PersistentDataContainer#" + method.getName());
};
return (PersistentDataContainer) Proxy.newProxyInstance(
BreweryListenerTest.class.getClassLoader(), new Class<?>[]{PersistentDataContainer.class}, handler);
}

@Test
void aBrewIsOnlyMarkedTheFirstTime() {
PersistentDataContainer data = stubData();
assertTrue(BreweryListener.markOnce(data, BreweryListener.DISTILLED, 7L));
assertFalse(BreweryListener.markOnce(data, BreweryListener.DISTILLED, 8L));
}

@Test
void aBrewThatNeverLeftItsSlotLosesOnlyItsOwnTag() {
PersistentDataContainer data = stubData();
BreweryListener.markOnce(data, BreweryListener.DISTILLED, 7L);
assertFalse(BreweryListener.unmarkIfStill(data, BreweryListener.DISTILLED, 8L));
assertTrue(BreweryListener.unmarkIfStill(data, BreweryListener.DISTILLED, 7L));
assertTrue(BreweryListener.markOnce(data, BreweryListener.DISTILLED, 9L));
}

@Test
void onlyClicksThatTakeTheItemCount() {
assertTrue(BreweryListener.TAKES.contains(InventoryAction.PICKUP_ALL));
assertTrue(BreweryListener.TAKES.contains(InventoryAction.MOVE_TO_OTHER_INVENTORY));
assertTrue(BreweryListener.TAKES.contains(InventoryAction.HOTBAR_SWAP));
assertTrue(BreweryListener.TAKES.contains(InventoryAction.PICKUP_ALL_INTO_BUNDLE));
assertTrue(BreweryListener.TAKES.contains(InventoryAction.PICKUP_SOME_INTO_BUNDLE));
assertFalse(BreweryListener.TAKES.contains(InventoryAction.PLACE_ALL));
assertFalse(BreweryListener.TAKES.contains(InventoryAction.NOTHING));
assertFalse(BreweryListener.TAKES.contains(InventoryAction.CLONE_STACK));
}
}