diff --git a/src/main/java/com/garbagemule/MobArena/ArenaClass.java b/src/main/java/com/garbagemule/MobArena/ArenaClass.java index b9b195b..e508d25 100644 --- a/src/main/java/com/garbagemule/MobArena/ArenaClass.java +++ b/src/main/java/com/garbagemule/MobArena/ArenaClass.java @@ -25,8 +25,8 @@ import java.util.stream.IntStream; public class ArenaClass { private String configName, lowercaseName; - private ItemStack helmet, chestplate, leggings, boots, offhand; - private List armor; + private Thing helmet, chestplate, leggings, boots, offhand; + private List armor; private List items; private Map perms; private Map lobbyperms; @@ -71,42 +71,42 @@ public class ArenaClass /** * Set the helmet slot for the class. - * @param helmet an item + * @param helmet a Thing */ - public void setHelmet(ItemStack helmet) { + public void setHelmet(Thing helmet) { this.helmet = helmet; } /** * Set the chestplate slot for the class. - * @param chestplate an item + * @param chestplate a Thing */ - public void setChestplate(ItemStack chestplate) { + public void setChestplate(Thing chestplate) { this.chestplate = chestplate; } /** * Set the leggings slot for the class. - * @param leggings an item + * @param leggings a Thing */ - public void setLeggings(ItemStack leggings) { + public void setLeggings(Thing leggings) { this.leggings = leggings; } /** * Set the boots slot for the class. - * @param boots an item + * @param boots a Thing */ - public void setBoots(ItemStack boots) { + public void setBoots(Thing boots) { this.boots = boots; } /** * Set the off-hand slot for the class. - * @param offHand + * @param offhand a Thing */ - public void setOffHand(ItemStack offHand) { - this.offhand = offHand; + public void setOffHand(Thing offhand) { + this.offhand = offhand; } /** @@ -131,9 +131,9 @@ public class ArenaClass /** * Replace the current armor list with the given list. - * @param armor a list of items + * @param armor a list of Things */ - public void setArmor(List armor) { + public void setArmor(List armor) { this.armor = armor; } @@ -152,36 +152,14 @@ public class ArenaClass items.forEach(item -> item.giveTo(p)); // Check for legacy armor-node items - if (!armor.isEmpty()) { - for (ItemStack piece : armor) { - ArmorType type = ArmorType.getType(piece); - if (type == null) continue; - - switch (type) { - case HELMET: - inv.setHelmet(piece); - break; - case CHESTPLATE: - inv.setChestplate(piece); - break; - case LEGGINGS: - inv.setLeggings(piece); - break; - case BOOTS: - inv.setBoots(piece); - break; - default: - break; - } - } - } - + armor.forEach(thing -> thing.giveTo(p)); + // Check type specifics. - if (helmet != null) inv.setHelmet(helmet); - if (chestplate != null) inv.setChestplate(chestplate); - if (leggings != null) inv.setLeggings(leggings); - if (boots != null) inv.setBoots(boots); - if (offhand != null) inv.setItemInOffHand(offhand); + if (helmet != null) helmet.giveTo(p); + if (chestplate != null) chestplate.giveTo(p); + if (leggings != null) leggings.giveTo(p); + if (boots != null) boots.giveTo(p); + if (offhand != null) offhand.giveTo(p); } /** diff --git a/src/main/java/com/garbagemule/MobArena/ArenaMasterImpl.java b/src/main/java/com/garbagemule/MobArena/ArenaMasterImpl.java index 23c7f91..739f34f 100644 --- a/src/main/java/com/garbagemule/MobArena/ArenaMasterImpl.java +++ b/src/main/java/com/garbagemule/MobArena/ArenaMasterImpl.java @@ -32,6 +32,7 @@ import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; +import java.util.function.Consumer; import java.util.stream.Collectors; public class ArenaMasterImpl implements ArenaMaster @@ -330,33 +331,8 @@ public class ArenaMasterImpl implements ArenaMaster // Load items loadClassItems(section, arenaClass); - // And the legacy armor-node - String armor = section.getString("armor", ""); - if (!armor.equals("")) { - List stacks = ItemParser.parseItems(armor); - arenaClass.setArmor(stacks); - } - - // Get armor strings - String head = section.getString("helmet", null); - String chest = section.getString("chestplate", null); - String legs = section.getString("leggings", null); - String feet = section.getString("boots", null); - String off = section.getString("offhand", null); - - // Parse to ItemStacks - ItemStack helmet = ItemParser.parseItem(head); - ItemStack chestplate = ItemParser.parseItem(chest); - ItemStack leggings = ItemParser.parseItem(legs); - ItemStack boots = ItemParser.parseItem(feet); - ItemStack offhand = ItemParser.parseItem(off); - - // Set in ArenaClass - arenaClass.setHelmet(helmet); - arenaClass.setChestplate(chestplate); - arenaClass.setLeggings(leggings); - arenaClass.setBoots(boots); - arenaClass.setOffHand(offhand); + // Load armor + loadClassArmor(section, arenaClass); // Per-class permissions loadClassPermissions(arenaClass, section); @@ -390,6 +366,49 @@ public class ArenaMasterImpl implements ArenaMaster arenaClass.setItems(things); } + private void loadClassArmor(ConfigurationSection section, ArenaClass arenaClass) { + // Legacy armor node + loadClassArmorLegacyNode(section, arenaClass); + + // Specific armor pieces + loadClassArmorPiece(section, "helmet", arenaClass::setHelmet); + loadClassArmorPiece(section, "chestplate", arenaClass::setChestplate); + loadClassArmorPiece(section, "leggings", arenaClass::setLeggings); + loadClassArmorPiece(section, "boots", arenaClass::setBoots); + loadClassArmorPiece(section, "offhand", arenaClass::setOffHand); + } + + private void loadClassArmorLegacyNode(ConfigurationSection section, ArenaClass arenaClass) { + List armor = section.getStringList("armor"); + if (armor == null || armor.isEmpty()) { + String value = section.getString("armor", ""); + armor = Arrays.asList(value.split(",")); + } + + // Prepend "armor:" for the armor thing parser + List things = armor.stream() + .map(String::trim) + .map(s -> "armor:" + s) + .map(plugin.getThingManager()::parse) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + + arenaClass.setArmor(things); + } + + private void loadClassArmorPiece(ConfigurationSection section, String slot, Consumer setter) { + String value = section.getString(slot, null); + if (value == null) { + return; + } + // Prepend the slot name for the item parser + Thing thing = plugin.getThingManager().parse(slot + ":" + value); + if (thing == null) { + return; + } + setter.accept(thing); + } + private void loadClassPermissions(ArenaClass arenaClass, ConfigurationSection section) { List perms = section.getStringList("permissions"); if (perms.isEmpty()) return; diff --git a/src/main/java/com/garbagemule/MobArena/things/BootsThing.java b/src/main/java/com/garbagemule/MobArena/things/BootsThing.java new file mode 100644 index 0000000..5c555a7 --- /dev/null +++ b/src/main/java/com/garbagemule/MobArena/things/BootsThing.java @@ -0,0 +1,27 @@ +package com.garbagemule.MobArena.things; + +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; + +public class BootsThing extends ItemStackThing { + public BootsThing(ItemStack stack) { + super(stack); + } + + @Override + public boolean giveTo(Player player) { + player.getInventory().setBoots(super.getItemStack()); + return true; + } + + @Override + public boolean takeFrom(Player player) { + player.getInventory().setBoots(null); + return true; + } + + @Override + public boolean heldBy(Player player) { + return super.getItemStack().equals(player.getInventory().getBoots()); + } +} diff --git a/src/main/java/com/garbagemule/MobArena/things/ChestplateThing.java b/src/main/java/com/garbagemule/MobArena/things/ChestplateThing.java new file mode 100644 index 0000000..ea2ef0e --- /dev/null +++ b/src/main/java/com/garbagemule/MobArena/things/ChestplateThing.java @@ -0,0 +1,27 @@ +package com.garbagemule.MobArena.things; + +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; + +public class ChestplateThing extends ItemStackThing { + public ChestplateThing(ItemStack stack) { + super(stack); + } + + @Override + public boolean giveTo(Player player) { + player.getInventory().setChestplate(super.getItemStack()); + return true; + } + + @Override + public boolean takeFrom(Player player) { + player.getInventory().setChestplate(null); + return true; + } + + @Override + public boolean heldBy(Player player) { + return super.getItemStack().equals(player.getInventory().getChestplate()); + } +} diff --git a/src/main/java/com/garbagemule/MobArena/things/Equippable.java b/src/main/java/com/garbagemule/MobArena/things/Equippable.java new file mode 100644 index 0000000..9293298 --- /dev/null +++ b/src/main/java/com/garbagemule/MobArena/things/Equippable.java @@ -0,0 +1,78 @@ +package com.garbagemule.MobArena.things; + +import org.bukkit.Material; +import org.bukkit.inventory.ItemStack; + +import java.util.EnumSet; + +class Equippable { + @FunctionalInterface + interface Wrapper { + ItemStackThing wrap(ItemStack stack); + } + + private static EnumSet helmets = EnumSet.of( + Material.LEATHER_HELMET, + Material.IRON_HELMET, + Material.CHAINMAIL_HELMET, + Material.GOLD_HELMET, + Material.DIAMOND_HELMET + ); + private static EnumSet chestplates = EnumSet.of( + Material.LEATHER_CHESTPLATE, + Material.IRON_CHESTPLATE, + Material.CHAINMAIL_CHESTPLATE, + Material.GOLD_CHESTPLATE, + Material.DIAMOND_CHESTPLATE + ); + private static EnumSet leggings = EnumSet.of( + Material.LEATHER_LEGGINGS, + Material.IRON_LEGGINGS, + Material.CHAINMAIL_LEGGINGS, + Material.GOLD_LEGGINGS, + Material.DIAMOND_LEGGINGS + ); + private static EnumSet boots = EnumSet.of( + Material.LEATHER_BOOTS, + Material.IRON_BOOTS, + Material.CHAINMAIL_BOOTS, + Material.GOLD_BOOTS, + Material.DIAMOND_BOOTS + ); + + static Wrapper getWrapperByPrefix(String prefix) { + if (prefix.equals("helmet")) { + return HelmetThing::new; + } + if (prefix.equals("chestplate")) { + return ChestplateThing::new; + } + if (prefix.equals("leggings")) { + return LeggingsThing::new; + } + if (prefix.equals("boots")) { + return BootsThing::new; + } + if (prefix.equals("offhand")) { + return OffHandThing::new; + } + return null; + } + + static Wrapper guessWrapperFromItemStack(ItemStack stack) { + Material type = stack.getType(); + if (helmets.contains(type)) { + return HelmetThing::new; + } + if (chestplates.contains(type)) { + return ChestplateThing::new; + } + if (leggings.contains(type)) { + return LeggingsThing::new; + } + if (boots.contains(type)) { + return BootsThing::new; + } + return null; + } +} diff --git a/src/main/java/com/garbagemule/MobArena/things/HelmetThing.java b/src/main/java/com/garbagemule/MobArena/things/HelmetThing.java new file mode 100644 index 0000000..2a8628a --- /dev/null +++ b/src/main/java/com/garbagemule/MobArena/things/HelmetThing.java @@ -0,0 +1,27 @@ +package com.garbagemule.MobArena.things; + +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; + +public class HelmetThing extends ItemStackThing { + public HelmetThing(ItemStack stack) { + super(stack); + } + + @Override + public boolean giveTo(Player player) { + player.getInventory().setHelmet(super.getItemStack()); + return true; + } + + @Override + public boolean takeFrom(Player player) { + player.getInventory().setHelmet(null); + return true; + } + + @Override + public boolean heldBy(Player player) { + return super.getItemStack().equals(player.getInventory().getHelmet()); + } +} diff --git a/src/main/java/com/garbagemule/MobArena/things/ItemStackParser.java b/src/main/java/com/garbagemule/MobArena/things/ItemStackParser.java new file mode 100644 index 0000000..3bcf22a --- /dev/null +++ b/src/main/java/com/garbagemule/MobArena/things/ItemStackParser.java @@ -0,0 +1,19 @@ +package com.garbagemule.MobArena.things; + +import org.bukkit.inventory.ItemStack; + +/** + * An ItemStack parser takes a string as input and returns either an instance + * of {@link ItemStack} or null. + */ +@FunctionalInterface +public interface ItemStackParser { + /** + * Parse the given string, returning an {@link ItemStack} instance on + * success, otherwise null. + * + * @param s a string to parse + * @return an instance of {@link ItemStack}, or null + */ + ItemStack parse(String s); +} diff --git a/src/main/java/com/garbagemule/MobArena/things/ItemStackThing.java b/src/main/java/com/garbagemule/MobArena/things/ItemStackThing.java index 28c37c0..2880816 100644 --- a/src/main/java/com/garbagemule/MobArena/things/ItemStackThing.java +++ b/src/main/java/com/garbagemule/MobArena/things/ItemStackThing.java @@ -26,6 +26,10 @@ public class ItemStackThing implements Thing { return player.getInventory().containsAtLeast(stack, stack.getAmount()); } + ItemStack getItemStack() { + return stack; + } + @Override public String toString() { String name = getName(); diff --git a/src/main/java/com/garbagemule/MobArena/things/ItemStackThingParser.java b/src/main/java/com/garbagemule/MobArena/things/ItemStackThingParser.java index 2a9fbd1..f63a85e 100644 --- a/src/main/java/com/garbagemule/MobArena/things/ItemStackThingParser.java +++ b/src/main/java/com/garbagemule/MobArena/things/ItemStackThingParser.java @@ -1,15 +1,78 @@ package com.garbagemule.MobArena.things; +import com.garbagemule.MobArena.things.Equippable.Wrapper; import com.garbagemule.MobArena.util.ItemParser; import org.bukkit.inventory.ItemStack; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + class ItemStackThingParser implements ThingParser { + private List parsers; + + ItemStackThingParser() { + this.parsers = new ArrayList<>(); + } + + public void register(ItemStackParser parser) { + parsers.add(parser); + } + @Override public ItemStackThing parse(String s) { - ItemStack stack = ItemParser.parseItem(s, false); + String[] parts = s.split(":", 2); + if (parts.length == 1) { + return genericItem(s); + } + + String prefix = parts[0]; + String rest = parts[1]; + + if (prefix.equals("armor")) { + return genericArmor(rest); + } + + return specificSlot(s, prefix, rest); + } + + private ItemStackThing genericItem(String s) { + ItemStack stack = parseItemStack(s); if (stack == null) { return null; } return new ItemStackThing(stack); } + + private ItemStackThing genericArmor(String s) { + ItemStack stack = parseItemStack(s); + if (stack == null) { + return null; + } + Wrapper wrapper = Equippable.guessWrapperFromItemStack(stack); + if (wrapper == null) { + return new ItemStackThing(stack); + } + return wrapper.wrap(stack); + } + + private ItemStackThing specificSlot(String s, String prefix, String rest) { + Wrapper wrapper = Equippable.getWrapperByPrefix(prefix); + if (wrapper == null) { + return genericItem(s); + } + ItemStack stack = parseItemStack(rest); + if (stack == null) { + return null; + } + return wrapper.wrap(stack); + } + + private ItemStack parseItemStack(String s) { + return parsers.stream() + .map(p -> p.parse(s)) + .filter(Objects::nonNull) + .findFirst() + .orElseGet(() -> ItemParser.parseItem(s, false)); + } } diff --git a/src/main/java/com/garbagemule/MobArena/things/LeggingsThing.java b/src/main/java/com/garbagemule/MobArena/things/LeggingsThing.java new file mode 100644 index 0000000..50e5b8b --- /dev/null +++ b/src/main/java/com/garbagemule/MobArena/things/LeggingsThing.java @@ -0,0 +1,27 @@ +package com.garbagemule.MobArena.things; + +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; + +public class LeggingsThing extends ItemStackThing { + public LeggingsThing(ItemStack stack) { + super(stack); + } + + @Override + public boolean giveTo(Player player) { + player.getInventory().setLeggings(super.getItemStack()); + return true; + } + + @Override + public boolean takeFrom(Player player) { + player.getInventory().setLeggings(null); + return true; + } + + @Override + public boolean heldBy(Player player) { + return super.getItemStack().equals(player.getInventory().getLeggings()); + } +} diff --git a/src/main/java/com/garbagemule/MobArena/things/OffHandThing.java b/src/main/java/com/garbagemule/MobArena/things/OffHandThing.java new file mode 100644 index 0000000..9a4f2b8 --- /dev/null +++ b/src/main/java/com/garbagemule/MobArena/things/OffHandThing.java @@ -0,0 +1,27 @@ +package com.garbagemule.MobArena.things; + +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; + +public class OffHandThing extends ItemStackThing { + public OffHandThing(ItemStack stack) { + super(stack); + } + + @Override + public boolean giveTo(Player player) { + player.getInventory().setItemInOffHand(super.getItemStack()); + return true; + } + + @Override + public boolean takeFrom(Player player) { + player.getInventory().setItemInOffHand(null); + return true; + } + + @Override + public boolean heldBy(Player player) { + return super.getItemStack().equals(player.getInventory().getItemInOffHand()); + } +} diff --git a/src/main/java/com/garbagemule/MobArena/things/ThingManager.java b/src/main/java/com/garbagemule/MobArena/things/ThingManager.java index 730721f..4dc72df 100644 --- a/src/main/java/com/garbagemule/MobArena/things/ThingManager.java +++ b/src/main/java/com/garbagemule/MobArena/things/ThingManager.java @@ -7,12 +7,17 @@ import java.util.List; public class ThingManager implements ThingParser { private List parsers; + private ItemStackThingParser items; - public ThingManager(MobArena plugin) { + public ThingManager(MobArena plugin, ItemStackThingParser parser) { parsers = new ArrayList<>(); parsers.add(new CommandThingParser()); parsers.add(new MoneyThingParser(plugin)); - parsers.add(new ItemStackThingParser()); + items = parser; + } + + public ThingManager(MobArena plugin) { + this(plugin, new ItemStackThingParser()); } /** @@ -43,6 +48,18 @@ public class ThingManager implements ThingParser { register(parser, false); } + /** + * Register a new ItemStack parser in the manager. + *

+ * The provided parser will be invoked if no other thing parsers return a + * non-null result, and if all other ItemStack parsers also return null. + * + * @param parser an ItemStack parser, non-null + */ + public void register(ItemStackParser parser) { + items.register(parser); + } + @Override public Thing parse(String s) { for (ThingParser parser : parsers) { @@ -51,6 +68,6 @@ public class ThingManager implements ThingParser { return thing; } } - return null; + return items.parse(s); } } diff --git a/src/test/java/com/garbagemule/MobArena/things/ItemStackThingParserTest.java b/src/test/java/com/garbagemule/MobArena/things/ItemStackThingParserTest.java new file mode 100644 index 0000000..4daefba --- /dev/null +++ b/src/test/java/com/garbagemule/MobArena/things/ItemStackThingParserTest.java @@ -0,0 +1,119 @@ +package com.garbagemule.MobArena.things; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.nullValue; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; +import static org.mockito.Mockito.when; + +import org.bukkit.Material; +import org.bukkit.inventory.ItemStack; +import org.junit.Before; +import org.junit.Test; +import org.mockito.InOrder; + +public class ItemStackThingParserTest { + + private ItemStackThingParser subject; + + @Before + public void setup() { + subject = new ItemStackThingParser(); + } + + @Test + public void emptyStringReturnsNull() { + ItemStackThing result = subject.parse(""); + + assertThat(result, is(nullValue())); + } + + @Test + public void gibberishReturnsNull() { + ItemStackThing result = subject.parse("I can't believe you've done this"); + + assertThat(result, is(nullValue())); + } + + @Test + public void fiveDirt() { + ItemStackThing result = subject.parse("dirt:5"); + + // ItemStack's equals() method does naughty things, so verify manually + ItemStack stack = result.getItemStack(); + assertThat(stack.getType(), equalTo(Material.DIRT)); + assertThat(stack.getAmount(), equalTo(5)); + } + + @Test + public void bareLederhosenReturnsGenericItemStackThing() { + ItemStackThing result = subject.parse("leather_leggings"); + + assertThat(result.getClass(), equalTo(ItemStackThing.class)); + } + + @Test + public void prefixedIronChestplateReturnsLeggingsThing() { + ItemStackThing result = subject.parse("leggings:leather_leggings"); + + assertThat(result.getClass(), equalTo(LeggingsThing.class)); + } + + @Test + public void genericArmorPrefixGuessesChestplateThing() { + ItemStackThing result = subject.parse("armor:iron_chestplate"); + + assertThat(result.getClass(), equalTo(ChestplateThing.class)); + } + + @Test + public void returnsNonNullCustomParserResult() { + String input = "something:soft"; + ItemStack stack = new ItemStack(Material.SPONGE, 3); + ItemStackParser parser = mock(ItemStackParser.class); + when(parser.parse(input)).thenReturn(stack); + subject.register(parser); + + ItemStackThing result = subject.parse(input); + + assertThat(result.getItemStack(), equalTo(stack)); + } + + @Test + public void customParsersInvokedInOrder() { + String input = "unicorns and rainbows"; + ItemStackParser first = mock(ItemStackParser.class); + ItemStackParser second = mock(ItemStackParser.class); + subject.register(first); + subject.register(second); + + subject.parse(input); + + InOrder order = inOrder(first, second); + order.verify(first).parse(input); + order.verify(second).parse(input); + } + + @Test + public void customParsersInvokedUntilNonNullResult() { + String input = "unicorns and rainbows"; + ItemStackParser first = mock(ItemStackParser.class); + ItemStackParser second = mock(ItemStackParser.class); + ItemStackParser third = mock(ItemStackParser.class); + ItemStack stack = new ItemStack(Material.GLOWSTONE, 9); + when(second.parse(input)).thenReturn(stack); + subject.register(first); + subject.register(second); + subject.register(third); + + subject.parse(input); + + verify(first).parse(input); + verifyZeroInteractions(third); + } + +}