Add support for saved items.

Introduces the concept of a _saved item_; an in-game item that has been
captured in a YAML file via Bukkit's item serialization mechanism. These
items can be referenced in the config-file in all places that any other
normal item can be used, assuming the ThingManager is in charge of the
parsing. This should help bridge the gap between class chests and the
config-file by allowing any Bukkit serializable item stack to be stored
and referenced as if MobArena's item syntax directly supported it.

Three new setup commands are introduced to help manage the items, such
that they can be created, deleted, and loaded (for "editing" purposes).
The commands are somewhat rough around the edges and may need a little
bit of polish going forward.

Together with the new inventory referencing Things, this functionality
should help provide most of the flexibility people have been missing
from the item syntax for about a decade... Hell, it's about time.

Closes #212
This commit is contained in:
Andreas Troelsen 2023-11-04 02:42:12 +01:00
parent d7336526e1
commit feb257213c
10 changed files with 425 additions and 0 deletions

View File

@ -13,6 +13,7 @@ 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.
- Support for saved items. The new `/ma save-item` command can be used to save the currently held item to disk, which allows it to be used in various places in the config-file. This should help bridge the gap between the config-file and class chests for config-file centric setups.
### 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`.

View File

@ -8,6 +8,7 @@ import com.garbagemule.MobArena.formula.FormulaMacros;
import com.garbagemule.MobArena.formula.FormulaManager;
import com.garbagemule.MobArena.framework.Arena;
import com.garbagemule.MobArena.framework.ArenaMaster;
import com.garbagemule.MobArena.items.SavedItemsManager;
import com.garbagemule.MobArena.listeners.MAGlobalListener;
import com.garbagemule.MobArena.metrics.ArenaCountChart;
import com.garbagemule.MobArena.metrics.ClassChestsChart;
@ -67,6 +68,8 @@ public class MobArena extends JavaPlugin
private FormulaManager formman;
private FormulaMacros macros;
private SavedItemsManager itemman;
private SignListeners signListeners;
@Override
@ -106,6 +109,7 @@ public class MobArena extends JavaPlugin
try {
createDataFolder();
setupFormulaMacros();
setupSavedItemsManager();
setupArenaMaster();
setupCommandHandler();
@ -134,6 +138,10 @@ public class MobArena extends JavaPlugin
macros = FormulaMacros.create(this);
}
private void setupSavedItemsManager() {
itemman = new SavedItemsManager(this);
}
private void setupArenaMaster() {
arenaMaster = new ArenaMasterImpl(this);
}
@ -190,6 +198,7 @@ public class MobArena extends JavaPlugin
reloadConfig();
reloadGlobalMessenger();
reloadFormulaMacros();
reloadSavedItemsManager();
reloadArenaMaster();
reloadAnnouncementsFile();
reloadSigns();
@ -226,6 +235,10 @@ public class MobArena extends JavaPlugin
}
}
private void reloadSavedItemsManager() {
itemman.reload();
}
private void reloadArenaMaster() {
arenaMaster.getArenas().forEach(Arena::forceEnd);
arenaMaster.initialize();
@ -317,4 +330,8 @@ public class MobArena extends JavaPlugin
public FormulaMacros getFormulaMacros() {
return macros;
}
public SavedItemsManager getSavedItemsManager() {
return itemman;
}
}

View File

@ -15,12 +15,15 @@ import com.garbagemule.MobArena.commands.setup.AutoGenerateCommand;
import com.garbagemule.MobArena.commands.setup.CheckDataCommand;
import com.garbagemule.MobArena.commands.setup.CheckSpawnsCommand;
import com.garbagemule.MobArena.commands.setup.ClassChestCommand;
import com.garbagemule.MobArena.commands.setup.DeleteItemCommand;
import com.garbagemule.MobArena.commands.setup.EditArenaCommand;
import com.garbagemule.MobArena.commands.setup.ListClassesCommand;
import com.garbagemule.MobArena.commands.setup.LoadItemCommand;
import com.garbagemule.MobArena.commands.setup.RemoveArenaCommand;
import com.garbagemule.MobArena.commands.setup.RemoveContainerCommand;
import com.garbagemule.MobArena.commands.setup.RemoveLeaderboardCommand;
import com.garbagemule.MobArena.commands.setup.RemoveSpawnpointCommand;
import com.garbagemule.MobArena.commands.setup.SaveItemCommand;
import com.garbagemule.MobArena.commands.setup.SettingCommand;
import com.garbagemule.MobArena.commands.setup.SetupCommand;
import com.garbagemule.MobArena.commands.user.ArenaListCommand;
@ -340,6 +343,10 @@ public class CommandHandler implements CommandExecutor, TabCompleter
register(RemoveLeaderboardCommand.class);
register(AutoGenerateCommand.class);
register(SaveItemCommand.class);
register(DeleteItemCommand.class);
register(LoadItemCommand.class);
}
/**

View File

@ -0,0 +1,60 @@
package com.garbagemule.MobArena.commands.setup;
import com.garbagemule.MobArena.commands.Command;
import com.garbagemule.MobArena.commands.CommandInfo;
import com.garbagemule.MobArena.framework.ArenaMaster;
import com.garbagemule.MobArena.items.SavedItemsManager;
import org.bukkit.ChatColor;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
@CommandInfo(
name = "delete-item",
pattern = "delete(-)?item",
usage = "/ma delete-item <identifier>",
desc = "delete the item with the given identifier",
permission = "mobarena.setup.deleteitem"
)
public class DeleteItemCommand implements Command {
@Override
public boolean execute(ArenaMaster am, CommandSender sender, String... args) {
if (args.length != 1) {
return false;
}
String key = args[0];
SavedItemsManager items = am.getPlugin().getSavedItemsManager();
try {
items.deleteItem(key);
} catch (Exception e) {
am.getGlobalMessenger().tell(sender, "Couldn't delete " + ChatColor.YELLOW + key + ChatColor.RESET + ", because: " + ChatColor.RED + e.getMessage());
return true;
}
am.getGlobalMessenger().tell(sender, "Saved item " + ChatColor.YELLOW + key + ChatColor.RESET + " deleted.");
return true;
}
@Override
public List<String> tab(ArenaMaster am, Player player, String... args) {
if (args.length > 1) {
return Collections.emptyList();
}
String prefix = args[0].toLowerCase();
SavedItemsManager items = am.getPlugin().getSavedItemsManager();
List<String> keys = items.getKeys();
return keys.stream()
.filter(key -> key.startsWith(prefix))
.collect(Collectors.toList());
}
}

View File

@ -0,0 +1,70 @@
package com.garbagemule.MobArena.commands.setup;
import com.garbagemule.MobArena.Msg;
import com.garbagemule.MobArena.commands.Command;
import com.garbagemule.MobArena.commands.CommandInfo;
import com.garbagemule.MobArena.commands.Commands;
import com.garbagemule.MobArena.framework.ArenaMaster;
import com.garbagemule.MobArena.items.SavedItemsManager;
import org.bukkit.ChatColor;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
@CommandInfo(
name = "load-item",
pattern = "load(-)?item",
usage = "/ma load-item <identifier>",
desc = "load the item saved by the given identifier into your hand",
permission = "mobarena.setup.loaditem"
)
public class LoadItemCommand implements Command {
@Override
public boolean execute(ArenaMaster am, CommandSender sender, String... args) {
if (!Commands.isPlayer(sender)) {
am.getGlobalMessenger().tell(sender, Msg.MISC_NOT_FROM_CONSOLE);
return true;
}
if (args.length != 1) {
return false;
}
String key = args[0];
SavedItemsManager items = am.getPlugin().getSavedItemsManager();
ItemStack stack = items.getItem(key);
if (stack == null) {
am.getGlobalMessenger().tell(sender, "No saved item with identifier " + ChatColor.YELLOW + key + ChatColor.RESET + " found.");
return true;
}
Player player = Commands.unwrap(sender);
player.getInventory().setItemInMainHand(stack);
am.getGlobalMessenger().tell(sender, "Saved item " + ChatColor.YELLOW + key + ChatColor.RESET + " loaded.");
return true;
}
@Override
public List<String> tab(ArenaMaster am, Player player, String... args) {
if (args.length > 1) {
return Collections.emptyList();
}
String prefix = args[0].toLowerCase();
SavedItemsManager items = am.getPlugin().getSavedItemsManager();
List<String> keys = items.getKeys();
return keys.stream()
.filter(key -> key.startsWith(prefix))
.collect(Collectors.toList());
}
}

View File

@ -0,0 +1,80 @@
package com.garbagemule.MobArena.commands.setup;
import com.garbagemule.MobArena.Msg;
import com.garbagemule.MobArena.commands.Command;
import com.garbagemule.MobArena.commands.CommandInfo;
import com.garbagemule.MobArena.commands.Commands;
import com.garbagemule.MobArena.framework.ArenaMaster;
import com.garbagemule.MobArena.items.SavedItemsManager;
import org.bukkit.ChatColor;
import org.bukkit.Material;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
@CommandInfo(
name = "save-item",
pattern = "save(-)?item",
usage = "/ma save-item <identifier>",
desc = "save the currently held item for use in the config-file",
permission = "mobarena.setup.saveitem"
)
public class SaveItemCommand implements Command {
@Override
public boolean execute(ArenaMaster am, CommandSender sender, String... args) {
if (!Commands.isPlayer(sender)) {
am.getGlobalMessenger().tell(sender, Msg.MISC_NOT_FROM_CONSOLE);
return true;
}
if (args.length != 1) {
return false;
}
String key = args[0];
if (!key.matches("^[\\p{IsAlphabetic}\\d_]+$")) {
am.getGlobalMessenger().tell(sender, "The identifier must contain only letters, numbers, and underscores");
return true;
}
Player player = Commands.unwrap(sender);
ItemStack stack = player.getInventory().getItemInMainHand();
if (stack.getType() == Material.AIR) {
am.getGlobalMessenger().tell(sender, "You must be holding an item.");
return true;
}
SavedItemsManager items = am.getPlugin().getSavedItemsManager();
try {
items.saveItem(key, stack);
} catch (Exception e) {
am.getGlobalMessenger().tell(sender, "Couldn't save " + ChatColor.YELLOW + key + ChatColor.RESET + ", because: " + ChatColor.RED + e.getMessage());
return true;
}
am.getGlobalMessenger().tell(sender, "Item saved as " + ChatColor.YELLOW + key + ChatColor.RESET + ".");
return true;
}
@Override
public List<String> tab(ArenaMaster am, Player player, String... args) {
if (args.length > 1) {
return Collections.emptyList();
}
String prefix = args[0].toLowerCase();
SavedItemsManager items = am.getPlugin().getSavedItemsManager();
List<String> keys = items.getKeys();
return keys.stream()
.filter(key -> key.startsWith(prefix))
.collect(Collectors.toList());
}
}

View File

@ -0,0 +1,117 @@
package com.garbagemule.MobArena.items;
import com.garbagemule.MobArena.MobArena;
import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.inventory.ItemStack;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Stream;
public class SavedItemsManager {
private static final String FOLDER_NAME = "items";
private static final String FILE_EXT = ".yml";
private static final String YAML_KEY = "item";
private final MobArena plugin;
private final Path folder;
private final Map<String, ItemStack> items;
public SavedItemsManager(MobArena plugin) {
this.plugin = plugin;
this.folder = plugin.getDataFolder().toPath().resolve(FOLDER_NAME);
this.items = new HashMap<>();
}
public void reload() {
try {
Files.createDirectories(folder);
} catch (Exception e) {
throw new IllegalStateException("Failed to create items folder", e);
}
loadItems(folder);
}
private void loadItems(Path folder) {
items.clear();
try (Stream<Path> candidates = Files.list(folder)) {
candidates.forEach(this::loadItem);
} catch (IOException e) {
throw new IllegalStateException("Failed to load saved items", e);
}
if (!items.isEmpty()) {
plugin.getLogger().info("Loaded " + items.size() + " saved item(s).");
}
}
private void loadItem(Path path) {
if (!Files.isRegularFile(path)) {
return;
}
String filename = path.getFileName().toString();
if (!filename.endsWith(FILE_EXT)) {
return;
}
YamlConfiguration yaml = new YamlConfiguration();
try {
yaml.load(path.toFile());
} catch (Exception e) {
throw new IllegalStateException("Failed to load saved item from " + path, e);
}
ItemStack stack = yaml.getItemStack(YAML_KEY);
if (stack == null) {
throw new IllegalStateException("No item found in saved item file " + path);
}
String key = filename.substring(0, filename.length() - FILE_EXT.length());
items.put(key, stack);
}
public List<String> getKeys() {
return new ArrayList<>(items.keySet());
}
public ItemStack getItem(String key) {
ItemStack stack = items.get(key);
if (stack == null) {
return null;
}
return stack.clone();
}
public void saveItem(String key, ItemStack stack) throws IOException {
Path file = folder.resolve(key + FILE_EXT);
Files.deleteIfExists(file);
YamlConfiguration yaml = new YamlConfiguration();
yaml.set(YAML_KEY, stack);
yaml.save(file.toFile());
items.put(key, stack.clone());
}
public void deleteItem(String key) throws IOException {
items.remove(key);
Path file = folder.resolve(key + FILE_EXT);
try {
Files.delete(file);
} catch (NoSuchFileException e) {
throw new IllegalArgumentException("File " + file + " not found");
}
}
}

View File

@ -0,0 +1,60 @@
package com.garbagemule.MobArena.things;
import com.garbagemule.MobArena.MobArena;
import com.garbagemule.MobArena.items.SavedItemsManager;
import org.bukkit.inventory.ItemStack;
class SavedItemParser implements ItemStackParser {
private final MobArena plugin;
SavedItemParser(MobArena plugin) {
this.plugin = plugin;
}
@Override
public ItemStack parse(String s) {
// Note that the saved items manager is not available during
// the load procedure, so we need to defer the call to the
// getter until we actually need it. We don't really have to
// check for null here, but without it, we would need to set
// up a dummy manager for the ThingManager test suite, which
// is a bit of a hassle.
SavedItemsManager items = plugin.getSavedItemsManager();
if (items == null) {
return null;
}
if (s.contains(":")) {
return parseWithAmount(s, items);
} else {
return parseSimple(s, items);
}
}
private ItemStack parseWithAmount(String s, SavedItemsManager items) {
String[] parts = s.split(":");
if (parts.length > 2) {
return null;
}
ItemStack stack = parseSimple(parts[0], items);
if (stack == null) {
return null;
}
try {
int amount = Integer.parseInt(parts[1]);
stack.setAmount(amount);
} catch (NumberFormatException e) {
return null;
}
return stack;
}
private ItemStack parseSimple(String s, SavedItemsManager items) {
return items.getItem(s);
}
}

View File

@ -18,6 +18,7 @@ public class ThingManager implements ThingParser {
parsers.add(new PotionEffectThingParser());
parsers.add(new InventoryThingParser(plugin.getServer()));
items = parser;
items.register(new SavedItemParser(plugin));
}
public ThingManager(MobArena plugin) {

View File

@ -85,6 +85,9 @@ permissions:
mobarena.setup.leaderboards: true
mobarena.setup.autogenerate: true
mobarena.setup.autodegenerate: true
mobarena.setup.saveitem: true
mobarena.setup.deleteitem: true
mobarena.setup.loaditem: true
mobarena.setup.config:
description: Save or reload the config-file
default: false
@ -130,3 +133,12 @@ permissions:
mobarena.setup.autodegenerate:
description: Auto-degenerate an arena.
default: false
mobarena.setup.saveitem:
description: Store the currently held item as a saved item.
default: false
mobarena.setup.deleteitem:
description: Delete a saved item.
default: false
mobarena.setup.loaditem:
description: Set a saved item as the currently held item.
default: false