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
6 changes: 6 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,12 @@
<version>5.14.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>5.20.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>net.tfminecraft</groupId>
<artifactId>magic</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -334,7 +334,13 @@ ItemStack createStatus(Node n) {
i.setType(Material.RED_STAINED_GLASS_PANE);
ItemMeta m = i.getItemMeta();
m.setDisplayName("§cINACTIVE");
if(n.getErrors().size() > 0) {
if(n.hasPendingRefund()) {
m.setDisplayName("§eRetry Refund");
List<String> lore = new ArrayList<>(n.getErrors());
lore.add("§7Restore the barrel and hopper, then click to retry.");
lore.add("§7Refunds must finish before changing or activating this node.");
m.setLore(lore);
} else if(n.getErrors().size() > 0) {
m.setLore(n.getErrors());
}
i.setItemMeta(m);
Expand Down
18 changes: 18 additions & 0 deletions src/main/java/net/tfminecraft/dowsing/managers/NodeManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,7 @@ public void validate() {
}
}
public void confirmClick(Player p, Node n, ConfirmType t) {
if(t != ConfirmType.DEACTIVATE && blockPendingRefund(p, n)) return;
if(t.equals(ConfirmType.DEACTIVATE)) {
n.deActivate();
InventoryManager inv = new InventoryManager();
Expand All @@ -254,6 +255,11 @@ public void confirmClick(Player p, Node n, ConfirmType t) {
currentNode.put(p, n);
}
}
private boolean blockPendingRefund(Player p, Node n) {
if(!n.hasPendingRefund()) return false;
p.sendMessage("§cRestore the barrel and hopper, then click Retry Refund before changing this node.");
return true;
}
@EventHandler(ignoreCancelled = true)
public void placeVanillaNode(BlockPlaceEvent e) {
NodeBlock b = BlockLoader.getByBlock(e.getBlock().getType());
Expand Down Expand Up @@ -387,6 +393,7 @@ public void invenClick(InventoryClickEvent e) {
InventoryManager inv = new InventoryManager();
if(e.getView().getTitle().equalsIgnoreCase("§7"+n.getBlock().getResource()+ " Node")) {
e.setCancelled(true);
if(e.getClickedInventory() != e.getView().getTopInventory()) return;
if(!n.hasGuild()) {
n.breakNode();
nodes.remove(n);
Expand All @@ -399,6 +406,7 @@ public void invenClick(InventoryClickEvent e) {
p.playSound(p.getLocation(), Sound.ENTITY_VILLAGER_NO, 1f, 1f);
return;
}
if(e.getSlot() != 17 && blockPendingRefund(p, n)) return;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
if(e.getSlot() == 8) {
if(n.getIsActive()) {
p.sendMessage("§cCannot upgrade while node is active");
Expand Down Expand Up @@ -436,6 +444,11 @@ public void invenClick(InventoryClickEvent e) {
if(n.getIsActive()) {
confirm.put(p, ConfirmType.DEACTIVATE);
inv.confirmView(p);
} else if(n.hasPendingRefund()) {
n.deActivate();
p.sendMessage(n.hasPendingRefund()
? "§cRefund still pending. Restore the barrel and hopper, then retry."
: "§aRefund complete. The node remains inactive.");
} else {
n.activate();
if(n.getIsActive()) {
Expand Down Expand Up @@ -482,6 +495,8 @@ public void invenClick(InventoryClickEvent e) {
}
} else if(currentSlot.get(p) != null && e.getView().getTitle().equalsIgnoreCase("§7"+n.getBlock().getResource()+" Node: "+WordUtils.capitalize(currentSlot.get(p).getId().replace("_", " ")))) {
e.setCancelled(true);
if(e.getClickedInventory() != e.getView().getTopInventory()) return;
if(e.getSlot() != 26 && blockPendingRefund(p, n)) return;
if(!n.hasGuild()) {
n.breakNode();
nodes.remove(n);
Expand Down Expand Up @@ -517,6 +532,8 @@ public void invenClick(InventoryClickEvent e) {
currentNode.put(p, n);
} else if(e.getView().getTitle().equalsIgnoreCase("§7"+n.getBlock().getResource()+" Node: Type")) {
e.setCancelled(true);
if(e.getClickedInventory() != e.getView().getTopInventory()) return;
if(e.getSlot() != 26 && blockPendingRefund(p, n)) return;
if(!n.hasGuild()) {
n.breakNode();
nodes.remove(n);
Expand Down Expand Up @@ -550,6 +567,7 @@ public void invenClick(InventoryClickEvent e) {
inv.confirmView(p);
} else if(e.getView().getTitle().equalsIgnoreCase("§7Confirm Action")) {
e.setCancelled(true);
if(e.getClickedInventory() != e.getView().getTopInventory()) return;
if(!n.hasGuild()) {
n.breakNode();
nodes.remove(n);
Expand Down
14 changes: 13 additions & 1 deletion src/main/java/net/tfminecraft/dowsing/objects/Node.java
Original file line number Diff line number Diff line change
Expand Up @@ -404,6 +404,10 @@ public void refund() {
ng.refund(this);;
}
public void activate() {
if(hasPendingRefund()) {
deActivate();
return;
}
if(isClaimable()) return;
NodeEngine ng = new NodeEngine();
Boolean failed = false;
Expand Down Expand Up @@ -456,12 +460,20 @@ public void activate() {
ng.takeInputs(this);
NodeManager.requestNodeBenefitSync();
}
/** Whether an inactive node still owes refunds from an interrupted cycle. */
public boolean hasPendingRefund() {
return !this.isActive && this.inputCounter > 0
&& this.cycleTime >= 0 && this.cycleTime < Cache.cycleLength;
}
public void deActivate() {
this.isActive = false;
if(isClaimable()) return;
if(this.cycleTime > 0 && this.cycleTime < Cache.cycleLength) {
if(this.cycleTime >= 0 && this.cycleTime < Cache.cycleLength) {
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;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
}
NodeManager.requestNodeBenefitSync();
Expand Down
155 changes: 155 additions & 0 deletions src/test/java/net/tfminecraft/dowsing/managers/NodeRefundMenuTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
package net.tfminecraft.dowsing.managers;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.contains;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertThrows;

import org.bukkit.entity.Player;
import org.bukkit.event.inventory.InventoryClickEvent;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.InventoryView;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.EnumSource;
import org.junit.jupiter.params.provider.ValueSource;

import net.tfminecraft.dowsing.enums.ConfirmType;
import net.tfminecraft.dowsing.objects.Node;
import net.tfminecraft.dowsing.objects.NodeBlock;
import net.tfminecraft.dowsing.objects.NodeSlot;

class NodeRefundMenuTest {
private NodeManager manager;
private Node node;
private Player player;
private InventoryClickEvent click;
private InventoryView view;

@BeforeEach
void setUp() {
manager = new NodeManager();
node = mock(Node.class);
player = mock(Player.class);
click = mock(InventoryClickEvent.class);
view = mock(InventoryView.class);
NodeBlock block = mock(NodeBlock.class);
when(node.getBlock()).thenReturn(block);
when(block.getResource()).thenReturn("Iron");
when(node.hasGuild()).thenReturn(true);
when(node.hasPendingRefund()).thenReturn(true);
when(node.getIsActive()).thenReturn(false);
when(player.hasPermission("dowsing.admin")).thenReturn(true);
when(click.getWhoClicked()).thenReturn(player);
when(click.getView()).thenReturn(view);
when(view.getTitle()).thenReturn("§7Iron Node");
Inventory top = mock(Inventory.class);
when(click.getClickedInventory()).thenReturn(top);
when(view.getTopInventory()).thenReturn(top);
manager.currentNode.put(player, node);
}

@ParameterizedTest
@ValueSource(strings = {"§7Iron Node", "§7Iron Node: Type", "§7Iron Node: Fuel", "§7Confirm Action"})
void lowerInventoryClicksCannotTriggerNodeActions(String title) {
NodeSlot slot = mock(NodeSlot.class);
when(slot.getId()).thenReturn("fuel");
manager.currentSlot.put(player, slot);
when(view.getTitle()).thenReturn(title);
when(click.getSlot()).thenReturn(17);
when(click.getClickedInventory()).thenReturn(mock(Inventory.class));

manager.invenClick(click);

verify(click).setCancelled(true);
verify(node, never()).deActivate();
verify(node, never()).activate();
verify(node, never()).update();
verify(node, never()).breakNode();
verify(player, never()).sendMessage(anyString());
}

@ParameterizedTest
@ValueSource(ints = {6, 8, 9, 18, 24, 26, 10})
void pendingRefundBlocksMainMenuChanges(int slot) {
when(click.getSlot()).thenReturn(slot);

manager.invenClick(click);

verify(click).setCancelled(true);
verify(player).sendMessage(contains("Retry Refund"));
verify(node, never()).setGuild(any());
verify(node, never()).breakNode();
verify(node, never()).activate();
}

@ParameterizedTest
@EnumSource(value = ConfirmType.class, names = {"DELETE_NODE", "CHANGE_TYPE"})
void pendingRefundBlocksStaleConfirmation(ConfirmType action) {
manager.confirmClick(player, node, action);

verify(player).sendMessage(contains("Retry Refund"));
verify(node, never()).breakNode();
verify(node, never()).setCurrentType(any());
}

@ParameterizedTest
@ValueSource(strings = {"§7Iron Node: Type", "§7Iron Node: Fuel"})
void pendingRefundBlocksAlreadyOpenSubmenus(String title) {
NodeSlot slot = mock(NodeSlot.class);
when(slot.getId()).thenReturn("fuel");
manager.currentSlot.put(player, slot);
when(view.getTitle()).thenReturn(title);

manager.invenClick(click);

verify(click).setCancelled(true);
verify(player).sendMessage(contains("Retry Refund"));
verify(slot, never()).setActivePm(any());
}

@Test
void statusButtonRetriesRefundWithoutActivating() {
when(click.getSlot()).thenReturn(17);

manager.invenClick(click);

verify(node).deActivate();
verify(node, never()).activate();
verify(player).sendMessage(contains("Refund still pending"));
}

@Test
void successfulRetryLeavesNodeInactive() {
when(click.getSlot()).thenReturn(17);
when(node.hasPendingRefund()).thenReturn(true, false);

manager.invenClick(click);

verify(node).deActivate();
verify(node, never()).activate();
verify(player).sendMessage(contains("Refund complete"));
}

@Test
void statusButtonStillActivatesWhenNoRefundIsPending() {
when(click.getSlot()).thenReturn(17);
when(node.hasPendingRefund()).thenReturn(false);
// Stop at the production boundary before Bukkit's server-backed sound registry is needed.
RuntimeException activationReached = new RuntimeException("Activation reached");
doThrow(activationReached).when(node).activate();

assertSame(activationReached, assertThrows(RuntimeException.class, () -> manager.invenClick(click)));

verify(node).activate();
verify(node, never()).deActivate();
verify(player, never()).sendMessage(anyString());
}
}
Loading