diff --git a/src/main/java/net/minestom/server/command/CommandManager.java b/src/main/java/net/minestom/server/command/CommandManager.java index 84353fa42..cb7c2db71 100644 --- a/src/main/java/net/minestom/server/command/CommandManager.java +++ b/src/main/java/net/minestom/server/command/CommandManager.java @@ -443,37 +443,6 @@ public final class CommandManager { return node; } - private static class IndexedArgument { - private final CommandSyntax syntax; - private final Argument argument; - private final int index; + private record IndexedArgument(CommandSyntax syntax, Argument argument, int index) {} - public IndexedArgument(CommandSyntax syntax, Argument argument, int index) { - this.syntax = syntax; - this.argument = argument; - this.index = index; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - IndexedArgument that = (IndexedArgument) o; - return index == that.index && Objects.equals(syntax, that.syntax) && Objects.equals(argument, that.argument); - } - - @Override - public int hashCode() { - return Objects.hash(syntax, argument, index); - } - - @Override - public String toString() { - return "IndexedArgument{" + - "syntax=" + syntax + - ", argument=" + argument + - ", index=" + index + - '}'; - } - } } diff --git a/src/main/java/net/minestom/server/command/builder/arguments/minecraft/ArgumentEntity.java b/src/main/java/net/minestom/server/command/builder/arguments/minecraft/ArgumentEntity.java index 249707ef7..835940207 100644 --- a/src/main/java/net/minestom/server/command/builder/arguments/minecraft/ArgumentEntity.java +++ b/src/main/java/net/minestom/server/command/builder/arguments/minecraft/ArgumentEntity.java @@ -220,7 +220,7 @@ public class ArgumentEntity extends Argument { final boolean include = !value.startsWith("!"); final String gameModeName = include ? value : value.substring(1); try { - final GameMode gameMode = GameMode.valueOf(gameModeName); + final GameMode gameMode = GameMode.valueOf(gameModeName.toUpperCase()); entityFinder.setGameMode(gameMode, include ? EntityFinder.ToggleableType.INCLUDE : EntityFinder.ToggleableType.EXCLUDE); } catch (IllegalArgumentException e) { throw new ArgumentSyntaxException("Invalid entity game mode", input, INVALID_ARGUMENT_VALUE); @@ -228,12 +228,16 @@ public class ArgumentEntity extends Argument { break; } case "limit": + int limit; try { - final int limit = Integer.parseInt(value); + limit = Integer.parseInt(value); entityFinder.setLimit(limit); } catch (NumberFormatException e) { throw new ArgumentSyntaxException("Invalid limit number", input, INVALID_ARGUMENT_VALUE); } + if (limit <= 0) { + throw new ArgumentSyntaxException("Limit must be positive", input, INVALID_ARGUMENT_VALUE); + } break; case "sort": try { diff --git a/src/main/java/net/minestom/server/command/builder/arguments/minecraft/ArgumentFloatRange.java b/src/main/java/net/minestom/server/command/builder/arguments/minecraft/ArgumentFloatRange.java index c848fdd6a..0ab74d016 100644 --- a/src/main/java/net/minestom/server/command/builder/arguments/minecraft/ArgumentFloatRange.java +++ b/src/main/java/net/minestom/server/command/builder/arguments/minecraft/ArgumentFloatRange.java @@ -10,7 +10,7 @@ import net.minestom.server.utils.math.FloatRange; public class ArgumentFloatRange extends ArgumentRange { public ArgumentFloatRange(String id) { - super(id, "minecraft:float_range", Float.MIN_VALUE, Float.MAX_VALUE, Float::parseFloat, FloatRange::new); + super(id, "minecraft:float_range", -Float.MAX_VALUE, Float.MAX_VALUE, Float::parseFloat, FloatRange::new); } @Override diff --git a/src/main/java/net/minestom/server/command/builder/arguments/relative/ArgumentRelativeVec.java b/src/main/java/net/minestom/server/command/builder/arguments/relative/ArgumentRelativeVec.java index f516e49e5..66400d552 100644 --- a/src/main/java/net/minestom/server/command/builder/arguments/relative/ArgumentRelativeVec.java +++ b/src/main/java/net/minestom/server/command/builder/arguments/relative/ArgumentRelativeVec.java @@ -56,7 +56,7 @@ abstract class ArgumentRelativeVec extends Argument { if (type == null) { type = modifierChar == LOCAL_CHAR ? LOCAL : RELATIVE; - } else if (type != (modifierChar == LOCAL_CHAR ? LOCAL : RELATIVE)) { + } else if ((type == LOCAL) != (modifierChar == LOCAL_CHAR)) { throw new ArgumentSyntaxException("Cannot mix world & local coordinates (everything must either use ^ or not)", input, MIXED_TYPE_ERROR); } diff --git a/src/main/java/net/minestom/server/command/builder/parser/ArgumentParser.java b/src/main/java/net/minestom/server/command/builder/parser/ArgumentParser.java index 39c246df1..8e4255ec0 100644 --- a/src/main/java/net/minestom/server/command/builder/parser/ArgumentParser.java +++ b/src/main/java/net/minestom/server/command/builder/parser/ArgumentParser.java @@ -41,10 +41,10 @@ public class ArgumentParser { ARGUMENT_FUNCTION_MAP.put("time", ArgumentTime::new); ARGUMENT_FUNCTION_MAP.put("enchantment", ArgumentEnchantment::new); ARGUMENT_FUNCTION_MAP.put("particle", ArgumentParticle::new); - ARGUMENT_FUNCTION_MAP.put("resourceLocation", ArgumentResourceLocation::new); + ARGUMENT_FUNCTION_MAP.put("resourcelocation", ArgumentResourceLocation::new); ARGUMENT_FUNCTION_MAP.put("potion", ArgumentPotionEffect::new); - ARGUMENT_FUNCTION_MAP.put("entityType", ArgumentEntityType::new); - ARGUMENT_FUNCTION_MAP.put("blockState", ArgumentBlockState::new); + ARGUMENT_FUNCTION_MAP.put("entitytype", ArgumentEntityType::new); + ARGUMENT_FUNCTION_MAP.put("blockstate", ArgumentBlockState::new); ARGUMENT_FUNCTION_MAP.put("intrange", ArgumentIntRange::new); ARGUMENT_FUNCTION_MAP.put("floatrange", ArgumentFloatRange::new); diff --git a/src/main/java/net/minestom/server/command/builder/suggestion/SuggestionEntry.java b/src/main/java/net/minestom/server/command/builder/suggestion/SuggestionEntry.java index 56225585f..7699dfc9b 100644 --- a/src/main/java/net/minestom/server/command/builder/suggestion/SuggestionEntry.java +++ b/src/main/java/net/minestom/server/command/builder/suggestion/SuggestionEntry.java @@ -4,6 +4,8 @@ import net.kyori.adventure.text.Component; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import java.util.Objects; + public class SuggestionEntry { private final String entry; private final Component tooltip; @@ -24,4 +26,17 @@ public class SuggestionEntry { public @Nullable Component getTooltip() { return tooltip; } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + SuggestionEntry that = (SuggestionEntry) o; + return Objects.equals(entry, that.entry) && Objects.equals(tooltip, that.tooltip); + } + + @Override + public int hashCode() { + return Objects.hash(entry, tooltip); + } } diff --git a/src/main/java/net/minestom/server/utils/location/RelativeVec.java b/src/main/java/net/minestom/server/utils/location/RelativeVec.java index 47b4b4b47..4e159bbc0 100644 --- a/src/main/java/net/minestom/server/utils/location/RelativeVec.java +++ b/src/main/java/net/minestom/server/utils/location/RelativeVec.java @@ -137,4 +137,17 @@ public final class RelativeVec { private interface CoordinateConverter { @NotNull Vec convert(Vec vec, Pos origin, boolean relativeX, boolean relativeY, boolean relativeZ); } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + RelativeVec that = (RelativeVec) o; + return relativeX == that.relativeX && relativeY == that.relativeY && relativeZ == that.relativeZ && Objects.equals(vec, that.vec) && coordinateType == that.coordinateType; + } + + @Override + public int hashCode() { + return Objects.hash(vec, coordinateType, relativeX, relativeY, relativeZ); + } } diff --git a/src/main/java/net/minestom/server/utils/math/Range.java b/src/main/java/net/minestom/server/utils/math/Range.java index b455b5d64..3c27a998d 100644 --- a/src/main/java/net/minestom/server/utils/math/Range.java +++ b/src/main/java/net/minestom/server/utils/math/Range.java @@ -1,5 +1,7 @@ package net.minestom.server.utils.math; +import java.util.Objects; + /** * Represents the base for any data type that is numeric. * @@ -74,4 +76,18 @@ public abstract class Range { * otherwise {@code false}. */ public abstract boolean isInRange(T value); + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Range range = (Range) o; + return Objects.equals(minimum, range.minimum) && Objects.equals(maximum, range.maximum); + } + + @Override + public int hashCode() { + return Objects.hash(minimum, maximum); + } + } diff --git a/src/test/java/net/minestom/server/command/ArgumentParserTest.java b/src/test/java/net/minestom/server/command/ArgumentParserTest.java new file mode 100644 index 000000000..d5da82040 --- /dev/null +++ b/src/test/java/net/minestom/server/command/ArgumentParserTest.java @@ -0,0 +1,58 @@ +package net.minestom.server.command; + +import net.minestom.server.command.builder.arguments.Argument; +import net.minestom.server.command.builder.arguments.ArgumentType; +import net.minestom.server.command.builder.parser.ArgumentParser; +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; + +public class ArgumentParserTest { + + @Test + public void testArgumentParser() { + + // Test each argument + assertParserEquals("Literal", ArgumentType.Literal("example")); + assertParserEquals("Boolean", ArgumentType.Boolean("example")); + assertParserEquals("Integer", ArgumentType.Integer("example")); + assertParserEquals("Double", ArgumentType.Double("example")); + assertParserEquals("Float", ArgumentType.Float("example")); + assertParserEquals("String", ArgumentType.String("example")); + assertParserEquals("Word", ArgumentType.Word("example")); + assertParserEquals("StringArray", ArgumentType.StringArray("example")); + assertParserEquals("Command", ArgumentType.Command("example")); + assertParserEquals("Color", ArgumentType.Color("example")); + assertParserEquals("Time", ArgumentType.Time("example")); + assertParserEquals("Enchantment", ArgumentType.Enchantment("example")); + assertParserEquals("Particle", ArgumentType.Particle("example")); + assertParserEquals("ResourceLocation", ArgumentType.ResourceLocation("example")); + assertParserEquals("Potion", ArgumentType.Potion("example")); + assertParserEquals("EntityType", ArgumentType.EntityType("example")); + assertParserEquals("BlockState", ArgumentType.BlockState("example")); + assertParserEquals("IntRange", ArgumentType.IntRange("example")); + assertParserEquals("FloatRange", ArgumentType.FloatRange("example")); + assertParserEquals("ItemStack", ArgumentType.ItemStack("example")); + assertParserEquals("Component", ArgumentType.Component("example")); + assertParserEquals("UUID", ArgumentType.UUID("example")); + assertParserEquals("NBT", ArgumentType.NBT("example")); + assertParserEquals("NBTCompound", ArgumentType.NbtCompound("example")); + assertParserEquals("RelativeBlockPosition", ArgumentType.RelativeBlockPosition("example")); + assertParserEquals("RelativeVec2", ArgumentType.RelativeVec2("example")); + assertParserEquals("RelativeVec3", ArgumentType.RelativeVec3("example")); + assertParserEquals("Entities", ArgumentType.Entity("example")); + assertParserEquals("Entity", ArgumentType.Entity("example").singleEntity(true)); + assertParserEquals("Players", ArgumentType.Entity("example").onlyPlayers(true)); + assertParserEquals("Player", ArgumentType.Entity("example").onlyPlayers(true).singleEntity(true)); + + // Test multiple argument functionality + assertParserEquals("NBT RelativeVec2", ArgumentType.NBT("arg1"), ArgumentType.RelativeVec2("arg2")); + assertParserEquals("Word UUID NBT", ArgumentType.Word("arg1"), ArgumentType.UUID("arg2"), ArgumentType.NBT("arg3")); + } + + private static void assertParserEquals(@NotNull String input, @NotNull Argument @NotNull ... args) { + assertArrayEquals(ArgumentParser.generate(input), args); + } + +} diff --git a/src/test/java/net/minestom/server/command/ArgumentTest.java b/src/test/java/net/minestom/server/command/ArgumentTest.java new file mode 100644 index 000000000..93cacde1f --- /dev/null +++ b/src/test/java/net/minestom/server/command/ArgumentTest.java @@ -0,0 +1,56 @@ +package net.minestom.server.command; + +import net.minestom.server.command.builder.CommandContext; +import net.minestom.server.command.builder.arguments.Argument; +import net.minestom.server.command.builder.arguments.ArgumentType; +import net.minestom.server.command.builder.suggestion.Suggestion; +import net.minestom.server.command.builder.suggestion.SuggestionEntry; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +public class ArgumentTest { + + @Test + public void testParseSelf() { + assertEquals("example", Argument.parse(ArgumentType.String("example"))); + assertEquals(55, Argument.parse(ArgumentType.Integer("55"))); + } + + @Test + public void testCallback() { + var arg = ArgumentType.String("id"); + + assertFalse(arg.hasErrorCallback()); + arg.setCallback((sender, exception) -> {}); + assertTrue(arg.hasErrorCallback()); + } + + @Test + public void testDefaultValue() { + var arg = ArgumentType.String("id"); + + assertFalse(arg.isOptional()); + arg.setDefaultValue("default value"); + assertTrue(arg.isOptional()); + assertEquals("default value", arg.getDefaultValue().get()); + } + + @Test + public void testSuggestionCallback() { + var arg = ArgumentType.String("id"); + + assertFalse(arg.hasSuggestion()); + + arg.setSuggestionCallback((sender, context, suggestion) -> suggestion.addEntry(new SuggestionEntry("entry"))); + assertTrue(arg.hasSuggestion()); + + Suggestion suggestion = new Suggestion("input", 2, 4); + arg.getSuggestionCallback().apply(new ServerSender(), new CommandContext("input"), suggestion); + + assertEquals(suggestion.getEntries(), List.of(new SuggestionEntry("entry"))); + } + +} \ No newline at end of file diff --git a/src/test/java/net/minestom/server/command/ArgumentTypeTest.java b/src/test/java/net/minestom/server/command/ArgumentTypeTest.java new file mode 100644 index 000000000..7e5d2f009 --- /dev/null +++ b/src/test/java/net/minestom/server/command/ArgumentTypeTest.java @@ -0,0 +1,539 @@ +package net.minestom.server.command; + +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.format.Style; +import net.kyori.adventure.text.format.TextDecoration; +import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer; +import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer; +import net.minestom.server.command.builder.CommandContext; +import net.minestom.server.command.builder.arguments.ArgumentEnum; +import net.minestom.server.command.builder.arguments.ArgumentType; +import net.minestom.server.command.builder.exception.ArgumentSyntaxException; +import net.minestom.server.coordinate.Vec; +import net.minestom.server.entity.EntityType; +import net.minestom.server.instance.block.Block; +import net.minestom.server.item.Enchantment; +import net.minestom.server.item.ItemStack; +import net.minestom.server.item.Material; +import net.minestom.server.particle.Particle; +import net.minestom.server.potion.PotionEffect; +import net.minestom.server.tag.Tag; +import net.minestom.server.utils.location.RelativeVec; +import net.minestom.server.utils.math.FloatRange; +import net.minestom.server.utils.math.IntRange; +import net.minestom.server.utils.time.TimeUnit; +import org.jglrxavpok.hephaistos.nbt.NBT; +import org.junit.jupiter.api.Test; + +import java.time.Duration; +import java.util.List; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.*; + +public class ArgumentTypeTest { + + @Test + public void testArgumentEnchantment() { + var arg = ArgumentType.Enchantment("enchantment"); + + assertThrows(ArgumentSyntaxException.class, () -> arg.parse("minecraft:invalid_enchantment")); + assertNotEquals(Enchantment.RESPIRATION, arg.parse(Enchantment.SWEEPING.namespace().asString())); + assertEquals(Enchantment.MENDING, arg.parse(Enchantment.MENDING.namespace().asString())); + + assertEquals("Enchantment", arg.toString()); + } + + @Test + public void testArgumentEntityType() { + var arg = ArgumentType.EntityType("entity_type"); + + assertThrows(ArgumentSyntaxException.class, () -> arg.parse("minecraft:invalid_entity_type")); + assertNotEquals(EntityType.ARMOR_STAND, arg.parse(EntityType.HUSK.namespace().asString())); + assertEquals(EntityType.PLAYER, arg.parse(EntityType.PLAYER.namespace().asString())); + + + assertEquals("EntityType", arg.toString()); + } + + @Test + public void testArgumentParticle() { + var arg = ArgumentType.Particle("particle"); + + assertThrows(ArgumentSyntaxException.class, () -> arg.parse("minecraft:invalid_particle")); + assertNotEquals(Particle.BLOCK, arg.parse(Particle.CAMPFIRE_SIGNAL_SMOKE.namespace().asString())); + assertEquals(Particle.TOTEM_OF_UNDYING, arg.parse(Particle.TOTEM_OF_UNDYING.namespace().asString())); + + assertEquals("Particle", arg.toString()); + } + + @Test + public void testArgumentPotionEffect() { + var arg = ArgumentType.Potion("potion"); + + assertThrows(ArgumentSyntaxException.class, () -> arg.parse("minecraft:invalid_potion")); + assertNotEquals(PotionEffect.SPEED, arg.parse(PotionEffect.JUMP_BOOST.namespace().asString())); + assertEquals(PotionEffect.INSTANT_DAMAGE, arg.parse(PotionEffect.INSTANT_DAMAGE.namespace().asString())); + + assertEquals("Potion", arg.toString()); + } + + @Test + public void testArgumentBlockState() { + var arg = ArgumentType.BlockState("block_state"); + + assertThrows(ArgumentSyntaxException.class, () -> arg.parse("minecraft:invalid_block[invalid_property=invalid_key]")); + assertThrows(ArgumentSyntaxException.class, () -> arg.parse("minecraft:stone[invalid_property=invalid_key]")); + assertThrows(ArgumentSyntaxException.class, () -> arg.parse("minecraft:kelp[age=invalid_key]")); + assertEquals(Block.COBBLESTONE, arg.parse("minecraft:cobblestone")); + assertEquals(Block.KELP.withProperty("age", "14"), arg.parse("minecraft:kelp[age=14]")); + assertNotEquals(Block.KELP.withProperty("age", "15"), arg.parse("minecraft:kelp[age=14]")); + assertNotEquals(Block.ATTACHED_MELON_STEM, arg.parse("minecraft:cobblestone")); + + assertEquals("BlockState", arg.toString()); + } + + @Test + public void testArgumentColor() { + var arg = ArgumentType.Color("color"); + + assertThrows(ArgumentSyntaxException.class, () -> arg.parse("invalid_color")); + assertNotEquals(Style.style(NamedTextColor.AQUA), arg.parse("blue")); + assertEquals(Style.style(NamedTextColor.DARK_PURPLE), arg.parse("dark_purple")); + assertEquals(Style.empty(), arg.parse("reset")); + + assertEquals("Color", arg.toString()); + } + + @Test + public void testArgumentComponent() { + var arg = ArgumentType.Component("component"); + + var component1 = Component.text("Example text", NamedTextColor.DARK_AQUA); + var component2 = Component.text("Other example text", Style.style(TextDecoration.OBFUSCATED)); + + var json1 = GsonComponentSerializer.gson().serialize(component1); + var json2 = GsonComponentSerializer.gson().serialize(component2); + + assertThrows(ArgumentSyntaxException.class, () -> arg.parse("invalid component")); + assertNotEquals(component1, arg.parse(json2)); + assertEquals(component1, arg.parse(json1)); + assertEquals("Other example text", PlainTextComponentSerializer.plainText().serialize(arg.parse(json2))); + + assertEquals("Component", arg.toString()); + } + + @Test + public void testArgumentEntity() { + var arg = ArgumentType.Entity("entity"); + + assertDoesNotThrow(() -> arg.parse("@a")); + assertDoesNotThrow(() -> arg.parse("@p")); + assertThrows(ArgumentSyntaxException.class, () -> arg.parse("@x")); + + assertDoesNotThrow(() -> arg.parse("@e[type=sheep]")); + assertDoesNotThrow(() -> arg.parse("@e[type=!cow]")); + assertThrows(ArgumentSyntaxException.class, () -> arg.parse("@e[type=invalid_entity]")); + assertThrows(ArgumentSyntaxException.class, () -> arg.parse("@e[type=!invalid_entity_two]")); + + assertDoesNotThrow(() -> arg.parse("@e[gamemode=creative]")); + assertDoesNotThrow(() -> arg.parse("@e[gamemode=!survival]")); + assertThrows(ArgumentSyntaxException.class, () -> arg.parse("@e[gamemode=invalid_gamemode]")); + assertThrows(ArgumentSyntaxException.class, () -> arg.parse("@e[gamemode=!invalid_gamemode_2]")); + + assertDoesNotThrow(() -> arg.parse("@e[limit=500]")); + assertThrows(ArgumentSyntaxException.class, () -> arg.parse("@e[limit=-500]")); + assertThrows(ArgumentSyntaxException.class, () -> arg.parse("@e[limit=invalid_integer]")); + assertThrows(ArgumentSyntaxException.class, () -> arg.parse("@e[limit=2147483648]")); + + assertDoesNotThrow(() -> arg.parse("@e[sort=nearest]")); + assertThrows(ArgumentSyntaxException.class, () -> arg.parse("@e[sort=invalid_sort]")); + + assertDoesNotThrow(() -> arg.parse("@e[level=55]")); + assertDoesNotThrow(() -> arg.parse("@e[level=100..500]")); + assertThrows(ArgumentSyntaxException.class, () -> arg.parse("@e[level=20-50]")); + assertThrows(ArgumentSyntaxException.class, () -> arg.parse("@e[level=2147483648]")); + + assertDoesNotThrow(() -> arg.parse("@e[distance=500]")); + assertDoesNotThrow(() -> arg.parse("@e[distance=50..150]")); + assertThrows(ArgumentSyntaxException.class, () -> arg.parse("@e[distance=-500-500]")); + assertThrows(ArgumentSyntaxException.class, () -> arg.parse("@e[distance=2147483648]")); + + assertEquals("Entities", arg.toString()); + } + + @Test + public void testArgumentFloatRange() { + var arg = ArgumentType.FloatRange("float_range"); + + assertEquals(new FloatRange(0f, 50f), arg.parse("0..50")); + assertEquals(new FloatRange(0f, 0f), arg.parse("0..0")); + assertEquals(new FloatRange(-50f, 0f), arg.parse("-50..0")); + assertEquals(new FloatRange(-Float.MAX_VALUE, 50f), arg.parse("..50")); + assertEquals(new FloatRange(0f, Float.MAX_VALUE), arg.parse("0..")); + assertEquals(new FloatRange(-Float.MAX_VALUE, Float.MAX_VALUE), arg.parse("-3.4028235E38..3.4028235E38")); + assertEquals(new FloatRange(0.5f, 24f), arg.parse("0.5..24")); + assertEquals(new FloatRange(12f, 45.6f), arg.parse("12..45.6")); + + assertThrows(ArgumentSyntaxException.class, () -> arg.parse("..")); + assertThrows(ArgumentSyntaxException.class, () -> arg.parse("0..50..")); + + assertEquals("FloatRange", arg.toString()); + } + + @Test + public void testArgumentIntRange() { + var arg = ArgumentType.IntRange("int_range"); + + assertEquals(new IntRange(0, 50), arg.parse("0..50")); + assertEquals(new IntRange(0, 0), arg.parse("0..0")); + assertEquals(new IntRange(-50, 0), arg.parse("-50..0")); + assertEquals(new IntRange(Integer.MIN_VALUE, 50), arg.parse("..50")); + assertEquals(new IntRange(0, Integer.MAX_VALUE), arg.parse("0..")); + assertEquals(new IntRange(Integer.MIN_VALUE, Integer.MAX_VALUE), arg.parse("-2147483648..2147483647")); + + assertThrows(ArgumentSyntaxException.class, () -> arg.parse("..")); + assertThrows(ArgumentSyntaxException.class, () -> arg.parse("-2147483649..2147483647")); + assertThrows(ArgumentSyntaxException.class, () -> arg.parse("-2147483648..2147483648")); + assertThrows(ArgumentSyntaxException.class, () -> arg.parse("0..50..")); + assertThrows(ArgumentSyntaxException.class, () -> arg.parse("0.5..24")); + assertThrows(ArgumentSyntaxException.class, () -> arg.parse("12..45.6")); + + assertEquals("IntRange", arg.toString()); + } + + @Test + public void testArgumentItemStack() { + var arg = ArgumentType.ItemStack("item_stack"); + + assertEquals(ItemStack.AIR, arg.parse("air")); + assertEquals(ItemStack.of(Material.GLASS_PANE).withTag(Tag.String("tag"), "value"), arg.parse("glass_pane{tag:value}")); + + assertEquals("ItemStack", arg.toString()); + } + + @Test + public void testArgumentNbtCompoundTag() { + var arg = ArgumentType.NbtCompound("nbt_compound"); + + assertEquals(NBT.Compound(mut -> mut.put("long_array", NBT.LongArray(12, 49, 119))), arg.parse("{\"long_array\":[L;12L,49L,119L]}")); + assertEquals(NBT.Compound(mut -> mut.put("nested", NBT.Compound(mut2 -> + mut2.put("complex", NBT.IntArray(124, 999, 33256)) + )) + ), arg.parse("{\"nested\": {\"complex\": [I;124,999,33256]}}")); + + assertThrows(ArgumentSyntaxException.class, () -> arg.parse("string")); + assertThrows(ArgumentSyntaxException.class, () -> arg.parse("\"string\"")); + assertThrows(ArgumentSyntaxException.class, () -> arg.parse("44")); + assertThrows(ArgumentSyntaxException.class, () -> arg.parse("[I;11,49,33]")); + + assertEquals("NbtCompound", arg.toString()); + } + + @Test + public void testArgumentNbtTag() { + var arg = ArgumentType.NBT("nbt"); + + assertEquals(NBT.String("string"), arg.parse("string")); + assertEquals(NBT.String("string"), arg.parse("\"string\"")); + assertEquals(NBT.Int(44), arg.parse("44")); + assertEquals(NBT.IntArray(11, 49, 33), arg.parse("[I;11,49,33]")); + assertEquals(NBT.Compound(mut -> mut.put("long_array", NBT.LongArray(12, 49, 119))), arg.parse("{\"long_array\":[L;12L,49L,119L]}")); + + assertThrows(ArgumentSyntaxException.class, () -> arg.parse("\"unbalanced string")); + assertThrows(ArgumentSyntaxException.class, () -> arg.parse("dd}")); + assertThrows(ArgumentSyntaxException.class, () -> arg.parse("{unquoted: string)}")); + assertThrows(ArgumentSyntaxException.class, () -> arg.parse("{\"array\": [D;123L,5L]}")); + + assertEquals("NBT", arg.toString()); + } + + @Test + public void testArgumentResourceLocation() { + var arg = ArgumentType.ResourceLocation("resource_location"); + + assertEquals("minecraft:resource_location_example", arg.parse("minecraft:resource_location_example")); + assertThrows(ArgumentSyntaxException.class, () -> arg.parse("minecraft:invalid resource location")); + + assertEquals("ResourceLocation", arg.toString()); + } + + @Test + public void testArgumentTime() { + var arg = ArgumentType.Time("time"); + + assertEquals(Duration.of(20, TimeUnit.SERVER_TICK), arg.parse("20")); + assertEquals(Duration.of(40, TimeUnit.SERVER_TICK), arg.parse("40t")); + assertEquals(Duration.of(60, TimeUnit.SECOND), arg.parse("60s")); + assertEquals(Duration.of(80, TimeUnit.DAY), arg.parse("80d")); + + assertThrows(ArgumentSyntaxException.class, () -> arg.parse("100x")); + assertThrows(ArgumentSyntaxException.class, () -> arg.parse("2147483648t")); + + assertEquals("Time