From 419fee1d35d29569ed8d5328e9e5f89eb267fe25 Mon Sep 17 00:00:00 2001 From: Robin Date: Mon, 22 Jan 2024 18:08:09 +0000 Subject: [PATCH 1/4] Start Unique Voucher Protection --- .../vouchers/listener/VoucherListener.java | 12 ++-- .../vouchers/manager/VoucherManager.java | 55 ++++++++++++++++++- .../xyz/oribuin/vouchers/model/Voucher.java | 44 ++++++++++++++- 3 files changed, 103 insertions(+), 8 deletions(-) diff --git a/src/main/java/xyz/oribuin/vouchers/listener/VoucherListener.java b/src/main/java/xyz/oribuin/vouchers/listener/VoucherListener.java index a7a60d0..bef7d96 100644 --- a/src/main/java/xyz/oribuin/vouchers/listener/VoucherListener.java +++ b/src/main/java/xyz/oribuin/vouchers/listener/VoucherListener.java @@ -5,6 +5,7 @@ import org.bukkit.event.Listener; import org.bukkit.event.block.Action; import org.bukkit.event.player.PlayerInteractEvent; +import org.bukkit.inventory.ItemStack; import xyz.oribuin.vouchers.VoucherPlugin; import xyz.oribuin.vouchers.manager.ConfigurationManager.Setting; import xyz.oribuin.vouchers.manager.VoucherManager; @@ -12,10 +13,10 @@ public class VoucherListener implements Listener { - private final VoucherPlugin plugin; + private final VoucherManager manager; public VoucherListener(VoucherPlugin plugin) { - this.plugin = plugin; + this.manager = plugin.getManager(VoucherManager.class); } @EventHandler(priority = EventPriority.HIGHEST) @@ -24,14 +25,15 @@ public void onInteract(PlayerInteractEvent event) { if (event.getItem() == null) return; if (event.getClickedBlock() != null && event.getClickedBlock().getType().isInteractable()) return; - Voucher voucher = this.plugin.getManager(VoucherManager.class).getVoucher(event.getItem()); + Voucher voucher = this.manager.getVoucher(event.getItem()); if (voucher == null) return; event.setCancelled(true); if (!Setting.REDEEM_WHILE_CROUCHING.getBoolean() && event.getPlayer().isSneaking()) return; - if (voucher.redeem(event.getPlayer())) { - event.getItem().setAmount(event.getItem().getAmount() - 1); + ItemStack item = event.getItem(); + if (voucher.redeem(event.getPlayer(), manager.getUniqueId(item))) { + item.setAmount(item.getAmount() - 1); } } diff --git a/src/main/java/xyz/oribuin/vouchers/manager/VoucherManager.java b/src/main/java/xyz/oribuin/vouchers/manager/VoucherManager.java index d0674c1..aeb4c0d 100644 --- a/src/main/java/xyz/oribuin/vouchers/manager/VoucherManager.java +++ b/src/main/java/xyz/oribuin/vouchers/manager/VoucherManager.java @@ -6,7 +6,6 @@ import dev.rosewood.rosegarden.config.CommentedConfigurationSection; import dev.rosewood.rosegarden.config.CommentedFileConfiguration; import dev.rosewood.rosegarden.manager.Manager; -import org.bukkit.configuration.ConfigurationSection; import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.meta.ItemMeta; import org.bukkit.persistence.PersistentDataContainer; @@ -17,6 +16,7 @@ import xyz.oribuin.vouchers.util.VoucherUtils; import java.io.File; +import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; @@ -28,6 +28,8 @@ public class VoucherManager extends Manager { private final Map vouchers = new HashMap<>(); private final Table voucherUses = HashBasedTable.create(); + private CommentedFileConfiguration uniqueUseConfig; + private File uniqueUseFile; public VoucherManager(RosePlugin rosePlugin) { super(rosePlugin); @@ -55,6 +57,17 @@ public void reload() { if (folders == null) return; Arrays.stream(folders).filter(File::isDirectory).forEach(this::loadFolder); + + // Load the unique use config + try { + this.uniqueUseFile = new File(this.rosePlugin.getDataFolder(), "voucher-uses.yml"); + if (!this.uniqueUseFile.exists()) { + this.uniqueUseFile.createNewFile(); + } + + this.uniqueUseConfig = CommentedFileConfiguration.loadConfiguration(this.uniqueUseFile); + } catch (IOException ignored) { + } } /** @@ -107,14 +120,15 @@ public void load(File file) { }); voucher.setRequirements(requirements); - voucher.setDenyCommands(section.getStringList(key + ".deny-commands")); } // Load all the basic easy values from the config + voucher.setDenyCommands(section.getStringList(key + ".deny-commands")); voucher.setRequirementMin(section.getInt(key + ".requirement-min", requirements.size())); voucher.setCommands(section.getStringList(key + ".commands")); voucher.setCooldown(VoucherUtils.getTime(section.getString(key + ".cooldown")).toMillis()); voucher.setCooldownActions(section.getStringList(key + ".on-cooldown")); + voucher.setUnique(section.getBoolean(key + ".unique", false)); this.vouchers.put(key.toLowerCase(), voucher); }); @@ -186,6 +200,43 @@ public long getCooldown(UUID uuid, Voucher voucher) { return voucher.getCooldown() - (System.currentTimeMillis() - lastUse); } + /** + * Get the unique id of a voucher. + * + * @param itemStack The item to get the unique id from. + * @return The unique id of the voucher. + */ + public UUID getUniqueId(ItemStack itemStack) { + ItemMeta meta = itemStack.getItemMeta(); + if (meta == null) return null; + + PersistentDataContainer container = meta.getPersistentDataContainer(); + String id = container.get(Voucher.UNIQUE_KEY, PersistentDataType.STRING); + if (id == null) return null; + + return UUID.fromString(id); + } + + /** + * Check how many times that voucher has been used. + * + * @param voucherId The id of the voucher. + * @return The amount of times the voucher has been used. + */ + public boolean hasBeenUsed(UUID voucherId) { + return this.uniqueUseConfig.contains(voucherId.toString()); + } + + /** + * Add a use to the voucher. + * + * @param voucher The voucher to add a use to. + */ + public void addUse(UUID voucher) { + this.uniqueUseConfig.set(voucher.toString(), 1); + this.uniqueUseConfig.save(this.uniqueUseFile); + } + public Map getVouchers() { return vouchers; } diff --git a/src/main/java/xyz/oribuin/vouchers/model/Voucher.java b/src/main/java/xyz/oribuin/vouchers/model/Voucher.java index f9ef0f8..da5322a 100644 --- a/src/main/java/xyz/oribuin/vouchers/model/Voucher.java +++ b/src/main/java/xyz/oribuin/vouchers/model/Voucher.java @@ -15,10 +15,12 @@ import java.util.ArrayList; import java.util.List; +import java.util.UUID; public class Voucher { public static final NamespacedKey DATA_KEY = new NamespacedKey("vouchers", "uses"); + public static final NamespacedKey UNIQUE_KEY = new NamespacedKey("vouchers", "unique"); private final String id; private final ItemStack display; @@ -28,6 +30,7 @@ public class Voucher { private List cooldownActions; private int requirementMin; private long cooldown; + private boolean unique; /** * Create a new voucher object for caching. @@ -44,6 +47,7 @@ public Voucher(String id, ItemStack display) { this.cooldownActions = new ArrayList<>(); this.requirementMin = -1; this.cooldown = 0; + this.unique = false; } /** @@ -55,6 +59,10 @@ public void give(Player player, int amt) { ItemStack item = this.display.clone(); item.setAmount(amt); + if (this.unique) { + this.applyUnique(item); + } + player.getInventory().addItem(item); } @@ -63,7 +71,7 @@ public void give(Player player, int amt) { * * @param player The player to redeem the voucher for. */ - public boolean redeem(Player player) { + public boolean redeem(Player player, UUID uniqueId) { VoucherManager manager = VoucherPlugin.get().getManager(VoucherManager.class); // Check if the player is on cooldown @@ -89,9 +97,20 @@ public boolean redeem(Player player) { } } + // Check if the player has used the voucher before + if (this.unique && uniqueId != null && manager.hasBeenUsed(uniqueId)) { + ActionType.run(player, this.denyCommands); + return false; + } + // Run all the commands and actions ActionType.run(player, this.commands); + // Add the use + if (this.unique && uniqueId != null) { + manager.addUse(uniqueId); + } + // Add the cooldown if (this.cooldown > 0) { manager.addCooldown(player.getUniqueId(), this); @@ -115,6 +134,21 @@ public ItemStack apply(ItemStack itemStack) { return itemStack; } + /** + * Apply the voucher data to an item. + * + * @param itemStack The item to apply the voucher data to. + */ + public ItemStack applyUnique(ItemStack itemStack) { + ItemMeta meta = itemStack.getItemMeta(); + if (meta == null) return itemStack; + + PersistentDataContainer container = meta.getPersistentDataContainer(); + container.set(UNIQUE_KEY, PersistentDataType.STRING, UUID.randomUUID().toString()); + itemStack.setItemMeta(meta); + return itemStack; + } + public String getId() { return id; } @@ -171,5 +205,13 @@ public void setCooldownActions(List cooldownActions) { this.cooldownActions = cooldownActions; } + public boolean isUnique() { + return unique; + } + + public void setUnique(boolean unique) { + this.unique = unique; + } + } From e188cbd09835bd8dee758ef972af31510480b834 Mon Sep 17 00:00:00 2001 From: Robin Date: Mon, 22 Jan 2024 18:16:06 +0000 Subject: [PATCH 2/4] Fixup Method for giving items in gui --- .../oribuin/vouchers/command/command/ListCommand.java | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/main/java/xyz/oribuin/vouchers/command/command/ListCommand.java b/src/main/java/xyz/oribuin/vouchers/command/command/ListCommand.java index 7a15a2c..66e3a6d 100644 --- a/src/main/java/xyz/oribuin/vouchers/command/command/ListCommand.java +++ b/src/main/java/xyz/oribuin/vouchers/command/command/ListCommand.java @@ -13,6 +13,7 @@ import net.kyori.adventure.text.Component; import org.bukkit.Material; import org.bukkit.entity.HumanEntity; +import org.bukkit.entity.Player; import org.bukkit.inventory.ItemStack; import xyz.oribuin.vouchers.manager.VoucherManager; import xyz.oribuin.vouchers.model.Voucher; @@ -49,10 +50,10 @@ public void execute(CommandContext context, @Optional Integer page) { vouchers.forEach(voucher -> { ItemStack item = voucher.getDisplay(); - GuiItem guiItem = ItemBuilder.from(item).asGuiItem(event -> event - .getWhoClicked() - .getInventory() - .addItem(voucher.getDisplay()) + GuiItem guiItem = ItemBuilder.from(item).asGuiItem(event -> voucher.give( + (Player) event.getWhoClicked(), + event.getWhoClicked().isSneaking() ? item.getMaxStackSize() : 1 + ) ); gui.addItem(guiItem); From 48265a8822ed83726a88e8fdcda2e723e9238345 Mon Sep 17 00:00:00 2001 From: Robin Date: Thu, 25 Jan 2024 01:39:35 +0000 Subject: [PATCH 3/4] Switch data from yml to sql --- build.gradle | 4 - .../xyz/oribuin/vouchers/VoucherPlugin.java | 3 +- .../oribuin/vouchers/manager/DataManager.java | 87 +++++++++++++++++++ .../vouchers/manager/LocaleManager.java | 1 - .../vouchers/manager/VoucherManager.java | 36 +------- .../migration/_1_CreateInitialTables.java | 28 ++++++ .../xyz/oribuin/vouchers/model/Voucher.java | 21 ++++- 7 files changed, 135 insertions(+), 45 deletions(-) create mode 100644 src/main/java/xyz/oribuin/vouchers/manager/DataManager.java create mode 100644 src/main/java/xyz/oribuin/vouchers/migration/_1_CreateInitialTables.java diff --git a/build.gradle b/build.gradle index e7cd478..e40ee27 100644 --- a/build.gradle +++ b/build.gradle @@ -55,10 +55,6 @@ shadowJar { relocate("dev.rosewood.rosegarden", "${project.group}.vouchers.libs.rosegarden") relocate("dev.triumphteam.gui", "${project.group}.vouchers.libs.triumphgui") - - // Remove comments if you're not using SQL/SQLite - exclude 'dev/rosewood/rosegarden/lib/hikaricp/**/*.class' - exclude 'dev/rosewood/rosegarden/lib/slf4j/**/*.class' } // Include version replacement diff --git a/src/main/java/xyz/oribuin/vouchers/VoucherPlugin.java b/src/main/java/xyz/oribuin/vouchers/VoucherPlugin.java index 3aa309c..db55948 100644 --- a/src/main/java/xyz/oribuin/vouchers/VoucherPlugin.java +++ b/src/main/java/xyz/oribuin/vouchers/VoucherPlugin.java @@ -6,6 +6,7 @@ import xyz.oribuin.vouchers.listener.VoucherListener; import xyz.oribuin.vouchers.manager.CommandManager; import xyz.oribuin.vouchers.manager.ConfigurationManager; +import xyz.oribuin.vouchers.manager.DataManager; import xyz.oribuin.vouchers.manager.LocaleManager; import xyz.oribuin.vouchers.manager.VoucherManager; @@ -18,7 +19,7 @@ public class VoucherPlugin extends RosePlugin { public VoucherPlugin() { super(114633, 20798, ConfigurationManager.class, - null, + DataManager.class, LocaleManager.class, CommandManager.class ); diff --git a/src/main/java/xyz/oribuin/vouchers/manager/DataManager.java b/src/main/java/xyz/oribuin/vouchers/manager/DataManager.java new file mode 100644 index 0000000..6afb2c0 --- /dev/null +++ b/src/main/java/xyz/oribuin/vouchers/manager/DataManager.java @@ -0,0 +1,87 @@ +package xyz.oribuin.vouchers.manager; + +import dev.rosewood.rosegarden.RosePlugin; +import dev.rosewood.rosegarden.database.DataMigration; +import dev.rosewood.rosegarden.manager.AbstractDataManager; +import xyz.oribuin.vouchers.migration._1_CreateInitialTables; + +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +public class DataManager extends AbstractDataManager { + + private final Map cachedUses = new HashMap<>(); + + public DataManager(RosePlugin rosePlugin) { + super(rosePlugin); + } + + @Override + public void reload() { + super.reload(); + + // Load all the uses from the database + // (This is better than loading straight from the database when needed) + this.cachedUses.clear(); + this.async(() -> this.databaseConnector.connect(connection -> { + String select = "SELECT * FROM " + this.getTablePrefix() + "uses"; + try (PreparedStatement statement = connection.prepareStatement(select)) { + ResultSet rs = statement.executeQuery(); + while (rs.next()) { + UUID voucher = UUID.fromString(rs.getString("voucher_id")); + int uses = rs.getInt("uses"); + + this.cachedUses.put(voucher, uses); + } + } + })); + } + + /** + * Add a use to the voucher with the specified UUID for the specified amount of uses + * + * @param voucher The voucher to add the use to + * @param uses The amount of uses to add + */ + public void addUse(UUID voucher, int uses) { + this.cachedUses.put(voucher, uses); + + this.async(() -> this.databaseConnector.connect(connection -> { + String insert = "REPLACE INTO " + this.getTablePrefix() + "uses (`voucher_id`, `uses`) VALUES (?, ?)"; + try (PreparedStatement statement = connection.prepareStatement(insert)) { + statement.setString(1, voucher.toString()); + statement.setInt(2, uses); + statement.executeUpdate(); + } + })); + } + + /** + * Get all the uses for a voucher + * + * @param voucher The voucher + * @return The amount of uses + */ + public int getUses(UUID voucher) { + return this.cachedUses.getOrDefault(voucher, 1); + } + + /** + * Run a task asynchronously + * + * @param runnable The task to run + */ + public void async(Runnable runnable) { + this.rosePlugin.getServer().getScheduler().runTaskAsynchronously(this.rosePlugin, runnable); + } + + @Override + public List> getDataMigrations() { + return List.of(_1_CreateInitialTables.class); + } + +} diff --git a/src/main/java/xyz/oribuin/vouchers/manager/LocaleManager.java b/src/main/java/xyz/oribuin/vouchers/manager/LocaleManager.java index ad5c790..06e30f2 100644 --- a/src/main/java/xyz/oribuin/vouchers/manager/LocaleManager.java +++ b/src/main/java/xyz/oribuin/vouchers/manager/LocaleManager.java @@ -17,7 +17,6 @@ public LocaleManager(RosePlugin rosePlugin) { super(rosePlugin); } - /** * Send a message to a CommandSender * diff --git a/src/main/java/xyz/oribuin/vouchers/manager/VoucherManager.java b/src/main/java/xyz/oribuin/vouchers/manager/VoucherManager.java index aeb4c0d..4d9d888 100644 --- a/src/main/java/xyz/oribuin/vouchers/manager/VoucherManager.java +++ b/src/main/java/xyz/oribuin/vouchers/manager/VoucherManager.java @@ -16,7 +16,6 @@ import xyz.oribuin.vouchers.util.VoucherUtils; import java.io.File; -import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; @@ -28,8 +27,6 @@ public class VoucherManager extends Manager { private final Map vouchers = new HashMap<>(); private final Table voucherUses = HashBasedTable.create(); - private CommentedFileConfiguration uniqueUseConfig; - private File uniqueUseFile; public VoucherManager(RosePlugin rosePlugin) { super(rosePlugin); @@ -57,17 +54,6 @@ public void reload() { if (folders == null) return; Arrays.stream(folders).filter(File::isDirectory).forEach(this::loadFolder); - - // Load the unique use config - try { - this.uniqueUseFile = new File(this.rosePlugin.getDataFolder(), "voucher-uses.yml"); - if (!this.uniqueUseFile.exists()) { - this.uniqueUseFile.createNewFile(); - } - - this.uniqueUseConfig = CommentedFileConfiguration.loadConfiguration(this.uniqueUseFile); - } catch (IOException ignored) { - } } /** @@ -129,7 +115,7 @@ public void load(File file) { voucher.setCooldown(VoucherUtils.getTime(section.getString(key + ".cooldown")).toMillis()); voucher.setCooldownActions(section.getStringList(key + ".on-cooldown")); voucher.setUnique(section.getBoolean(key + ".unique", false)); - + voucher.setUniqueCommands(section.getStringList(key + ".unique-commands")); this.vouchers.put(key.toLowerCase(), voucher); }); @@ -217,26 +203,6 @@ public UUID getUniqueId(ItemStack itemStack) { return UUID.fromString(id); } - /** - * Check how many times that voucher has been used. - * - * @param voucherId The id of the voucher. - * @return The amount of times the voucher has been used. - */ - public boolean hasBeenUsed(UUID voucherId) { - return this.uniqueUseConfig.contains(voucherId.toString()); - } - - /** - * Add a use to the voucher. - * - * @param voucher The voucher to add a use to. - */ - public void addUse(UUID voucher) { - this.uniqueUseConfig.set(voucher.toString(), 1); - this.uniqueUseConfig.save(this.uniqueUseFile); - } - public Map getVouchers() { return vouchers; } diff --git a/src/main/java/xyz/oribuin/vouchers/migration/_1_CreateInitialTables.java b/src/main/java/xyz/oribuin/vouchers/migration/_1_CreateInitialTables.java new file mode 100644 index 0000000..824e9b3 --- /dev/null +++ b/src/main/java/xyz/oribuin/vouchers/migration/_1_CreateInitialTables.java @@ -0,0 +1,28 @@ +package xyz.oribuin.vouchers.migration; + +import dev.rosewood.rosegarden.database.DataMigration; +import dev.rosewood.rosegarden.database.DatabaseConnector; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; + +public class _1_CreateInitialTables extends DataMigration { + + public _1_CreateInitialTables() { + super(1); + } + + @Override + public void migrate(DatabaseConnector connector, Connection connection, String tablePrefix) throws SQLException { + String createTable = "CREATE TABLE IF NOT EXISTS " + tablePrefix + "uses (" + + "`voucher_id` VARCHAR(36) NOT NULL, " + + "`uses` INT NOT NULL, " + + "primary key (`voucher_id`))"; + + try (PreparedStatement statement = connection.prepareStatement(createTable)) { + statement.executeUpdate(); + } + } + +} diff --git a/src/main/java/xyz/oribuin/vouchers/model/Voucher.java b/src/main/java/xyz/oribuin/vouchers/model/Voucher.java index da5322a..1e3129f 100644 --- a/src/main/java/xyz/oribuin/vouchers/model/Voucher.java +++ b/src/main/java/xyz/oribuin/vouchers/model/Voucher.java @@ -9,6 +9,7 @@ import org.bukkit.persistence.PersistentDataType; import xyz.oribuin.vouchers.VoucherPlugin; import xyz.oribuin.vouchers.action.ActionType; +import xyz.oribuin.vouchers.manager.DataManager; import xyz.oribuin.vouchers.manager.VoucherManager; import xyz.oribuin.vouchers.requirement.Requirement; import xyz.oribuin.vouchers.util.VoucherUtils; @@ -27,6 +28,7 @@ public class Voucher { private List requirements; private List commands; private List denyCommands; + private List uniqueCommands; private List cooldownActions; private int requirementMin; private long cooldown; @@ -44,6 +46,7 @@ public Voucher(String id, ItemStack display) { this.requirements = new ArrayList<>(); this.commands = new ArrayList<>(); this.denyCommands = new ArrayList<>(); + this.uniqueCommands = new ArrayList<>(); this.cooldownActions = new ArrayList<>(); this.requirementMin = -1; this.cooldown = 0; @@ -73,6 +76,7 @@ public void give(Player player, int amt) { */ public boolean redeem(Player player, UUID uniqueId) { VoucherManager manager = VoucherPlugin.get().getManager(VoucherManager.class); + DataManager data = VoucherPlugin.get().getManager(DataManager.class); // Check if the player is on cooldown long cooldown = manager.getCooldown(player.getUniqueId(), this); @@ -98,8 +102,9 @@ public boolean redeem(Player player, UUID uniqueId) { } // Check if the player has used the voucher before - if (this.unique && uniqueId != null && manager.hasBeenUsed(uniqueId)) { - ActionType.run(player, this.denyCommands); + int uses = data.getUses(uniqueId); + if (this.unique && uniqueId != null && uses <= 0) { + ActionType.run(player, this.uniqueCommands); return false; } @@ -108,7 +113,7 @@ public boolean redeem(Player player, UUID uniqueId) { // Add the use if (this.unique && uniqueId != null) { - manager.addUse(uniqueId); + data.addUse(uniqueId, uses - 1); } // Add the cooldown @@ -144,7 +149,7 @@ public ItemStack applyUnique(ItemStack itemStack) { if (meta == null) return itemStack; PersistentDataContainer container = meta.getPersistentDataContainer(); - container.set(UNIQUE_KEY, PersistentDataType.STRING, UUID.randomUUID().toString()); + container.set(UNIQUE_KEY, PersistentDataType.STRING, UUID.randomUUID().toString()); itemStack.setItemMeta(meta); return itemStack; } @@ -213,5 +218,13 @@ public void setUnique(boolean unique) { this.unique = unique; } + public List getUniqueCommands() { + return uniqueCommands; + } + + public void setUniqueCommands(List uniqueCommands) { + this.uniqueCommands = uniqueCommands; + } + } From 3f3923160f74292090fa43669b40d966e2758ae0 Mon Sep 17 00:00:00 2001 From: Robin Date: Thu, 25 Jan 2024 01:41:52 +0000 Subject: [PATCH 4/4] Version Bump --- build.gradle | 2 +- src/main/resources/vouchers/pouches/money.yml | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index e40ee27..b446352 100644 --- a/build.gradle +++ b/build.gradle @@ -7,7 +7,7 @@ plugins { } group = 'xyz.oribuin' -version = '1.0.1' +version = '1.0.2' java { toolchain { diff --git a/src/main/resources/vouchers/pouches/money.yml b/src/main/resources/vouchers/pouches/money.yml index b292edc..5bf5d10 100644 --- a/src/main/resources/vouchers/pouches/money.yml +++ b/src/main/resources/vouchers/pouches/money.yml @@ -14,6 +14,9 @@ small-money-pouch: - '' amount: 1 glow: true + unique: true # Makes the voucher one time use only + unique-commands: # Commands to run when the voucher is used + - '[message] &cThis item has already been redeemed!' commands: - '[console] eco give %player_name% %rng_1000,5000%'