diff --git a/cloud-minestom/src/main/java/org/incendo/cloud/minestom/MinestomCommandManager.java b/cloud-minestom/src/main/java/org/incendo/cloud/minestom/MinestomCommandManager.java
index ba968eff..61decdab 100644
--- a/cloud-minestom/src/main/java/org/incendo/cloud/minestom/MinestomCommandManager.java
+++ b/cloud-minestom/src/main/java/org/incendo/cloud/minestom/MinestomCommandManager.java
@@ -40,6 +40,7 @@
import org.incendo.cloud.minestom.parser.EntityTypeParser;
import org.incendo.cloud.minestom.parser.GameModeParser;
import org.incendo.cloud.minestom.parser.InstanceParser;
+import org.incendo.cloud.minestom.parser.ItemStackParser;
import org.incendo.cloud.minestom.parser.PlayerParser;
import org.incendo.cloud.minestom.parser.location.PosParser;
import org.incendo.cloud.minestom.parser.location.VecParser;
@@ -82,7 +83,8 @@ public MinestomCommandManager(
.registerParser(GameModeParser.gameModeParser())
.registerParser(DimensionTypeParser.dimensionTypeParser())
.registerParser(PosParser.posParser())
- .registerParser(VecParser.vecParser());
+ .registerParser(VecParser.vecParser())
+ .registerParser(ItemStackParser.itemStackParser());
this.captionRegistry().registerProvider(new MinestomDefaultCaptionsProvider<>());
diff --git a/cloud-minestom/src/main/java/org/incendo/cloud/minestom/caption/MinestomCaptionKeys.java b/cloud-minestom/src/main/java/org/incendo/cloud/minestom/caption/MinestomCaptionKeys.java
index 48558016..4a8f706f 100644
--- a/cloud-minestom/src/main/java/org/incendo/cloud/minestom/caption/MinestomCaptionKeys.java
+++ b/cloud-minestom/src/main/java/org/incendo/cloud/minestom/caption/MinestomCaptionKeys.java
@@ -72,6 +72,11 @@ public final class MinestomCaptionKeys {
*/
public static final Caption ARGUMENT_PARSE_FAILURE_POS = of("argument.parse.failure.pos");
+ /**
+ * Variables: {@code }
+ */
+ public static final Caption ARGUMENT_PARSE_FAILURE_ITEM_STACK = of("argument.parse.failure.item_stack");
+
private MinestomCaptionKeys() {
}
diff --git a/cloud-minestom/src/main/java/org/incendo/cloud/minestom/caption/MinestomDefaultCaptionsProvider.java b/cloud-minestom/src/main/java/org/incendo/cloud/minestom/caption/MinestomDefaultCaptionsProvider.java
index 973b8b2d..bc7f8993 100644
--- a/cloud-minestom/src/main/java/org/incendo/cloud/minestom/caption/MinestomDefaultCaptionsProvider.java
+++ b/cloud-minestom/src/main/java/org/incendo/cloud/minestom/caption/MinestomDefaultCaptionsProvider.java
@@ -69,6 +69,11 @@ public final class MinestomDefaultCaptionsProvider extends DelegatingCaptionP
*/
public static final String ARGUMENT_PARSE_FAILURE_POS = "'' is not a valid pos (expected: x y z, optionally with yaw and pitch)";
+ /**
+ * Default caption for {@link MinestomCaptionKeys#ARGUMENT_PARSE_FAILURE_ITEM_STACK}
+ */
+ public static final String ARGUMENT_PARSE_FAILURE_ITEM_STACK = "'' is not a valid item";
+
private static final CaptionProvider> PROVIDER = CaptionProvider.constantProvider()
.putCaption(MinestomCaptionKeys.ARGUMENT_PARSE_FAILURE_PLAYER, ARGUMENT_PARSE_FAILURE_PLAYER)
.putCaption(MinestomCaptionKeys.ARGUMENT_PARSE_FAILURE_ENTITY_TYPE, ARGUMENT_PARSE_FAILURE_ENTITY_TYPE)
@@ -77,6 +82,7 @@ public final class MinestomDefaultCaptionsProvider extends DelegatingCaptionP
.putCaption(MinestomCaptionKeys.ARGUMENT_PARSE_FAILURE_DIMENSION_TYPE, ARGUMENT_PARSE_FAILURE_DIMENSION_TYPE)
.putCaption(MinestomCaptionKeys.ARGUMENT_PARSE_FAILURE_VEC, ARGUMENT_PARSE_FAILURE_VEC)
.putCaption(MinestomCaptionKeys.ARGUMENT_PARSE_FAILURE_POS, ARGUMENT_PARSE_FAILURE_POS)
+ .putCaption(MinestomCaptionKeys.ARGUMENT_PARSE_FAILURE_ITEM_STACK, ARGUMENT_PARSE_FAILURE_ITEM_STACK)
.build();
@SuppressWarnings("unchecked")
diff --git a/cloud-minestom/src/main/java/org/incendo/cloud/minestom/data/ProtoItemStack.java b/cloud-minestom/src/main/java/org/incendo/cloud/minestom/data/ProtoItemStack.java
new file mode 100644
index 00000000..266f5475
--- /dev/null
+++ b/cloud-minestom/src/main/java/org/incendo/cloud/minestom/data/ProtoItemStack.java
@@ -0,0 +1,67 @@
+//
+// MIT License
+//
+// Copyright (c) 2024 Incendo
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+//
+package org.incendo.cloud.minestom.data;
+
+import net.minestom.server.item.ItemStack;
+import net.minestom.server.item.Material;
+import org.apiguardian.api.API;
+import org.checkerframework.checker.nullness.qual.NonNull;
+
+/**
+ * Intermediary result for an argument which parses a {@link net.minestom.server.item.Material} and optional NBT data.
+ *
+ * @since 2.1.0
+ */
+@API(status = API.Status.STABLE, since = "2.0.0")
+public final class ProtoItemStack {
+
+ private final Material material;
+
+ /**
+ * Creates a new proto item stack.
+ *
+ * @param material the material
+ */
+ public ProtoItemStack(final @NonNull Material material) {
+ this.material = material;
+ }
+
+ /**
+ * Creates an {@link ItemStack} from this proto item stack with the given amount.
+ *
+ * @param amount the stack size
+ * @return the created item stack
+ * @throws IllegalArgumentException if the amount exceeds the material's max stack size
+ */
+ public @NonNull ItemStack createItemStack(final int amount) {
+ if (amount > this.material.maxStackSize()) {
+ throw new IllegalArgumentException(String.format(
+ "The maximum stack size for %s is %d",
+ this.material.name(),
+ this.material.maxStackSize()
+ ));
+ }
+ return ItemStack.of(this.material, amount);
+ }
+}
diff --git a/cloud-minestom/src/main/java/org/incendo/cloud/minestom/data/package-info.java b/cloud-minestom/src/main/java/org/incendo/cloud/minestom/data/package-info.java
new file mode 100644
index 00000000..6c1624f0
--- /dev/null
+++ b/cloud-minestom/src/main/java/org/incendo/cloud/minestom/data/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * cloud-minestom data holders
+ */
+package org.incendo.cloud.minestom.data;
diff --git a/cloud-minestom/src/main/java/org/incendo/cloud/minestom/parser/ItemStackParser.java b/cloud-minestom/src/main/java/org/incendo/cloud/minestom/parser/ItemStackParser.java
new file mode 100644
index 00000000..4c1846ed
--- /dev/null
+++ b/cloud-minestom/src/main/java/org/incendo/cloud/minestom/parser/ItemStackParser.java
@@ -0,0 +1,139 @@
+//
+// MIT License
+//
+// Copyright (c) 2024 Incendo
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+//
+package org.incendo.cloud.minestom.parser;
+
+import java.util.stream.Collectors;
+import net.kyori.adventure.key.Key;
+import net.minestom.server.item.Material;
+import org.apiguardian.api.API;
+import org.incendo.cloud.caption.CaptionVariable;
+import org.incendo.cloud.component.CommandComponent;
+import org.incendo.cloud.context.CommandContext;
+import org.incendo.cloud.context.CommandInput;
+import org.incendo.cloud.exception.parsing.ParserException;
+import org.incendo.cloud.minestom.caption.MinestomCaptionKeys;
+import org.incendo.cloud.minestom.data.ProtoItemStack;
+import org.incendo.cloud.parser.ArgumentParseResult;
+import org.incendo.cloud.parser.ArgumentParser;
+import org.incendo.cloud.parser.ParserDescriptor;
+import org.incendo.cloud.suggestion.BlockingSuggestionProvider;
+import org.jspecify.annotations.NonNull;
+
+/**
+ * Parser for {@link ProtoItemStack}.
+ *
+ * @param command sender type
+ */
+public final class ItemStackParser implements ArgumentParser, BlockingSuggestionProvider.Strings {
+
+ /**
+ * Creates a new item stack parser.
+ *
+ * @param command sender type
+ * @return the created parser
+ * @since 2.0.0
+ */
+ @API(status = API.Status.STABLE, since = "2.0.0")
+ public static @NonNull ParserDescriptor itemStackParser() {
+ return ParserDescriptor.of(new ItemStackParser<>(), ProtoItemStack.class);
+ }
+
+ /**
+ * Returns a {@link CommandComponent.Builder} using {@link #itemStackParser()} as the parser.
+ *
+ * @param the command sender type
+ * @return the component builder
+ * @since 2.0.0
+ */
+ @API(status = API.Status.STABLE, since = "2.0.0")
+ public static CommandComponent.@NonNull Builder itemStackComponent() {
+ return CommandComponent.builder().parser(itemStackParser());
+ }
+
+ @Override
+ public @NonNull ArgumentParseResult<@NonNull ProtoItemStack> parse(
+ final @NonNull CommandContext<@NonNull C> commandContext,
+ final @NonNull CommandInput commandInput
+ ) {
+ final String input = commandInput.readString();
+ final Key key;
+ try {
+ key = Key.key(input);
+ } catch (final Exception e) {
+ return ArgumentParseResult.failure(new ItemStackParseException(input, commandContext));
+ }
+
+ final Material material = Material.fromKey(key);
+ if (material == null) {
+ return ArgumentParseResult.failure(new ItemStackParseException(input, commandContext));
+ }
+ return ArgumentParseResult.success(new ProtoItemStack(material));
+ }
+
+ @Override
+ public @NonNull Iterable<@NonNull String> stringSuggestions(
+ final @NonNull CommandContext commandContext,
+ final @NonNull CommandInput input
+ ) {
+ return Material.values().stream()
+ .map(m -> m.key().value())
+ .collect(Collectors.toList());
+ }
+
+ /**
+ * Exception thrown when an item stack cannot be parsed from the input provided.
+ */
+ public static final class ItemStackParseException extends ParserException {
+
+ private final String input;
+
+ /**
+ * Create a new item stack parse exception.
+ *
+ * @param input string input
+ * @param context command context
+ */
+ public ItemStackParseException(
+ final @NonNull String input,
+ final @NonNull CommandContext> context
+ ) {
+ super(
+ ItemStackParser.class,
+ context,
+ MinestomCaptionKeys.ARGUMENT_PARSE_FAILURE_ITEM_STACK,
+ CaptionVariable.of("input", input)
+ );
+ this.input = input;
+ }
+
+ /**
+ * Returns the supplied input.
+ *
+ * @return input value
+ */
+ public @NonNull String input() {
+ return this.input;
+ }
+ }
+}