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 @@ -429,6 +429,12 @@ public void invenClick(InventoryClickEvent e) {
Player p = (Player) e.getWhoClicked();
if(!currentNode.containsKey(p)) return;
Node n = currentNode.get(p);
if(!nodes.contains(n)) {
// The node was already removed; stale menus must not act on it.
currentNode.remove(p);
p.closeInventory();
return;
}
InventoryManager inv = new InventoryManager();
if(e.getView().getTitle().equalsIgnoreCase("§7"+n.getBlock().getResource()+ " Node")) {
e.setCancelled(true);
Expand Down
36 changes: 36 additions & 0 deletions src/main/java/net/tfminecraft/dowsing/objects/Node.java
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ public class Node {
Integer multiplier;
Double costIncrease;
Map<String, Integer> lastResult = new HashMap<>();
Map<String, Integer> pendingResult;
boolean broken;
List<String> addedDrops = new ArrayList<String>();
List<String> errors = new ArrayList<String>();
Map<String, Double> completeDrop = new HashMap<>();
Expand Down Expand Up @@ -151,6 +153,13 @@ public Map<String, Integer> getLastResult() {
public void setLastResult(Map<String, Integer> lastResult) {
this.lastResult = lastResult;
}
/** Rolled output that could not be delivered yet; null when no cycle output is waiting. */
public Map<String, Integer> getPendingResult() {
return pendingResult;
}
public void setPendingResult(Map<String, Integer> pendingResult) {
this.pendingResult = pendingResult;
}
public NodeBlock getBlock() {
return block;
}
Expand Down Expand Up @@ -409,6 +418,7 @@ public void input() {
ng.takeInputs(this);
}
public void refund() {
if(this.broken) return;
NodeEngine ng = new NodeEngine();
Boolean failed = false;
this.errors.clear();
Expand All @@ -431,6 +441,7 @@ public void refund() {
ng.refund(this);;
}
public void activate() {
if(this.broken) return;
if(hasPendingRefund()) {
deActivate();
return;
Expand Down Expand Up @@ -480,6 +491,13 @@ public void activate() {
if(failed) {
return;
}
if(this.pendingResult != null && !this.pendingResult.isEmpty()) {
// Settle held output before new inputs are taken, so a later refund cannot cancel it.
if(!new ItemDropper().dropItems(this)) {
this.errors.add("§7Held output does not fit in the hopper");
return;
}
}
this.isActive = true;
this.timeLeft = this.modifiedTime;
this.cycleTime = 0;
Expand All @@ -493,14 +511,19 @@ public boolean hasPendingRefund() {
}
public void deActivate() {
this.isActive = false;
// A broken node is terminal: stale menus must not refund or restart it.
if(this.broken) return;
if(isClaimable()) return;
if(this.cycleTime >= 0 && this.cycleTime < Cache.cycleLength) {
boolean refunded = this.getInputCounter() > 0;
while(this.getInputCounter() > 0) {
int pendingInputs = this.getInputCounter();
refund();
// A missing barrel or hopper prevents refunds. Keep the pending inputs for retry.
if(this.getInputCounter() >= pendingInputs) break;
}
// Returning this cycle's inputs cancels its held output; incomplete refunds keep it.
if(refunded && this.getInputCounter() == 0) setPendingResult(null);
}
NodeManager.requestNodeBenefitSync();
}
Expand Down Expand Up @@ -667,6 +690,9 @@ public Integer getNaturalYieldFromChunk(String resource, Location l) throws IOEx
return 0;
}
public void breakNode() {
// Stale menus can repeat removal; the node item and held output must drop only once.
if(this.broken) return;
this.broken = true;
String path = this.getBlock().getBlock();
String type = path.split("\\.")[0];
if(type.equalsIgnoreCase("v")) {
Expand All @@ -680,6 +706,16 @@ public void breakNode() {
Location loc = new Location(this.getLoc().getWorld(), this.getLoc().getX(), this.getLoc().getY(), this.getLoc().getZ());
loc.add(0.5,0,0.5);
ItemDropper dropper = new ItemDropper();
Map<String, Integer> heldOutput = this.pendingResult;
setPendingResult(null);
if(heldOutput != null) {
// Removing the node returns its held output instead of discarding it.
for(Map.Entry<String, Integer> entry : heldOutput.entrySet()) {
for(int count = 0; count < entry.getValue(); count++) {
dropper.dropItem(loc, entry.getKey(), false);
}
}
}
dropper.dropItem(loc, path, false);
loc.getWorld().playSound(loc, Sound.ENTITY_GLOW_ITEM_FRAME_REMOVE_ITEM, 0.5f, 1f);
NodeManager.requestNodeBenefitSync();
Expand Down
7 changes: 7 additions & 0 deletions src/main/java/net/tfminecraft/dowsing/utils/Database.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.UUID;
import java.util.logging.Level;
Expand Down Expand Up @@ -38,6 +39,7 @@

@SuppressWarnings("deprecation") // Stored configuration text uses Bukkit legacy color codes.
public class Database {
static final String PENDING_RESULT_KEY = "pending result";
private JSONObject json = new JSONObject(); // org.json.simple
JSONParser parser = new JSONParser();
private File[] listFiles(File directory) {
Expand Down Expand Up @@ -116,6 +118,10 @@ public void loadNodes() {
if(json.containsKey(LastCycleResult.KEY)) {
n.setLastResult(LastCycleResult.fromJson(json.get(LastCycleResult.KEY)));
}
if(json.containsKey(PENDING_RESULT_KEY)) {
Map<String, Integer> pendingResult = LastCycleResult.fromJson(json.get(PENDING_RESULT_KEY));
if(!pendingResult.isEmpty()) n.setPendingResult(pendingResult);
}
NodeManager.nodes.add(n);
} catch (Exception ex) {
ex.printStackTrace();
Expand Down Expand Up @@ -248,6 +254,7 @@ public boolean saveNode(Node n) {
defaults.put("active pms", pmArray);
defaults.put("efficiency", n.getEfficiency());
defaults.put(LastCycleResult.KEY, LastCycleResult.toJson(n.getLastResult()));
defaults.put(PENDING_RESULT_KEY, LastCycleResult.toJson(n.getPendingResult()));
return save(file, defaults);
} catch (Throwable ex) {
ex.printStackTrace();
Expand Down
102 changes: 74 additions & 28 deletions src/main/java/net/tfminecraft/dowsing/utils/ItemDropper.java
Original file line number Diff line number Diff line change
@@ -1,15 +1,21 @@
package net.tfminecraft.dowsing.utils;

import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import java.util.HashSet;
import java.util.function.Function;
import java.util.function.Consumer;

import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.Sound;
import org.bukkit.block.Block;
import org.bukkit.block.Hopper;
import org.bukkit.entity.Item;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.ItemStack;
import org.bukkit.util.Vector;

Expand All @@ -29,10 +35,17 @@ public ItemDropper() {
this.warning = warning;
}

/** Returns false when a configured reward cannot be created, so the cycle can retry. */
/**
* Delivers output into the hopper above the node when one is present, so blocks
* above the hopper cannot trap and despawn it. Returns false when a configured
* reward cannot be created or the hopper cannot hold the output, so the cycle
* can retry instead of losing items.
*/
public boolean dropItems(Node n) {
Map<String, Integer> pending = n.getPendingResult();
boolean retained = pending != null && !pending.isEmpty();
Map<String, ItemStack> items = new HashMap<>();
for(String key : n.getCompleteDrop().keySet()) {
for(String key : retained ? pending.keySet() : n.getCompleteDrop().keySet()) {
if(key.equalsIgnoreCase("nothing")) continue;
ItemStack item = itemFactory.apply(key);
if(item == null) {
Expand All @@ -46,44 +59,77 @@ public boolean dropItems(Node n) {
items.put(key, item);
}
Map<String, Integer> result = new HashMap<>();
Double maxWeight = 0.0;
for(String key : n.getCompleteDrop().keySet()) {
maxWeight = maxWeight+n.getCompleteDrop().get(key);
}
int safety = 0;
Integer dropped = 0;
while(dropped < n.getYield() && safety < n.getYield() * 10) {
safety++;
Double random = Math.random();
Double previous = 0.0;
if(retained) {
// A held cycle keeps its rolled output; retries must not reroll it.
result = new LinkedHashMap<>(pending);
} else {
Double maxWeight = 0.0;
for(String key : n.getCompleteDrop().keySet()) {
if(maxWeight <= 0) {
maxWeight = 1.0;
}
Double chance = n.getCompleteDrop().get(key) / maxWeight;
Double max = previous+chance;
if(random <= max && random > previous) {
dropped++;
if(!key.equalsIgnoreCase("nothing")) {
result.merge(key, 1, Integer::sum);
maxWeight = maxWeight+n.getCompleteDrop().get(key);
}
int safety = 0;
Integer dropped = 0;
while(dropped < n.getYield() && safety < n.getYield() * 10) {
safety++;
Double random = Math.random();
Double previous = 0.0;
for(String key : n.getCompleteDrop().keySet()) {
if(maxWeight <= 0) {
maxWeight = 1.0;
}
Double chance = n.getCompleteDrop().get(key) / maxWeight;
Double max = previous+chance;
if(random <= max && random > previous) {
dropped++;
if(!key.equalsIgnoreCase("nothing")) {
result.merge(key, 1, Integer::sum);
}
break;
}
break;
previous = max;
}
previous = max;
}
}
n.update();
Location loc = n.getLoc().clone().add(0.5, 2, 0.5);
for(Map.Entry<String, Integer> entry : result.entrySet()) {
for(int count = 0; count < entry.getValue(); count++) {
Item e = loc.getWorld().dropItem(loc, items.get(entry.getKey()).clone());
e.setVelocity(new Vector());
loc.getWorld().playSound(loc, Sound.ENTITY_ITEM_PICKUP, 0.5f, 1f);
Block above = n.getLoc().clone().add(0, 1, 0).getBlock();
if(above != null && above.getType() == Material.HOPPER && above.getState() instanceof Hopper hopper) {
if(!deliverToHopper(n, hopper.getInventory(), items, result, loc)) {
n.setPendingResult(result);
return false;
}
} else {
for(Map.Entry<String, Integer> entry : result.entrySet()) {
for(int count = 0; count < entry.getValue(); count++) {
Item e = loc.getWorld().dropItem(loc, items.get(entry.getKey()).clone());
e.setVelocity(new Vector());
loc.getWorld().playSound(loc, Sound.ENTITY_ITEM_PICKUP, 0.5f, 1f);
}
}
}
n.setPendingResult(null);
n.setLastResult(result);
return true;
}
/** One transaction: a full hopper keeps its contents and the pending output for a later retry. */
private boolean deliverToHopper(Node n, Inventory inventory, Map<String, ItemStack> items, Map<String, Integer> result, Location loc) {
ItemStack[] before = Arrays.stream(inventory.getContents())
.map(item -> item == null ? null : item.clone()).toArray(ItemStack[]::new);
for(Map.Entry<String, Integer> entry : result.entrySet()) {
ItemStack stack = items.get(entry.getKey()).clone();
stack.setAmount(entry.getValue());
if(!inventory.addItem(stack).isEmpty()) {
inventory.setContents(before);
if(unavailableRewardsLogged.add("blocked:" + n.getId())) {
warning.accept("[Dowsing] Cannot complete node " + n.getId() + ": output hopper is full");
}
return false;
}
}
unavailableRewardsLogged.remove("blocked:" + n.getId());
if(!result.isEmpty()) loc.getWorld().playSound(loc, Sound.ENTITY_ITEM_PICKUP, 0.5f, 1f);
return true;
}
public void dropItem(Location loc, String path, Boolean noV) {
ItemCreator ic = new ItemCreator();
ItemStack item = ic.getItemFromPath(path);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -193,13 +193,24 @@ void unrelatedInventoryClicksAreIgnored() {
verify(event, never()).setCancelled(true);
}

@Test
void staleMenusCannotActOnRemovedNodes() {
InventoryClickEvent event = f.click("§7Confirm Action", 11);
f.manager.confirm.put(f.player, ConfirmType.DEACTIVATE);
NodeManager.nodes.remove(f.node);
f.manager.invenClick(event);
verify(f.player).closeInventory();
assertFalse(f.manager.currentNode.containsKey(f.player));
verify(f.node, never()).deActivate(); verify(f.node, never()).breakNode();
}

@ParameterizedTest
@ValueSource(strings = {"§7Iron Node", "§7Iron Node: Fuel", "§7Iron Node: Type", "§7Confirm Action"})
void everyManagedMenuCancelsBottomInventoryClicksAndRemovesOrphanNodes(String title) {
f.manager.currentSlot.put(f.player, f.slot);
InventoryClickEvent event = f.click(title, 0); when(event.getClickedInventory()).thenReturn(mock(Inventory.class));
f.manager.invenClick(event); verify(event).setCancelled(true); verify(f.node, never()).breakNode();
when(f.node.hasGuild()).thenReturn(false); NodeManager.nodes.add(f.node);
when(f.node.hasGuild()).thenReturn(false);
event = f.click(title, 0); f.manager.invenClick(event);
verify(f.node).breakNode(); verify(f.player).closeInventory(); assertTrue(NodeManager.nodes.isEmpty());
}
Expand Down Expand Up @@ -361,7 +372,7 @@ void confirmationMenusSupportCancelAcceptAndAbsentActions() {
f.manager.confirm.put(f.player, ConfirmType.DELETE_NODE);
f.manager.invenClick(f.click("§7Confirm Action", 15)); verify(f.lastInventory()).nodeView(f.player, f.node);
f.manager.invenClick(f.click("§7Confirm Action", 0)); verify(f.node, never()).breakNode();
NodeManager.nodes.add(f.node); f.manager.invenClick(f.click("§7Confirm Action", 11));
f.manager.invenClick(f.click("§7Confirm Action", 11));
verify(f.node).breakNode(); assertTrue(NodeManager.nodes.isEmpty()); assertFalse(f.manager.confirm.containsKey(f.player));
}

Expand Down Expand Up @@ -567,6 +578,7 @@ PlayerInteractEvent interact(Action action) {
}
InventoryClickEvent click(String title, int index) {
manager.currentNode.put(player, node);
if (!NodeManager.nodes.contains(node)) NodeManager.nodes.add(node);
InventoryClickEvent event = mock(InventoryClickEvent.class); InventoryView view = mock(InventoryView.class);
when(event.getWhoClicked()).thenReturn(player); when(event.getView()).thenReturn(view); when(view.getTitle()).thenReturn(title);
when(view.getTopInventory()).thenReturn(inventory); when(event.getClickedInventory()).thenReturn(inventory); when(event.getSlot()).thenReturn(index); return event;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import org.bukkit.event.inventory.InventoryClickEvent;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.InventoryView;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
Expand Down Expand Up @@ -54,6 +55,12 @@ void setUp() {
when(click.getClickedInventory()).thenReturn(top);
when(view.getTopInventory()).thenReturn(top);
manager.currentNode.put(player, node);
NodeManager.nodes.add(node);
}

@AfterEach
void tearDown() {
NodeManager.nodes.remove(node);
}

@ParameterizedTest
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ void persistedStateAndAccessorsRetainTheRequestedValues() {
assertEquals(0.0, node.getTimeModifier()); assertEquals(1, node.getMultiplier());
assertEquals(1.0, node.getCostIncrease()); assertTrue(node.getAddedDrops().isEmpty());
assertTrue(node.getErrors().isEmpty()); assertTrue(node.getLastResult().isEmpty());
assertNull(node.getPendingResult());
UUID replacement = UUID.randomUUID();
Location moved = new Location(f.world, 8, 65, 9);
NodeBlock replacementBlock = mock(NodeBlock.class); NodeType replacementType = mock(NodeType.class);
Expand All @@ -67,6 +68,7 @@ void persistedStateAndAccessorsRetainTheRequestedValues() {
node.setWealthModifier(2.5); node.setPrestigeGain(3.5); node.setTimeModifier(-20.0);
node.setMultiplier(3); node.setCostIncrease(1.5); node.setAddedDrops(new ArrayList<>(List.of("v.STONE(1)")));
node.setErrors(new ArrayList<>(List.of("broken"))); node.setLastResult(new HashMap<>(Map.of("v.STONE", 2)));
node.setPendingResult(new HashMap<>(Map.of("v.DIRT", 5)));
assertEquals(replacement, node.getId()); assertSame(replacementBlock, node.getBlock());
assertSame(moved, node.getLoc()); assertSame(replacementType, node.getCurrentType());
assertSame(f.guild, node.getGuild()); assertFalse(node.getIsActive()); assertEquals(3, node.getLevel());
Expand All @@ -78,6 +80,7 @@ void persistedStateAndAccessorsRetainTheRequestedValues() {
assertEquals(3, node.getMultiplier()); assertEquals(1.5, node.getCostIncrease());
assertEquals(List.of("v.STONE(1)"), node.getAddedDrops());
assertEquals(List.of("broken"), node.getErrors()); assertEquals(Map.of("v.STONE", 2), node.getLastResult());
assertEquals(Map.of("v.DIRT", 5), node.getPendingResult());
}

@Test
Expand Down
Loading