From d7336526e14173f6069b1c530f15ab778b551727 Mon Sep 17 00:00:00 2001 From: Andreas Troelsen Date: Sat, 7 Aug 2021 22:43:08 +0200 Subject: [PATCH] Add InventoryThing collection. Adds three new Thing types that can be used to reference items in chests (or any block-based InventoryHolder): - InventoryIndexThing looks up an item by index/slot in an inventory. - InventoryGroupThing groups all non-null/non-air items in an inventory into a ThingGroup. - InventoryRangeThing groups all non-null/non-air items in a given range of an inventory into a ThingGroup. The new Thing types aim to bridge a gap between the class chests and the rest of the Thing-based parts of the config-file. The goal is two-fold: allow for more in-game configuration so access to the config-file isn't _quite_ as crucial, and propagate the item-wise feature completeness of class chests to other parts of the plugin. While class chests are low configuration and a bit "all or nothing", the inventory Thing types require manually punching in the coords for chests and possibly indices/ranges for items. This means that the initial setup could be a bit unwieldy, and highly volatile wave setups are definitely not a good fit. If the wave setup is mostly pre-defined, it is fairly easy to tweak upgrade waves and rewards in the same way class chests are tweaked. As for item-wise feature completeness, the inventory Thing types share the same "if Bukkit can copy it, it will work" rule of thumb as class chests do, which means items with metadata such as custom names, lore, or even NBTs, should just work. This could remove the need to employ other plugins. By no means can this solution be considered "optimal", but it it _does_ enable some long-requested features. Closes #456 --- changelog.md | 3 + .../MobArena/things/InventoryGroupThing.java | 42 ++++++ .../MobArena/things/InventoryIndexThing.java | 40 ++++++ .../MobArena/things/InventoryRangeThing.java | 56 ++++++++ .../MobArena/things/InventoryThing.java | 68 ++++++++++ .../MobArena/things/InventoryThingParser.java | 80 +++++++++++ .../MobArena/things/ThingManager.java | 1 + .../things/InventoryThingParserTest.java | 124 ++++++++++++++++++ 8 files changed, 414 insertions(+) create mode 100644 src/main/java/com/garbagemule/MobArena/things/InventoryGroupThing.java create mode 100644 src/main/java/com/garbagemule/MobArena/things/InventoryIndexThing.java create mode 100644 src/main/java/com/garbagemule/MobArena/things/InventoryRangeThing.java create mode 100644 src/main/java/com/garbagemule/MobArena/things/InventoryThing.java create mode 100644 src/main/java/com/garbagemule/MobArena/things/InventoryThingParser.java create mode 100644 src/test/java/com/garbagemule/MobArena/things/InventoryThingParserTest.java diff --git a/changelog.md b/changelog.md index fa79747..c8c89e6 100644 --- a/changelog.md +++ b/changelog.md @@ -11,6 +11,9 @@ These changes will (most likely) be included in the next version. ## [Unreleased] +### Added +- Support for chest references in item syntax. The new `inv` syntax allows for referencing container indices in the config-file. This should help bridge the gap between class chests and various other parts of the config-file, such as rewards and upgrade waves. + ### Fixed - Explosion damage caused by Exploding Sheep now correctly counts as monster damage. This means that the explosions only affect other mobs if the per-arena setting `monster-infight` is set to `true`. - Explosion damage caused by the boss ability `obsidian-bomb` now correctly counts as monster damage. This means that the explosions only affect other mobs if the per-arena setting `monster-infight` is set to `true`. diff --git a/src/main/java/com/garbagemule/MobArena/things/InventoryGroupThing.java b/src/main/java/com/garbagemule/MobArena/things/InventoryGroupThing.java new file mode 100644 index 0000000..6e2c5a7 --- /dev/null +++ b/src/main/java/com/garbagemule/MobArena/things/InventoryGroupThing.java @@ -0,0 +1,42 @@ +package com.garbagemule.MobArena.things; + +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.ItemStack; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Supplier; + +class InventoryGroupThing extends InventoryThing { + + InventoryGroupThing(Supplier location) { + super(location); + } + + @Override + Thing load() { + Inventory inventory = super.getInventory(); + if (inventory == null) { + return null; + } + + List things = new ArrayList<>(); + for (ItemStack stack : inventory) { + if (stack != null && stack.getType() != Material.AIR) { + things.add(new ItemStackThing(stack)); + } + } + + if (things.isEmpty()) { + return null; + } + if (things.size() == 1) { + return things.get(0); + } + + return new ThingGroup(things); + } + +} diff --git a/src/main/java/com/garbagemule/MobArena/things/InventoryIndexThing.java b/src/main/java/com/garbagemule/MobArena/things/InventoryIndexThing.java new file mode 100644 index 0000000..37cff6c --- /dev/null +++ b/src/main/java/com/garbagemule/MobArena/things/InventoryIndexThing.java @@ -0,0 +1,40 @@ +package com.garbagemule.MobArena.things; + +import org.bukkit.Location; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.ItemStack; + +import java.util.function.Supplier; + +class InventoryIndexThing extends InventoryThing { + + private final int index; + + InventoryIndexThing( + Supplier location, + int index + ) { + super(location); + this.index = index; + } + + @Override + Thing load() { + Inventory inventory = super.getInventory(); + if (inventory == null) { + return null; + } + if (inventory.getSize() <= index) { + return null; + } + + ItemStack stack = inventory.getItem(index); + if (stack == null) { + return null; + } + + ItemStack clone = stack.clone(); + return new ItemStackThing(clone); + } + +} diff --git a/src/main/java/com/garbagemule/MobArena/things/InventoryRangeThing.java b/src/main/java/com/garbagemule/MobArena/things/InventoryRangeThing.java new file mode 100644 index 0000000..fa95d15 --- /dev/null +++ b/src/main/java/com/garbagemule/MobArena/things/InventoryRangeThing.java @@ -0,0 +1,56 @@ +package com.garbagemule.MobArena.things; + +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.ItemStack; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Supplier; + +class InventoryRangeThing extends InventoryThing { + + private final int first; + private final int last; + + InventoryRangeThing( + Supplier location, + int first, + int last + ) { + super(location); + this.first = first; + this.last = last; + } + + @Override + Thing load() { + Inventory inventory = super.getInventory(); + if (inventory == null) { + return null; + } + if (inventory.getSize() <= last) { + return null; + } + + List things = new ArrayList<>(); + ItemStack[] content = inventory.getContents(); + for (int i = first; i <= last; i++) { + ItemStack stack = content[i]; + if (stack != null && stack.getType() != Material.AIR) { + things.add(new ItemStackThing(stack)); + } + } + + if (things.isEmpty()) { + return null; + } + if (things.size() == 1) { + return things.get(0); + } + + return new ThingGroup(things); + } + +} diff --git a/src/main/java/com/garbagemule/MobArena/things/InventoryThing.java b/src/main/java/com/garbagemule/MobArena/things/InventoryThing.java new file mode 100644 index 0000000..d032194 --- /dev/null +++ b/src/main/java/com/garbagemule/MobArena/things/InventoryThing.java @@ -0,0 +1,68 @@ +package com.garbagemule.MobArena.things; + +import org.bukkit.Location; +import org.bukkit.block.Block; +import org.bukkit.block.BlockState; +import org.bukkit.entity.Player; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.InventoryHolder; + +import java.util.function.Supplier; + +abstract class InventoryThing implements Thing { + + private final Supplier location; + + protected InventoryThing(Supplier location) { + this.location = location; + } + + @Override + public boolean giveTo(Player player) { + Thing thing = load(); + if (thing == null) { + return false; + } + return thing.giveTo(player); + } + + @Override + public boolean takeFrom(Player player) { + Thing thing = load(); + if (thing == null) { + return false; + } + return thing.takeFrom(player); + } + + @Override + public boolean heldBy(Player player) { + Thing thing = load(); + if (thing == null) { + return false; + } + return thing.heldBy(player); + } + + abstract Thing load(); + + Inventory getInventory() { + Location location = this.location.get(); + if (location != null) { + Block block = location.getBlock(); + BlockState state = block.getState(); + if (state instanceof InventoryHolder) { + InventoryHolder holder = (InventoryHolder) state; + return holder.getInventory(); + } + } + return null; + } + + @Override + public String toString() { + Thing thing = load(); + return (thing != null) ? thing.toString() : "nothing"; + } + +} diff --git a/src/main/java/com/garbagemule/MobArena/things/InventoryThingParser.java b/src/main/java/com/garbagemule/MobArena/things/InventoryThingParser.java new file mode 100644 index 0000000..14e361b --- /dev/null +++ b/src/main/java/com/garbagemule/MobArena/things/InventoryThingParser.java @@ -0,0 +1,80 @@ +package com.garbagemule.MobArena.things; + +import org.bukkit.Location; +import org.bukkit.Server; +import org.bukkit.World; + +import java.util.function.Supplier; + +public class InventoryThingParser implements ThingParser { + + private static final String PREFIX = "inv("; + private static final String SUFFIX = ")"; + + private final Server server; + + InventoryThingParser(Server server) { + this.server = server; + } + + @Override + public InventoryThing parse(String s) { + if (!s.startsWith(PREFIX) || !s.endsWith(SUFFIX)) { + return null; + } + + // Trim prefix and suffix + int start = PREFIX.length(); + int end = s.length() - SUFFIX.length(); + String inner = s.substring(start, end); + + // Split by whitespace to get all the parts + String[] parts = inner.split("\\s+"); + if (parts.length != 5) { + throw new IllegalArgumentException("Expected format " + PREFIX + "world x y z slot" + SUFFIX + ", got: " + s); + } + + // Extract location + String name = parts[0]; + int x = Integer.parseInt(parts[1]); + int y = Integer.parseInt(parts[2]); + int z = Integer.parseInt(parts[3]); + Supplier location = () -> { + World world = server.getWorld(name); + return new Location(world, x, y ,z); + }; + + // Determine type by slot value + String slot = parts[4]; + if (slot.equals("all")) { + return group(location); + } + if (slot.contains("-")) { + return range(location, slot); + } + return index(location, slot); + } + + private InventoryThing group(Supplier location) { + return new InventoryGroupThing(location); + } + + private InventoryThing range(Supplier location, String slot) { + String[] indices = slot.split("-"); + if (indices.length != 2) { + throw new IllegalArgumentException("Expected range format (e.g. 0-8), got: " + slot); + } + int first = Integer.parseInt(indices[0]); + int last = Integer.parseInt(indices[1]); + if (last < first) { + throw new IllegalArgumentException("Range end is less than range start: " + slot); + } + return new InventoryRangeThing(location, first, last); + } + + private InventoryThing index(Supplier location, String slot) { + int index = Integer.parseInt(slot); + return new InventoryIndexThing(location, index); + } + +} diff --git a/src/main/java/com/garbagemule/MobArena/things/ThingManager.java b/src/main/java/com/garbagemule/MobArena/things/ThingManager.java index 994fdee..3843c68 100644 --- a/src/main/java/com/garbagemule/MobArena/things/ThingManager.java +++ b/src/main/java/com/garbagemule/MobArena/things/ThingManager.java @@ -16,6 +16,7 @@ public class ThingManager implements ThingParser { parsers.add(new MoneyThingParser(plugin)); parsers.add(new PermissionThingParser(plugin)); parsers.add(new PotionEffectThingParser()); + parsers.add(new InventoryThingParser(plugin.getServer())); items = parser; } diff --git a/src/test/java/com/garbagemule/MobArena/things/InventoryThingParserTest.java b/src/test/java/com/garbagemule/MobArena/things/InventoryThingParserTest.java new file mode 100644 index 0000000..b5b42f5 --- /dev/null +++ b/src/test/java/com/garbagemule/MobArena/things/InventoryThingParserTest.java @@ -0,0 +1,124 @@ +package com.garbagemule.MobArena.things; + +import org.junit.Before; +import org.junit.Test; + +import static org.hamcrest.CoreMatchers.*; +import static org.hamcrest.MatcherAssert.*; +import static org.junit.Assert.assertThrows; + +public class InventoryThingParserTest { + + private InventoryThingParser subject; + + @Before + public void setup() { + subject = new InventoryThingParser(null); + } + + @Test + public void returnsNullOnWrongType() { + String input = "dirt"; + + Thing result = subject.parse(input); + + assertThat(result, nullValue()); + } + + @Test + public void returnsNullOnMismatchedParenthesis() { + String input = "inv(world -10 20 130 15"; + + Thing result = subject.parse(input); + + assertThat(result, nullValue()); + } + + @Test + public void throwsOnTooFewArguments() { + String input = "inv(world -10 20 130)"; + + assertThrows( + IllegalArgumentException.class, + () -> subject.parse(input) + ); + } + + @Test + public void throwsOnTooManyArguments() { + String input = "inv(world -10 20 130 15 16)"; + + assertThrows( + IllegalArgumentException.class, + () -> subject.parse(input) + ); + } + + @Test + public void throwsOnUnknownSlotType() { + String input = "inv(world -10 20 130 potato)"; + + assertThrows( + IllegalArgumentException.class, + () -> subject.parse(input) + ); + } + + @Test + public void parsesIndexThing() { + String input = "inv(world -10 20 130 15)"; + + Thing result = subject.parse(input); + + assertThat(result, instanceOf(InventoryIndexThing.class)); + } + + @Test + public void throwsIfIndexIsNegative() { + String input = "inv(world -10 20 130 -1)"; + + assertThrows( + IllegalArgumentException.class, + () -> subject.parse(input) + ); + } + + @Test + public void parsesRangeThing() { + String input = "inv(world -10 20 130 0-8)"; + + Thing result = subject.parse(input); + + assertThat(result, instanceOf(InventoryRangeThing.class)); + } + + @Test + public void throwsIfRangeStartIsLessThanZero() { + String input = "inv(world -10 20 130 -1-8)"; + + assertThrows( + IllegalArgumentException.class, + () -> subject.parse(input) + ); + } + + @Test + public void throwsIfRangeEndIsLessThanRangeStart() { + String input = "inv(world -10 20 130 5-2)"; + + assertThrows( + IllegalArgumentException.class, + () -> subject.parse(input) + ); + } + + @Test + public void parsesAllGroup() { + String input = "inv(world -10 20 130 all)"; + + Thing result = subject.parse(input); + + assertThat(result, instanceOf(InventoryGroupThing.class)); + } + +}