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
This commit is contained in:
Andreas Troelsen 2021-08-07 22:43:08 +02:00
parent 1a7109a1d4
commit d7336526e1
8 changed files with 414 additions and 0 deletions

View File

@ -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`.

View File

@ -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> location) {
super(location);
}
@Override
Thing load() {
Inventory inventory = super.getInventory();
if (inventory == null) {
return null;
}
List<Thing> 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);
}
}

View File

@ -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> 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);
}
}

View File

@ -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> 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<Thing> 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);
}
}

View File

@ -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> location;
protected InventoryThing(Supplier<Location> 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";
}
}

View File

@ -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> 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> location) {
return new InventoryGroupThing(location);
}
private InventoryThing range(Supplier<Location> 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> location, String slot) {
int index = Integer.parseInt(slot);
return new InventoryIndexThing(location, index);
}
}

View File

@ -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;
}

View File

@ -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));
}
}