Update from Git Manager GUI

This commit is contained in:
2026-02-05 14:37:34 +01:00
parent 113ce58f06
commit ba7c477cbc
104 changed files with 10074 additions and 0 deletions

View File

@@ -0,0 +1,71 @@
package io.github.mviper.bomberman;
import io.github.mviper.bomberman.events.BmGameListIntent;
import io.github.mviper.bomberman.events.BmGameLookupIntent;
import io.github.mviper.bomberman.messaging.Context;
import io.github.mviper.bomberman.messaging.Expander;
import io.github.mviper.bomberman.messaging.Formattable;
import io.github.mviper.bomberman.messaging.Message;
import io.github.mviper.bomberman.messaging.SenderWrapper;
import me.clip.placeholderapi.expansion.PlaceholderExpansion;
import org.bukkit.entity.Player;
public class BmPlaceholder extends PlaceholderExpansion {
@Override
public String getIdentifier() {
return "bomberman";
}
@Override
public String getAuthor() {
return "mviper";
}
@Override
public String getVersion() {
return "internal";
}
@Override
public boolean persist() {
return true;
}
@Override
public String onPlaceholderRequest(Player player, String params) {
String[] content = params.split("_");
String command = content.length > 0 ? content[0] : "";
switch (command) {
case "info": {
if (content.length < 2) {
return "info <name> <stat>";
}
String gameName = content[1];
Formattable game = BmGameLookupIntent.find(gameName);
if (game == null) {
return "";
}
Formattable format = game;
for (int i = 2; i < content.length; i++) {
format = format.applyModifier(Message.of(content[i]));
}
return format.format(new Context()).toString();
}
case "msg": {
try {
Context context = new Context(false)
.plus("games", BmGameListIntent.listGames());
if (player != null) {
context = context.plus("player", new SenderWrapper(player));
}
return Expander.expand(params.substring("msg_".length()), context).toString();
} catch (RuntimeException e) {
String message = e.getMessage() != null ? e.getMessage() : "Error";
return Message.error(message).toString();
}
}
default:
return "<info|msg> ...";
}
}
}

View File

@@ -0,0 +1,132 @@
package io.github.mviper.bomberman;
import io.github.mviper.bomberman.commands.BaseCommand;
import io.github.mviper.bomberman.game.GamePlayer;
import io.github.mviper.bomberman.game.GameSave;
import io.github.mviper.bomberman.game.GameSettings;
import io.github.mviper.bomberman.utils.DataRestorer;
import org.bukkit.Bukkit;
import org.bukkit.command.PluginCommand;
import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.configuration.serialization.ConfigurationSerialization;
import org.bukkit.event.Listener;
import org.bukkit.plugin.java.JavaPlugin;
import javax.annotation.Nonnull;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Objects;
public class Bomberman extends JavaPlugin implements Listener {
@SuppressWarnings("NotNullFieldNotInitialized")
@Nonnull
public static Bomberman instance;
@Override
public void onEnable() {
instance = this;
ConfigurationSerialization.registerClass(GameSettings.class);
ConfigurationSerialization.registerClass(DataRestorer.class, "io.github.mviper.bomberman.game.Game$BuildFlags");
getDataFolder().mkdirs();
BaseCommand bmCmd = new BaseCommand();
PluginCommand bukkitBmCmd = Objects.requireNonNull(getCommand("bomberman"));
bukkitBmCmd.setExecutor(bmCmd);
bukkitBmCmd.setTabCompleter(bmCmd);
if (Bukkit.getPluginManager().getPlugin("PlaceholderAPI") != null) {
new BmPlaceholder().register();
}
// Update the old file system
GameSave.updatePre080Saves();
// Archive the old schematics folder
File schematics = new File(getDataFolder(), "schematics");
if (schematics.exists()) {
File schemPath = new File(getDataFolder(), "old/schematics");
schemPath.getParentFile().mkdirs();
schematics.renameTo(schemPath);
}
// Archive the old config
FileConfiguration config = getConfig();
if (config.contains("default-game-settings")) {
File configFile = new File(getDataFolder(), "config.yml");
File configOut = new File(getDataFolder(), "old/config.yml");
configOut.getParentFile().mkdirs();
configFile.renameTo(configOut);
}
new File(getDataFolder(), "sample_config.yml").delete();
// Copy resources
saveResource("config.yml", true);
saveResource("messages.yml", false);
saveResource("default_messages.yml", true);
saveResource("games/templates/purple.game.zip", true);
saveResource("games/templates/experimental.game.zip", true);
saveResource("games/README.yml", true);
saveResource("games/templates/README.txt", true);
saveResource("temp/README.txt", true);
GameSave.loadGames();
GamePlayer.setupLoginWatcher();
}
public Path gameSaves() {
try {
Path dir = getDataFolder().toPath().resolve("games");
if (!Files.exists(dir)) {
Files.createDirectories(dir);
}
return dir;
} catch (IOException e) {
throw new RuntimeException("No write access", e);
}
}
public Path templates() {
try {
Path dir = getDataFolder().toPath().resolve("games/templates");
if (!Files.exists(dir)) {
Files.createDirectories(dir);
}
return dir;
} catch (IOException e) {
throw new RuntimeException("No write access", e);
}
}
public Path tempGameData() {
try {
Path dir = getDataFolder().toPath().resolve("temp/game");
if (!Files.exists(dir)) {
Files.createDirectories(dir);
}
return dir;
} catch (IOException e) {
throw new RuntimeException("No write access", e);
}
}
public Path tempPlayerData() {
try {
Path dir = getDataFolder().toPath().resolve("temp/player");
if (!Files.exists(dir)) {
Files.createDirectories(dir);
}
return dir;
} catch (IOException e) {
throw new RuntimeException("No write access", e);
}
}
public Path language() {
return getDataFolder().toPath().resolve("messages.yml");
}
}

View File

@@ -0,0 +1,234 @@
package io.github.mviper.bomberman.commands;
import io.github.mviper.bomberman.commands.game.Configure;
import io.github.mviper.bomberman.commands.game.GameCreate;
import io.github.mviper.bomberman.commands.game.GameDelete;
import io.github.mviper.bomberman.commands.game.GameInfo;
import io.github.mviper.bomberman.commands.game.GameJoin;
import io.github.mviper.bomberman.commands.game.GameLeave;
import io.github.mviper.bomberman.commands.game.GameList;
import io.github.mviper.bomberman.commands.game.GameReload;
import io.github.mviper.bomberman.commands.game.RunStart;
import io.github.mviper.bomberman.commands.game.RunStop;
import io.github.mviper.bomberman.commands.game.UndoBuild;
import io.github.mviper.bomberman.messaging.CollectionWrapper;
import io.github.mviper.bomberman.messaging.Context;
import io.github.mviper.bomberman.messaging.Formattable;
import io.github.mviper.bomberman.messaging.Message;
import io.github.mviper.bomberman.messaging.Text;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabCompleter;
import org.bukkit.util.StringUtil;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
public class BaseCommand extends Cmd implements TabCompleter, CommandExecutor {
private static final String F_HELP = "?";
private final List<Cmd> children = new ArrayList<>();
public BaseCommand() {
this(true);
}
public BaseCommand(boolean addChildren) {
super(null);
if (addChildren) {
addChildren(
new Configure(this),
new GameCreate(this),
new GameInfo(this),
new GameJoin(this),
new GameLeave(this),
new GameDelete(this),
new RunStart(this),
new RunStop(this),
new GameList(this),
new GameReload(this),
new UndoBuild(this)
);
}
}
public void addChildren(Cmd... children) {
this.children.addAll(Arrays.asList(children));
}
@Override
public Formattable name() {
return Message.of("bomberman");
}
@Override
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
FlagSplit split = separateFlags(args);
run(sender, split.arguments, split.flags);
return true;
}
@Override
public boolean run(CommandSender sender, List<String> args, Map<String, String> flags) {
if (args.isEmpty()) {
Text.COMMAND_GROUP_HELP.format(cmdContext().plus("sender", sender)).sendTo(sender);
return true;
}
Cmd child = children.stream()
.filter(c -> c.name().format(c.cmdContext()).toString().equalsIgnoreCase(args.get(0)))
.findFirst()
.orElse(null);
if (child == null) {
Text.UNKNOWN_COMMAND.format(cmdContext().plus("attempt", args.get(0))).sendTo(sender);
return true;
}
if (!child.permission().isAllowedBy(sender)) {
Text.DENY_PERMISSION.format(child.cmdContext()).sendTo(sender);
return true;
}
if (flags.containsKey(F_HELP)) {
Text.COMMAND_HELP.format(child.cmdContext().plus("sender", sender)).sendTo(sender);
return true;
}
boolean result = child.run(sender, args.subList(1, args.size()), flags);
if (!result) {
List<Formattable> attempt = args.subList(1, args.size()).stream()
.map(Message::of)
.collect(Collectors.toList());
Text.INCORRECT_USAGE.format(child.cmdContext().plus("attempt", new CollectionWrapper<>(attempt))).sendTo(sender);
}
return result;
}
@Override
public List<String> onTabComplete(CommandSender sender, Command command, String alias, String[] args) {
FlagSplit split = separateFlags(args);
List<String> arguments = split.arguments;
String currentlyTyping = args[args.length - 1];
Cmd cmd = children.stream()
.filter(c -> c.name().format(c.cmdContext()).toString().equalsIgnoreCase(arguments.isEmpty() ? "" : arguments.get(0)))
.findFirst()
.orElse(this);
if (!cmd.permission().isAllowedBy(sender)) {
return List.of();
}
List<String> allOptions;
if (currentlyTyping.startsWith("-")) {
int splitIndex = currentlyTyping.indexOf('=');
if (splitIndex == -1) {
allOptions = cmd.flags(sender).stream()
.map(flag -> "-" + flag)
.collect(Collectors.toList());
allOptions.add("-" + F_HELP);
} else {
String key = currentlyTyping.substring(1, splitIndex);
allOptions = cmd.flagOptions(key).stream()
.map(option -> "-" + key + "=" + option)
.toList();
}
} else {
allOptions = cmd.options(sender, arguments.size() > 0 ? arguments.subList(1, arguments.size()) : List.of());
}
return allOptions.stream()
.filter(option -> StringUtil.startsWithIgnoreCase(option, currentlyTyping))
.toList();
}
private FlagSplit separateFlags(String[] args) {
List<String> flagStrings = new ArrayList<>();
List<String> arguments = new ArrayList<>();
for (String arg : args) {
if (arg.startsWith("-")) {
flagStrings.add(arg);
} else {
arguments.add(arg);
}
}
Map<String, String> flags = flagStrings.stream().collect(Collectors.toMap(
flag -> {
int separator = flag.indexOf('=');
if (separator == -1) {
return flag.substring(1);
}
return flag.substring(1, separator);
},
flag -> {
int separator = flag.indexOf('=');
if (separator == -1) {
return "";
}
return flag.substring(separator + 1);
},
(a, b) -> b
));
return new FlagSplit(arguments, flags);
}
@Override
public List<String> options(CommandSender sender, List<String> args) {
if (args.size() <= 1) {
return children.stream()
.filter(cmd -> cmd.permission().isAllowedBy(sender))
.map(cmd -> cmd.name().format(cmd.cmdContext()).toString())
.collect(Collectors.toList());
}
return List.of();
}
@Override
public Permission permission() {
return Permissions.BASE;
}
@Override
public Formattable description() {
return Text.BOMBERMAN_DESCRIPTION;
}
@Override
public Formattable extra() {
return Text.COMMAND_GROUP_EXTRA;
}
@Override
public Formattable example() {
return Text.COMMAND_GROUP_EXAMPLE;
}
@Override
public Formattable usage() {
return Text.COMMAND_GROUP_USAGE;
}
@Override
public Formattable applyModifier(Message arg) {
if ("children".equalsIgnoreCase(arg.toString())) {
return new CollectionWrapper<>(children);
}
return super.applyModifier(arg);
}
private static final class FlagSplit {
private final List<String> arguments;
private final Map<String, String> flags;
private FlagSplit(List<String> arguments, Map<String, String> flags) {
this.arguments = arguments;
this.flags = flags;
}
}
}

View File

@@ -0,0 +1,134 @@
package io.github.mviper.bomberman.commands;
import io.github.mviper.bomberman.messaging.CollectionWrapper;
import io.github.mviper.bomberman.messaging.Context;
import io.github.mviper.bomberman.messaging.Formattable;
import io.github.mviper.bomberman.messaging.Message;
import org.bukkit.Bukkit;
import org.bukkit.command.CommandSender;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Set;
public abstract class Cmd implements Formattable {
protected Cmd parent;
protected Cmd(Cmd parent) {
this.parent = parent;
}
public abstract Formattable name();
public abstract List<String> options(CommandSender sender, List<String> args);
public Set<String> flags(CommandSender sender) {
return Collections.emptySet();
}
public Set<String> flagOptions(String flag) {
return Collections.emptySet();
}
public Formattable flagExtension(String flag) {
return Message.empty;
}
public Formattable flagDescription(String flag) {
return Message.empty;
}
public abstract boolean run(CommandSender sender, List<String> args, java.util.Map<String, String> flags);
public abstract Formattable extra();
public abstract Formattable example();
public abstract Formattable description();
public abstract Formattable usage();
public abstract Permission permission();
private String path(String separator) {
StringBuilder path = new StringBuilder();
if (parent != null) {
path.append(parent.path(separator)).append(separator);
}
path.append(name().format(new Context()).toString());
return path.toString();
}
private String path() {
return path(" ");
}
public Context cmdContext() {
return new Context(false).plus("command", this);
}
@Override
public Message format(Context context) {
return applyModifier(Message.of("name")).format(context);
}
@Override
public Formattable applyModifier(Message arg) {
String key = arg.toString().toLowerCase(Locale.ROOT);
switch (key) {
case "name":
return name();
case "path":
return Message.of(path());
case "usage":
return usage();
case "extra":
return extra();
case "example":
return example();
case "description":
return description();
case "permission":
return Message.of(permission().value());
case "flags": {
List<Formattable> wrapped = new ArrayList<>();
for (String flag : flags(Bukkit.getConsoleSender())) {
wrapped.add(new FlagWrapper(flag));
}
return new CollectionWrapper<>(wrapped);
}
default:
throw new IllegalArgumentException("Unknown command value '" + arg + "'");
}
}
private final class FlagWrapper implements Formattable {
private final String flag;
private FlagWrapper(String flag) {
this.flag = flag;
}
@Override
public Formattable applyModifier(Message arg) {
String key = arg.toString().toLowerCase(Locale.ROOT);
switch (key) {
case "name":
return Message.of(flag);
case "ext":
return flagExtension(flag);
case "description":
return flagDescription(flag);
default:
throw new IllegalArgumentException("Unknown flag value '" + arg + "'`");
}
}
@Override
public Message format(Context context) {
return applyModifier(Message.of("name")).format(context);
}
}
}

View File

@@ -0,0 +1,38 @@
package io.github.mviper.bomberman.commands;
import io.github.mviper.bomberman.events.BmGameListIntent;
import io.github.mviper.bomberman.events.BmGameLookupIntent;
import io.github.mviper.bomberman.game.Game;
import org.bukkit.command.CommandSender;
import java.util.List;
public abstract class GameCommand extends Cmd {
protected GameCommand(Cmd parent) {
super(parent);
}
@Override
public List<String> options(CommandSender sender, List<String> args) {
if (args.size() <= 1) {
return BmGameListIntent.listGames().stream().map(Game::getName).toList();
}
return gameOptions(args.subList(1, args.size()));
}
protected abstract List<String> gameOptions(List<String> args);
@Override
public boolean run(CommandSender sender, List<String> args, java.util.Map<String, String> flags) {
if (args.isEmpty()) {
return false;
}
Game game = BmGameLookupIntent.find(args.get(0));
if (game == null) {
return false;
}
return gameRun(sender, args.subList(1, args.size()), flags, game);
}
protected abstract boolean gameRun(CommandSender sender, List<String> args, java.util.Map<String, String> flags, Game game);
}

View File

@@ -0,0 +1,9 @@
package io.github.mviper.bomberman.commands;
import org.bukkit.command.CommandSender;
public interface Permission {
boolean isAllowedBy(CommandSender sender);
String value();
}

View File

@@ -0,0 +1,36 @@
package io.github.mviper.bomberman.commands;
import org.bukkit.command.CommandSender;
public enum Permissions implements Permission {
BASE("bomberman.bm"),
CREATE("bomberman.create"),
DELETE("bomberman.delete"),
UNDO("bomberman.undo"),
RELOAD("bomberman.reload"),
CONFIGURE("bomberman.configure"),
START("bomberman.start"),
STOP("bomberman.stop"),
INFO("bomberman.info"),
LIST("bomberman.list"),
JOIN("bomberman.join"),
JOIN_REMOTE("bomberman.join.remote"),
LEAVE("bomberman.leave"),
LEAVE_REMOTE("bomberman.leave.remote");
private final String permission;
Permissions(String permission) {
this.permission = permission;
}
@Override
public boolean isAllowedBy(CommandSender sender) {
return sender.hasPermission(permission);
}
@Override
public String value() {
return permission;
}
}

View File

@@ -0,0 +1,652 @@
package io.github.mviper.bomberman.commands.game;
import io.github.mviper.bomberman.commands.Cmd;
import io.github.mviper.bomberman.commands.GameCommand;
import io.github.mviper.bomberman.commands.Permission;
import io.github.mviper.bomberman.commands.Permissions;
import io.github.mviper.bomberman.game.Game;
import io.github.mviper.bomberman.game.GameSettingsBuilder;
import io.github.mviper.bomberman.messaging.Formattable;
import io.github.mviper.bomberman.messaging.Text;
import org.bukkit.GameMode;
import org.bukkit.Material;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;
public class Configure extends GameCommand {
public Configure(Cmd parent) {
super(parent);
}
@Override
public Formattable name() {
return Text.CONFIGURE_NAME;
}
@Override
protected List<String> gameOptions(List<String> args) {
return List.of();
}
@Override
public Permission permission() {
return Permissions.CONFIGURE;
}
@Override
public Formattable extra() {
return Text.CONFIGURE_EXTRA;
}
@Override
public Formattable example() {
return Text.CONFIGURE_EXAMPLE;
}
@Override
public Formattable description() {
return Text.CONFIGURE_DESCRIPTION;
}
@Override
public Formattable usage() {
return Text.CONFIGURE_USAGE;
}
@Override
protected boolean gameRun(CommandSender sender, List<String> args, Map<String, String> flags, Game game) {
if (!args.isEmpty()) {
return false;
}
if (!(sender instanceof Player player)) {
Text.MUST_BE_PLAYER.format(cmdContext()).sendTo(sender);
return true;
}
if (player.getGameMode() != GameMode.CREATIVE) {
Text.CONFIGURE_PROMPT_CREATIVE.format(cmdContext()).sendTo(sender);
return true;
}
showMainMenu(player, game);
return true;
}
private void showMainMenu(Player player, Game game) {
GuiBuilder.show(
player,
Text.CONFIGURE_TITLE_MAIN.format(cmdContext()).toString(),
new CharSequence[]{
" ",
" s b l i ",
" "
},
index -> {
return switch (index.getSection()) {
case 's' -> new GuiBuilder.ItemSlot(Material.REDSTONE)
.unMovable()
.displayName(Text.CONFIGURE_TITLE_GENERAL.format(cmdContext()).toString());
case 'b' -> new GuiBuilder.ItemSlot(Material.DIRT)
.unMovable()
.displayName(Text.CONFIGURE_TITLE_BLOCKS.format(cmdContext()).toString());
case 'l' -> new GuiBuilder.ItemSlot(Material.GOLDEN_APPLE)
.unMovable()
.displayName(Text.CONFIGURE_TITLE_LOOT.format(cmdContext()).toString());
case 'i' -> new GuiBuilder.ItemSlot(Material.CHEST)
.unMovable()
.displayName(Text.CONFIGURE_TITLE_INVENTORY.format(cmdContext()).toString());
default -> GuiBuilder.blank;
};
},
(index, currentItem, cursorItem) -> {
switch (index.getSection()) {
case 's' -> showGeneralConfig(player, game);
case 'b' -> showBlockSettings(player, game);
case 'l' -> showLootSettings(player, game);
case 'i' -> showInventorySettings(player, game);
default -> {
}
}
},
slots -> {
}
);
}
private void showGeneralConfig(Player player, Game game) {
GameSettingsBuilder settings = new GameSettingsBuilder(game.getSettings());
GuiBuilder.show(
player,
Text.CONFIGURE_TITLE_GENERAL.format(cmdContext()).toString(),
new CharSequence[]{
" ^^^^ ",
"< lfbitg ",
" vvvv "
},
index -> {
index.getInventory().setMaxStackSize(100000);
return switch (index.getSection()) {
case '<' -> new GuiBuilder.ItemSlot(Material.PAPER)
.unMovable()
.displayName(stringify(Text.CONFIGURE_BACK));
case 'l' -> new GuiBuilder.ItemSlot(setQty(new ItemStack(Material.PLAYER_HEAD), settings.lives))
.unMovable()
.displayName(stringify(Text.CONFIGURE_LIVES));
case 'f' -> new GuiBuilder.ItemSlot(setQty(new ItemStack(Material.TNT), settings.fuseTicks))
.unMovable()
.displayName(stringify(Text.CONFIGURE_FUSE_TICKS));
case 'b' -> new GuiBuilder.ItemSlot(setQty(new ItemStack(Material.FLINT_AND_STEEL), settings.fireTicks))
.unMovable()
.displayName(stringify(Text.CONFIGURE_FIRE_TICKS));
case 'i' -> new GuiBuilder.ItemSlot(setQty(new ItemStack(Material.MILK_BUCKET), settings.immunityTicks))
.unMovable()
.displayName(stringify(Text.CONFIGURE_IMMUNITY_TICKS));
case 't' -> new GuiBuilder.ItemSlot(settings.bombItem)
.unMovable()
.displayName(stringify(Text.CONFIGURE_TNT_BLOCK));
case 'g' -> new GuiBuilder.ItemSlot(settings.powerItem)
.unMovable()
.displayName(stringify(Text.CONFIGURE_FIRE_ITEM));
case '^' -> new GuiBuilder.ItemSlot(Material.STONE_BUTTON)
.unMovable()
.displayName("+");
case 'v' -> new GuiBuilder.ItemSlot(Material.STONE_BUTTON)
.unMovable()
.displayName("-");
default -> GuiBuilder.blank;
};
},
(index, slot, cursor) -> {
index.getInventory().setMaxStackSize(100000);
switch (index.getSection()) {
case '<' -> showMainMenu(player, game);
case '^' -> {
ItemStack item = index.getInventory().getItem(index.getInvIndex() + 9);
if (item != null) {
setQty(item, item.getAmount() + 1);
}
}
case 'v' -> {
ItemStack item = index.getInventory().getItem(index.getInvIndex() - 9);
if (item != null) {
setQty(item, Math.max(item.getAmount() - 1, 1));
}
}
case 't', 'g' -> {
if (cursor != null && cursor.getAmount() != 0 && slot != null) {
slot.setType(cursor.getType());
}
}
default -> {
}
}
},
slots -> {
for (GuiBuilder.SlotItem slotItem : slots) {
GuiBuilder.Index index = slotItem.getIndex();
ItemStack item = slotItem.getItem();
if (item == null) {
continue;
}
switch (index.getSection()) {
case 'l' -> settings.lives = item.getAmount();
case 'f' -> settings.fuseTicks = item.getAmount();
case 'b' -> settings.fireTicks = item.getAmount();
case 'i' -> settings.immunityTicks = item.getAmount();
case 't' -> settings.bombItem = item.getType();
case 'g' -> settings.powerItem = item.getType();
default -> {
}
}
}
game.setSettings(settings.build());
}
);
}
private void showBlockSettings(Player player, Game game) {
GameSettingsBuilder builder = new GameSettingsBuilder(game.getSettings());
showBlockSettings(
player,
game,
builder,
0,
stringify(Text.CONFIGURE_DESTRUCTIBLE_DESC),
game.getSettings().getDestructible(),
materials -> builder.destructible = materials
);
}
private void showBlockSettings(
Player player,
Game game,
GameSettingsBuilder builder,
int selected,
String description,
Set<Material> types,
Consumer<Set<Material>> result
) {
List<Material> typesList = types.stream()
.filter(Material::isItem)
.filter(Material::isBlock)
.toList();
GuiBuilder.show(
player,
Text.CONFIGURE_TITLE_BLOCKS.format(cmdContext()).toString(),
new CharSequence[]{
"<d s p n ",
" c cEc c ",
"iiiiiiiii",
"iiiiiiiii"
},
index -> {
return switch (index.getSection()) {
case 'E' -> new GuiBuilder.ItemSlot(Material.EMERALD)
.unMovable()
.displayName(description);
case '<' -> new GuiBuilder.ItemSlot(Material.PAPER)
.unMovable()
.displayName(stringify(Text.CONFIGURE_BACK));
case 'd' -> new GuiBuilder.ItemSlot(Material.DIRT)
.unMovable()
.displayName(stringify(Text.CONFIGURE_DESTRUCTIBLE));
case 's' -> new GuiBuilder.ItemSlot(Material.OBSIDIAN)
.unMovable()
.displayName(stringify(Text.CONFIGURE_INDESTRUCTIBLE));
case 'p' -> new GuiBuilder.ItemSlot(Material.OXEYE_DAISY)
.unMovable()
.displayName(stringify(Text.CONFIGURE_PASS_DESTROY));
case 'n' -> new GuiBuilder.ItemSlot(Material.OAK_SIGN)
.unMovable()
.displayName(stringify(Text.CONFIGURE_PASS_KEEP));
case 'c' -> {
if (index.getSecIndex() == selected) {
yield new GuiBuilder.ItemSlot(Material.YELLOW_STAINED_GLASS_PANE)
.unMovable()
.displayName(" ");
}
yield GuiBuilder.blank;
}
case 'i' -> {
Material mat = index.getSecIndex() < typesList.size() ? typesList.get(index.getSecIndex()) : null;
if (mat == null) {
yield new GuiBuilder.ItemSlot((ItemStack) null);
}
yield new GuiBuilder.ItemSlot(mat);
}
default -> GuiBuilder.blank;
};
},
(index, currentItem, cursorItem) -> {
switch (index.getSection()) {
case '<' -> showMainMenu(player, game);
case 'd' -> {
if (selected != 0) {
showBlockSettings(
player,
game,
builder,
0,
stringify(Text.CONFIGURE_DESTRUCTIBLE_DESC),
builder.destructible,
materials -> builder.destructible = materials
);
}
}
case 's' -> {
if (selected != 1) {
showBlockSettings(
player,
game,
builder,
1,
stringify(Text.CONFIGURE_INDESTRUCTIBLE_DESC),
builder.indestructible,
materials -> builder.indestructible = materials
);
}
}
case 'p' -> {
if (selected != 2) {
showBlockSettings(
player,
game,
builder,
2,
stringify(Text.CONFIGURE_PASS_DESTROY_DESC),
builder.passDestroy,
materials -> builder.passDestroy = materials
);
}
}
case 'n' -> {
if (selected != 3) {
showBlockSettings(
player,
game,
builder,
3,
stringify(Text.CONFIGURE_PASS_KEEP_DESC),
builder.passKeep,
materials -> builder.passKeep = materials
);
}
}
default -> {
}
}
},
slots -> {
Set<Material> blocks = new HashSet<>();
for (GuiBuilder.SlotItem slotItem : slots) {
GuiBuilder.Index index = slotItem.getIndex();
ItemStack stack = slotItem.getItem();
if (index.getSection() == 'i' && stack != null) {
blocks.addAll(expandSimilarMaterials(stack.getType()));
}
}
result.accept(blocks);
game.setSettings(builder.build());
}
);
}
private List<Material> expandSimilarMaterials(Material mat) {
String wallVariant = mat.getKey().getKey()
.replace("sign", "wall_sign")
.replace("banner", "wall_banner")
.replace("fan", "wall_fan")
.replace("torch", "wall_torch")
.replace("head", "wall_head")
.replace("skull", "skull_head");
Material wallType = Material.matchMaterial(wallVariant);
if (wallType == null) {
return List.of(mat);
}
return List.of(mat, wallType);
}
private void showInventorySettings(Player player, Game game) {
GuiBuilder.show(
player,
stringify(Text.CONFIGURE_TITLE_INVENTORY),
new CharSequence[]{
"< HCLBS ",
" aaaas ",
"iiiiiiiii",
"iiiiiiiii",
"iiiiiiiii",
"hhhhhhhhh"
},
index -> {
List<ItemStack> initialItems = new ArrayList<>(game.getSettings().getInitialItems());
while (initialItems.size() < 9 * 4 + 5) {
initialItems.add(null);
}
return switch (index.getSection()) {
case '<' -> new GuiBuilder.ItemSlot(Material.PAPER)
.unMovable()
.displayName(stringify(Text.CONFIGURE_BACK));
case 'a' -> new GuiBuilder.ItemSlot(initialItems.get((3 - index.getSecIndex()) + 9 * 4));
case 's' -> new GuiBuilder.ItemSlot(initialItems.get(9 * 4 + 4));
case 'h' -> new GuiBuilder.ItemSlot(initialItems.get(index.getSecIndex()));
case 'i' -> new GuiBuilder.ItemSlot(initialItems.get(index.getSecIndex() + 9));
case 'H' -> new GuiBuilder.ItemSlot(Material.IRON_HELMET)
.unMovable()
.hideAttributes();
case 'C' -> new GuiBuilder.ItemSlot(Material.IRON_CHESTPLATE)
.unMovable()
.hideAttributes();
case 'L' -> new GuiBuilder.ItemSlot(Material.IRON_LEGGINGS)
.unMovable()
.hideAttributes();
case 'B' -> new GuiBuilder.ItemSlot(Material.IRON_BOOTS)
.unMovable()
.hideAttributes();
case 'S' -> new GuiBuilder.ItemSlot(Material.SHIELD)
.unMovable()
.hideAttributes();
default -> GuiBuilder.blank;
};
},
(index, currentItem, cursorItem) -> {
if (index.getSection() == '<') {
showMainMenu(player, game);
}
},
slots -> {
ItemStack[] closingItems = new ItemStack[9 * 4 + 5];
for (GuiBuilder.SlotItem slotItem : slots) {
GuiBuilder.Index index = slotItem.getIndex();
ItemStack item = slotItem.getItem();
switch (index.getSection()) {
case 'a' -> closingItems[9 * 4 + (3 - index.getSecIndex())] = item;
case 's' -> closingItems[9 * 4 + 4] = item;
case 'h' -> closingItems[index.getSecIndex()] = item;
case 'i' -> closingItems[9 + index.getSecIndex()] = item;
default -> {
}
}
}
GameSettingsBuilder builder = new GameSettingsBuilder(game.getSettings());
builder.initialItems = Arrays.asList(closingItems);
game.setSettings(builder.build());
}
);
}
private void showLootSettings(Player player, Game game) {
Map<Material, Map<ItemStack, Integer>> gameLoot = game.getSettings().getBlockLoot();
Map<Map<ItemStack, Integer>, Set<Material>> lootBlock = new HashMap<>();
for (Map.Entry<Material, Map<ItemStack, Integer>> entry : gameLoot.entrySet()) {
lootBlock.computeIfAbsent(entry.getValue(), key -> new HashSet<>()).add(entry.getKey());
}
List<LootSlot> blockLoot = new ArrayList<>();
for (Map.Entry<Map<ItemStack, Integer>, Set<Material>> entry : lootBlock.entrySet()) {
Map<ItemStack, Integer> loot = entry.getKey();
Set<Material> matList = entry.getValue();
List<WeightedItem> lootList = new ArrayList<>();
for (Map.Entry<ItemStack, Integer> lootEntry : loot.entrySet()) {
ItemStack stack = lootEntry.getKey();
int weight = lootEntry.getValue();
int brokenWeight = weight;
do {
lootList.add(new WeightedItem(stack, Math.min(brokenWeight, 64)));
brokenWeight -= 64;
} while (brokenWeight > 0);
}
blockLoot.add(new LootSlot(new ArrayList<>(matList), lootList));
}
showLootSettings(player, game, 0, blockLoot);
}
private void showLootSettings(Player player, Game game, int slot, List<LootSlot> loot) {
GuiBuilder.show(
player,
stringify(Text.CONFIGURE_TITLE_LOOT),
new CharSequence[]{
"<SSSSSSSS",
"Kkkkkkkkk",
" ",
"Vvvvvvvvv",
"Wwwwwwwww"
},
index -> {
LootSlot selected = slot < loot.size() ? loot.get(slot) : null;
return switch (index.getSection()) {
case '<' -> new GuiBuilder.ItemSlot(Material.PAPER)
.unMovable()
.displayName(stringify(Text.CONFIGURE_BACK));
case 'S' -> {
Material icon;
if (index.getSecIndex() == slot) {
icon = Material.YELLOW_CONCRETE;
} else if (index.getSecIndex() >= loot.size()) {
icon = Material.GRAY_CONCRETE;
} else {
LootSlot candidate = loot.get(index.getSecIndex());
if (candidate.materials.isEmpty() && candidate.items.isEmpty()) {
icon = Material.GRAY_CONCRETE;
} else {
icon = Material.WHITE_CONCRETE;
}
}
yield new GuiBuilder.ItemSlot(icon)
.unMovable()
.displayName(Text.CONFIGURE_LOOT_SLOT.format(cmdContext().plus("slot", index.getSecIndex())).toString());
}
case 'K' -> new GuiBuilder.ItemSlot(Material.EMERALD)
.unMovable()
.displayName(Text.CONFIGURE_LOOT_BLOCK.format(cmdContext().plus("slot", slot)).toString());
case 'k' -> {
ItemStack stack = null;
if (selected != null && index.getSecIndex() < selected.materials.size()) {
stack = new ItemStack(selected.materials.get(index.getSecIndex()));
}
yield new GuiBuilder.ItemSlot(stack);
}
case 'V' -> new GuiBuilder.ItemSlot(Material.EMERALD)
.unMovable()
.displayName(Text.CONFIGURE_LOOT_ITEM.format(cmdContext().plus("slot", slot)).toString());
case 'v' -> {
ItemStack stack = null;
if (selected != null && index.getSecIndex() < selected.items.size()) {
stack = selected.items.get(index.getSecIndex()).item;
}
yield new GuiBuilder.ItemSlot(stack);
}
case 'W' -> new GuiBuilder.ItemSlot(Material.EMERALD)
.unMovable()
.displayName(Text.CONFIGURE_LOOT_WEIGHT.format(cmdContext().plus("slot", slot)).toString());
case 'w' -> {
int weight = 0;
if (selected != null && index.getSecIndex() < selected.items.size()) {
weight = selected.items.get(index.getSecIndex()).weight;
}
yield new GuiBuilder.ItemSlot(Material.GOLD_NUGGET, weight);
}
default -> GuiBuilder.blank;
};
},
(index, currentItem, cursorItem) -> {
if (index.getSection() == '<') {
showMainMenu(player, game);
} else if (index.getSection() == 'S') {
showLootSettings(player, game, index.getSecIndex(), loot);
}
},
slots -> {
List<Material> matsSaved = new ArrayList<>();
Map<ItemStack, AtomicInteger> itemsSaved = new HashMap<>();
for (GuiBuilder.SlotItem slotItem : slots) {
GuiBuilder.Index index = slotItem.getIndex();
ItemStack item = slotItem.getItem();
switch (index.getSection()) {
case 'k' -> {
if (item != null) {
matsSaved.add(item.getType());
}
}
case 'v' -> {
int weight = 0;
ItemStack weightItem = index.getInventory().getItem(index.getInvIndex() + 9);
if (weightItem != null) {
weight = weightItem.getAmount();
}
itemsSaved.computeIfAbsent(item, key -> new AtomicInteger(0)).addAndGet(weight);
}
default -> {
}
}
}
while (loot.size() <= slot) {
loot.add(new LootSlot(new ArrayList<>(), new ArrayList<>()));
}
List<WeightedItem> itemsList = new ArrayList<>();
for (Map.Entry<ItemStack, AtomicInteger> entry : itemsSaved.entrySet()) {
ItemStack stack = entry.getKey();
int weight = entry.getValue().get();
if (weight == 0 && stack == null) {
continue;
}
ItemStack resolved = stack != null ? stack : new ItemStack(Material.AIR, 0);
itemsList.add(new WeightedItem(resolved, weight));
}
loot.set(slot, new LootSlot(matsSaved, itemsList));
GameSettingsBuilder builder = new GameSettingsBuilder(game.getSettings());
builder.blockLoot = toBlockLootMap(loot);
game.setSettings(builder.build());
}
);
}
private Map<Material, Map<ItemStack, Integer>> toBlockLootMap(List<LootSlot> loot) {
Map<Material, Map<ItemStack, Integer>> result = new HashMap<>();
for (LootSlot slot : loot) {
Map<ItemStack, Integer> itemWeights = new HashMap<>();
for (WeightedItem item : slot.items) {
itemWeights.put(item.item, item.weight);
}
for (Material mat : slot.materials) {
for (Material expanded : expandSimilarMaterials(mat)) {
result.put(expanded, new HashMap<>(itemWeights));
}
}
}
return result;
}
private ItemStack setQty(ItemStack itemStack, int amount) {
itemStack.setAmount(amount);
ItemMeta meta = itemStack.getItemMeta();
if (meta != null) {
meta.setLore(List.of(String.valueOf(amount)));
itemStack.setItemMeta(meta);
}
return itemStack;
}
private String stringify(Text text) {
return text.format(cmdContext()).toString();
}
private static final class WeightedItem {
private final ItemStack item;
private final int weight;
private WeightedItem(ItemStack item, int weight) {
this.item = item;
this.weight = weight;
}
}
private static final class LootSlot {
private final List<Material> materials;
private final List<WeightedItem> items;
private LootSlot(List<Material> materials, List<WeightedItem> items) {
this.materials = materials;
this.items = items;
}
}
}

View File

@@ -0,0 +1,189 @@
package io.github.mviper.bomberman.commands.game;
import io.github.mviper.bomberman.Bomberman;
import io.github.mviper.bomberman.commands.Cmd;
import io.github.mviper.bomberman.commands.Permission;
import io.github.mviper.bomberman.commands.Permissions;
import io.github.mviper.bomberman.messaging.Formattable;
import io.github.mviper.bomberman.messaging.Message;
import org.bukkit.Bukkit;
import org.bukkit.ChatColor;
import org.bukkit.command.CommandSender;
import org.bukkit.event.HandlerList;
import org.bukkit.plugin.RegisteredListener;
import org.bukkit.scheduler.BukkitTask;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.atomic.AtomicReference;
public class DevInfo extends Cmd {
public DevInfo(Cmd parent) {
super(parent);
}
@Override
public Formattable name() {
return Message.of("dev");
}
@Override
public List<String> options(CommandSender sender, List<String> args) {
return List.of(
"handlerlist",
"handlercount",
"handlerwatch",
"nocancelled",
"tasklist",
"taskcount",
"taskwatch",
"watch",
"permissions"
);
}
@Override
public boolean run(CommandSender sender, List<String> args, java.util.Map<String, String> flags) {
String action = args.isEmpty() ? "" : args.get(0).toLowerCase(Locale.ROOT);
switch (action) {
case "watch" -> {
run(sender, List.of("nocancelled"), flags);
run(sender, List.of("handlerwatch"), flags);
run(sender, List.of("taskwatch"), flags);
run(sender, List.of("taskcount"), flags);
run(sender, List.of("handlercount"), flags);
return true;
}
case "handlerlist" -> {
String filter = flags.getOrDefault("class", "");
var handlers = HandlerList.getRegisteredListeners(Bomberman.instance).stream()
.map(listener -> listener.getListener())
.filter(listener -> listener.getClass().getName().contains(filter))
.toList();
sender.sendMessage(handlers.toString());
return true;
}
case "handlercount" -> {
var handlers = HandlerList.getRegisteredListeners(Bomberman.instance);
sender.sendMessage("Handlers: " + handlers.size());
return true;
}
case "nocancelled" -> {
Bukkit.getScheduler().scheduleSyncRepeatingTask(Bomberman.instance, () -> {
HandlerList.getRegisteredListeners(Bomberman.instance).forEach(listener -> {
if (!listener.isIgnoringCancelled()) {
sender.sendMessage("Not watching for cancelled: " + listener.getListener());
}
});
}, 1L, 1L);
sender.sendMessage("Watching for handlers watching for ignored events");
return true;
}
case "handlerwatch" -> {
AtomicReference<List<RegisteredListener>> handlersRef = new AtomicReference<>(
HandlerList.getRegisteredListeners(Bomberman.instance).stream().toList()
);
int startCount = handlersRef.get().size();
Bukkit.getScheduler().scheduleSyncRepeatingTask(Bomberman.instance, () -> {
List<RegisteredListener> updated = HandlerList.getRegisteredListeners(Bomberman.instance).stream().toList();
List<RegisteredListener> handlers = handlersRef.get();
var added = updated.stream().filter(h -> !handlers.contains(h)).toList();
var removed = handlers.stream().filter(h -> !updated.contains(h)).toList();
for (var handler : added) {
sender.sendMessage(" " + ChatColor.RED + "+" + ChatColor.RESET + " " + handler.getListener());
}
for (var handler : removed) {
sender.sendMessage(" " + ChatColor.GREEN + "-" + ChatColor.RESET + " " + handler.getListener());
}
if (handlers.size() != updated.size()) {
int countDiff = updated.size() - startCount;
sender.sendMessage((countDiff > 0 ? ChatColor.RED : ChatColor.GREEN) + " " + countDiff + " handlers");
}
handlersRef.set(updated);
}, 1L, 1L);
sender.sendMessage("Watching for new handlers");
return true;
}
case "tasklist" -> {
Bukkit.getScheduler().getPendingTasks().forEach(task -> {
if (task.getOwner() == Bomberman.instance) {
sender.sendMessage("Task with id: " + task.getTaskId());
}
});
return true;
}
case "taskcount" -> {
long count = Bukkit.getScheduler().getPendingTasks().stream()
.filter(task -> task.getOwner() == Bomberman.instance)
.count();
sender.sendMessage("Tasks: " + count);
return true;
}
case "taskwatch" -> {
AtomicReference<List<BukkitTask>> tasksRef = new AtomicReference<>(
Bukkit.getScheduler().getPendingTasks().stream()
.filter(task -> task.getOwner() == Bomberman.instance)
.toList()
);
int startCount = tasksRef.get().size() + 1;
Bukkit.getScheduler().scheduleSyncRepeatingTask(Bomberman.instance, () -> {
List<BukkitTask> updated = Bukkit.getScheduler().getPendingTasks().stream()
.filter(task -> task.getOwner() == Bomberman.instance)
.toList();
List<BukkitTask> tasks = tasksRef.get();
var added = updated.stream().filter(t -> !tasks.contains(t)).toList();
var removed = tasks.stream().filter(t -> !updated.contains(t)).toList();
for (var task : added) {
sender.sendMessage(" " + ChatColor.RED + "+" + ChatColor.RESET + " " + task.getTaskId());
}
for (var task : removed) {
sender.sendMessage(" " + ChatColor.GREEN + "-" + ChatColor.RESET + " " + task.getTaskId());
}
if (tasks.size() != updated.size()) {
int countDiff = updated.size() - startCount;
sender.sendMessage((countDiff > 0 ? ChatColor.RED : ChatColor.GREEN) + " " + countDiff + " tasks");
}
tasksRef.set(updated);
}, 1L, 1L);
sender.sendMessage("Watching for new tasks");
return true;
}
case "permissions" -> {
if (args.size() == 1) {
sender.getEffectivePermissions().forEach(perm -> sender.sendMessage(" - " + perm.getPermission()));
} else {
sender.sendMessage(args.get(1) + " : " + sender.hasPermission(args.get(1)));
}
return true;
}
default -> {
return false;
}
}
}
@Override
public Permission permission() {
return Permissions.BASE;
}
@Override
public Formattable example() {
return Message.empty;
}
@Override
public Formattable extra() {
return Message.empty;
}
@Override
public Formattable description() {
return Message.of("Dev commands. Was meant to remove this before shipping");
}
@Override
public Formattable usage() {
return Message.empty;
}
}

View File

@@ -0,0 +1,291 @@
package io.github.mviper.bomberman.commands.game;
import com.sk89q.worldedit.WorldEdit;
import com.sk89q.worldedit.bukkit.BukkitAdapter;
import com.sk89q.worldedit.extent.clipboard.io.BuiltInClipboardFormat;
import com.sk89q.worldedit.extent.clipboard.io.ClipboardFormats;
import com.sk89q.worldedit.session.SessionOwner;
import io.github.mviper.bomberman.Bomberman;
import io.github.mviper.bomberman.commands.Cmd;
import io.github.mviper.bomberman.commands.Permission;
import io.github.mviper.bomberman.commands.Permissions;
import io.github.mviper.bomberman.events.BmGameListIntent;
import io.github.mviper.bomberman.events.BmGameLookupIntent;
import io.github.mviper.bomberman.game.Game;
import io.github.mviper.bomberman.game.GameSave;
import io.github.mviper.bomberman.game.GameSettings;
import io.github.mviper.bomberman.game.GameSettingsBuilder;
import io.github.mviper.bomberman.messaging.Formattable;
import io.github.mviper.bomberman.messaging.Message;
import io.github.mviper.bomberman.messaging.Text;
import io.github.mviper.bomberman.utils.WorldEditUtils;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.logging.Level;
import java.util.stream.Collectors;
public class GameCreate extends Cmd {
private static final Bomberman PLUGIN = Bomberman.instance;
private static final WorldEdit WE = WorldEdit.getInstance();
private static final String F_SCHEMA = "s";
private static final String F_TEMPLATE = "t";
private static final String F_GAME = "g";
private static final String F_WAND = "w";
private static final String F_PLUGIN = "p";
public GameCreate(Cmd parent) {
super(parent);
}
@Override
public Formattable name() {
return Text.CREATE_NAME;
}
@Override
public List<String> options(CommandSender sender, List<String> args) {
if (args.size() == 1) {
return BmGameListIntent.listGames().stream().map(Game::getName).toList();
}
return List.of();
}
@Override
public Set<String> flags(CommandSender sender) {
return Set.of(F_SCHEMA, F_WAND, F_GAME, F_TEMPLATE);
}
@Override
public Set<String> flagOptions(String flag) {
return switch (flag) {
case F_SCHEMA -> {
Path weDir = WE.getWorkingDirectoryPath(WE.getConfiguration().saveDir);
List<String> schemaExtensions = new ArrayList<>();
for (BuiltInClipboardFormat format : BuiltInClipboardFormat.values()) {
schemaExtensions.addAll(format.getFileExtensions());
}
yield allFiles(weDir, weDir).stream()
.map(Path::toString)
.filter(fileName -> schemaExtensions.stream().anyMatch(fileName::endsWith))
.collect(Collectors.toSet());
}
case F_TEMPLATE -> {
Path templatesDir = PLUGIN.templates();
yield allFiles(templatesDir, templatesDir).stream()
.map(Path::toString)
.filter(fileName -> fileName.endsWith(".game.zip"))
.map(fileName -> fileName.replaceAll("(.*)\\.game\\.zip", "$1"))
.collect(Collectors.toSet());
}
case F_GAME -> BmGameListIntent.listGames().stream().map(Game::getName).collect(Collectors.toSet());
default -> Set.of();
};
}
@Override
public Formattable flagDescription(String flag) {
return switch (flag) {
case F_SCHEMA -> Text.CREATE_FLAG_SCHEMA;
case F_TEMPLATE -> Text.CREATE_FLAG_TEMPLATE;
case F_GAME -> Text.CREATE_FLAG_GAME;
case F_WAND -> Text.CREATE_FLAG_WAND;
default -> Message.empty;
};
}
@Override
public Formattable flagExtension(String flag) {
return switch (flag) {
case F_SCHEMA -> Text.CREATE_FLAG_SCHEMA_EXT;
case F_TEMPLATE -> Text.CREATE_FLAG_TEMPLATE_EXT;
case F_GAME -> Text.CREATE_FLAG_GAME_EXT;
default -> Message.empty;
};
}
@Override
public boolean run(CommandSender sender, List<String> args, java.util.Map<String, String> flags) {
try {
if (args.size() != 1) {
return false;
}
if (!(sender instanceof Player player)) {
Text.MUST_BE_PLAYER.format(cmdContext()).sendTo(sender);
return true;
}
String gameName = args.get(0);
Game existing = BmGameLookupIntent.find(gameName);
if (existing != null) {
Text.CREATE_GAME_EXISTS.format(cmdContext().plus("game", existing)).sendTo(sender);
return true;
}
if (Files.exists(PLUGIN.gameSaves().resolve(GameSave.sanitize(gameName + ".game.zip")))) {
Text.CREATE_GAME_FILE_CONFLICT.format(cmdContext()
.plus("game", gameName)
.plus("file", GameSave.sanitize(gameName + ".game.zip")))
.sendTo(sender);
return true;
}
int flagCount = (flags.containsKey(F_WAND) ? 1 : 0)
+ (flags.containsKey(F_GAME) ? 1 : 0)
+ (flags.containsKey(F_TEMPLATE) ? 1 : 0)
+ (flags.containsKey(F_SCHEMA) ? 1 : 0)
+ (flags.containsKey(F_PLUGIN) ? 1 : 0);
if (flagCount != 1) {
return false;
}
if (flags.containsKey(F_WAND)) {
makeFromSelection(gameName, player, new GameSettingsBuilder().build());
return true;
}
var schemaAndSettings = flags.containsKey(F_SCHEMA)
? fromSchema(flags.get(F_SCHEMA), sender)
: flags.containsKey(F_GAME)
? fromGame(flags.get(F_GAME))
: flags.containsKey(F_TEMPLATE)
? fromTemplate(flags.get(F_TEMPLATE), sender)
: fromPlugin(flags.get(F_PLUGIN));
if (schemaAndSettings == null) {
return true;
}
Game game = Game.buildGameFromSchema(gameName, player.getLocation(), schemaAndSettings.schema, schemaAndSettings.settings);
Text.CREATE_SUCCESS.format(cmdContext().plus("game", game)).sendTo(sender);
return true;
} catch (Exception e) {
Text.CREATE_ERROR.format(cmdContext().plus("error", e.getMessage() != null ? e.getMessage() : ""))
.sendTo(sender);
PLUGIN.getLogger().log(Level.WARNING, "Error creating game", e);
}
return true;
}
private SchemaSettings fromSchema(String file, CommandSender sender) throws Exception {
if (file == null || file.isEmpty()) {
return null;
}
Path path = WE.getWorkingDirectoryPath(WE.getConfiguration().saveDir).resolve(file);
if (!Files.exists(path)) {
Text.CREATE_GAME_FILE_NOT_FOUND.format(cmdContext()
.plus("file", path.toString())
.plus("filename", path.getFileName().toString()))
.sendTo(sender);
return null;
}
var format = ClipboardFormats.findByFile(path.toFile());
if (format == null) {
throw new IllegalArgumentException("Unknown file format: '" + file + "'");
}
var clipboard = format.getReader(Files.newInputStream(path)).read();
return new SchemaSettings(clipboard, new GameSettingsBuilder().build());
}
private SchemaSettings fromGame(String name) throws Exception {
Game existing = BmGameLookupIntent.find(name);
if (existing == null) {
return null;
}
return new SchemaSettings(existing.getClipboard(), existing.getSettings());
}
private SchemaSettings fromTemplate(String file, CommandSender sender) throws Exception {
if (file == null || file.isEmpty()) {
return null;
}
String fullFileName = file.toLowerCase(Locale.ROOT).endsWith(".game.zip") ? file : file + ".game.zip";
Path path = PLUGIN.templates().resolve(fullFileName);
if (!Files.exists(path)) {
Text.CREATE_GAME_FILE_NOT_FOUND.format(cmdContext()
.plus("file", path.toString())
.plus("filename", path.getFileName().toString()))
.sendTo(sender);
return null;
}
GameSave save = GameSave.loadSave(path);
return new SchemaSettings(save.getSchematic(), save.getSettings());
}
private SchemaSettings fromPlugin(String value) throws Exception {
if (value != null && value.toLowerCase(Locale.ROOT).equals("bm")) {
Path path = PLUGIN.templates().resolve("purple.game.zip");
GameSave save = GameSave.loadSave(path);
return new SchemaSettings(save.getSchematic(), save.getSettings());
}
return null;
}
private void makeFromSelection(String gameName, Player sender, GameSettings settings) {
SessionOwner owner = BukkitAdapter.adapt(sender);
var session = WorldEdit.getInstance().getSessionManager().getIfPresent(owner);
if (session == null || session.getSelectionWorld() == null || !session.isSelectionDefined(session.getSelectionWorld())) {
Text.CREATE_NEED_SELECTION.format(cmdContext()).sendTo(sender);
} else {
try {
var region = session.getSelection(session.getSelectionWorld());
var box = WorldEditUtils.selectionBounds(region);
Game game = Game.buildGameFromRegion(gameName, box, settings);
Text.CREATE_SUCCESS.format(cmdContext().plus("game", game)).sendTo(sender);
} catch (com.sk89q.worldedit.IncompleteRegionException e) {
Text.CREATE_NEED_SELECTION.format(cmdContext()).sendTo(sender);
}
}
}
@Override
public Permission permission() {
return Permissions.CREATE;
}
@Override
public Formattable example() {
return Text.CREATE_EXAMPLE;
}
@Override
public Formattable extra() {
return Text.CREATE_EXTRA;
}
@Override
public Formattable description() {
return Text.CREATE_DESCRIPTION;
}
@Override
public Formattable usage() {
return Text.CREATE_USAGE;
}
private static List<Path> allFiles(Path root, Path relative) {
if (!Files.isDirectory(root, LinkOption.NOFOLLOW_LINKS)) {
return List.of();
}
List<Path> fileList = new ArrayList<>();
try (var stream = Files.newDirectoryStream(root)) {
for (Path file : stream) {
if (Files.isDirectory(file, LinkOption.NOFOLLOW_LINKS)) {
fileList.addAll(allFiles(file, relative));
} else {
fileList.add(relative != null ? relative.relativize(file) : file);
}
}
} catch (Exception ignored) {
}
return fileList;
}
private record SchemaSettings(com.sk89q.worldedit.extent.clipboard.Clipboard schema, GameSettings settings) {}
}

View File

@@ -0,0 +1,64 @@
package io.github.mviper.bomberman.commands.game;
import io.github.mviper.bomberman.commands.Cmd;
import io.github.mviper.bomberman.commands.GameCommand;
import io.github.mviper.bomberman.commands.Permission;
import io.github.mviper.bomberman.commands.Permissions;
import io.github.mviper.bomberman.events.BmGameDeletedIntent;
import io.github.mviper.bomberman.game.Game;
import io.github.mviper.bomberman.messaging.Formattable;
import io.github.mviper.bomberman.messaging.Text;
import org.bukkit.command.CommandSender;
import java.util.List;
public class GameDelete extends GameCommand {
public GameDelete(Cmd parent) {
super(parent);
}
@Override
public Formattable name() {
return Text.DELETE_NAME;
}
@Override
protected List<String> gameOptions(List<String> args) {
return List.of();
}
@Override
protected boolean gameRun(CommandSender sender, List<String> args, java.util.Map<String, String> flags, Game game) {
if (!args.isEmpty()) {
return false;
}
BmGameDeletedIntent.delete(game, true);
Text.DELETE_SUCCESS.format(cmdContext().plus("game", game)).sendTo(sender);
return true;
}
@Override
public Permission permission() {
return Permissions.DELETE;
}
@Override
public Formattable example() {
return Text.DELETE_EXAMPLE;
}
@Override
public Formattable extra() {
return Text.DELETE_EXTRA;
}
@Override
public Formattable description() {
return Text.DELETE_DESCRIPTION;
}
@Override
public Formattable usage() {
return Text.DELETE_USAGE;
}
}

View File

@@ -0,0 +1,62 @@
package io.github.mviper.bomberman.commands.game;
import io.github.mviper.bomberman.commands.Cmd;
import io.github.mviper.bomberman.commands.GameCommand;
import io.github.mviper.bomberman.commands.Permission;
import io.github.mviper.bomberman.commands.Permissions;
import io.github.mviper.bomberman.game.Game;
import io.github.mviper.bomberman.messaging.Formattable;
import io.github.mviper.bomberman.messaging.Text;
import org.bukkit.command.CommandSender;
import java.util.List;
public class GameInfo extends GameCommand {
public GameInfo(Cmd parent) {
super(parent);
}
@Override
public Formattable name() {
return Text.INFO_NAME;
}
@Override
public Permission permission() {
return Permissions.INFO;
}
@Override
protected List<String> gameOptions(List<String> args) {
return List.of();
}
@Override
protected boolean gameRun(CommandSender sender, List<String> args, java.util.Map<String, String> flags, Game game) {
if (!args.isEmpty()) {
return false;
}
Text.INFO_DETAILS.format(cmdContext().plus("game", game)).sendTo(sender);
return true;
}
@Override
public Formattable extra() {
return Text.INFO_EXTRA;
}
@Override
public Formattable example() {
return Text.INFO_EXAMPLE;
}
@Override
public Formattable description() {
return Text.INFO_DESCRIPTION;
}
@Override
public Formattable usage() {
return Text.INFO_USAGE;
}
}

View File

@@ -0,0 +1,140 @@
package io.github.mviper.bomberman.commands.game;
import io.github.mviper.bomberman.commands.Cmd;
import io.github.mviper.bomberman.commands.GameCommand;
import io.github.mviper.bomberman.commands.Permission;
import io.github.mviper.bomberman.commands.Permissions;
import io.github.mviper.bomberman.events.BmPlayerJoinGameIntent;
import io.github.mviper.bomberman.game.Game;
import io.github.mviper.bomberman.messaging.Formattable;
import io.github.mviper.bomberman.messaging.Message;
import io.github.mviper.bomberman.messaging.Text;
import org.bukkit.Bukkit;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import java.util.List;
import java.util.Set;
public class GameJoin extends GameCommand {
private static final String F_TARGET = "t";
public GameJoin(Cmd parent) {
super(parent);
}
public static List<Player> select(String target, CommandSender source) throws IllegalArgumentException {
return Bukkit.selectEntities(source, target).stream()
.filter(entity -> entity instanceof Player)
.map(entity -> (Player) entity)
.toList();
}
@Override
public Formattable name() {
return Text.JOIN_NAME;
}
@Override
protected List<String> gameOptions(List<String> args) {
return List.of();
}
@Override
public Set<String> flags(CommandSender sender) {
return Set.of(F_TARGET);
}
@Override
public Set<String> flagOptions(String flag) {
if (F_TARGET.equals(flag)) {
Set<String> options = Bukkit.getOnlinePlayers().stream().map(Player::getName).collect(java.util.stream.Collectors.toSet());
options.addAll(List.of("@a", "@p", "@r", "@s"));
return options;
}
return Set.of();
}
@Override
public Formattable flagDescription(String flag) {
return F_TARGET.equals(flag) ? Text.JOIN_FLAG_TARGET : Message.empty;
}
@Override
public Formattable flagExtension(String flag) {
return F_TARGET.equals(flag) ? Text.JOIN_FLAG_TARGET_EXT : Message.empty;
}
@Override
protected boolean gameRun(CommandSender sender, List<String> args, java.util.Map<String, String> flags, Game game) {
if (!args.isEmpty()) {
return false;
}
List<Player> targets;
if (flags.get(F_TARGET) != null) {
String selection = flags.get(F_TARGET);
if (!Permissions.JOIN_REMOTE.isAllowedBy(sender)) {
Text.DENY_PERMISSION.format(cmdContext()).sendTo(sender);
return true;
}
try {
targets = select(selection, sender);
} catch (IllegalArgumentException e) {
Text.INVALID_TARGET_SELECTOR.format(cmdContext().plus("selector", selection)).sendTo(sender);
return true;
}
} else {
if (!(sender instanceof Player player)) {
Text.MUST_BE_PLAYER.format(cmdContext()).sendTo(sender);
return true;
}
targets = List.of(player);
}
for (Player target : targets) {
var event = BmPlayerJoinGameIntent.join(game, target);
if (event.isCancelled()) {
if (event.cancelledReason() != null) {
event.cancelledReason().sendTo(sender);
} else {
Text.COMMAND_CANCELLED.format(cmdContext()
.plus("game", game)
.plus("player", target))
.sendTo(target);
}
} else {
Text.JOIN_SUCCESS.format(cmdContext()
.plus("game", game)
.plus("player", target))
.sendTo(target);
}
}
return true;
}
@Override
public Permission permission() {
return Permissions.JOIN;
}
@Override
public Formattable extra() {
return Text.JOIN_EXTRA;
}
@Override
public Formattable example() {
return Text.JOIN_EXAMPLE;
}
@Override
public Formattable description() {
return Text.JOIN_DESCRIPTION;
}
@Override
public Formattable usage() {
return Text.JOIN_USAGE;
}
}

View File

@@ -0,0 +1,124 @@
package io.github.mviper.bomberman.commands.game;
import io.github.mviper.bomberman.commands.Cmd;
import io.github.mviper.bomberman.commands.Permission;
import io.github.mviper.bomberman.commands.Permissions;
import io.github.mviper.bomberman.events.BmPlayerLeaveGameIntent;
import io.github.mviper.bomberman.messaging.Formattable;
import io.github.mviper.bomberman.messaging.Message;
import io.github.mviper.bomberman.messaging.Text;
import org.bukkit.Bukkit;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import java.util.List;
import java.util.Set;
public class GameLeave extends Cmd {
private static final String F_TARGET = "t";
public GameLeave(Cmd parent) {
super(parent);
}
@Override
public Formattable name() {
return Text.LEAVE_NAME;
}
@Override
public List<String> options(CommandSender sender, List<String> args) {
return List.of();
}
@Override
public Set<String> flags(CommandSender sender) {
return Set.of(F_TARGET);
}
@Override
public Set<String> flagOptions(String flag) {
if (F_TARGET.equals(flag)) {
Set<String> options = Bukkit.getOnlinePlayers().stream().map(Player::getName).collect(java.util.stream.Collectors.toSet());
options.addAll(List.of("@a", "@p", "@r", "@s"));
return options;
}
return Set.of();
}
@Override
public Formattable flagDescription(String flag) {
return F_TARGET.equals(flag) ? Text.LEAVE_FLAG_TARGET : Message.empty;
}
@Override
public Formattable flagExtension(String flag) {
return F_TARGET.equals(flag) ? Text.LEAVE_FLAG_TARGET_EXT : Message.empty;
}
@Override
public boolean run(CommandSender sender, List<String> args, java.util.Map<String, String> flags) {
if (!args.isEmpty()) {
return false;
}
List<Player> targets;
if (flags.get(F_TARGET) != null) {
String selection = flags.get(F_TARGET);
if (!Permissions.LEAVE_REMOTE.isAllowedBy(sender)) {
Text.DENY_PERMISSION.format(cmdContext()).sendTo(sender);
return true;
}
try {
targets = GameJoin.select(selection, sender);
} catch (IllegalArgumentException e) {
Text.INVALID_TARGET_SELECTOR.format(cmdContext().plus("selector", selection)).sendTo(sender);
return true;
}
} else {
if (!(sender instanceof Player player)) {
Text.MUST_BE_PLAYER.format(cmdContext()).sendTo(sender);
return true;
}
targets = List.of(player);
}
for (Player target : targets) {
var event = BmPlayerLeaveGameIntent.leave(target);
if (event.isHandled()) {
Text.LEAVE_SUCCESS.format(cmdContext()
.plus("player", target)
.plus("game", event.getGame() != null ? event.getGame() : Message.error("none")))
.sendTo(target);
} else {
Text.LEAVE_NOT_JOINED.format(cmdContext().plus("player", target)).sendTo(target);
}
}
return true;
}
@Override
public Permission permission() {
return Permissions.LEAVE;
}
@Override
public Formattable extra() {
return Text.LEAVE_EXTRA;
}
@Override
public Formattable description() {
return Text.LEAVE_DESCRIPTION;
}
@Override
public Formattable usage() {
return Text.LEAVE_USAGE;
}
@Override
public Formattable example() {
return Text.JOIN_EXAMPLE;
}
}

View File

@@ -0,0 +1,62 @@
package io.github.mviper.bomberman.commands.game;
import io.github.mviper.bomberman.commands.Cmd;
import io.github.mviper.bomberman.commands.Permission;
import io.github.mviper.bomberman.commands.Permissions;
import io.github.mviper.bomberman.events.BmGameListIntent;
import io.github.mviper.bomberman.messaging.Formattable;
import io.github.mviper.bomberman.messaging.Text;
import org.bukkit.command.CommandSender;
import java.util.List;
public class GameList extends Cmd {
public GameList(Cmd parent) {
super(parent);
}
@Override
public Formattable name() {
return Text.GAMELIST_NAME;
}
@Override
public List<String> options(CommandSender sender, List<String> args) {
return List.of();
}
@Override
public boolean run(CommandSender sender, List<String> args, java.util.Map<String, String> flags) {
if (!args.isEmpty()) {
return false;
}
var games = BmGameListIntent.listGames();
Text.GAMELIST_GAMES.format(cmdContext().plus("games", games)).sendTo(sender);
return true;
}
@Override
public Permission permission() {
return Permissions.LIST;
}
@Override
public Formattable example() {
return Text.GAMELIST_EXAMPLE;
}
@Override
public Formattable extra() {
return Text.GAMELIST_EXTRA;
}
@Override
public Formattable description() {
return Text.GAMELIST_DESCRIPTION;
}
@Override
public Formattable usage() {
return Text.GAMELIST_USAGE;
}
}

View File

@@ -0,0 +1,76 @@
package io.github.mviper.bomberman.commands.game;
import io.github.mviper.bomberman.Bomberman;
import io.github.mviper.bomberman.commands.Cmd;
import io.github.mviper.bomberman.commands.GameCommand;
import io.github.mviper.bomberman.commands.Permission;
import io.github.mviper.bomberman.commands.Permissions;
import io.github.mviper.bomberman.events.BmGameBuildIntent;
import io.github.mviper.bomberman.events.BmGameDeletedIntent;
import io.github.mviper.bomberman.game.Game;
import io.github.mviper.bomberman.game.GameSave;
import io.github.mviper.bomberman.messaging.Formattable;
import io.github.mviper.bomberman.messaging.Text;
import org.bukkit.command.CommandSender;
import java.io.IOException;
import java.util.List;
public class GameReload extends GameCommand {
public GameReload(Cmd parent) {
super(parent);
}
@Override
public Formattable name() {
return Text.RELOAD_NAME;
}
@Override
protected boolean gameRun(CommandSender sender, List<String> args, java.util.Map<String, String> flags, Game game) {
if (!args.isEmpty()) {
return false;
}
BmGameDeletedIntent.delete(game, false);
try {
Game newGame = GameSave.loadGame(
Bomberman.instance.gameSaves().resolve(GameSave.sanitize(game.getName() + ".game.zip"))
);
BmGameBuildIntent.build(newGame);
Text.RELOAD_SUCCESS.format(cmdContext().plus("game", newGame)).sendTo(sender);
} catch (IOException e) {
Text.RELOAD_CANNOT_LOAD.format(cmdContext().plus("game", game.getName())).sendTo(sender);
}
return true;
}
@Override
public Permission permission() {
return Permissions.RELOAD;
}
@Override
protected List<String> gameOptions(List<String> args) {
return List.of();
}
@Override
public Formattable extra() {
return Text.RELOAD_EXTRA;
}
@Override
public Formattable example() {
return Text.RELOAD_EXAMPLE;
}
@Override
public Formattable description() {
return Text.RELOAD_DESCRIPTION;
}
@Override
public Formattable usage() {
return Text.RELOAD_USAGE;
}
}

View File

@@ -0,0 +1,244 @@
package io.github.mviper.bomberman.commands.game;
import io.github.mviper.bomberman.Bomberman;
import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.NamespacedKey;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.inventory.InventoryClickEvent;
import org.bukkit.event.inventory.InventoryCloseEvent;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.InventoryView;
import org.bukkit.inventory.ItemFlag;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import org.bukkit.inventory.meta.tags.ItemTagType;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;
import java.util.function.Function;
public class GuiBuilder implements Listener {
public static final class Index {
private final int x;
private final int y;
private final int invIndex;
private final char section;
private final int secIndex;
private final Inventory inventory;
public Index(int x, int y, int invIndex, char section, int secIndex, Inventory inventory) {
this.x = x;
this.y = y;
this.invIndex = invIndex;
this.section = section;
this.secIndex = secIndex;
this.inventory = inventory;
}
public int getX() {
return x;
}
public int getY() {
return y;
}
public int getInvIndex() {
return invIndex;
}
public char getSection() {
return section;
}
public int getSecIndex() {
return secIndex;
}
public Inventory getInventory() {
return inventory;
}
}
public static final class ItemSlot {
private final ItemStack item;
public ItemSlot(ItemStack item) {
this.item = item;
}
public ItemSlot(Material type, int qty) {
this(new ItemStack(type, qty));
}
public ItemSlot(Material type) {
this(new ItemStack(type));
}
public ItemStack getItem() {
return item;
}
private ItemSlot alterMeta(Consumer<ItemMeta> mod) {
if (item == null) {
return this;
}
ItemStack newItem = item.clone();
ItemMeta itemMeta = newItem.getItemMeta();
if (itemMeta == null) {
return new ItemSlot(newItem);
}
mod.accept(itemMeta);
newItem.setItemMeta(itemMeta);
return new ItemSlot(newItem);
}
public ItemSlot unMovable() {
return alterMeta(meta -> meta.getCustomTagContainer().setCustomTag(NO_MOVE_KEY, ItemTagType.BYTE, (byte) 1));
}
public ItemSlot hideAttributes() {
return alterMeta(meta -> meta.addItemFlags(ItemFlag.HIDE_ATTRIBUTES));
}
public ItemSlot displayName(String title) {
return alterMeta(meta -> meta.setDisplayName(title));
}
}
public static final class SlotItem {
private final Index index;
private final ItemStack item;
public SlotItem(Index index, ItemStack item) {
this.index = index;
this.item = item;
}
public Index getIndex() {
return index;
}
public ItemStack getItem() {
return item;
}
}
@FunctionalInterface
public interface ClickHandler {
void handle(Index index, ItemStack currentItem, ItemStack cursorItem);
}
public static void show(
Player player,
String name,
CharSequence[] contents,
Function<Index, ItemSlot> onInit
) {
show(player, name, contents, onInit, (index, currentItem, cursorItem) -> {
}, slots -> {
});
}
public static void show(
Player player,
String name,
CharSequence[] contents,
Function<Index, ItemSlot> onInit,
ClickHandler onClick,
Consumer<List<SlotItem>> onClose
) {
int size = contents.length * 9;
Inventory inventory = Bukkit.createInventory(null, size, name);
InventoryView view = player.openInventory(inventory);
if (view == null) {
return;
}
List<Index> slotLookup = new ArrayList<>();
Map<Character, AtomicInteger> sectionCount = new HashMap<>();
for (int i = 0; i < size; i++) {
int x = i % 9;
int y = i / 9;
char c = contents[y].charAt(x);
AtomicInteger count = sectionCount.computeIfAbsent(c, key -> new AtomicInteger(0));
Index index = new Index(x, y, i, c, count.getAndIncrement(), inventory);
slotLookup.add(index);
ItemSlot slot = onInit.apply(index);
inventory.setItem(i, slot != null ? slot.getItem() : null);
}
LOOKUP.put(view, new InvMemory(slotLookup, onClick, onClose));
}
private static final Bomberman PLUGIN = Bomberman.instance;
private static final NamespacedKey NO_MOVE_KEY = new NamespacedKey(PLUGIN, "no-move");
private static final Map<InventoryView, InvMemory> LOOKUP = new HashMap<>();
public static final ItemSlot blank = new ItemSlot(Material.BLACK_STAINED_GLASS_PANE)
.hideAttributes()
.displayName(" ")
.unMovable();
static {
Bukkit.getPluginManager().registerEvents(new GuiBuilder(), PLUGIN);
}
private static boolean isNotMovable(ItemStack item) {
ItemMeta meta = item.getItemMeta();
if (meta == null) {
return false;
}
Byte value = meta.getCustomTagContainer().getCustomTag(NO_MOVE_KEY, ItemTagType.BYTE);
return value != null && value == (byte) 1;
}
private static final class InvMemory {
private final List<Index> slots;
private final ClickHandler onClick;
private final Consumer<List<SlotItem>> onClose;
private InvMemory(List<Index> slots, ClickHandler onClick, Consumer<List<SlotItem>> onClose) {
this.slots = slots;
this.onClick = onClick;
this.onClose = onClose;
}
}
@EventHandler
public void onInventoryClosed(InventoryCloseEvent event) {
InvMemory memory = LOOKUP.remove(event.getView());
if (memory == null) {
return;
}
Inventory inventory = event.getInventory();
List<SlotItem> items = new ArrayList<>();
for (Index index : memory.slots) {
items.add(new SlotItem(index, inventory.getItem(index.getInvIndex())));
}
memory.onClose.accept(items);
}
@EventHandler
public void onInventoryItemClicked(InventoryClickEvent event) {
InvMemory memory = LOOKUP.get(event.getView());
if (memory == null) {
return;
}
if (event.getClickedInventory() != event.getInventory()) {
return;
}
Index index = memory.slots.get(event.getSlot());
ItemStack currentItem = event.getCurrentItem();
ItemStack cursorItem = event.getCursor();
memory.onClick.handle(index, currentItem, cursorItem);
if ((currentItem != null && isNotMovable(currentItem)) || (cursorItem != null && isNotMovable(cursorItem))) {
event.setCancelled(true);
}
}
}

View File

@@ -0,0 +1,116 @@
package io.github.mviper.bomberman.commands.game;
import io.github.mviper.bomberman.commands.Cmd;
import io.github.mviper.bomberman.commands.GameCommand;
import io.github.mviper.bomberman.commands.Permission;
import io.github.mviper.bomberman.commands.Permissions;
import io.github.mviper.bomberman.events.BmRunStartCountDownIntent;
import io.github.mviper.bomberman.game.Game;
import io.github.mviper.bomberman.messaging.Formattable;
import io.github.mviper.bomberman.messaging.Message;
import io.github.mviper.bomberman.messaging.Text;
import org.bukkit.command.CommandSender;
import java.util.List;
import java.util.Map;
import java.util.Set;
public class RunStart extends GameCommand {
public RunStart(Cmd parent) {
super(parent);
}
@Override
public Formattable name() {
return Text.START_NAME;
}
@Override
protected List<String> gameOptions(List<String> args) {
return List.of();
}
@Override
public Set<String> flags(CommandSender sender) {
return Set.of("d", "o");
}
@Override
public Formattable flagExtension(String flag) {
return switch (flag) {
case "d" -> Text.START_FLAG_DELAY_EXT;
default -> Message.empty;
};
}
@Override
public Formattable flagDescription(String flag) {
return switch (flag) {
case "d" -> Text.START_FLAG_DELAY_DESC;
case "o" -> Text.START_FLAG_OVERRIDE_DESC;
default -> Message.empty;
};
}
@Override
protected boolean gameRun(CommandSender sender, List<String> args, Map<String, String> flags, Game game) {
if (!args.isEmpty()) {
return false;
}
String delayValue = flags.containsKey("d") ? flags.get("d") : flags.get("delay");
int delay;
if (delayValue == null) {
delay = 3;
} else {
try {
delay = Integer.parseInt(delayValue);
} catch (NumberFormatException ex) {
Text.INVALID_NUMBER.format(cmdContext().plus("number", delayValue)).sendTo(sender);
return true;
}
if (delay < 0) {
Text.INVALID_NUMBER.format(cmdContext().plus("number", delayValue)).sendTo(sender);
return true;
}
}
BmRunStartCountDownIntent event = BmRunStartCountDownIntent.startGame(game, delay, flags.containsKey("o"));
if (event.isCancelled()) {
Formattable reason = event.getCancelledReason();
if (reason == null) {
Text.COMMAND_CANCELLED.format(cmdContext().plus("game", game)).sendTo(sender);
} else {
reason.format(cmdContext()).sendTo(sender);
}
} else {
Text.GAME_START_SUCCESS.format(cmdContext().plus("game", game)).sendTo(sender);
}
return true;
}
@Override
public Permission permission() {
return Permissions.START;
}
@Override
public Formattable extra() {
return Text.START_EXTRA;
}
@Override
public Formattable example() {
return Text.START_EXAMPLE;
}
@Override
public Formattable description() {
return Text.START_DESCRIPTION;
}
@Override
public Formattable usage() {
return Text.START_USAGE;
}
}

View File

@@ -0,0 +1,73 @@
package io.github.mviper.bomberman.commands.game;
import io.github.mviper.bomberman.commands.Cmd;
import io.github.mviper.bomberman.commands.GameCommand;
import io.github.mviper.bomberman.commands.Permission;
import io.github.mviper.bomberman.commands.Permissions;
import io.github.mviper.bomberman.events.BmRunStoppedIntent;
import io.github.mviper.bomberman.game.Game;
import io.github.mviper.bomberman.messaging.Formattable;
import io.github.mviper.bomberman.messaging.Text;
import org.bukkit.command.CommandSender;
import java.util.List;
public class RunStop extends GameCommand {
public RunStop(Cmd parent) {
super(parent);
}
@Override
public Formattable name() {
return Text.STOP_NAME;
}
@Override
protected boolean gameRun(CommandSender sender, List<String> args, java.util.Map<String, String> flags, Game game) {
if (!args.isEmpty()) {
return false;
}
BmRunStoppedIntent event = BmRunStoppedIntent.stopGame(game);
if (!event.isCancelled()) {
Text.STOP_SUCCESS.format(cmdContext().plus("game", game)).sendTo(sender);
} else {
Formattable reason = event.cancelledReason();
if (reason == null) {
Text.COMMAND_CANCELLED.format(cmdContext().plus("command", this)).sendTo(sender);
} else {
reason.format(cmdContext()).sendTo(sender);
}
}
return true;
}
@Override
public Permission permission() {
return Permissions.STOP;
}
@Override
protected List<String> gameOptions(List<String> args) {
return List.of();
}
@Override
public Formattable extra() {
return Text.STOP_EXTRA;
}
@Override
public Formattable example() {
return Text.STOP_EXAMPLE;
}
@Override
public Formattable description() {
return Text.STOP_DESCRIPTION;
}
@Override
public Formattable usage() {
return Text.STOP_USAGE;
}
}

View File

@@ -0,0 +1,161 @@
package io.github.mviper.bomberman.commands.game;
import com.sk89q.worldedit.WorldEdit;
import com.sk89q.worldedit.bukkit.BukkitAdapter;
import com.sk89q.worldedit.extent.clipboard.BlockArrayClipboard;
import com.sk89q.worldedit.extent.clipboard.Clipboard;
import com.sk89q.worldedit.function.operation.ForwardExtentCopy;
import com.sk89q.worldedit.function.operation.Operations;
import com.sk89q.worldedit.math.BlockVector3;
import com.sk89q.worldedit.session.ClipboardHolder;
import io.github.mviper.bomberman.Bomberman;
import io.github.mviper.bomberman.commands.Cmd;
import io.github.mviper.bomberman.commands.Permission;
import io.github.mviper.bomberman.commands.Permissions;
import io.github.mviper.bomberman.events.BmGameDeletedIntent;
import io.github.mviper.bomberman.events.BmGameLookupIntent;
import io.github.mviper.bomberman.game.Game;
import io.github.mviper.bomberman.messaging.Formattable;
import io.github.mviper.bomberman.messaging.Text;
import io.github.mviper.bomberman.utils.Box;
import io.github.mviper.bomberman.utils.BukkitUtils;
import io.github.mviper.bomberman.utils.WorldEditUtils;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.command.CommandSender;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class UndoBuild extends Cmd {
private static final Map<String, UndoHistory> GAME_MEMORY = new HashMap<>();
public UndoBuild(Cmd parent) {
super(parent);
}
public static void retainHistory(String name, Box box) {
var region = WorldEditUtils.convert(box);
Clipboard clipboard = new BlockArrayClipboard(region);
try (var editSession = WorldEdit.getInstance().newEditSession(BukkitAdapter.adapt(box.world))) {
ForwardExtentCopy forwardExtentCopy = new ForwardExtentCopy(
editSession,
region,
clipboard,
region.getMinimumPoint()
);
try {
Operations.complete(forwardExtentCopy);
} catch (com.sk89q.worldedit.WorldEditException e) {
throw new RuntimeException("Failed to copy region", e);
}
}
int[] handle = new int[1];
handle[0] = Bukkit.getScheduler().scheduleSyncDelayedTask(Bomberman.instance, () -> {
UndoHistory memory = GAME_MEMORY.get(name);
if (memory != null && memory.taskId == handle[0]) {
GAME_MEMORY.remove(name);
Bomberman.instance.getLogger().info("Game '" + name + "' undo history expired");
}
}, 10 * 60 * 20L);
GAME_MEMORY.put(name, new UndoHistory(BukkitUtils.boxLoc1(box), clipboard, handle[0]));
}
public static void removeHistory(String name) {
GAME_MEMORY.remove(name);
}
@Override
public Formattable name() {
return Text.UNDO_NAME;
}
@Override
public List<String> options(CommandSender sender, List<String> args) {
if (args.size() == 1) {
List<String> names = new ArrayList<>(GAME_MEMORY.keySet());
names.sort(String::compareTo);
return names;
}
return List.of();
}
@Override
public boolean run(CommandSender sender, List<String> args, java.util.Map<String, String> flags) {
if (args.size() != 1) {
return false;
}
String gameName = args.get(0);
UndoHistory memory = GAME_MEMORY.get(gameName);
if (memory == null || memory.clipboard == null || memory.origin == null) {
Text.UNDO_UNKNOWN_GAME.format(cmdContext().plus("game", gameName)).sendTo(sender);
return true;
}
Game game = BmGameLookupIntent.find(gameName);
if (game != null) {
BmGameDeletedIntent.delete(game, true);
Text.UNDO_DELETED.format(cmdContext().plus("game", game)).sendTo(sender);
}
try (var editSession = WorldEdit.getInstance().newEditSession(BukkitAdapter.adapt(memory.origin.getWorld()))) {
var operation = new ClipboardHolder(memory.clipboard)
.createPaste(editSession)
.to(BlockVector3.at(memory.origin.getBlockX(), memory.origin.getBlockY(), memory.origin.getBlockZ()))
.copyEntities(true)
.build();
try {
Operations.complete(operation);
} catch (com.sk89q.worldedit.WorldEditException e) {
throw new RuntimeException("Failed to paste clipboard", e);
}
}
GAME_MEMORY.remove(gameName);
Text.UNDO_SUCCESS.format(cmdContext().plus("game", gameName)).sendTo(sender);
return true;
}
@Override
public Permission permission() {
return Permissions.UNDO;
}
@Override
public Formattable example() {
return Text.UNDO_EXAMPLE;
}
@Override
public Formattable extra() {
return Text.UNDO_EXTRA;
}
@Override
public Formattable description() {
return Text.UNDO_DESCRIPTION;
}
@Override
public Formattable usage() {
return Text.UNDO_USAGE;
}
private static final class UndoHistory {
private final Location origin;
private final Clipboard clipboard;
private final int taskId;
private UndoHistory(Location origin, Clipboard clipboard, int taskId) {
this.origin = origin;
this.clipboard = clipboard;
this.taskId = taskId;
}
}
}

View File

@@ -0,0 +1,20 @@
package io.github.mviper.bomberman.events;
import org.bukkit.event.Cancellable;
/**
* Simple implementation of Cancellable interface.
*/
public class BmCancellable implements Cancellable {
private boolean cancelled = false;
@Override
public boolean isCancelled() {
return cancelled;
}
@Override
public void setCancelled(boolean cancel) {
cancelled = cancel;
}
}

View File

@@ -0,0 +1,52 @@
package io.github.mviper.bomberman.events;
import io.github.mviper.bomberman.game.Explosion.BlockPlan;
import io.github.mviper.bomberman.game.Game;
import org.bukkit.Location;
import org.bukkit.entity.Player;
import org.bukkit.event.HandlerList;
import org.bukkit.inventory.ItemStack;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
/**
* Called when a bomb turns from a tnt block into a fire '+'.
*/
public class BmDropLootEvent extends BmEvent implements org.bukkit.event.Cancellable {
private static final HandlerList HANDLER_LIST = new HandlerList();
public final Game game;
public final Player cause;
public final Set<BlockPlan> ignited;
public final Map<Location, Set<ItemStack>> drops;
private final BmCancellable delegate = new BmCancellable();
public BmDropLootEvent(Game game, Player cause, Set<BlockPlan> ignited, Map<Location, Set<ItemStack>> drops) {
this.game = game;
this.cause = cause;
this.ignited = ignited;
this.drops = new HashMap<>(drops);
}
@Override
public HandlerList getHandlers() {
return HANDLER_LIST;
}
public static HandlerList getHandlerList() {
return HANDLER_LIST;
}
@Override
public boolean isCancelled() {
return delegate.isCancelled();
}
@Override
public void setCancelled(boolean cancel) {
delegate.setCancelled(cancel);
}
}

View File

@@ -0,0 +1,9 @@
package io.github.mviper.bomberman.events;
import org.bukkit.event.Event;
/**
* All Bomberman events extend this.
*/
public abstract class BmEvent extends Event {
}

View File

@@ -0,0 +1,47 @@
package io.github.mviper.bomberman.events;
import io.github.mviper.bomberman.game.Explosion.BlockPlan;
import io.github.mviper.bomberman.game.Game;
import org.bukkit.entity.Player;
import org.bukkit.event.HandlerList;
import java.util.HashSet;
import java.util.Set;
/**
* Called when a bomb turns from a tnt block into a fire '+'.
*/
public class BmExplosionEvent extends BmEvent implements org.bukkit.event.Cancellable {
private static final HandlerList HANDLER_LIST = new HandlerList();
public final Game game;
public final Player cause;
public final Set<BlockPlan> igniting;
private final BmCancellable delegate = new BmCancellable();
public BmExplosionEvent(Game game, Player cause, Set<BlockPlan> igniting) {
this.game = game;
this.cause = cause;
this.igniting = new HashSet<>(igniting);
}
@Override
public HandlerList getHandlers() {
return HANDLER_LIST;
}
public static HandlerList getHandlerList() {
return HANDLER_LIST;
}
@Override
public boolean isCancelled() {
return delegate.isCancelled();
}
@Override
public void setCancelled(boolean cancel) {
delegate.setCancelled(cancel);
}
}

View File

@@ -0,0 +1,49 @@
package io.github.mviper.bomberman.events;
import io.github.mviper.bomberman.game.Game;
import org.bukkit.Bukkit;
import org.bukkit.event.HandlerList;
/**
* Called when a game is built.
*/
public class BmGameBuildIntent extends BmEvent implements Intent {
private static final HandlerList HANDLER_LIST = new HandlerList();
public final Game game;
private final BmIntent delegate = new BmIntent();
private BmGameBuildIntent(Game game) {
this.game = game;
}
public static void build(Game game) {
BmGameBuildIntent event = new BmGameBuildIntent(game);
Bukkit.getPluginManager().callEvent(event);
event.verifyHandled();
}
@Override
public HandlerList getHandlers() {
return HANDLER_LIST;
}
public static HandlerList getHandlerList() {
return HANDLER_LIST;
}
@Override
public boolean isHandled() {
return delegate.isHandled();
}
@Override
public void setHandled() {
delegate.setHandled();
}
@Override
public void verifyHandled() {
delegate.verifyHandled();
}
}

View File

@@ -0,0 +1,62 @@
package io.github.mviper.bomberman.events;
import io.github.mviper.bomberman.game.Game;
import org.bukkit.Bukkit;
import org.bukkit.event.HandlerList;
/**
* Called when a game is completely deleted from the server.
*/
public class BmGameDeletedIntent extends BmEvent implements IntentCancellable {
private static final HandlerList HANDLER_LIST = new HandlerList();
public final Game game;
public boolean isDeletingSave = false;
private final BmIntentCancellable delegate = new BmIntentCancellable();
private BmGameDeletedIntent(Game game) {
this.game = game;
}
public static void delete(Game game, boolean deleteSave) {
BmGameDeletedIntent event = new BmGameDeletedIntent(game);
event.isDeletingSave = deleteSave;
Bukkit.getPluginManager().callEvent(event);
event.verifyHandled();
}
@Override
public HandlerList getHandlers() {
return HANDLER_LIST;
}
public static HandlerList getHandlerList() {
return HANDLER_LIST;
}
@Override
public boolean isHandled() {
return delegate.isHandled();
}
@Override
public void setHandled() {
delegate.setHandled();
}
@Override
public void verifyHandled() {
delegate.verifyHandled();
}
@Override
public boolean isCancelled() {
return delegate.isCancelled();
}
@Override
public void setCancelled(boolean cancel) {
delegate.setCancelled(cancel);
}
}

View File

@@ -0,0 +1,32 @@
package io.github.mviper.bomberman.events;
import io.github.mviper.bomberman.game.Game;
import org.bukkit.Bukkit;
import org.bukkit.event.HandlerList;
import java.util.HashSet;
import java.util.Set;
/**
* Called to find a listing of every active game.
*/
public class BmGameListIntent extends BmEvent {
private static final HandlerList HANDLER_LIST = new HandlerList();
public final Set<Game> games = new HashSet<>();
public static Set<Game> listGames() {
BmGameListIntent event = new BmGameListIntent();
Bukkit.getPluginManager().callEvent(event);
return event.games;
}
@Override
public HandlerList getHandlers() {
return HANDLER_LIST;
}
public static HandlerList getHandlerList() {
return HANDLER_LIST;
}
}

View File

@@ -0,0 +1,34 @@
package io.github.mviper.bomberman.events;
import io.github.mviper.bomberman.game.Game;
import org.bukkit.Bukkit;
import org.bukkit.event.HandlerList;
/**
* Called to find a game instance.
*/
public class BmGameLookupIntent extends BmEvent {
private static final HandlerList HANDLER_LIST = new HandlerList();
public final String name;
public Game game;
public BmGameLookupIntent(String name) {
this.name = name;
}
public static Game find(String name) {
BmGameLookupIntent event = new BmGameLookupIntent(name);
Bukkit.getPluginManager().callEvent(event);
return event.game;
}
@Override
public HandlerList getHandlers() {
return HANDLER_LIST;
}
public static HandlerList getHandlerList() {
return HANDLER_LIST;
}
}

View File

@@ -0,0 +1,51 @@
package io.github.mviper.bomberman.events;
import io.github.mviper.bomberman.game.Game;
import org.bukkit.Bukkit;
import org.bukkit.event.HandlerList;
/**
* Called whenever a game is being removed - may be because game deleted or because
* server is shutting down. Is possible game starts back when server starts back.
* All event listeners for the game should destroy themselves on this event.
*/
public class BmGameTerminatedIntent extends BmEvent implements Intent {
private static final HandlerList HANDLER_LIST = new HandlerList();
public final Game game;
private final BmIntent delegate = new BmIntent();
private BmGameTerminatedIntent(Game game) {
this.game = game;
}
public static void terminateGame(Game game) {
BmGameTerminatedIntent event = new BmGameTerminatedIntent(game);
Bukkit.getPluginManager().callEvent(event);
event.verifyHandled();
}
@Override
public HandlerList getHandlers() {
return HANDLER_LIST;
}
public static HandlerList getHandlerList() {
return HANDLER_LIST;
}
@Override
public boolean isHandled() {
return delegate.isHandled();
}
@Override
public void setHandled() {
delegate.setHandled();
}
@Override
public void verifyHandled() {
delegate.verifyHandled();
}
}

View File

@@ -0,0 +1,22 @@
package io.github.mviper.bomberman.events;
public class BmIntent implements Intent {
private boolean handled = false;
@Override
public boolean isHandled() {
return handled;
}
@Override
public void setHandled() {
handled = true;
}
@Override
public void verifyHandled() {
if (!handled) {
throw new RuntimeException("Event not handled: " + this);
}
}
}

View File

@@ -0,0 +1,33 @@
package io.github.mviper.bomberman.events;
public class BmIntentCancellable implements IntentCancellable {
private boolean cancelled = false;
private boolean handled = false;
@Override
public void verifyHandled() {
if (!handled && !isCancelled()) {
throw new RuntimeException("Event not handled: " + this);
}
}
@Override
public boolean isCancelled() {
return cancelled;
}
@Override
public void setCancelled(boolean cancel) {
cancelled = cancel;
}
@Override
public boolean isHandled() {
return handled;
}
@Override
public void setHandled() {
handled = true;
}
}

View File

@@ -0,0 +1,18 @@
package io.github.mviper.bomberman.events;
import io.github.mviper.bomberman.messaging.Message;
public class BmIntentCancellableReasoned extends BmIntentCancellable implements IntentCancellableReasoned {
private Message reason;
@Override
public void cancelFor(Message reason) {
this.reason = reason;
setCancelled(true);
}
@Override
public Message cancelledReason() {
return reason;
}
}

View File

@@ -0,0 +1,63 @@
package io.github.mviper.bomberman.events;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.event.HandlerList;
/**
* Event that occurs whenever a player is standing on a bomb. Will be called every tick that the player remains on the
* bomb.
*/
public class BmPlayerHitIntent extends BmEvent implements IntentCancellable {
private static final HandlerList HANDLER_LIST = new HandlerList();
public final Player player;
public final Player cause;
private final BmIntentCancellable delegate = new BmIntentCancellable();
private BmPlayerHitIntent(Player player, Player cause) {
this.player = player;
this.cause = cause;
}
public static void hit(Player player, Player cause) {
BmPlayerHitIntent event = new BmPlayerHitIntent(player, cause);
Bukkit.getPluginManager().callEvent(event);
event.verifyHandled();
}
@Override
public HandlerList getHandlers() {
return HANDLER_LIST;
}
public static HandlerList getHandlerList() {
return HANDLER_LIST;
}
@Override
public boolean isHandled() {
return delegate.isHandled();
}
@Override
public void setHandled() {
delegate.setHandled();
}
@Override
public void verifyHandled() {
delegate.verifyHandled();
}
@Override
public boolean isCancelled() {
return delegate.isCancelled();
}
@Override
public void setCancelled(boolean cancel) {
delegate.setCancelled(cancel);
}
}

View File

@@ -0,0 +1,65 @@
package io.github.mviper.bomberman.events;
import io.github.mviper.bomberman.game.Game;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.event.HandlerList;
/**
* Called whenever a bm player takes damage.
*/
public class BmPlayerHurtIntent extends BmEvent implements IntentCancellable {
private static final HandlerList HANDLER_LIST = new HandlerList();
public final Game game;
public final Player player;
public final Player attacker;
private final BmIntentCancellable delegate = new BmIntentCancellable();
public BmPlayerHurtIntent(Game game, Player player, Player attacker) {
this.game = game;
this.player = player;
this.attacker = attacker;
}
public static void run(Game game, Player player, Player cause) {
BmPlayerHurtIntent hurtEvent = new BmPlayerHurtIntent(game, player, cause);
Bukkit.getPluginManager().callEvent(hurtEvent);
hurtEvent.verifyHandled();
}
@Override
public HandlerList getHandlers() {
return HANDLER_LIST;
}
public static HandlerList getHandlerList() {
return HANDLER_LIST;
}
@Override
public boolean isHandled() {
return delegate.isHandled();
}
@Override
public void setHandled() {
delegate.setHandled();
}
@Override
public void verifyHandled() {
delegate.verifyHandled();
}
@Override
public boolean isCancelled() {
return delegate.isCancelled();
}
@Override
public void setCancelled(boolean cancel) {
delegate.setCancelled(cancel);
}
}

View File

@@ -0,0 +1,76 @@
package io.github.mviper.bomberman.events;
import io.github.mviper.bomberman.game.Game;
import io.github.mviper.bomberman.messaging.Message;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.event.HandlerList;
/**
* Called whenever a player attempts to join a game. If there are not enough spawns in the game or the player cannot
* afford entry, or ..., the event will be cancelled.
*/
public class BmPlayerJoinGameIntent extends BmEvent implements IntentCancellableReasoned {
private static final HandlerList HANDLER_LIST = new HandlerList();
public final Game game;
public final Player player;
private final BmIntentCancellableReasoned delegate = new BmIntentCancellableReasoned();
private BmPlayerJoinGameIntent(Game game, Player player) {
this.game = game;
this.player = player;
}
public static BmPlayerJoinGameIntent join(Game game, Player player) {
BmPlayerJoinGameIntent event = new BmPlayerJoinGameIntent(game, player);
Bukkit.getPluginManager().callEvent(event);
event.verifyHandled();
return event;
}
@Override
public HandlerList getHandlers() {
return HANDLER_LIST;
}
public static HandlerList getHandlerList() {
return HANDLER_LIST;
}
@Override
public boolean isHandled() {
return delegate.isHandled();
}
@Override
public void setHandled() {
delegate.setHandled();
}
@Override
public void verifyHandled() {
delegate.verifyHandled();
}
@Override
public boolean isCancelled() {
return delegate.isCancelled();
}
@Override
@Deprecated
public void setCancelled(boolean cancel) {
delegate.setCancelled(cancel);
}
@Override
public void cancelFor(Message reason) {
delegate.cancelFor(reason);
}
@Override
public Message cancelledReason() {
return delegate.cancelledReason();
}
}

View File

@@ -0,0 +1,55 @@
package io.github.mviper.bomberman.events;
import io.github.mviper.bomberman.game.Game;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.event.HandlerList;
/**
* Called whenever a bm player is killed.
*/
public class BmPlayerKilledIntent extends BmEvent implements Intent {
private static final HandlerList HANDLER_LIST = new HandlerList();
public final Game game;
public final Player player;
public final Player attacker;
private final BmIntent delegate = new BmIntent();
private BmPlayerKilledIntent(Game game, Player player, Player attacker) {
this.game = game;
this.player = player;
this.attacker = attacker;
}
public static void kill(Game game, Player player, Player cause) {
BmPlayerKilledIntent event = new BmPlayerKilledIntent(game, player, cause);
Bukkit.getPluginManager().callEvent(event);
event.verifyHandled();
}
@Override
public HandlerList getHandlers() {
return HANDLER_LIST;
}
public static HandlerList getHandlerList() {
return HANDLER_LIST;
}
@Override
public boolean isHandled() {
return delegate.isHandled();
}
@Override
public void setHandled() {
delegate.setHandled();
}
@Override
public void verifyHandled() {
delegate.verifyHandled();
}
}

View File

@@ -0,0 +1,58 @@
package io.github.mviper.bomberman.events;
import io.github.mviper.bomberman.game.Game;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.event.HandlerList;
public class BmPlayerLeaveGameIntent extends BmEvent implements Intent {
private static final HandlerList HANDLER_LIST = new HandlerList();
public final Player player;
private Game game;
private final BmIntent delegate = new BmIntent();
private BmPlayerLeaveGameIntent(Player player) {
this.player = player;
}
public static BmPlayerLeaveGameIntent leave(Player player) {
BmPlayerLeaveGameIntent leave = new BmPlayerLeaveGameIntent(player);
Bukkit.getPluginManager().callEvent(leave);
// Leave event may not be handled if player was not joined
return leave;
}
public Game getGame() {
return game;
}
public void setHandled(Game game) {
this.game = game;
setHandled();
}
@Override
public HandlerList getHandlers() {
return HANDLER_LIST;
}
public static HandlerList getHandlerList() {
return HANDLER_LIST;
}
@Override
public boolean isHandled() {
return delegate.isHandled();
}
@Override
public void setHandled() {
delegate.setHandled();
}
@Override
public void verifyHandled() {
delegate.verifyHandled();
}
}

View File

@@ -0,0 +1,42 @@
package io.github.mviper.bomberman.events;
import io.github.mviper.bomberman.game.Game;
import org.bukkit.Location;
import org.bukkit.entity.Player;
import org.bukkit.event.HandlerList;
/**
* Called whenever a bm player moves. Cannot modify the event.
*/
public class BmPlayerMovedEvent extends BmEvent {
private static final HandlerList HANDLER_LIST = new HandlerList();
public final Game game;
public final Player player;
private final Location from;
private final Location to;
public BmPlayerMovedEvent(Game game, Player player, Location from, Location to) {
this.game = game;
this.player = player;
this.from = from;
this.to = to;
}
public Location getFrom() {
return from.clone();
}
public Location getTo() {
return to.clone();
}
@Override
public HandlerList getHandlers() {
return HANDLER_LIST;
}
public static HandlerList getHandlerList() {
return HANDLER_LIST;
}
}

View File

@@ -0,0 +1,50 @@
package io.github.mviper.bomberman.events;
import io.github.mviper.bomberman.game.Game;
import io.github.mviper.bomberman.game.GamePlayer;
import org.bukkit.block.Block;
import org.bukkit.entity.Player;
import org.bukkit.event.HandlerList;
/**
* Called when a player places down a block of TNT (or whatever the game configured as the tnt block). Cancelling the
* event will remove the tnt from the ground as if the player never clicked.
*/
public class BmPlayerPlacedBombEvent extends BmEvent implements org.bukkit.event.Cancellable {
private static final HandlerList HANDLER_LIST = new HandlerList();
public final Game game;
public final Player player;
public final Block block;
public int fuse;
public final int strength;
private final BmCancellable delegate = new BmCancellable();
public BmPlayerPlacedBombEvent(Game game, Player player, Block block, int fuse) {
this.game = game;
this.player = player;
this.block = block;
this.fuse = fuse;
this.strength = GamePlayer.bombStrength(game, player);
}
@Override
public HandlerList getHandlers() {
return HANDLER_LIST;
}
public static HandlerList getHandlerList() {
return HANDLER_LIST;
}
@Override
public boolean isCancelled() {
return delegate.isCancelled();
}
@Override
public void setCancelled(boolean cancel) {
delegate.setCancelled(cancel);
}
}

View File

@@ -0,0 +1,26 @@
package io.github.mviper.bomberman.events;
import io.github.mviper.bomberman.game.Game;
import org.bukkit.entity.Player;
import org.bukkit.event.HandlerList;
public class BmPlayerWonEvent extends BmEvent {
private static final HandlerList HANDLER_LIST = new HandlerList();
public final Game game;
public final Player player;
public BmPlayerWonEvent(Game game, Player player) {
this.game = game;
this.player = player;
}
@Override
public HandlerList getHandlers() {
return HANDLER_LIST;
}
public static HandlerList getHandlerList() {
return HANDLER_LIST;
}
}

View File

@@ -0,0 +1,76 @@
package io.github.mviper.bomberman.events;
import io.github.mviper.bomberman.game.Game;
import io.github.mviper.bomberman.messaging.Message;
import org.bukkit.Bukkit;
import org.bukkit.event.HandlerList;
/**
* Called whenever a run is attempted to be started.
*/
public class BmRunStartCountDownIntent extends BmEvent implements IntentCancellable {
private static final HandlerList HANDLER_LIST = new HandlerList();
public final Game game;
public int delay;
public final boolean override;
private final BmIntentCancellable delegate = new BmIntentCancellable();
private Message cancelReason;
private BmRunStartCountDownIntent(Game game, int delay, boolean override) {
this.game = game;
this.delay = delay;
this.override = override;
}
public static BmRunStartCountDownIntent startGame(Game game, int delay, boolean override) {
BmRunStartCountDownIntent event = new BmRunStartCountDownIntent(game, delay, override);
Bukkit.getPluginManager().callEvent(event);
event.verifyHandled();
return event;
}
@Override
public HandlerList getHandlers() {
return HANDLER_LIST;
}
public static HandlerList getHandlerList() {
return HANDLER_LIST;
}
public Message getCancelledReason() {
return cancelReason;
}
public void cancelBecause(Message cancelReason) {
this.cancelReason = cancelReason;
setCancelled(true);
}
@Override
public boolean isHandled() {
return delegate.isHandled();
}
@Override
public void setHandled() {
delegate.setHandled();
}
@Override
public void verifyHandled() {
delegate.verifyHandled();
}
@Override
public boolean isCancelled() {
return delegate.isCancelled();
}
@Override
public void setCancelled(boolean cancel) {
delegate.setCancelled(cancel);
}
}

View File

@@ -0,0 +1,49 @@
package io.github.mviper.bomberman.events;
import io.github.mviper.bomberman.game.Game;
import org.bukkit.Bukkit;
import org.bukkit.event.HandlerList;
/**
* Called whenever a run is attempted to be started.
*/
public class BmRunStartedIntent extends BmEvent implements Intent {
private static final HandlerList HANDLER_LIST = new HandlerList();
public final Game game;
private final BmIntent delegate = new BmIntent();
private BmRunStartedIntent(Game game) {
this.game = game;
}
public static void startRun(Game game) {
BmRunStartedIntent event = new BmRunStartedIntent(game);
Bukkit.getPluginManager().callEvent(event);
event.verifyHandled();
}
@Override
public HandlerList getHandlers() {
return HANDLER_LIST;
}
public static HandlerList getHandlerList() {
return HANDLER_LIST;
}
@Override
public boolean isHandled() {
return delegate.isHandled();
}
@Override
public void setHandled() {
delegate.setHandled();
}
@Override
public void verifyHandled() {
delegate.verifyHandled();
}
}

View File

@@ -0,0 +1,72 @@
package io.github.mviper.bomberman.events;
import io.github.mviper.bomberman.game.Game;
import io.github.mviper.bomberman.messaging.Message;
import org.bukkit.Bukkit;
import org.bukkit.event.HandlerList;
/**
* Called whenever a game run is stopped. May be due to game finishing, game forcefully stopped, server shutdown, etc.
*/
public class BmRunStoppedIntent extends BmEvent implements IntentCancellableReasoned {
private static final HandlerList HANDLER_LIST = new HandlerList();
public final Game game;
private final BmIntentCancellableReasoned delegate = new BmIntentCancellableReasoned();
private BmRunStoppedIntent(Game game) {
this.game = game;
}
public static BmRunStoppedIntent stopGame(Game game) {
BmRunStoppedIntent event = new BmRunStoppedIntent(game);
Bukkit.getPluginManager().callEvent(event);
event.verifyHandled();
return event;
}
@Override
public HandlerList getHandlers() {
return HANDLER_LIST;
}
public static HandlerList getHandlerList() {
return HANDLER_LIST;
}
@Override
public boolean isHandled() {
return delegate.isHandled();
}
@Override
public void setHandled() {
delegate.setHandled();
}
@Override
public void verifyHandled() {
delegate.verifyHandled();
}
@Override
public boolean isCancelled() {
return delegate.isCancelled();
}
@Override
@Deprecated
public void setCancelled(boolean cancel) {
delegate.setCancelled(cancel);
}
@Override
public void cancelFor(Message reason) {
delegate.cancelFor(reason);
}
@Override
public Message cancelledReason() {
return delegate.cancelledReason();
}
}

View File

@@ -0,0 +1,28 @@
package io.github.mviper.bomberman.events;
import io.github.mviper.bomberman.game.Game;
import org.bukkit.event.HandlerList;
/**
* Called whenever a game run is stopped. May be due to game finishing, game forcefully stopped, server shutdown, etc.
*/
public class BmTimerCountedEvent extends BmEvent {
private static final HandlerList HANDLER_LIST = new HandlerList();
public final Game game;
public int count;
public BmTimerCountedEvent(Game game, int count) {
this.game = game;
this.count = count;
}
@Override
public HandlerList getHandlers() {
return HANDLER_LIST;
}
public static HandlerList getHandlerList() {
return HANDLER_LIST;
}
}

View File

@@ -0,0 +1,9 @@
package io.github.mviper.bomberman.events;
public interface Intent {
boolean isHandled();
void setHandled();
void verifyHandled();
}

View File

@@ -0,0 +1,6 @@
package io.github.mviper.bomberman.events;
import org.bukkit.event.Cancellable;
public interface IntentCancellable extends Intent, Cancellable {
}

View File

@@ -0,0 +1,14 @@
package io.github.mviper.bomberman.events;
import io.github.mviper.bomberman.messaging.Message;
import org.bukkit.event.Cancellable;
public interface IntentCancellableReasoned extends Cancellable, Intent {
@Deprecated
@Override
void setCancelled(boolean cancel);
void cancelFor(Message reason);
Message cancelledReason();
}

View File

@@ -0,0 +1,75 @@
package io.github.mviper.bomberman.game;
import io.github.mviper.bomberman.Bomberman;
import io.github.mviper.bomberman.events.BmExplosionEvent;
import io.github.mviper.bomberman.events.BmPlayerPlacedBombEvent;
import io.github.mviper.bomberman.events.BmRunStoppedIntent;
import org.bukkit.Bukkit;
import org.bukkit.block.Block;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.HandlerList;
import org.bukkit.event.Listener;
import org.bukkit.plugin.Plugin;
public class Bomb implements Listener {
private static final Plugin PLUGIN = Bomberman.instance;
private final Game game;
private final Player player;
private final Block block;
private final int strength;
private final int taskId;
private boolean noExplode = false;
private Bomb(Game game, Player player, Block block, int strength, long fuse) {
this.game = game;
this.player = player;
this.block = block;
this.strength = strength;
this.taskId = Bukkit.getScheduler().scheduleSyncDelayedTask(PLUGIN, this::explode, fuse);
}
@EventHandler(ignoreCancelled = true, priority = EventPriority.HIGH)
public void onExplosion(BmExplosionEvent event) {
if (event.game != game) {
return;
}
if (!noExplode && event.igniting.stream().anyMatch(plan -> plan.block.equals(block))) {
Bukkit.getScheduler().cancelTask(taskId);
explode();
}
}
@EventHandler(ignoreCancelled = true, priority = EventPriority.HIGH)
public void onRunStopped(BmRunStoppedIntent event) {
if (event.game != game) {
return;
}
HandlerList.unregisterAll(this);
Bukkit.getScheduler().cancelTask(taskId);
noExplode = true;
}
private void explode() {
HandlerList.unregisterAll(this);
if (noExplode) {
return;
}
noExplode = true;
Explosion.spawnExplosion(game, block.getLocation(), player, strength);
}
public static boolean spawnBomb(Game game, Player player, Block block) {
BmPlayerPlacedBombEvent event = new BmPlayerPlacedBombEvent(game, player, block, game.getSettings().getFuseTicks());
Bukkit.getPluginManager().callEvent(event);
if (event.isCancelled()) {
return false;
}
Bomb bomb = new Bomb(game, player, block, event.strength, event.fuse);
Bukkit.getPluginManager().registerEvents(bomb, PLUGIN);
return true;
}
}

View File

@@ -0,0 +1,243 @@
package io.github.mviper.bomberman.game;
import io.github.mviper.bomberman.Bomberman;
import io.github.mviper.bomberman.events.BmDropLootEvent;
import io.github.mviper.bomberman.events.BmExplosionEvent;
import io.github.mviper.bomberman.events.BmPlayerHitIntent;
import io.github.mviper.bomberman.events.BmPlayerMovedEvent;
import io.github.mviper.bomberman.events.BmRunStoppedIntent;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.Sound;
import org.bukkit.block.Block;
import org.bukkit.block.BlockState;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.HandlerList;
import org.bukkit.event.Listener;
import org.bukkit.inventory.ItemStack;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
public class Explosion implements Listener {
public static class BlockPlan {
public final Block block;
public final BlockState prior;
public final BlockState ignited;
public final BlockState destroyed;
public BlockPlan(Block block, BlockState prior, BlockState ignited, BlockState destroyed) {
this.block = block;
this.prior = prior;
this.ignited = ignited;
this.destroyed = destroyed;
}
}
private final Game game;
private final Set<BlockPlan> blocks;
private final Player cause;
private final int taskId;
private boolean noExplode = false;
private Explosion(Game game, Set<BlockPlan> blocks, Player cause) {
this.game = game;
this.blocks = blocks;
this.cause = cause;
this.taskId = Bukkit.getScheduler().scheduleSyncDelayedTask(
Bomberman.instance,
this::cleanup,
game.getSettings().getFireTicks()
);
}
private void cleanup() {
if (noExplode) {
HandlerList.unregisterAll(this);
return;
}
for (BlockPlan plan : blocks) {
if (plan.ignited.getType() == plan.block.getType()) {
plan.destroyed.update(true);
}
}
if (cause.getScoreboardTags().contains("bm_player")) {
cause.getInventory().addItem(new ItemStack(game.getSettings().getBombItem(), 1));
}
Map<Location, Set<ItemStack>> dropsPlanned = planDrops();
BmDropLootEvent lootEvent = new BmDropLootEvent(game, cause, blocks, dropsPlanned);
Bukkit.getPluginManager().callEvent(lootEvent);
if (!lootEvent.isCancelled()) {
lootEvent.drops.forEach((location, items) -> {
for (ItemStack item : items) {
if (item.getAmount() > 0) {
if (location.getWorld() != null) {
location.getWorld().dropItem(location.clone().add(0.5, 0.5, 0.5), item);
}
}
}
});
}
HandlerList.unregisterAll(this);
}
private Map<Location, Set<ItemStack>> planDrops() {
Map<Material, Map<ItemStack, Integer>> loot = game.getSettings().getBlockLoot();
Map<Location, Set<ItemStack>> planned = new HashMap<>();
for (BlockPlan plan : blocks) {
Map<ItemStack, Integer> table = loot.getOrDefault(plan.prior.getType(), Map.of());
planned.put(plan.block.getLocation(), lootSelect(table));
}
return planned;
}
@EventHandler(priority = EventPriority.LOWEST, ignoreCancelled = true)
public void onPlayerMove(BmPlayerMovedEvent event) {
if (event.game != game) {
return;
}
Set<Block> fireBlocks = new HashSet<>();
for (BlockPlan plan : blocks) {
fireBlocks.add(plan.block);
}
if (isTouching(event.player, fireBlocks)) {
BmPlayerHitIntent.hit(event.player, cause);
}
}
@EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true)
public void onAnotherExplosion(BmExplosionEvent event) {
blocks.removeIf(thisBlock -> event.igniting.stream().anyMatch(plan -> plan.block.equals(thisBlock.block)));
}
@EventHandler(ignoreCancelled = true, priority = EventPriority.HIGH)
public void onRunStopped(BmRunStoppedIntent event) {
if (event.game != game) {
return;
}
Bukkit.getScheduler().cancelTask(taskId);
HandlerList.unregisterAll(this);
noExplode = true;
}
public static boolean spawnExplosion(Game game, Location center, Player cause, int strength) {
Set<Block> firePlanned = planFire(center, game, strength);
Set<BlockPlan> plannedTypes = new HashSet<>();
for (Block block : firePlanned) {
BlockState prior = block.getState();
BlockState ignited = block.getState();
if (!game.getSettings().getPassKeep().contains(block.getType())) {
ignited.setType(game.getSettings().getFireType());
}
BlockState converted = block.getState();
if (!game.getSettings().getPassKeep().contains(block.getType())) {
converted.setType(Material.AIR);
}
plannedTypes.add(new BlockPlan(block, prior, ignited, converted));
}
BmExplosionEvent event = new BmExplosionEvent(game, cause, plannedTypes);
Bukkit.getPluginManager().callEvent(event);
if (event.isCancelled()) {
return false;
}
for (BlockPlan plan : event.igniting) {
plan.ignited.update(true);
}
if (center.getWorld() != null) {
center.getWorld().playSound(center, Sound.ENTITY_GENERIC_EXPLODE, 1f, (float) Math.random() + 0.5f);
}
Explosion explosion = new Explosion(game, event.igniting, cause);
Bukkit.getPluginManager().registerEvents(explosion, Bomberman.instance);
return true;
}
public static boolean isTouching(Player player, Set<Block> blocks) {
for (Block block : blocks) {
double margin = 0.295;
Location playerLocation = player.getLocation();
Location min = block.getLocation().add(0.0, -1.0, 0.0);
Location max = block.getLocation().add(1.0, 2.0, 1.0);
if (playerLocation.getX() >= min.getX() - margin && playerLocation.getX() <= max.getX() + margin
&& playerLocation.getY() >= min.getY() - margin && playerLocation.getY() <= max.getY() + margin
&& playerLocation.getZ() >= min.getZ() - margin && playerLocation.getZ() <= max.getZ() + margin) {
return true;
}
}
return false;
}
private static Set<Block> planFire(Location center, Game game, int strength) {
Set<Block> blocks = new HashSet<>();
blocks.addAll(planFire(center, game, strength, 0, 1));
blocks.addAll(planFire(center, game, strength, 0, -1));
blocks.addAll(planFire(center, game, strength, 1, 0));
blocks.addAll(planFire(center, game, strength, -1, 0));
planFire(center, game, 0, -1, 0, blocks);
planFire(center, game, 0, 1, 0, blocks);
blocks.add(center.getBlock());
return blocks;
}
private static Set<Block> planFire(Location center, Game game, int strength, int xstep, int zstep) {
Set<Block> blocks = new HashSet<>();
for (int i = 1; i <= strength; i++) {
planFire(center, game, i * xstep, 1, i * zstep, blocks);
planFire(center, game, i * xstep, -1, i * zstep, blocks);
if (planFire(center, game, i * xstep, 0, i * zstep, blocks)) {
return blocks;
}
}
return blocks;
}
private static boolean planFire(Location center, Game game, int x, int y, int z, Set<Block> blocks) {
Location location = center.clone().add((double) z, (double) y, (double) x);
Block block = location.getBlock();
if (isPassing(block, game.getSettings())) {
blocks.add(block);
return false;
}
if (game.getSettings().getDestructible().contains(block.getType())) {
blocks.add(block);
}
return true;
}
private static boolean isPassing(Block block, GameSettings settings) {
Material type = block.getType();
return type == Material.AIR || type == settings.getFireType()
|| (block.isPassable() && !(settings.getIndestructible().contains(type) || settings.getDestructible().contains(type)))
|| settings.getPassDestroy().contains(type)
|| settings.getPassKeep().contains(type);
}
public static <T> Set<T> lootSelect(Map<? extends T, Integer> loot) {
int sum = loot.values().stream().mapToInt(Integer::intValue).sum();
for (Map.Entry<? extends T, Integer> entry : loot.entrySet()) {
if (sum * Math.random() <= entry.getValue()) {
return Set.of(entry.getKey());
}
sum -= entry.getValue();
}
if (sum == 0) {
return Set.of();
}
throw new RuntimeException("Explosion.drop didn't select (should never happen)");
}
}

View File

@@ -0,0 +1,648 @@
package io.github.mviper.bomberman.game;
import com.sk89q.worldedit.WorldEdit;
import com.sk89q.worldedit.bukkit.BukkitAdapter;
import com.sk89q.worldedit.extent.clipboard.BlockArrayClipboard;
import com.sk89q.worldedit.extent.clipboard.Clipboard;
import com.sk89q.worldedit.function.mask.BlockTypeMask;
import com.sk89q.worldedit.function.mask.Masks;
import com.sk89q.worldedit.function.operation.ForwardExtentCopy;
import com.sk89q.worldedit.function.operation.Operations;
import com.sk89q.worldedit.math.BlockVector3;
import com.sk89q.worldedit.session.ClipboardHolder;
import com.sk89q.worldedit.world.block.BlockTypes;
import io.github.mviper.bomberman.Bomberman;
import io.github.mviper.bomberman.commands.game.UndoBuild;
import io.github.mviper.bomberman.events.BmGameBuildIntent;
import io.github.mviper.bomberman.events.BmGameDeletedIntent;
import io.github.mviper.bomberman.events.BmGameListIntent;
import io.github.mviper.bomberman.events.BmGameLookupIntent;
import io.github.mviper.bomberman.events.BmGameTerminatedIntent;
import io.github.mviper.bomberman.events.BmPlayerJoinGameIntent;
import io.github.mviper.bomberman.events.BmPlayerLeaveGameIntent;
import io.github.mviper.bomberman.events.BmPlayerMovedEvent;
import io.github.mviper.bomberman.events.BmPlayerWonEvent;
import io.github.mviper.bomberman.events.BmRunStartCountDownIntent;
import io.github.mviper.bomberman.events.BmRunStartedIntent;
import io.github.mviper.bomberman.events.BmRunStoppedIntent;
import io.github.mviper.bomberman.messaging.CollectionWrapper;
import io.github.mviper.bomberman.messaging.Context;
import io.github.mviper.bomberman.messaging.Expander;
import io.github.mviper.bomberman.messaging.Formattable;
import io.github.mviper.bomberman.messaging.Message;
import io.github.mviper.bomberman.messaging.RequiredArg;
import io.github.mviper.bomberman.messaging.SenderWrapper;
import io.github.mviper.bomberman.messaging.Text;
import io.github.mviper.bomberman.utils.Box;
import io.github.mviper.bomberman.utils.BukkitUtils;
import io.github.mviper.bomberman.utils.WorldEditUtils;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.HandlerList;
import org.bukkit.event.Listener;
import org.bukkit.event.entity.EntityDamageEvent;
import org.bukkit.event.server.PluginDisableEvent;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;
public class Game implements Formattable, Listener {
private static final Bomberman PLUGIN = Bomberman.instance;
private final GameSave save;
public final String name;
private final Set<Player> players = new HashSet<>();
private boolean running = false;
private final YamlConfiguration tempData;
private Box box;
private Set<Location> spawns;
public Game(GameSave save) {
this.save = save;
this.name = save.name;
Path tempPath = tempDataFile(this);
if (Files.exists(tempPath)) {
try (var reader = Files.newBufferedReader(tempPath)) {
tempData = YamlConfiguration.loadConfiguration(reader);
} catch (IOException e) {
throw new RuntimeException("Failed to read temp data", e);
}
} else {
tempData = new YamlConfiguration();
}
if (tempData.getBoolean("rebuild-needed", false)) {
Bukkit.getScheduler().scheduleSyncDelayedTask(PLUGIN, () -> BmGameBuildIntent.build(this));
}
Bukkit.getPluginManager().registerEvents(this, PLUGIN);
}
public static Game buildGameFromRegion(String name, Box box, GameSettings settings) {
var region = WorldEditUtils.convert(box);
Clipboard clipboard = new BlockArrayClipboard(region);
try (var editSession = WorldEdit.getInstance().newEditSession(BukkitAdapter.adapt(box.world))) {
ForwardExtentCopy copy = new ForwardExtentCopy(editSession, region, clipboard, region.getMinimumPoint());
try {
Operations.complete(copy);
} catch (com.sk89q.worldedit.WorldEditException e) {
throw new RuntimeException("Failed to copy region", e);
}
}
GameSave save = GameSave.createNewSave(name, BukkitUtils.boxLoc1(box), settings, clipboard);
UndoBuild.removeHistory(name);
Game game = new Game(save);
game.makeCages();
return game;
}
public static Game buildGameFromSchema(String name, Location loc, Clipboard clipboard, GameSettings settings) {
GameSave save = GameSave.createNewSave(name, loc, settings, clipboard);
Game game = new Game(save);
UndoBuild.retainHistory(game.name, game.getBox());
BmGameBuildIntent.build(game);
return game;
}
private static Path tempDataFile(Game game) {
return PLUGIN.tempGameData().resolve(GameSave.sanitize(game.name + ".yml"));
}
public GameSettings getSettings() {
try {
return save.getSettings();
} catch (IOException e) {
throw new RuntimeException("Failed to load settings", e);
}
}
public String getName() {
return name;
}
public void setSettings(GameSettings value) {
try {
save.updateSettings(value);
} catch (IOException e) {
throw new RuntimeException("Failed to update settings", e);
}
}
public Location getOrigin() {
return save.origin;
}
public Clipboard getClipboard() throws IOException {
return save.getSchematic();
}
private Box getBox() {
if (box == null) {
try {
box = WorldEditUtils.pastedBounds(getOrigin(), getClipboard());
} catch (IOException e) {
throw new RuntimeException("Failed to read schematic", e);
}
}
return box;
}
private Set<Location> getSpawns() {
if (spawns == null) {
List<?> list = tempData.getList("spawn-points");
if (list != null) {
Set<Location> result = new HashSet<>();
for (Object item : list) {
if (item instanceof Location) {
result.add((Location) item);
}
}
spawns = result;
} else {
spawns = searchSpawns();
writeTempData("spawns", new ArrayList<>(spawns));
}
}
return spawns;
}
private void writeTempData(String path, Object obj) {
tempData.set(path, obj);
try (var writer = Files.newBufferedWriter(tempDataFile(this))) {
writer.write(tempData.saveToString());
} catch (IOException e) {
throw new RuntimeException("Failed to write temp data", e);
}
}
private Set<Location> searchSpawns() {
PLUGIN.getLogger().info("Searching for spawns...");
Set<Location> result = new HashSet<>();
try {
Clipboard clipboard = getClipboard();
for (BlockVector3 loc : clipboard.getRegion()) {
var block = clipboard.getFullBlock(loc);
var nbt = block.getNbtData();
if (nbt != null) {
boolean isSpawn = nbt.getString("Text1").contains("[spawn]")
|| nbt.getString("Text2").contains("[spawn]")
|| nbt.getString("Text3").contains("[spawn]")
|| nbt.getString("Text4").contains("[spawn]");
if (isSpawn) {
Location worldLocation = BukkitAdapter.adapt(getBox().world, loc.subtract(clipboard.getOrigin()))
.add(getOrigin()).getBlock().getLocation();
result.add(worldLocation);
}
}
}
} catch (IOException e) {
throw new RuntimeException("Failed to search spawns", e);
}
PLUGIN.getLogger().info(" " + result.size() + " spawns found");
return result;
}
private Location findSpareSpawn() {
for (Location spawn : getSpawns()) {
boolean occupied = false;
for (Player player : players) {
Location loc = player.getLocation();
if (spawn.getBlockX() == loc.getBlockX()
&& spawn.getBlockY() == loc.getBlockY()
&& spawn.getBlockZ() == loc.getBlockZ()) {
occupied = true;
break;
}
}
if (!occupied) {
return spawn;
}
}
return null;
}
private void removeCages() {
try {
try (var editSession = WorldEdit.getInstance().newEditSession(BukkitAdapter.adapt(save.origin.getWorld()))) {
BlockVector3 offset = BlockVector3.at(save.origin.getX(), save.origin.getY(), save.origin.getZ())
.subtract(save.getSchematic().getOrigin());
for (SpawnBlock entry : spawnBlocks()) {
if (!entry.isSpawn) {
Location loc = entry.block.getLocation();
BlockVector3 blockVec = BlockVector3.at(loc.getX(), loc.getY(), loc.getZ());
BlockVector3 clipLocation = blockVec.subtract(offset);
var blockState = save.getSchematic().getFullBlock(clipLocation);
editSession.setBlock(blockVec, blockState);
}
}
}
} catch (IOException | com.sk89q.worldedit.WorldEditException e) {
throw new RuntimeException("Failed to remove cages", e);
}
}
private void makeCages() {
for (SpawnBlock entry : spawnBlocks()) {
if (entry.isSpawn) {
entry.block.setType(Material.AIR);
} else if (entry.block.isPassable()) {
entry.block.setType(getSettings().getCageBlock());
}
}
}
private List<SpawnBlock> spawnBlocks() {
List<SpawnBlock> blocks = new ArrayList<>();
for (Location location : getSpawns()) {
for (int i = -1; i <= 1; i++) {
for (int j = -1; j <= 2; j++) {
for (int k = -1; k <= 1; k++) {
Location blockLoc = location.clone().add(i, j, k);
if (!getBox().contains(blockLoc)) {
continue;
}
var block = blockLoc.getBlock();
if ((j == 0 || j == 1) && (i == 0 && k == 0)) {
blocks.add(new SpawnBlock(true, block));
} else if (((j == 0 || j == 1) && (i == 0 || k == 0)) || (i == 0 && k == 0)) {
blocks.add(new SpawnBlock(false, block));
}
}
}
}
}
return blocks;
}
@EventHandler(ignoreCancelled = true, priority = EventPriority.LOW)
public void onGameListing(BmGameListIntent event) {
event.games.add(this);
}
@EventHandler(ignoreCancelled = true, priority = EventPriority.LOW)
public void onGameLookup(BmGameLookupIntent event) {
if (event.name.equalsIgnoreCase(name)) {
event.game = this;
}
}
@EventHandler(ignoreCancelled = true, priority = EventPriority.HIGHEST)
public void onPlayerJoinGame(BmPlayerJoinGameIntent event) {
if (event.game != this) {
return;
}
if (running) {
event.cancelFor(Text.GAME_ALREADY_STARTED.format(new Context(false)
.plus("game", this)
.plus("player", event.player)));
return;
}
Location gameSpawn = findSpareSpawn();
if (gameSpawn == null) {
event.cancelFor(Text.JOIN_GAME_FULL.format(new Context(false)
.plus("game", this)
.plus("player", event.player)));
return;
}
GamePlayer.spawnGamePlayer(event.player, this, gameSpawn);
players.add(event.player);
event.setHandled();
}
@EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST)
public void onPlayerDamaged(EntityDamageEvent event) {
if (!players.contains(event.getEntity())) {
return;
}
Map<String, Map<String, String>> damageSources = getSettings().getDamageSources();
List<Map<String, String>> damageChanges = new ArrayList<>();
for (Map.Entry<String, Map<String, String>> entry : damageSources.entrySet()) {
if (Pattern.compile(entry.getKey(), Pattern.CASE_INSENSITIVE)
.matcher(event.getCause().toString()).matches()) {
damageChanges.add(entry.getValue());
}
}
if (damageChanges.isEmpty() && event.getCause() != EntityDamageEvent.DamageCause.CUSTOM) {
event.setCancelled(true);
return;
}
for (Map<String, String> change : damageChanges) {
String cancelExpression = null;
for (Map.Entry<String, String> rule : change.entrySet()) {
if (rule.getKey().equalsIgnoreCase("cancel")) {
cancelExpression = rule.getValue();
break;
}
}
if (cancelExpression == null) {
continue;
}
String result = Expander.expand(cancelExpression, new Context(false)
.plus("base", Message.of(event.getDamage()))
.plus("final", Message.of(event.getFinalDamage()))
.plus("cause", Message.of(event.getCause().toString()))
.plus("player", new SenderWrapper((Player) event.getEntity()))
.plus("game", this)).toString();
try {
double asDouble = Double.parseDouble(result);
if (asDouble > 0.000001 || asDouble < -0.000001) {
event.setCancelled(true);
return;
}
} catch (NumberFormatException ignored) {
}
}
for (Map<String, String> rules : damageChanges) {
for (EntityDamageEvent.DamageModifier modifier : EntityDamageEvent.DamageModifier.values()) {
if (!event.isApplicable(modifier)) {
continue;
}
for (Map.Entry<String, String> rule : rules.entrySet()) {
if (!Pattern.compile(rule.getKey(), Pattern.CASE_INSENSITIVE)
.matcher(modifier.toString()).matches()) {
continue;
}
var result = Expander.expand(rule.getValue(), new Context(false)
.plus("base", Message.of(event.getDamage()))
.plus("damage", Message.of(event.getDamage(modifier)))
.plus("final", Message.of(event.getFinalDamage()))
.plus("cause", Message.of(event.getCause().toString()))
.plus("player", new SenderWrapper((Player) event.getEntity()))
.plus("game", this)
.plus("modifier", Message.of(modifier.toString())));
try {
double damage = Double.parseDouble(result.toString());
event.setDamage(modifier, damage);
} catch (NumberFormatException ignored) {
}
}
}
}
}
@EventHandler(ignoreCancelled = true, priority = EventPriority.HIGHEST)
public void onRunStartCountDown(BmRunStartCountDownIntent event) {
if (event.game != this) {
return;
}
if (running) {
event.cancelBecause(Text.GAME_ALREADY_STARTED.format(new Context(false).plus("game", this)));
return;
}
if (players.isEmpty()) {
event.cancelBecause(Text.GAME_NO_PLAYERS.format(new Context(false).plus("game", this)));
return;
}
StartTimer.createTimer(this, event.delay);
event.setHandled();
}
@EventHandler(ignoreCancelled = true, priority = EventPriority.HIGH)
public void onRunStarted(BmRunStartedIntent event) {
if (event.game != this) {
return;
}
running = true;
removeCages();
GameProtection.protect(this, getBox());
writeTempData("rebuild-needed", true);
event.setHandled();
}
@EventHandler(ignoreCancelled = true, priority = EventPriority.NORMAL)
public void onPlayerMoveOutOfArena(BmPlayerMovedEvent event) {
if (event.game != this) {
return;
}
if (!getBox().contains(event.getTo())) {
BmPlayerLeaveGameIntent.leave(event.player);
}
}
@EventHandler(ignoreCancelled = true, priority = EventPriority.HIGHEST)
public void onPlayerLeave(BmPlayerLeaveGameIntent event) {
if (players.contains(event.player)) {
players.remove(event.player);
if (players.size() < 1 || !running) {
Bukkit.getScheduler().scheduleSyncDelayedTask(PLUGIN, () -> BmRunStoppedIntent.stopGame(this));
} else if (players.size() == 1) {
for (Player player : players) {
Bukkit.getPluginManager().callEvent(new BmPlayerWonEvent(this, player));
}
Bukkit.getScheduler().scheduleSyncDelayedTask(PLUGIN, () -> BmRunStoppedIntent.stopGame(this), 5 * 20L);
}
}
}
@EventHandler(ignoreCancelled = true, priority = EventPriority.NORMAL)
public void onRunStoppedWhileRunning(BmRunStoppedIntent event) {
if (event.game != this) {
return;
}
if (!running) {
event.cancelFor(Text.STOP_NOT_STARTED.format(new Context(false).plus("game", this)));
}
}
@EventHandler(ignoreCancelled = true, priority = EventPriority.HIGH)
public void onRunStopped(BmRunStoppedIntent event) {
if (event.game != this) {
return;
}
if (running) {
running = false;
BmGameBuildIntent.build(this);
}
event.setHandled();
}
@EventHandler(ignoreCancelled = true, priority = EventPriority.HIGH)
public void onGameRebuild(BmGameBuildIntent event) {
if (event.game != this) {
return;
}
PLUGIN.getLogger().info("Building schematic ...");
getBox().world.getNearbyEntities(BukkitUtils.convert(getBox()))
.stream()
.filter(entity -> !(entity instanceof Player))
.forEach(entity -> entity.remove());
try (var editSession = WorldEdit.getInstance().newEditSession(BukkitAdapter.adapt(getBox().world))) {
var operation = new ClipboardHolder(getClipboard())
.createPaste(editSession)
.to(BlockVector3.at(getOrigin().getBlockX(), getOrigin().getBlockY(), getOrigin().getBlockZ()))
.copyEntities(true)
.maskSource(Masks.negate(new BlockTypeMask(editSession,
getSettings().getSourceMask().stream()
.map(mat -> BlockTypes.get(mat.getKey().toString()))
.filter(obj -> obj != null)
.toList())))
.build();
Operations.complete(operation);
} catch (IOException | com.sk89q.worldedit.WorldEditException e) {
throw new RuntimeException("Failed to rebuild game", e);
}
makeCages();
PLUGIN.getLogger().info("Rebuild done");
writeTempData("rebuild-needed", false);
event.setHandled();
}
@EventHandler(ignoreCancelled = true, priority = EventPriority.HIGH)
public void onGameTerminated(BmGameTerminatedIntent event) {
if (event.game != this) {
return;
}
BmRunStoppedIntent.stopGame(this);
HandlerList.unregisterAll(this);
event.setHandled();
}
@EventHandler(ignoreCancelled = true, priority = EventPriority.HIGH)
public void onGameDeleted(BmGameDeletedIntent event) {
if (event.game != this) {
return;
}
PLUGIN.getLogger().info("Deleting " + name + (event.isDeletingSave ? "" : " (keeping data)"));
BmGameTerminatedIntent.terminateGame(this);
try {
Files.deleteIfExists(tempDataFile(this));
} catch (IOException e) {
PLUGIN.getLogger().warning("Unable to delete temp data for " + name);
}
if (event.isDeletingSave) {
try {
Files.deleteIfExists(PLUGIN.gameSaves().resolve(GameSave.sanitize(name + ".game.zip")));
} catch (IOException e) {
PLUGIN.getLogger().warning("Unable to delete save for " + name);
}
}
event.setHandled();
}
@EventHandler(ignoreCancelled = true, priority = EventPriority.HIGH)
public void onServerStop(PluginDisableEvent event) {
if (event.getPlugin() != PLUGIN) {
return;
}
BmGameTerminatedIntent.terminateGame(this);
}
@Override
public Formattable applyModifier(Message arg) {
String key = arg.toString().toLowerCase(Locale.ROOT);
switch (key) {
case "name":
return Message.of(name);
case "spawns": {
List<Formattable> spawnList = new ArrayList<>();
for (Location location : getSpawns()) {
spawnList.add(new RequiredArg(spawnArg -> {
String spawnKey = spawnArg.toString().toLowerCase(Locale.ROOT);
return switch (spawnKey) {
case "world", "w" -> Message.of(location.getWorld() != null ? location.getWorld().getName() : "unknown");
case "x" -> Message.of((int) location.getX());
case "y" -> Message.of((int) location.getY());
case "z" -> Message.of((int) location.getZ());
default -> throw new IllegalArgumentException("Unknown spawn format " + spawnArg);
};
}));
}
return new CollectionWrapper<>(spawnList);
}
case "players": {
List<Formattable> playerList = new ArrayList<>();
for (Player player : players) {
playerList.add(new SenderWrapper(player));
}
return new CollectionWrapper<>(playerList);
}
case "power": {
int sum = 0;
for (var item : getSettings().getInitialItems()) {
if (item != null && item.getType() == getSettings().getBombItem()) {
sum += item.getAmount();
}
}
return Message.of(sum);
}
case "bombs": {
int sum = 0;
for (var item : getSettings().getInitialItems()) {
if (item != null && item.getType() == getSettings().getPowerItem()) {
sum += item.getAmount();
}
}
return Message.of(sum);
}
case "lives":
return Message.of(Integer.toString(getSettings().getLives()));
case "w":
case "world":
return Message.of(getOrigin().getWorld() != null ? getOrigin().getWorld().getName() : "unknown");
case "x":
return Message.of((int) getOrigin().getX());
case "y":
return Message.of((int) getOrigin().getY());
case "z":
return Message.of((int) getOrigin().getZ());
case "xsize":
return Message.of(getBox().getSize().x);
case "ysize":
return Message.of(getBox().getSize().y);
case "zsize":
return Message.of(getBox().getSize().z);
case "running":
return Message.of(running ? "true" : "false");
case "schema":
return this;
default:
return Message.empty;
}
}
@Override
public Message format(Context context) {
return applyModifier(Message.of("name")).format(context);
}
private static final class SpawnBlock {
private final boolean isSpawn;
private final org.bukkit.block.Block block;
private SpawnBlock(boolean isSpawn, org.bukkit.block.Block block) {
this.isSpawn = isSpawn;
this.block = block;
}
}
}

View File

@@ -0,0 +1,465 @@
package io.github.mviper.bomberman.game;
import com.sk89q.jnbt.StringTag;
import com.sk89q.worldedit.bukkit.BukkitAdapter;
import io.github.mviper.bomberman.Bomberman;
import io.github.mviper.bomberman.events.BmGameTerminatedIntent;
import io.github.mviper.bomberman.events.BmPlayerHitIntent;
import io.github.mviper.bomberman.events.BmPlayerHurtIntent;
import io.github.mviper.bomberman.events.BmPlayerJoinGameIntent;
import io.github.mviper.bomberman.events.BmPlayerKilledIntent;
import io.github.mviper.bomberman.events.BmPlayerLeaveGameIntent;
import io.github.mviper.bomberman.events.BmPlayerMovedEvent;
import io.github.mviper.bomberman.events.BmPlayerWonEvent;
import io.github.mviper.bomberman.events.BmRunStartedIntent;
import io.github.mviper.bomberman.events.BmRunStoppedIntent;
import io.github.mviper.bomberman.events.BmTimerCountedEvent;
import io.github.mviper.bomberman.messaging.Context;
import io.github.mviper.bomberman.messaging.Text;
import org.bukkit.Bukkit;
import org.bukkit.GameMode;
import org.bukkit.Location;
import org.bukkit.attribute.Attribute;
import org.bukkit.block.Block;
import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.entity.Item;
import org.bukkit.entity.Player;
import org.bukkit.event.Event;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.HandlerList;
import org.bukkit.event.Listener;
import org.bukkit.event.block.Action;
import org.bukkit.event.block.BlockPlaceEvent;
import org.bukkit.event.entity.EntityRegainHealthEvent;
import org.bukkit.event.player.PlayerInteractEvent;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerMoveEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import org.bukkit.event.player.PlayerRespawnEvent;
import org.bukkit.inventory.ItemStack;
import org.bukkit.potion.PotionEffect;
import org.bukkit.potion.PotionEffectType;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.logging.Level;
public class GamePlayer implements Listener {
private static final Bomberman PLUGIN = Bomberman.instance;
public static void spawnGamePlayer(Player player, Game game, Location start) {
GamePlayer gamePlayer = new GamePlayer(player, game);
PLUGIN.getServer().getPluginManager().registerEvents(gamePlayer, PLUGIN);
YamlConfiguration dataFile = new YamlConfiguration();
dataFile.set("location", player.getLocation());
dataFile.set("gamemode", player.getGameMode().name().toLowerCase(Locale.ROOT));
dataFile.set("health", player.getHealth());
dataFile.set("health-scale", player.getHealthScale());
dataFile.set("health-max", player.getAttribute(Attribute.GENERIC_MAX_HEALTH).getBaseValue());
dataFile.set("food-level", player.getFoodLevel());
dataFile.set("inventory", java.util.Arrays.asList(player.getInventory().getContents()));
dataFile.set("is-flying", player.isFlying());
try (var writer = Files.newBufferedWriter(tempDataFile(player))) {
writer.write(dataFile.saveToString());
} catch (Exception e) {
PLUGIN.getLogger().log(Level.WARNING, "Unable to store player data", e);
}
try {
player.addAttachment(PLUGIN, "group.bomberman", true);
} catch (Exception e) {
PLUGIN.getLogger().log(Level.WARNING, "Unable to add permissions", e);
}
start.getWorld().getNearbyEntities(start, 2.0, 3.0, 2.0).stream()
.filter(entity -> entity instanceof Item)
.forEach(entity -> entity.remove());
var maxHealth = player.getAttribute(Attribute.GENERIC_MAX_HEALTH);
maxHealth.setBaseValue(game.getSettings().getLives());
maxHealth.getModifiers().forEach(maxHealth::removeModifier);
player.setHealth(game.getSettings().getLives());
Bukkit.getScheduler().scheduleSyncDelayedTask(PLUGIN, () ->
player.setHealthScale(game.getSettings().getLives() * 2.0)
);
if (!player.teleport(start.clone().add(0.5, 0.01, 0.5))) {
Bukkit.getScheduler().scheduleSyncDelayedTask(PLUGIN, () ->
BmPlayerLeaveGameIntent.leave(player)
);
}
player.setGameMode(GameMode.SURVIVAL);
player.setExhaustion(0f);
player.setFoodLevel(100000);
player.setFlying(false);
player.getInventory().clear();
List<ItemStack> initialItems = game.getSettings().getInitialItems();
for (int i = 0; i < initialItems.size() && i < player.getInventory().getSize(); i++) {
ItemStack stack = initialItems.get(i);
player.getInventory().setItem(i, stack != null ? stack.clone() : null);
}
removePotionEffects(player);
player.addScoreboardTag("bm_player");
}
public static int bombStrength(Game game, Player player) {
int strength = 1;
for (ItemStack stack : player.getInventory().getContents()) {
if (stack != null && stack.getType() == game.getSettings().getPowerItem()) {
strength += stack.getAmount();
}
}
return Math.max(strength, 1);
}
public static void setupLoginWatcher() {
Bukkit.getPluginManager().registerEvents(new Listener() {
@EventHandler(ignoreCancelled = true, priority = EventPriority.LOW)
public void onPlayerLogin(PlayerJoinEvent event) {
Player player = event.getPlayer();
if (player.isDead()) {
return;
}
Path save = tempDataFile(player);
if (Files.exists(save)) {
reset(player);
}
}
@EventHandler(ignoreCancelled = true, priority = EventPriority.LOW)
public void onPlayerRespawn(PlayerRespawnEvent event) {
Player player = event.getPlayer();
Path save = tempDataFile(player);
if (Files.exists(save)) {
event.setRespawnLocation(reset(player));
}
}
}, PLUGIN);
}
private static Location reset(Player player) {
Path file = tempDataFile(player);
YamlConfiguration dataFile;
try (var reader = Files.newBufferedReader(file)) {
dataFile = YamlConfiguration.loadConfiguration(reader);
} catch (Exception e) {
throw new RuntimeException("Unable to load player data", e);
}
String gamemode = dataFile.getString("gamemode");
GameMode mode = GameMode.SURVIVAL;
if (gamemode != null) {
try {
mode = GameMode.valueOf(gamemode.toUpperCase(Locale.ROOT));
} catch (IllegalArgumentException ignored) {
}
}
player.setGameMode(mode);
player.setHealthScale(dataFile.getDouble("health-scale", 20));
var maxHealth = player.getAttribute(Attribute.GENERIC_MAX_HEALTH);
maxHealth.setBaseValue(dataFile.getDouble("health-max", 20.0));
maxHealth.getModifiers().forEach(maxHealth::removeModifier);
double health = dataFile.getDouble("health", 20.0);
player.setHealth(Math.min(health, maxHealth.getValue()));
player.setFoodLevel(dataFile.getInt("food-level", 20));
List<?> inventory = dataFile.getList("inventory", new ArrayList<>());
ItemStack[] contents = new ItemStack[inventory.size()];
for (int i = 0; i < inventory.size(); i++) {
Object item = inventory.get(i);
contents[i] = item instanceof ItemStack ? (ItemStack) item : null;
}
player.getInventory().setContents(contents);
player.setFlying(dataFile.getBoolean("is-flying", false));
Location location = dataFile.getLocation("location");
if (location == null && !Bukkit.getServer().getWorlds().isEmpty()) {
location = Bukkit.getServer().getWorlds().get(0).getSpawnLocation();
}
if (location != null) {
player.teleport(location);
}
player.removeScoreboardTag("bm_player");
try {
player.addAttachment(PLUGIN, "group.bomberman", false);
} catch (Exception e) {
PLUGIN.getLogger().log(Level.WARNING, "Unable to remove permissions", e);
}
try {
Files.deleteIfExists(file);
} catch (Exception e) {
PLUGIN.getLogger().log(Level.WARNING, "Unable to delete player data", e);
}
removePotionEffects(player);
return location;
}
private static void removePotionEffects(Player player) {
Bukkit.getScheduler().scheduleSyncDelayedTask(PLUGIN, () -> {
player.setFireTicks(0);
for (PotionEffect effect : player.getActivePotionEffects()) {
player.removePotionEffect(effect.getType());
}
});
}
private static Path tempDataFile(Player player) {
return PLUGIN.tempPlayerData().resolve(player.getName() + ".yml");
}
private final Player player;
private final Game game;
private boolean immunity = false;
private GamePlayer(Player player, Game game) {
this.player = player;
this.game = game;
}
private void resetStuffAndUnregister() {
player.getWorld().getNearbyEntities(player.getLocation(), 1.0, 2.0, 1.0).stream()
.filter(entity -> entity instanceof Item)
.forEach(entity -> entity.remove());
reset(player);
HandlerList.unregisterAll(this);
}
@EventHandler(ignoreCancelled = true, priority = EventPriority.LOW)
public void onPlayerJoinGame(BmPlayerJoinGameIntent event) {
if (event.player == player) {
event.cancelFor(Text.JOIN_ALREADY_JOINED.format(new Context(false)
.plus("game", event.game)
.plus("player", player)));
}
}
@EventHandler(ignoreCancelled = true, priority = EventPriority.HIGH)
public void onCount(BmTimerCountedEvent event) {
if (event.game != game) {
return;
}
if (event.count > 0) {
Text.GAME_COUNT.format(new Context(false)
.plus("time", event.count)
.plus("game", game))
.sendTo(player);
}
}
@EventHandler(ignoreCancelled = true, priority = EventPriority.HIGH)
public void onRunStarted(BmRunStartedIntent event) {
if (event.game != game) {
return;
}
Text.GAME_STARTED.format(new Context(false).plus("game", game)).sendTo(player);
event.setHandled();
}
@EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST)
public void onPlayerRegen(EntityRegainHealthEvent event) {
if (event.getEntity() != player) {
return;
}
if (event.getRegainReason() == EntityRegainHealthEvent.RegainReason.MAGIC) {
event.setAmount(1.0);
} else {
event.setCancelled(true);
}
}
@EventHandler(ignoreCancelled = true, priority = EventPriority.LOW)
public void onPlayerBreakBlockWithWrongTool(PlayerInteractEvent event) {
if (event.getPlayer() != player) {
return;
}
if (event.getAction() != Action.LEFT_CLICK_BLOCK || !event.hasBlock()) {
return;
}
String key = event.getClickedBlock() != null
? event.getClickedBlock().getBlockData().getMaterial().getKey().toString()
: null;
if (event.getItem() != null && key != null) {
var nbt = BukkitAdapter.adapt(event.getItem()).getNbtData();
var list = nbt != null ? nbt.getList("CanDestroy", StringTag.class) : null;
if (list != null && !list.isEmpty()) {
boolean allowed = list.stream().anyMatch(tag -> key.equalsIgnoreCase(tag.getValue()));
if (allowed) {
return;
}
}
}
event.setCancelled(true);
event.setUseInteractedBlock(Event.Result.DENY);
event.setUseItemInHand(Event.Result.DENY);
event.getPlayer().addPotionEffect(new PotionEffect(PotionEffectType.MINING_FATIGUE, 20, 1));
}
@EventHandler(ignoreCancelled = true, priority = EventPriority.LOW)
public void onPlayerPlaceBlock(BlockPlaceEvent event) {
if (event.getPlayer() != player) {
return;
}
String key = event.getBlockAgainst().getBlockData().getMaterial().getKey().toString();
var nbt = BukkitAdapter.adapt(event.getItemInHand()).getNbtData();
var list = nbt != null ? nbt.getList("CanPlaceOn", StringTag.class) : null;
if (list != null && !list.isEmpty()) {
boolean allowed = list.stream().anyMatch(tag -> key.equalsIgnoreCase(tag.getValue()));
if (!allowed) {
event.setCancelled(true);
}
}
}
@EventHandler(ignoreCancelled = true, priority = EventPriority.HIGHEST)
public void onPlayerPlaceTNT(BlockPlaceEvent event) {
if (event.getPlayer() != player) {
return;
}
var block = event.getBlock();
if (block.getType() == game.getSettings().getBombItem()) {
if (!Bomb.spawnBomb(game, player, block)) {
event.setCancelled(true);
}
}
}
@EventHandler(ignoreCancelled = true, priority = EventPriority.LOW)
public void onPlayerMoved(PlayerMoveEvent event) {
if (event.getPlayer() != player) {
return;
}
Location from = event.getFrom();
Location to = event.getTo() != null ? event.getTo() : event.getFrom();
Bukkit.getPluginManager().callEvent(new BmPlayerMovedEvent(game, player, from, to));
}
@EventHandler(ignoreCancelled = true, priority = EventPriority.HIGH)
public void onExplosion(io.github.mviper.bomberman.events.BmExplosionEvent event) {
if (event.game != game) {
return;
}
Set<Block> blocks = new java.util.HashSet<>();
for (Explosion.BlockPlan plan : event.igniting) {
blocks.add(plan.block);
}
if (Explosion.isTouching(player, blocks)) {
BmPlayerHitIntent.hit(player, event.cause);
}
}
@EventHandler(ignoreCancelled = true, priority = EventPriority.HIGH)
public void onPlayerHit(BmPlayerHitIntent event) {
if (event.player != player) {
return;
}
BmPlayerHurtIntent.run(game, player, event.cause);
event.setHandled();
}
@EventHandler(ignoreCancelled = true, priority = EventPriority.LOW)
public void onPlayerHurtWithImmunity(BmPlayerHurtIntent event) {
if (event.player != player) {
return;
}
if (immunity) {
event.setCancelled(true);
}
}
@EventHandler(ignoreCancelled = true, priority = EventPriority.HIGH)
public void onPlayerDamaged(BmPlayerHurtIntent event) {
if (event.player != player) {
return;
}
if (player.getHealth() > 1) {
player.damage(1.0);
immunity = true;
player.setFireTicks(game.getSettings().getImmunityTicks());
Bukkit.getScheduler().scheduleSyncDelayedTask(PLUGIN, () -> {
immunity = false;
player.setFireTicks(0);
Bukkit.getPluginManager().callEvent(new BmPlayerMovedEvent(game, player, player.getLocation(), player.getLocation()));
}, game.getSettings().getImmunityTicks());
} else {
BmPlayerKilledIntent.kill(game, player, event.attacker);
}
event.setHandled();
}
@EventHandler(ignoreCancelled = true, priority = EventPriority.HIGH)
public void onPlayerKilledInGame(BmPlayerKilledIntent event) {
if (event.player != player) {
return;
}
player.setHealth(0.0);
BmPlayerLeaveGameIntent.leave(player);
event.setHandled();
}
@EventHandler(ignoreCancelled = true, priority = EventPriority.HIGH)
public void onPlayerWon(BmPlayerWonEvent event) {
if (event.player != player) {
return;
}
Text.PLAYER_WON.format(new Context(false).plus("player", player)).sendTo(player);
immunity = true;
}
@EventHandler(ignoreCancelled = true, priority = EventPriority.HIGH)
public void onPlayerLeaveGameEvent(BmPlayerLeaveGameIntent event) {
if (event.player != player) {
return;
}
player.getWorld().getNearbyEntities(player.getLocation(), 2.0, 3.0, 2.0).stream()
.filter(entity -> entity instanceof Item)
.forEach(entity -> entity.remove());
if (player.isDead()) {
HandlerList.unregisterAll(this);
} else {
resetStuffAndUnregister();
}
event.setHandled(game);
}
@EventHandler(ignoreCancelled = true, priority = EventPriority.HIGHEST)
public void onGameStopped(BmRunStoppedIntent event) {
if (event.game != game) {
return;
}
BmPlayerLeaveGameIntent.leave(player);
}
@EventHandler(ignoreCancelled = true, priority = EventPriority.HIGHEST)
public void onGameTerminated(BmGameTerminatedIntent event) {
if (event.game != game) {
return;
}
BmPlayerLeaveGameIntent.leave(player);
}
@EventHandler(ignoreCancelled = true, priority = EventPriority.LOW)
public void onPlayerLogout(PlayerQuitEvent event) {
if (event.getPlayer() == player) {
BmPlayerLeaveGameIntent.leave(player);
}
}
}

View File

@@ -0,0 +1,65 @@
package io.github.mviper.bomberman.game;
import io.github.mviper.bomberman.Bomberman;
import io.github.mviper.bomberman.events.BmRunStoppedIntent;
import io.github.mviper.bomberman.utils.Box;
import org.bukkit.Bukkit;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.HandlerList;
import org.bukkit.event.Listener;
import org.bukkit.event.block.BlockBurnEvent;
import org.bukkit.event.block.BlockIgniteEvent;
import org.bukkit.event.block.BlockSpreadEvent;
import org.bukkit.plugin.Plugin;
/**
* Protects an arena from getting damaged from the game.
*
* It is up to Server Owners to protect the arena from griefers.
*/
public class GameProtection implements Listener {
private static final Plugin PLUGIN = Bomberman.instance;
private final Game game;
private final Box bounds;
private GameProtection(Game game, Box bounds) {
this.game = game;
this.bounds = bounds;
}
@EventHandler(ignoreCancelled = true, priority = EventPriority.HIGH)
public void onRunStopped(BmRunStoppedIntent event) {
if (event.game == game) {
HandlerList.unregisterAll(this);
}
}
@EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST)
public void onBlockBurn(BlockBurnEvent event) {
if (bounds.contains(event.getBlock().getLocation())) {
event.setCancelled(true);
}
}
@EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST)
public void onBlockIgnite(BlockIgniteEvent event) {
if (event.getCause() == BlockIgniteEvent.IgniteCause.SPREAD
&& bounds.contains(event.getBlock().getLocation())) {
event.setCancelled(true);
}
}
@EventHandler(ignoreCancelled = true, priority = EventPriority.LOW)
public void onFireSpread(BlockSpreadEvent event) {
if (bounds.contains(event.getBlock().getLocation())) {
event.setCancelled(true);
}
}
public static void protect(Game game, Box bounds) {
GameProtection protection = new GameProtection(game, bounds);
Bukkit.getPluginManager().registerEvents(protection, PLUGIN);
}
}

View File

@@ -0,0 +1,255 @@
package io.github.mviper.bomberman.game;
import com.sk89q.worldedit.extent.clipboard.Clipboard;
import com.sk89q.worldedit.extent.clipboard.io.BuiltInClipboardFormat;
import com.sk89q.worldedit.extent.clipboard.io.ClipboardFormat;
import com.sk89q.worldedit.extent.clipboard.io.ClipboardFormats;
import io.github.mviper.bomberman.Bomberman;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.configuration.file.YamlConfiguration;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.lang.ref.WeakReference;
import java.net.URI;
import java.nio.file.DirectoryStream;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
/**
* Handles reading/writing a game's data to disk.
*/
public class GameSave {
private static final Bomberman PLUGIN = Bomberman.instance;
public final String name;
public final Location origin;
private final Path zipPath;
private WeakReference<Clipboard> schematicCache;
private GameSettings settingsCache;
private GameSave(String name, Location origin, Path zipPath) {
this.name = name;
this.origin = origin;
this.zipPath = zipPath;
}
public static GameSave createNewSave(String name, Location origin, GameSettings settings, Clipboard schematic) {
Path zipPath = PLUGIN.gameSaves().resolve(sanitize(name + ".game.zip"));
URI fileUri = zipPath.toUri();
URI zipUri = URI.create("jar:" + fileUri);
Map<String, String> env = new HashMap<>();
if (!Files.exists(zipPath)) {
env.put("create", "true");
}
try (FileSystem fs = FileSystems.newFileSystem(zipUri, env)) {
Path arenaPath = fs.getPath("arena.schem");
try (var os = Files.newOutputStream(arenaPath)) {
try (var writer = BuiltInClipboardFormat.SPONGE_SCHEMATIC.getWriter(os)) {
writer.write(schematic);
}
}
Path settingsPath = fs.getPath("settings.yml");
YamlConfiguration settingsYml = new YamlConfiguration();
settingsYml.set("settings", settings);
Files.write(settingsPath, java.util.List.of(settingsYml.saveToString()));
Path configPath = fs.getPath("config.yml");
YamlConfiguration configYml = new YamlConfiguration();
configYml.set("name", name);
configYml.set("origin", origin);
Files.write(configPath, java.util.List.of(configYml.saveToString()));
Path readmePath = fs.getPath("README.txt");
try (var in = PLUGIN.getResource("zip README.txt")) {
if (in != null) {
Files.copy(in, readmePath, StandardCopyOption.REPLACE_EXISTING);
}
}
} catch (IOException e) {
throw new RuntimeException("Failed to create save file", e);
}
GameSave save = new GameSave(name, origin, zipPath);
save.schematicCache = new WeakReference<>(schematic);
save.settingsCache = settings;
return save;
}
public static void loadGames() {
Path data = PLUGIN.gameSaves();
try (DirectoryStream<Path> files = Files.newDirectoryStream(data, "*.game.zip")) {
for (Path file : files) {
try {
loadGame(file);
} catch (Exception e) {
PLUGIN.getLogger().log(Level.WARNING, "Exception occurred while loading: " + file, e);
}
}
} catch (IOException e) {
PLUGIN.getLogger().log(Level.WARNING, "Exception occurred while listing saves", e);
}
}
public static GameSave loadSave(Path zipFile) throws IOException {
PLUGIN.getLogger().info("Reading " + zipFile);
try (FileSystem fs = FileSystems.newFileSystem(zipFile, (ClassLoader) null)) {
Path configPath = fs.getPath("config.yml");
try (var reader = Files.newBufferedReader(configPath)) {
YamlConfiguration config = YamlConfiguration.loadConfiguration(reader);
String name = config.getString("name");
Location origin = config.getSerializable("origin", Location.class);
if (name == null || origin == null) {
throw new IOException("Cannot read 'config.yml' in '" + zipFile + "'");
}
PLUGIN.getLogger().info(" Data read");
return new GameSave(name, origin, zipFile);
}
}
}
public static Game loadGame(Path zipFile) throws IOException {
return new Game(loadSave(zipFile));
}
public static void updatePre080Saves() {
try (DirectoryStream<Path> files = Files.newDirectoryStream(PLUGIN.gameSaves(), "*.yml")) {
for (Path file : files) {
try {
PLUGIN.getLogger().info("Updating old save: " + file);
YamlConfiguration config;
try (var reader = Files.newBufferedReader(file)) {
config = YamlConfiguration.loadConfiguration(reader);
}
String name = config.getString("name");
String schema = config.getString("schema");
Location origin = config.getSerializable("origin", Location.class);
GameSettings settings = config.getSerializable("settings", GameSettings.class);
if (settings == null) {
settings = new GameSettingsBuilder().build();
}
GameSettingsBuilder builder = new GameSettingsBuilder(settings);
if (config.getBoolean("build-flags.skip-air", false)) {
builder.sourceMask = Set.of(Material.AIR);
} else {
builder.sourceMask = Set.of();
}
settings = builder.build();
if (name == null || schema == null || origin == null) {
PLUGIN.getLogger().info(" Skipping update as file missing data");
continue;
}
File schemaFile = new File(schema);
PLUGIN.getLogger().info(" Loading schematic: " + schemaFile.getPath());
ClipboardFormat format = ClipboardFormats.findByFile(schemaFile);
if (format == null) {
throw new IllegalArgumentException("Unknown file format: '" + schemaFile.getPath() + "'");
}
Clipboard clipboard;
try (var input = new FileInputStream(schemaFile)) {
clipboard = format.getReader(input).read();
}
createNewSave(name, origin, settings, clipboard);
Files.deleteIfExists(file);
PLUGIN.getLogger().info(" Save Updated");
} catch (Exception e) {
PLUGIN.getLogger().log(Level.WARNING, "Exception occurred while updating " + file, e);
}
}
} catch (IOException e) {
PLUGIN.getLogger().log(Level.WARNING, "Exception occurred while listing old saves", e);
}
}
public static String sanitize(String filename) {
return filename.toLowerCase().replaceAll("[^a-z0-9._-]", "_");
}
public Clipboard getSchematic() throws NoSuchFileException {
Clipboard cached = schematicCache != null ? schematicCache.get() : null;
if (cached != null) {
return cached;
}
PLUGIN.getLogger().info("Reading schematic data: " + zipPath.getFileName());
try (FileSystem fs = FileSystems.newFileSystem(zipPath, (ClassLoader) null)) {
Path arenaPath = fs.getPath("arena.schem");
Clipboard schematic;
try (var reader = Files.newInputStream(arenaPath)) {
schematic = BuiltInClipboardFormat.SPONGE_SCHEMATIC.getReader(reader).read();
}
PLUGIN.getLogger().info("Data read");
schematicCache = new WeakReference<>(schematic);
return schematic;
} catch (IOException e) {
if (e instanceof NoSuchFileException) {
throw (NoSuchFileException) e;
}
throw new RuntimeException("Failed to read schematic", e);
}
}
public GameSettings getSettings() throws NoSuchFileException {
if (settingsCache != null) {
return settingsCache;
}
PLUGIN.getLogger().info("Reading game settings: " + zipPath.getFileName());
try (FileSystem fs = FileSystems.newFileSystem(zipPath, (ClassLoader) null)) {
Path settingsPath = fs.getPath("settings.yml");
try (var reader = Files.newBufferedReader(settingsPath)) {
YamlConfiguration settingsYml = YamlConfiguration.loadConfiguration(reader);
GameSettings settings = settingsYml.getSerializable("settings", GameSettings.class);
if (settings == null) {
settings = new GameSettingsBuilder().build();
}
settingsCache = settings;
PLUGIN.getLogger().info("Data read");
return settings;
}
} catch (IOException e) {
if (e instanceof NoSuchFileException) {
throw (NoSuchFileException) e;
}
throw new RuntimeException("Failed to read game settings", e);
}
}
public void updateSettings(GameSettings settings) throws IOException {
settingsCache = null;
YamlConfiguration yml = new YamlConfiguration();
yml.set("settings", settings);
String ymlString = yml.saveToString();
try (FileSystem fs = FileSystems.newFileSystem(zipPath, (ClassLoader) null)) {
Path zipConfigPath = fs.getPath("settings.yml");
Files.copy(
new ByteArrayInputStream(ymlString.getBytes()),
zipConfigPath,
StandardCopyOption.REPLACE_EXISTING
);
}
settingsCache = settings;
}
}

View File

@@ -0,0 +1,426 @@
package io.github.mviper.bomberman.game;
import io.github.mviper.bomberman.Bomberman;
import io.github.mviper.bomberman.utils.RefectAccess;
import org.bukkit.Material;
import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.configuration.serialization.ConfigurationSerializable;
import org.bukkit.inventory.ItemStack;
import java.io.InputStreamReader;
import java.io.Reader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
/**
* Defines the settings for how a game operates. Class is immutable.
*/
public class GameSettings implements ConfigurationSerializable {
private final Material bombItem;
private final Material powerItem;
private final Material fireType;
private final Map<Material, Map<ItemStack, Integer>> blockLoot;
private final Set<Material> destructible;
private final Set<Material> indestructible;
private final Set<Material> passKeep;
private final Set<Material> passDestroy;
private final List<ItemStack> initialItems;
private final int lives;
private final int fuseTicks;
private final int fireTicks;
private final int immunityTicks;
private final Map<String, Map<String, String>> damageSources;
private final Set<Material> sourceMask;
private final Material cageBlock;
private static volatile boolean loadingDefault = false;
private static volatile GameSettings defaultSettings;
public GameSettings(
Material bombItem,
Material powerItem,
Material fireType,
Map<Material, Map<ItemStack, Integer>> blockLoot,
Set<Material> destructible,
Set<Material> indestructible,
Set<Material> passKeep,
Set<Material> passDestroy,
List<ItemStack> initialItems,
int lives,
int fuseTicks,
int fireTicks,
int immunityTicks,
Map<String, Map<String, String>> damageSources,
Set<Material> sourceMask,
Material cageBlock
) {
this.bombItem = bombItem;
this.powerItem = powerItem;
this.fireType = fireType;
this.blockLoot = blockLoot;
this.destructible = destructible;
this.indestructible = indestructible;
this.passKeep = passKeep;
this.passDestroy = passDestroy;
this.initialItems = initialItems;
this.lives = lives;
this.fuseTicks = fuseTicks;
this.fireTicks = fireTicks;
this.immunityTicks = immunityTicks;
this.damageSources = damageSources;
this.sourceMask = sourceMask;
this.cageBlock = cageBlock;
}
public Material getBombItem() {
return bombItem;
}
public Material getPowerItem() {
return powerItem;
}
public Material getFireType() {
return fireType;
}
public Map<Material, Map<ItemStack, Integer>> getBlockLoot() {
return blockLoot;
}
public Set<Material> getDestructible() {
return destructible;
}
public Set<Material> getIndestructible() {
return indestructible;
}
public Set<Material> getPassKeep() {
return passKeep;
}
public Set<Material> getPassDestroy() {
return passDestroy;
}
public List<ItemStack> getInitialItems() {
return initialItems;
}
public int getLives() {
return lives;
}
public int getFuseTicks() {
return fuseTicks;
}
public int getFireTicks() {
return fireTicks;
}
public int getImmunityTicks() {
return immunityTicks;
}
public Map<String, Map<String, String>> getDamageSources() {
return damageSources;
}
public Set<Material> getSourceMask() {
return sourceMask;
}
public Material getCageBlock() {
return cageBlock;
}
@RefectAccess
public static GameSettings deserialize(Map<String, Object> data) {
GameSettings defaults = loadingDefault ? null : getDefaultSettings();
Material bombItem = readMaterial(data.get("bomb"), defaults != null ? defaults.bombItem : null);
Material powerItem = readMaterial(data.get("power"), defaults != null ? defaults.powerItem : null);
Material fireType = readMaterial(data.get("fire"), defaults != null ? defaults.fireType : null);
Map<Material, Map<ItemStack, Integer>> blockLoot = readLootTable(data.get("loot-table"));
if (blockLoot == null && defaults != null) {
blockLoot = defaults.blockLoot;
}
Set<Material> destructible = readMaterials(data.get("destructible"));
if (destructible == null && defaults != null) {
destructible = defaults.destructible;
}
Set<Material> indestructible = readMaterials(data.get("indestructible"));
if (indestructible == null && defaults != null) {
indestructible = defaults.indestructible;
}
Set<Material> passKeep = readMaterials(data.get("pass-keep"));
if (passKeep == null && defaults != null) {
passKeep = defaults.passKeep;
}
Set<Material> passDestroy = readMaterials(data.get("pass-destroy"));
if (passDestroy == null && defaults != null) {
passDestroy = defaults.passDestroy;
}
List<ItemStack> initialItems = readItemList(data.get("initial-items"));
if (initialItems == null && defaults != null) {
initialItems = defaults.initialItems;
}
int lives = readInt(data.get("lives"), defaults != null ? defaults.lives : 0);
int fuseTicks = Math.max(0, readInt(data.get("fuse-ticks"), defaults != null ? defaults.fuseTicks : 0));
int fireTicks = Math.max(0, readInt(data.get("fire-ticks"), defaults != null ? defaults.fireTicks : 0));
int immunityTicks = Math.max(0, readInt(data.get("immunity-ticks"), defaults != null ? defaults.immunityTicks : 0));
Map<String, Map<String, String>> damageSources = readDamageSources(data.get("damage-source"));
if (damageSources == null && defaults != null) {
damageSources = defaults.damageSources;
}
Set<Material> sourceMask = readMaterials(data.get("source-mask"));
if (sourceMask == null && defaults != null) {
sourceMask = defaults.sourceMask;
}
Material cageBlock = readMaterial(data.get("cage-block"), defaults != null ? defaults.cageBlock : null);
return new GameSettings(
bombItem,
powerItem,
fireType,
blockLoot,
destructible,
indestructible,
passKeep,
passDestroy,
initialItems,
lives,
fuseTicks,
fireTicks,
immunityTicks,
damageSources,
sourceMask,
cageBlock
);
}
private static int readInt(Object value, int fallback) {
if (value instanceof Number) {
return ((Number) value).intValue();
}
return fallback;
}
private static Material readMaterial(Object value, Material fallback) {
if (value instanceof String) {
Material material = Material.matchMaterial((String) value);
if (material != null) {
return material;
}
}
return fallback;
}
private static Set<Material> readMaterials(Object value) {
if (!(value instanceof List<?>)) {
return null;
}
Set<Material> result = new HashSet<>();
for (Object item : (List<?>) value) {
if (item instanceof String) {
Material material = Material.matchMaterial((String) item);
if (material != null) {
result.add(material);
}
}
}
return result;
}
private static List<ItemStack> readItemList(Object value) {
if (!(value instanceof List<?>)) {
return null;
}
List<ItemStack> result = new ArrayList<>();
for (Object item : (List<?>) value) {
if (item instanceof ItemStack) {
result.add((ItemStack) item);
} else {
result.add(null);
}
}
return result;
}
@SuppressWarnings("unchecked")
private static Map<Material, Map<ItemStack, Integer>> readLootTable(Object value) {
if (!(value instanceof List<?>)) {
return null;
}
Map<Material, Map<ItemStack, Integer>> result = new HashMap<>();
for (Object sectionObj : (List<?>) value) {
if (!(sectionObj instanceof Map<?, ?>)) {
continue;
}
Map<?, ?> section = (Map<?, ?>) sectionObj;
List<?> blocksRaw = (List<?>) section.get("blocks");
List<?> lootRaw = (List<?>) section.get("loot");
if (blocksRaw == null || lootRaw == null) {
continue;
}
List<Material> blocks = new ArrayList<>();
for (Object blockObj : blocksRaw) {
if (blockObj instanceof String) {
Material material = Material.matchMaterial((String) blockObj);
if (material != null) {
blocks.add(material);
}
}
}
Map<ItemStack, Integer> loot = new HashMap<>();
for (Object lootObj : lootRaw) {
if (!(lootObj instanceof Map<?, ?>)) {
continue;
}
Map<?, ?> lootEntry = (Map<?, ?>) lootObj;
Object weightObj = lootEntry.get("weight");
Object itemObj = lootEntry.get("item");
if (weightObj instanceof Number && itemObj instanceof ItemStack) {
int weight = ((Number) weightObj).intValue();
if (weight > 0) {
loot.put((ItemStack) itemObj, weight);
}
}
}
if (!blocks.isEmpty() && !loot.isEmpty()) {
for (Material block : blocks) {
result.put(block, loot);
}
}
}
return result;
}
@SuppressWarnings("unchecked")
private static Map<String, Map<String, String>> readDamageSources(Object value) {
if (!(value instanceof Map<?, ?>)) {
return null;
}
Map<String, Map<String, String>> result = new HashMap<>();
for (Map.Entry<?, ?> entry : ((Map<?, ?>) value).entrySet()) {
String key = Objects.toString(entry.getKey());
Object cause = entry.getValue();
if (cause instanceof Map<?, ?>) {
Map<String, String> nested = new HashMap<>();
for (Map.Entry<?, ?> inner : ((Map<?, ?>) cause).entrySet()) {
nested.put(Objects.toString(inner.getKey()), Objects.toString(inner.getValue()));
}
result.put(key, nested);
} else if (cause == null) {
result.put(key, new HashMap<>());
} else {
result.put(key, Map.of("base", Objects.toString(cause)));
}
}
return result;
}
@Override
public Map<String, Object> serialize() {
Map<String, Object> objs = new HashMap<>();
objs.put("bomb", bombItem.getKey().toString());
objs.put("power", powerItem.getKey().toString());
objs.put("fire", fireType.getKey().toString());
Map<Map<ItemStack, Integer>, Set<Material>> lootBlock = new HashMap<>();
for (Map.Entry<Material, Map<ItemStack, Integer>> entry : blockLoot.entrySet()) {
lootBlock.computeIfAbsent(entry.getValue(), key -> new HashSet<>()).add(entry.getKey());
}
List<Map<String, Object>> lootTable = new ArrayList<>();
for (Map.Entry<Map<ItemStack, Integer>, Set<Material>> entry : lootBlock.entrySet()) {
Map<String, Object> section = new HashMap<>();
List<String> blocks = new ArrayList<>();
for (Material material : entry.getValue()) {
blocks.add(material.getKey().toString());
}
List<Map<String, Object>> loot = new ArrayList<>();
for (Map.Entry<ItemStack, Integer> lootEntry : entry.getKey().entrySet()) {
Map<String, Object> lootItem = new HashMap<>();
lootItem.put("item", lootEntry.getKey());
lootItem.put("weight", lootEntry.getValue());
loot.add(lootItem);
}
section.put("blocks", blocks);
section.put("loot", loot);
lootTable.add(section);
}
objs.put("loot-table", lootTable);
objs.put("destructible", destructible.stream().map(material -> material.getKey().toString()).toList());
objs.put("indestructible", indestructible.stream().map(material -> material.getKey().toString()).toList());
objs.put("pass-keep", passKeep.stream().map(material -> material.getKey().toString()).toList());
objs.put("pass-destroy", passDestroy.stream().map(material -> material.getKey().toString()).toList());
objs.put("initial-items", trimTrailingNulls(initialItems));
objs.put("lives", lives);
objs.put("fuse-ticks", fuseTicks);
objs.put("fire-ticks", fireTicks);
objs.put("immunity-ticks", immunityTicks);
objs.put("damage-source", damageSources);
objs.put("source-mask", sourceMask.stream().map(material -> material.getKey().toString()).toList());
objs.put("cage-block", cageBlock.getKey().toString());
return objs;
}
private static List<ItemStack> trimTrailingNulls(List<ItemStack> items) {
List<ItemStack> result = new ArrayList<>(items);
for (int i = result.size() - 1; i >= 0; i--) {
if (result.get(i) != null) {
break;
}
result.remove(i);
}
return result;
}
public static GameSettings getDefaultSettings() {
if (defaultSettings == null) {
synchronized (GameSettings.class) {
if (defaultSettings == null) {
defaultSettings = loadDefaultSettings();
}
}
}
return defaultSettings;
}
private static GameSettings loadDefaultSettings() {
try (Reader reader = new InputStreamReader(
Objects.requireNonNull(Bomberman.instance.getResource("games/README.yml"))
)) {
loadingDefault = true;
GameSettings result = YamlConfiguration.loadConfiguration(reader)
.getSerializable("settings", GameSettings.class);
loadingDefault = false;
return Objects.requireNonNull(result);
} catch (Exception e) {
loadingDefault = false;
throw new RuntimeException("Failed to load default settings", e);
}
}
}

View File

@@ -0,0 +1,71 @@
package io.github.mviper.bomberman.game;
import org.bukkit.Material;
import org.bukkit.inventory.ItemStack;
import java.util.List;
import java.util.Map;
import java.util.Set;
public class GameSettingsBuilder {
public Material bombItem;
public Material powerItem;
public Material fireType;
public Map<Material, Map<ItemStack, Integer>> blockLoot;
public Set<Material> destructible;
public Set<Material> indestructible;
public Set<Material> passKeep;
public Set<Material> passDestroy;
public List<ItemStack> initialItems;
public int lives;
public int fuseTicks;
public int fireTicks;
public int immunityTicks;
public Map<String, Map<String, String>> damageSources;
public Set<Material> sourceMask;
public Material cageBlock;
public GameSettingsBuilder() {
this(GameSettings.getDefaultSettings());
}
public GameSettingsBuilder(GameSettings settings) {
this.bombItem = settings.getBombItem();
this.powerItem = settings.getPowerItem();
this.fireType = settings.getFireType();
this.blockLoot = settings.getBlockLoot();
this.destructible = settings.getDestructible();
this.indestructible = settings.getIndestructible();
this.passKeep = settings.getPassKeep();
this.passDestroy = settings.getPassDestroy();
this.initialItems = settings.getInitialItems();
this.lives = settings.getLives();
this.fuseTicks = settings.getFuseTicks();
this.fireTicks = settings.getFireTicks();
this.immunityTicks = settings.getImmunityTicks();
this.damageSources = settings.getDamageSources();
this.sourceMask = settings.getSourceMask();
this.cageBlock = settings.getCageBlock();
}
public GameSettings build() {
return new GameSettings(
bombItem,
powerItem,
fireType,
blockLoot,
destructible,
indestructible,
passKeep,
passDestroy,
initialItems,
lives,
fuseTicks,
fireTicks,
immunityTicks,
damageSources,
sourceMask,
cageBlock
);
}
}

View File

@@ -0,0 +1,81 @@
package io.github.mviper.bomberman.game;
import io.github.mviper.bomberman.Bomberman;
import io.github.mviper.bomberman.events.BmRunStartCountDownIntent;
import io.github.mviper.bomberman.events.BmRunStartedIntent;
import io.github.mviper.bomberman.events.BmRunStoppedIntent;
import io.github.mviper.bomberman.events.BmTimerCountedEvent;
import io.github.mviper.bomberman.messaging.Context;
import io.github.mviper.bomberman.messaging.Text;
import org.bukkit.Bukkit;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.HandlerList;
import org.bukkit.event.Listener;
public class StartTimer implements Runnable, Listener {
private static final Bomberman PLUGIN = Bomberman.instance;
public static void createTimer(Game game, int time) {
Bukkit.getPluginManager().registerEvents(new StartTimer(game, time), PLUGIN);
}
private final Game game;
private int time;
private boolean killed = false;
private final int taskId;
private StartTimer(Game game, int time) {
this.game = game;
this.time = time;
this.taskId = PLUGIN.getServer().getScheduler().scheduleSyncRepeatingTask(PLUGIN, this, 1, 20);
}
@Override
public void run() {
if (killed) {
return;
}
BmTimerCountedEvent event = new BmTimerCountedEvent(game, time);
Bukkit.getPluginManager().callEvent(event);
time = event.count;
if (time > 0) {
time--;
} else {
BmRunStartedIntent.startRun(game);
killSelf();
}
}
private void killSelf() {
killed = true;
Bukkit.getScheduler().cancelTask(taskId);
HandlerList.unregisterAll(this);
}
@EventHandler(ignoreCancelled = true, priority = EventPriority.LOW)
public void onTimerStarted(BmRunStartCountDownIntent event) {
if (event.game != game) {
return;
}
if (event.override) {
killSelf();
} else {
event.cancelBecause(Text.GAME_ALREADY_COUNTING.format(new Context(false)
.plus("game", game)
.plus("time", time)));
}
}
@EventHandler(ignoreCancelled = true, priority = EventPriority.LOW)
public void onGameStop(BmRunStoppedIntent event) {
if (event.game != game) {
return;
}
killSelf();
event.cancelFor(Text.STOP_TIMER_STOPPED.format(new Context(false)
.plus("time", time)
.plus("game", game)));
}
}

View File

@@ -0,0 +1,23 @@
package io.github.mviper.bomberman.messaging;
import java.util.Collections;
import java.util.List;
import java.util.function.Function;
public class AdditionalArgs implements Formattable {
private final Function<List<Message>, Formattable> function;
public AdditionalArgs(Function<List<Message>, Formattable> function) {
this.function = function;
}
@Override
public Formattable applyModifier(Message arg) {
return new ExtraArgsHolder((context, args) -> function.apply(args), arg);
}
@Override
public Message format(Context context) {
return function.apply(Collections.emptyList()).format(context);
}
}

View File

@@ -0,0 +1,129 @@
package io.github.mviper.bomberman.messaging;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.List;
public class CollectionWrapper<T extends Formattable> implements Formattable {
private final Collection<T> list;
private final Formattable delegate;
public CollectionWrapper(Collection<T> list) {
this.list = list;
this.delegate = new DefaultArg("length", argProperty -> {
String option = argProperty.toString().toLowerCase();
switch (option) {
case "length":
return Message.of(list.size());
case "map":
return new RequiredArg(argMapper -> new ContextArg(context -> {
String mapper = argMapper.toString();
List<Formattable> newList = new ArrayList<>();
int i = 0;
for (T item : list) {
newList.add(Expander.expand(
mapper,
context.plus("it", item).plus("index", i)
));
i++;
}
return new CollectionWrapper<>(newList);
}));
case "join":
return new DefaultArg("", separator -> new ContextArg(context -> {
List<Message> formatted = new ArrayList<>();
for (T item : list) {
formatted.add(item.format(context));
}
if (formatted.isEmpty()) {
formatted.add(Message.empty);
}
Message result = formatted.get(0);
for (int i = 1; i < formatted.size(); i++) {
result = result.append(separator).append(formatted.get(i));
}
return result;
}));
case "foreach":
return new DefaultArg("({index}: {it})", argMapper -> new DefaultArg(" ", argSeparator -> new ContextArg(context -> {
String mapper = argMapper.toString();
List<Message> mapped = new ArrayList<>();
int i = 0;
for (T item : list) {
mapped.add(Expander.expand(
mapper,
context.plus("it", item).plus("index", i)
));
i++;
}
if (mapped.isEmpty()) {
mapped.add(Message.empty);
}
Message result = mapped.get(0);
for (int j = 1; j < mapped.size(); j++) {
result = result.append(argSeparator).append(mapped.get(j));
}
return result;
})));
case "sort":
return new DefaultArg("{it}", argMapper -> new ContextArg(context -> {
String mapper = argMapper.toString();
List<Indexed<T>> indexed = new ArrayList<>();
int i = 0;
for (T item : list) {
indexed.add(new Indexed<>(i, item));
i++;
}
indexed.sort(Comparator.comparing(entry -> Expander.expand(
mapper,
context.plus("it", entry.value).plus("index", Message.of(entry.index))
).toString()));
List<T> sorted = new ArrayList<>();
for (Indexed<T> entry : indexed) {
sorted.add(entry.value);
}
return new CollectionWrapper<>(sorted);
}));
case "filter":
return new RequiredArg(argFilter -> new ContextArg(context -> {
List<T> filtered = new ArrayList<>();
int i = 0;
for (T item : list) {
String filterOutput = Expander.expand(
argFilter.toString(),
context.plus("it", item).plus("index", Message.of(i))
).toString();
if (!filterOutput.isBlank() && !"0".equals(filterOutput) && !"0.0".equals(filterOutput)) {
filtered.add(item);
}
i++;
}
return new CollectionWrapper<>(filtered);
}));
default:
throw new IllegalArgumentException("Unknown list option: " + argProperty);
}
});
}
@Override
public Formattable applyModifier(Message arg) {
return delegate.applyModifier(arg);
}
@Override
public Message format(Context context) {
return delegate.format(context);
}
private static final class Indexed<T> {
private final int index;
private final T value;
private Indexed(int index, T value) {
this.index = index;
this.value = value;
}
}
}

View File

@@ -0,0 +1,21 @@
package io.github.mviper.bomberman.messaging;
import org.bukkit.ChatColor;
public class ColorWrapper implements Formattable {
private final Formattable delegate;
public ColorWrapper(ChatColor color) {
this.delegate = new RequiredArg(arg -> arg.color(color));
}
@Override
public Formattable applyModifier(Message arg) {
return delegate.applyModifier(arg);
}
@Override
public Message format(Context context) {
return delegate.format(context);
}
}

View File

@@ -0,0 +1,95 @@
package io.github.mviper.bomberman.messaging;
import org.bukkit.command.CommandSender;
import org.bukkit.inventory.ItemStack;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
/**
* An immutable set of things referenced in the current scope.
*/
public class Context {
private final Map<String, Formattable> objects;
private final Set<Function<String, String>> functions;
public final boolean elevated;
public Context() {
this(false);
}
public Context(boolean elevated) {
this.objects = Collections.emptyMap();
this.functions = Collections.emptySet();
this.elevated = elevated;
}
private Context(Map<String, Formattable> objects, Set<Function<String, String>> functions, boolean elevated) {
this.objects = objects;
this.functions = functions;
this.elevated = elevated;
}
public Formattable get(String key) {
return objects.get(key);
}
public String getFunction(String key) {
for (Function<String, String> function : functions) {
String result = function.apply(key);
if (result != null) {
return result;
}
}
return null;
}
public Context addFunctions(Function<String, String> function) {
Set<Function<String, String>> next = new HashSet<>(functions);
next.add(function);
return new Context(objects, next, elevated);
}
public Context newScope() {
return new Context(Collections.emptyMap(), functions, elevated);
}
public Context plus(String key, Formattable thing) {
Map<String, Formattable> next = new HashMap<>(objects);
next.put(key, thing);
return new Context(next, functions, elevated);
}
public Context plus(Context context) {
Map<String, Formattable> next = new HashMap<>(objects);
next.putAll(context.objects);
Set<Function<String, String>> nextFunctions = new HashSet<>(functions);
nextFunctions.addAll(context.functions);
return new Context(next, nextFunctions, elevated || context.elevated);
}
public Context plus(String key, String value) {
return plus(key, Message.of(value));
}
public Context plus(String key, int value) {
return plus(key, Message.of(value));
}
public Context plus(String key, Collection<? extends Formattable> value) {
return plus(key, new CollectionWrapper<>(value));
}
public Context plus(String key, ItemStack stack) {
return plus(key, new ItemWrapper(stack));
}
public Context plus(String key, CommandSender sender) {
return plus(key, new SenderWrapper(sender));
}
}

View File

@@ -0,0 +1,28 @@
package io.github.mviper.bomberman.messaging;
import java.util.function.Function;
public class ContextArg implements Formattable {
private final Function<Context, Formattable> function;
public ContextArg(Function<Context, Formattable> function) {
this.function = function;
}
@Override
public Formattable applyModifier(Message arg) {
return new ExtraArgsHolder((context, args) -> {
Formattable initial = function.apply(context);
Formattable current = initial;
for (Message message : args) {
current = current.applyModifier(message);
}
return current;
}, arg);
}
@Override
public Message format(Context context) {
return function.apply(context).format(context);
}
}

View File

@@ -0,0 +1,30 @@
package io.github.mviper.bomberman.messaging;
import java.util.List;
public class CustomPath implements Formattable {
private final Formattable delegate = new RequiredArg(functionArg ->
new AdditionalArgs(args -> new ContextArg(context -> {
String function = functionArg.toString();
String text = context.getFunction(function);
if (text == null) {
return Message.error("{#|" + function + "}");
}
Context callContext = context.newScope();
for (int i = 0; i < args.size(); i++) {
callContext = callContext.plus("arg" + i, args.get(i));
}
return Expander.expand(text, callContext);
}))
);
@Override
public Formattable applyModifier(Message arg) {
return delegate.applyModifier(arg);
}
@Override
public Message format(Context context) {
return delegate.format(context);
}
}

View File

@@ -0,0 +1,27 @@
package io.github.mviper.bomberman.messaging;
import java.util.function.Function;
public class DefaultArg implements Formattable {
private final Message text;
private final Function<Message, Formattable> function;
public DefaultArg(Message text, Function<Message, Formattable> function) {
this.text = text;
this.function = function;
}
public DefaultArg(String text, Function<Message, Formattable> function) {
this(Message.of(text), function);
}
@Override
public Formattable applyModifier(Message arg) {
return function.apply(arg);
}
@Override
public Message format(Context context) {
return function.apply(text).format(context);
}
}

View File

@@ -0,0 +1,118 @@
package io.github.mviper.bomberman.messaging;
import net.objecthunter.exp4j.ExpressionBuilder;
import net.objecthunter.exp4j.function.Function;
import net.objecthunter.exp4j.operator.Operator;
import java.math.BigDecimal;
public class Equation implements Formattable {
private static final double EPSILON = 0.00000001d;
private static final int PRECEDENCE_NOT = Operator.PRECEDENCE_UNARY_MINUS;
private static final int PRECEDENCE_AND = Operator.PRECEDENCE_ADDITION - 10;
private static final int PRECEDENCE_OR = Operator.PRECEDENCE_ADDITION - 20;
private static final int PRECEDENCE_COMPARE = Operator.PRECEDENCE_ADDITION - 100;
private static final int PRECEDENCE_EQUAL = Operator.PRECEDENCE_ADDITION - 1000;
private static final Operator NOT = new Operator("!", 1, true, PRECEDENCE_NOT) {
@Override
public double apply(double... args) {
return (args[0] > -EPSILON && args[0] < EPSILON) ? 1.0 : 0.0;
}
};
private static final Operator OR = new Operator("$", 2, true, PRECEDENCE_OR) {
@Override
public double apply(double... args) {
return (((args[0] < -EPSILON) || (args[0] > EPSILON)) || ((args[1] < -EPSILON) || (args[1] > EPSILON))) ? 1.0 : 0.0;
}
};
private static final Operator AND = new Operator("&", 2, true, PRECEDENCE_AND) {
@Override
public double apply(double... args) {
return (((args[0] < -EPSILON) || (args[0] > EPSILON)) && ((args[1] < -EPSILON) || (args[1] > EPSILON))) ? 1.0 : 0.0;
}
};
private static final Operator GREATER = new Operator(">", 2, true, PRECEDENCE_COMPARE) {
@Override
public double apply(double... args) {
return (args[0] > args[1] + EPSILON) ? 1.0 : 0.0;
}
};
private static final Operator LESSER = new Operator("<", 2, true, PRECEDENCE_COMPARE) {
@Override
public double apply(double... args) {
return (args[0] + EPSILON < args[1]) ? 1.0 : 0.0;
}
};
private static final Operator GREATER_EQUAL = new Operator(">=", 2, true, PRECEDENCE_COMPARE) {
@Override
public double apply(double... args) {
return (args[0] + EPSILON >= args[1]) ? 1.0 : 0.0;
}
};
private static final Operator LESSER_EQUAL = new Operator("<=", 2, true, PRECEDENCE_COMPARE) {
@Override
public double apply(double... args) {
return (args[0] <= args[1] + EPSILON) ? 1.0 : 0.0;
}
};
private static final Operator EQUAL = new Operator("==", 2, true, PRECEDENCE_EQUAL) {
@Override
public double apply(double... args) {
return (args[0] > args[1] - EPSILON && args[0] < args[1] + EPSILON) ? 1.0 : 0.0;
}
};
private static final Operator NOT_EQUAL = new Operator("!=", 2, true, PRECEDENCE_EQUAL) {
@Override
public double apply(double... args) {
return (args[0] < args[1] - EPSILON || args[0] > args[1] + EPSILON) ? 1.0 : 0.0;
}
};
private static final Function ROUND = new Function("round", 1) {
@Override
public double apply(double... args) {
return Math.rint(args[0]);
}
};
private final Formattable delegate = new RequiredArg(argEquation -> {
try {
double answer = new ExpressionBuilder(argEquation.toString())
.operator(GREATER)
.operator(LESSER)
.operator(GREATER_EQUAL)
.operator(LESSER_EQUAL)
.operator(EQUAL)
.operator(NOT_EQUAL)
.operator(AND)
.operator(OR)
.operator(NOT)
.function(ROUND)
.build()
.evaluate();
return Message.of(BigDecimal.valueOf(answer).stripTrailingZeros().toPlainString());
} catch (Exception e) {
return Message.error("{" + argEquation + "}");
}
});
@Override
public Formattable applyModifier(Message arg) {
return delegate.applyModifier(arg);
}
@Override
public Message format(Context context) {
return delegate.format(context);
}
}

View File

@@ -0,0 +1,24 @@
package io.github.mviper.bomberman.messaging;
import org.bukkit.Bukkit;
public class Execute implements Formattable {
private final Formattable delegate = new RequiredArg(command ->
new ContextArg(context -> {
if (context.elevated) {
Bukkit.getServer().dispatchCommand(Bukkit.getConsoleSender(), command.toString());
}
return Message.empty;
})
);
@Override
public Formattable applyModifier(Message arg) {
return delegate.applyModifier(arg);
}
@Override
public Message format(Context context) {
return delegate.format(context);
}
}

View File

@@ -0,0 +1,173 @@
package io.github.mviper.bomberman.messaging;
import org.bukkit.ChatColor;
import java.util.Collections;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
public final class Expander {
private static final Map<String, Formattable> FUNCTIONS;
static {
Map<String, Formattable> functions = new HashMap<>();
functions.put("=", new Equation());
functions.put("#switch", new Switch());
functions.put("#title", new TitleExpander());
functions.put("#raw", Message.rawFlag);
functions.put("#", new CustomPath());
functions.put("#regex", new RegexExpander());
functions.put("#len", new LengthExpander());
functions.put("#sub", new SubstringExpander());
functions.put("#padl", new PadLeftExpander());
functions.put("#padr", new PadRightExpander());
functions.put("#exec", new Execute());
functions.put("#rand", new RandomExpander());
for (ChatColor color : ChatColor.values()) {
functions.put("#" + color.name().toLowerCase(Locale.ROOT), new ColorWrapper(color));
}
FUNCTIONS = Collections.unmodifiableMap(functions);
}
private Expander() {
}
/**
* Expands all braces in text. The number of open braces must be balanced with close braces.
*
* @param text the text to expand
* @param context Reference-able things
* @return the expanded text
*/
public static Message expand(String text, Context context) {
Message expanded = Message.empty;
StringBuilder building = new StringBuilder();
boolean ignoreNextSpecial = false;
int i = 0;
while (i < text.length()) {
char c = text.charAt(i);
if (c == '{' && !ignoreNextSpecial) {
if (building.length() > 0) {
expanded = expanded.append(Message.of(building.toString()));
building.setLength(0);
}
String subtext = toNext(text, '}', i);
if (subtext == null) {
throw new IllegalArgumentException("Braces unmatched: '" + text + "'");
}
Message expandedBrace;
if (subtext.startsWith("{!")) {
expandedBrace = Message.of(subtext.replaceFirst("!", ""));
} else {
expandedBrace = expandBrace(subtext, context);
}
expanded = expanded.append(expandedBrace);
i += subtext.length() - 1;
} else if (c == '\\' && !ignoreNextSpecial) {
ignoreNextSpecial = true;
} else {
building.append(c);
ignoreNextSpecial = false;
}
i++;
}
if (building.length() > 0) {
expanded = expanded.append(Message.of(building.toString()));
}
return expanded;
}
/**
* Expands a brace in a message.
* Each sub brace will be expanded with the same arguments as this message.
*
* @param text the text to expands formatted as "{ key | arg1 | ... | argN }"
* @return the expanded string
*/
private static Message expandBrace(String text, Context context) {
if (text.charAt(0) != '{' || text.charAt(text.length() - 1) != '}') {
throw new RuntimeException("expandBrace() must start and end with a brace");
}
String keyString = toNext(text, '|', 1);
if (keyString == null) {
throw new RuntimeException("Text bad: '" + text + "'");
}
AtomicInteger index = new AtomicInteger(keyString.length());
java.util.List<Message> args = new java.util.ArrayList<>();
while (true) {
String subArg = toNext(text, '|', index.get());
if (subArg == null) {
break;
}
args.add(Message.lazyExpand(subArg.substring(1, subArg.length() - 1), context));
index.addAndGet(subArg.length() - 1);
}
keyString = keyString.substring(0, keyString.length() - 1).trim().toLowerCase(Locale.ROOT);
if ("#exec".equals(keyString) && !context.elevated) {
return Message.empty;
}
boolean reference = keyString.startsWith("@");
if (reference) {
keyString = keyString.substring(1);
}
Formattable thing = context.get(keyString);
if (thing == null) {
thing = FUNCTIONS.get(keyString);
}
if (thing == null) {
return Message.error(text);
}
Formattable modified = thing;
for (Message arg : args) {
modified = modified.applyModifier(arg);
}
if (reference) {
return Message.reference(modified, context);
}
return modified.format(context);
}
/**
* Gets the substring of sequence from index to the next endingChar but takes into account brace skipping.
* The returned string will include both the start and end characters. If a closing brace
* is found before the wanted character, then the remaining to that brace is returned. If the end of sequence is
* reached, then null is returned.
*/
private static String toNext(String sequence, char endingChar, int startIndex) {
int size = sequence.length();
int openBracesFound = 0;
boolean ignoreNextSpecial = false;
for (int i = startIndex + 1; i < size; i++) {
if (ignoreNextSpecial) {
ignoreNextSpecial = false;
} else {
char c = sequence.charAt(i);
if (c == endingChar && openBracesFound == 0) {
return sequence.substring(startIndex, i + 1);
}
if (c == '{') {
openBracesFound++;
} else if (c == '}') {
openBracesFound--;
if (openBracesFound < 0) {
return sequence.substring(startIndex, i + 1);
}
} else if (c == '\\') {
ignoreNextSpecial = true;
}
}
}
return null;
}
}

View File

@@ -0,0 +1,28 @@
package io.github.mviper.bomberman.messaging;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.function.BiFunction;
class ExtraArgsHolder implements Formattable {
private final BiFunction<Context, List<Message>, Formattable> callback;
private final List<Message> extraArgs;
ExtraArgsHolder(BiFunction<Context, List<Message>, Formattable> callback, Message... args) {
this.callback = callback;
this.extraArgs = new ArrayList<>(Arrays.asList(args));
}
@Override
public Formattable applyModifier(Message arg) {
List<Message> next = new ArrayList<>(extraArgs);
next.add(arg);
return new ExtraArgsHolder(callback, next.toArray(new Message[0]));
}
@Override
public Message format(Context context) {
return callback.apply(context, extraArgs).format(context);
}
}

View File

@@ -0,0 +1,21 @@
package io.github.mviper.bomberman.messaging;
public interface Formattable {
/**
* Applies a modifier to this object.
*
* @param arg the value of the modifier
*/
Formattable applyModifier(Message arg);
default Formattable applyModifier(String arg) {
return applyModifier(Message.of(arg));
}
/**
* Formats this custom object into "simple" text.
*
* @param context additional variables in scope
*/
Message format(Context context);
}

View File

@@ -0,0 +1,29 @@
package io.github.mviper.bomberman.messaging;
import org.bukkit.inventory.ItemStack;
public class ItemWrapper implements Formattable {
private final ItemStack item;
public ItemWrapper(ItemStack item) {
this.item = item;
}
@Override
public Message format(Context context) {
return Text.ITEM_FORMAT.format(context.newScope().plus("item", this));
}
@Override
public Formattable applyModifier(Message arg) {
String option = arg.toString().toLowerCase();
switch (option) {
case "amount":
return Message.of(item.getAmount());
case "type":
return Message.of(item.getType().getKey().toString());
default:
return Message.empty;
}
}
}

View File

@@ -0,0 +1,15 @@
package io.github.mviper.bomberman.messaging;
public class LengthExpander implements Formattable {
private final Formattable delegate = new RequiredArg(arg -> Message.of(arg.toString().length()));
@Override
public Formattable applyModifier(Message arg) {
return delegate.applyModifier(arg);
}
@Override
public Message format(Context context) {
return delegate.format(context);
}
}

View File

@@ -0,0 +1,475 @@
package io.github.mviper.bomberman.messaging;
import org.bukkit.ChatColor;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import javax.annotation.CheckReturnValue;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
/**
* A message is an immutable coloured/formatted expanded string.
*/
@CheckReturnValue
public class Message implements Formattable {
private final TreeNode contents;
private Message(TreeNode contents) {
this.contents = contents;
}
public static Message of(String text) {
return new Message(new StringNode(text));
}
public static Message of(Number num) {
return of(num.toString());
}
public static final Message empty = of("");
public static Message title(Message text, Message subtitle, int fadeIn, int duration, int fadeOut) {
return new Message(new TitleNode(text, subtitle, fadeIn, duration, fadeOut));
}
public static final Message rawFlag = new Message(new RawNode());
public static Message error(String text) {
return of(text).color(ChatColor.RED);
}
public static Message lazyExpand(String text, Context context) {
return new Message(new LazyNode(text, context));
}
public static Message reference(Formattable item, Context context) {
return new Message(new ReferenceNode(item, context));
}
private static final class Style {
private final ChatColor color;
private final Set<ChatColor> formats;
private Style(ChatColor color, Set<ChatColor> formats) {
this.color = color;
this.formats = formats;
}
}
private static final class Cursor {
private final StringBuilder content = new StringBuilder();
private final Deque<Style> colorStack = new ArrayDeque<>();
private Cursor() {
colorStack.add(new Style(ChatColor.RESET, Set.of()));
}
private void addStyle(ChatColor color) {
Style currentStyle = colorStack.getLast();
Style newStyle;
if (color.isColor()) {
newStyle = new Style(color, currentStyle.formats);
} else if (color.isFormat()) {
Set<ChatColor> newFormats = new LinkedHashSet<>(currentStyle.formats);
newFormats.add(color);
newStyle = new Style(currentStyle.color, newFormats);
} else {
newStyle = new Style(ChatColor.RESET, Set.of());
}
colorStack.addLast(newStyle);
appendConversionString(currentStyle, newStyle);
}
private void appendConversionString(Style from, Style to) {
Set<ChatColor> formatsRemoved = new LinkedHashSet<>(from.formats);
formatsRemoved.removeAll(to.formats);
Set<ChatColor> formatsAdded = new LinkedHashSet<>(to.formats);
formatsAdded.removeAll(from.formats);
if (!Objects.equals(from.color, to.color) || !formatsRemoved.isEmpty()) {
content.append(to.color);
for (ChatColor format : to.formats) {
content.append(format);
}
} else if (!formatsAdded.isEmpty()) {
for (ChatColor format : formatsAdded) {
content.append(format);
}
}
}
private void write(String text) {
content.append(text);
}
private void popColor() {
Style from = colorStack.removeLast();
Style to = colorStack.getLast();
appendConversionString(from, to);
}
@Override
public String toString() {
return content.toString();
}
}
private static final class Title {
private final String title;
private final String subtitle;
private final int fadeIn;
private final int stay;
private final int fadeOut;
private Title(String title, String subtitle, int fadeIn, int stay, int fadeOut) {
this.title = title;
this.subtitle = subtitle;
this.fadeIn = fadeIn;
this.stay = stay;
this.fadeOut = fadeOut;
}
}
private interface TreeNode {
void expand(Cursor cursor);
boolean isRaw();
Title expandTitle();
Formattable applyModifier(Message arg);
}
private static final class StringNode implements TreeNode {
private final String text;
private StringNode(String text) {
this.text = text;
}
@Override
public void expand(Cursor cursor) {
cursor.write(text);
}
@Override
public boolean isRaw() {
return false;
}
@Override
public Title expandTitle() {
return null;
}
@Override
public Formattable applyModifier(Message arg) {
return null;
}
}
private static final class Joined implements TreeNode {
private final List<TreeNode> parts;
private Joined(List<TreeNode> parts) {
List<TreeNode> flattened = new ArrayList<>();
for (TreeNode part : parts) {
if (part instanceof Joined) {
flattened.addAll(((Joined) part).parts);
} else if (part != empty.contents) {
flattened.add(part);
}
}
this.parts = flattened;
}
private Joined(TreeNode a, TreeNode b) {
this(List.of(a, b));
}
@Override
public void expand(Cursor cursor) {
for (TreeNode part : parts) {
part.expand(cursor);
}
}
@Override
public boolean isRaw() {
for (TreeNode part : parts) {
if (part.isRaw()) {
return true;
}
}
return false;
}
@Override
public Title expandTitle() {
for (TreeNode part : parts) {
Title title = part.expandTitle();
if (title != null) {
return title;
}
}
return null;
}
@Override
public Formattable applyModifier(Message arg) {
for (TreeNode part : parts) {
Formattable result = part.applyModifier(arg);
if (result != null) {
return result;
}
}
return null;
}
}
private static final class Colored implements TreeNode {
private final TreeNode content;
private final ChatColor color;
private Colored(TreeNode content, ChatColor color) {
this.content = content;
this.color = color;
}
@Override
public void expand(Cursor cursor) {
cursor.addStyle(color);
content.expand(cursor);
cursor.popColor();
}
@Override
public boolean isRaw() {
return content.isRaw();
}
@Override
public Title expandTitle() {
return null;
}
@Override
public Formattable applyModifier(Message arg) {
return null;
}
}
private static final class RawNode implements TreeNode {
@Override
public void expand(Cursor cursor) {
}
@Override
public boolean isRaw() {
return true;
}
@Override
public Title expandTitle() {
return null;
}
@Override
public Formattable applyModifier(Message arg) {
return null;
}
}
private static final class TitleNode implements TreeNode {
private final Message title;
private final Message subtitle;
private final int fadeIn;
private final int stay;
private final int fadeOut;
private TitleNode(Message title, Message subtitle, int fadeIn, int stay, int fadeOut) {
this.title = title;
this.subtitle = subtitle;
this.fadeIn = fadeIn;
this.stay = stay;
this.fadeOut = fadeOut;
}
@Override
public void expand(Cursor cursor) {
}
@Override
public boolean isRaw() {
return false;
}
@Override
public Title expandTitle() {
Cursor titleCursor = new Cursor();
title.contents.expand(titleCursor);
String titleString = titleCursor.toString();
Cursor subtitleCursor = new Cursor();
subtitle.contents.expand(subtitleCursor);
String subtitleString = subtitleCursor.toString();
return new Title(titleString, subtitleString, fadeIn, stay, fadeOut);
}
@Override
public Formattable applyModifier(Message arg) {
return null;
}
}
private static final class LazyNode implements TreeNode {
private final String text;
private final Context context;
private TreeNode content;
private LazyNode(String text, Context context) {
this.text = text;
this.context = context;
}
private TreeNode content() {
if (content == null) {
content = Expander.expand(text, context).contents;
}
return content;
}
@Override
public void expand(Cursor cursor) {
content().expand(cursor);
}
@Override
public boolean isRaw() {
return content().isRaw();
}
@Override
public Title expandTitle() {
return content().expandTitle();
}
@Override
public Formattable applyModifier(Message arg) {
return content().applyModifier(arg);
}
}
private static final class ReferenceNode implements TreeNode {
private final Formattable item;
private final Context context;
private TreeNode content;
private ReferenceNode(Formattable item, Context context) {
this.item = item;
this.context = context;
}
private TreeNode content() {
if (content == null) {
content = item.format(context).contents;
}
return content;
}
@Override
public void expand(Cursor cursor) {
content().expand(cursor);
}
@Override
public boolean isRaw() {
return content().isRaw();
}
@Override
public Title expandTitle() {
return content().expandTitle();
}
@Override
public Formattable applyModifier(Message arg) {
return item.applyModifier(arg);
}
}
public Message color(ChatColor color) {
return new Message(new Colored(contents, color));
}
public Message append(Message text) {
return new Message(new Joined(contents, text.contents));
}
@Override
public Message format(Context context) {
return this;
}
@Override
public Formattable applyModifier(Message arg) {
Formattable result = contents.applyModifier(arg);
if (result == null) {
throw new IllegalArgumentException("Cannot accept extra arguments");
}
return result;
}
public void sendTo(CommandSender sender) {
try {
TreeNode sendContents;
if (contents.isRaw()) {
sendContents = contents;
} else {
sendContents = new Switch()
.applyModifier(this)
.applyModifier(empty)
.applyModifier(empty)
.applyModifier(Text.MESSAGE_FORMAT.format(new Context(false).plus("message", this)))
.format(new Context(false))
.contents;
}
Cursor cursor = new Cursor();
sendContents.expand(cursor);
if (!cursor.toString().isBlank()) {
String[] lines = cursor.toString().split("\n|\r");
StringBuilder text = new StringBuilder();
for (String line : lines) {
if (!line.isBlank()) {
if (text.length() > 0) {
text.append("\n");
}
text.append(line);
}
}
sender.sendMessage(text.toString());
}
if (sender instanceof Player) {
Title title = contents.expandTitle();
if (title != null) {
((Player) sender).sendTitle(title.title, title.subtitle, title.fadeIn, title.stay, title.fadeOut);
}
}
} catch (RuntimeException e) {
sender.sendMessage(ChatColor.RED + "Message format invalid");
}
}
@Override
public String toString() {
Cursor cursor = new Cursor();
contents.expand(cursor);
return cursor.toString();
}
}

View File

@@ -0,0 +1,48 @@
package io.github.mviper.bomberman.messaging;
import java.util.Iterator;
import java.util.function.Function;
public class PadExpander implements Formattable {
private final Function<String, String> startText;
private final Function<String, String> endText;
private final Formattable delegate;
public PadExpander(Function<String, String> startText, Function<String, String> endText) {
this.startText = startText;
this.endText = endText;
this.delegate = new RequiredArg(argText ->
new RequiredArg(argLength ->
new DefaultArg(" ", argPadText -> {
int length = Integer.parseInt(argLength.toString());
String padText = argPadText.toString();
if (padText.isEmpty()) {
padText = " ";
}
StringBuilder result = new StringBuilder(startText.apply(argText.toString()));
String endString = endText.apply(argText.toString());
Iterator<Character> iterator = padText.chars().mapToObj(c -> (char) c).iterator();
while (result.length() < length - endString.length()) {
if (!iterator.hasNext()) {
iterator = padText.chars().mapToObj(c -> (char) c).iterator();
}
result.append(iterator.next());
}
result.append(endString);
return Message.of(result.toString());
})
)
);
}
@Override
public Formattable applyModifier(Message arg) {
return delegate.applyModifier(arg);
}
@Override
public Message format(Context context) {
return delegate.format(context);
}
}

View File

@@ -0,0 +1,7 @@
package io.github.mviper.bomberman.messaging;
public class PadLeftExpander extends PadExpander {
public PadLeftExpander() {
super(text -> "", text -> text);
}
}

View File

@@ -0,0 +1,7 @@
package io.github.mviper.bomberman.messaging;
public class PadRightExpander extends PadExpander {
public PadRightExpander() {
super(text -> text, text -> "");
}
}

View File

@@ -0,0 +1,23 @@
package io.github.mviper.bomberman.messaging;
import java.util.concurrent.ThreadLocalRandom;
public class RandomExpander implements Formattable {
private final Formattable delegate = new DefaultArg(Message.of(1), max ->
new DefaultArg(Message.of(0), min -> {
double minValue = Double.parseDouble(min.toString());
double maxValue = Double.parseDouble(max.toString());
return Message.of(Double.toString(ThreadLocalRandom.current().nextDouble(minValue, maxValue)));
})
);
@Override
public Formattable applyModifier(Message arg) {
return delegate.applyModifier(arg);
}
@Override
public Message format(Context context) {
return delegate.format(context);
}
}

View File

@@ -0,0 +1,24 @@
package io.github.mviper.bomberman.messaging;
public class RegexExpander implements Formattable {
private final Formattable delegate = new RequiredArg(argText ->
new RequiredArg(argPattern ->
new RequiredArg(argReplace -> {
String text = argText.toString();
String pattern = argPattern.toString();
String replace = argReplace.toString();
return Message.of(text.replaceAll(pattern, replace));
})
)
);
@Override
public Formattable applyModifier(Message arg) {
return delegate.applyModifier(arg);
}
@Override
public Message format(Context context) {
return delegate.format(context);
}
}

View File

@@ -0,0 +1,21 @@
package io.github.mviper.bomberman.messaging;
import java.util.function.Function;
public class RequiredArg implements Formattable {
private final Function<Message, Formattable> function;
public RequiredArg(Function<Message, Formattable> function) {
this.function = function;
}
@Override
public Formattable applyModifier(Message arg) {
return function.apply(arg);
}
@Override
public Message format(Context context) {
throw new IllegalArgumentException("Extra argument required");
}
}

View File

@@ -0,0 +1,47 @@
package io.github.mviper.bomberman.messaging;
import org.bukkit.Bukkit;
import org.bukkit.command.CommandSender;
public class SenderWrapper implements Formattable {
private final CommandSender sender;
private final Formattable delegate;
public SenderWrapper(CommandSender sender) {
this.sender = sender;
this.delegate = new DefaultArg("name", arg -> {
String option = arg.toString().toLowerCase();
switch (option) {
case "name":
return Message.of(sender.getName());
case "msg":
return new RequiredArg(arg2 -> {
arg2.sendTo(sender);
return Message.empty;
});
case "exec":
return new RequiredArg(arg2 -> new ContextArg(context -> {
if (context.elevated) {
String cmd = arg2.toString();
Bukkit.getServer().dispatchCommand(sender, cmd);
}
return Message.empty;
}));
case "haspermission":
return new RequiredArg(arg2 -> Message.of(sender.hasPermission(arg2.toString()) ? 1 : 0));
default:
throw new IllegalArgumentException("Unknown CommandSender option: " + arg);
}
});
}
@Override
public Formattable applyModifier(Message arg) {
return delegate.applyModifier(arg);
}
@Override
public Message format(Context context) {
return delegate.format(context);
}
}

View File

@@ -0,0 +1,50 @@
package io.github.mviper.bomberman.messaging;
public class SubstringExpander implements Formattable {
private final Formattable delegate = new RequiredArg(argText ->
new RequiredArg(argStart ->
new DefaultArg(Message.of(Integer.MAX_VALUE), argLength -> {
String text = argText.toString();
int start = Integer.parseInt(argStart.toString());
if (start < 0) {
start = text.length() - Math.abs(start);
}
int length = Integer.parseInt(argLength.toString());
int end;
if (length < 0) {
end = text.length() - Math.abs(length);
} else {
end = start + Math.min(length, text.length());
}
if (start < 0) {
start = 0;
}
if (start > text.length()) {
start = text.length();
}
if (end < 0) {
end = 0;
}
if (end > text.length()) {
end = text.length();
}
if (end < start) {
end = start;
}
return Message.of(text.substring(start, end));
})
)
);
@Override
public Formattable applyModifier(Message arg) {
return delegate.applyModifier(arg);
}
@Override
public Message format(Context context) {
return delegate.format(context);
}
}

View File

@@ -0,0 +1,35 @@
package io.github.mviper.bomberman.messaging;
import java.util.List;
public class Switch implements Formattable {
private final Formattable delegate = new RequiredArg(valueArg ->
new AdditionalArgs(args -> {
String value = valueArg.toString();
int size = args.size();
int i = 0;
while (i < size) {
Message test = args.get(i);
if (i + 1 < args.size()) {
if (value.equals(test.toString())) {
return args.get(i + 1);
}
} else {
return test; // default
}
i += 2;
}
return Message.empty;
})
);
@Override
public Formattable applyModifier(Message arg) {
return delegate.applyModifier(arg);
}
@Override
public Message format(Context context) {
return delegate.format(context);
}
}

View File

@@ -0,0 +1,215 @@
package io.github.mviper.bomberman.messaging;
import io.github.mviper.bomberman.Bomberman;
import org.bukkit.configuration.InvalidConfigurationException;
import org.bukkit.configuration.file.YamlConfiguration;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.nio.file.Files;
import java.nio.file.Path;
/**
* A list of Contexteds that are defined in messages.yml. If messages.yml has not been created, then it will default to
* default_messages.yml in the resource folder.
*/
public enum Text implements Formattable {
MESSAGE_FORMAT("format.message"),
ITEM_FORMAT("format.item"),
PLAYER_WON("game-play.player-won"),
GAME_COUNT("game-play.count"),
GAME_STARTED("game-play.started"),
DENY_PERMISSION("command.deny-permission"),
INCORRECT_USAGE("command.incorrect-usage"),
UNKNOWN_COMMAND("command.unknown-command"),
MUST_BE_PLAYER("command.must-be-player"),
INVALID_NUMBER("command.invalid-number"),
INVALID_TARGET_SELECTOR("command.invalid-target-selector"),
COMMAND_GROUP_HELP("command.group.help"),
COMMAND_GROUP_USAGE("command.group.usage"),
COMMAND_GROUP_EXAMPLE("command.group.example"),
COMMAND_GROUP_EXTRA("command.group.extra"),
COMMAND_HELP("command.help"),
COMMAND_CANCELLED("command.cancelled"),
BOMBERMAN_DESCRIPTION("command.bomberman.description"),
START_NAME("command.start.name"),
START_DESCRIPTION("command.start.description"),
START_USAGE("command.start.usage"),
START_EXAMPLE("command.start.example"),
START_EXTRA("command.start.extra"),
START_FLAG_OVERRIDE_DESC("command.start.flags.o.description"),
START_FLAG_DELAY_DESC("command.start.flags.d.description"),
START_FLAG_DELAY_EXT("command.start.flags.d.ext"),
GAME_ALREADY_STARTED("command.start.already-started"),
GAME_ALREADY_COUNTING("command.start.already-counting"),
GAME_NO_PLAYERS("command.start.no-players"),
GAME_START_SUCCESS("command.start.success"),
STOP_NAME("command.stop.name"),
STOP_DESCRIPTION("command.stop.description"),
STOP_USAGE("command.stop.usage"),
STOP_EXAMPLE("command.stop.example"),
STOP_EXTRA("command.stop.extra"),
STOP_NOT_STARTED("command.stop.not-started"),
STOP_SUCCESS("command.stop.success"),
STOP_TIMER_STOPPED("command.stop.timer-stopped"),
RELOAD_NAME("command.reload.name"),
RELOAD_DESCRIPTION("command.reload.description"),
RELOAD_USAGE("command.reload.usage"),
RELOAD_EXAMPLE("command.reload.example"),
RELOAD_EXTRA("command.reload.extra"),
RELOAD_SUCCESS("command.reload.success"),
RELOAD_CANNOT_LOAD("command.reload.cannot-load"),
CONFIGURE_NAME("command.configure.name"),
CONFIGURE_DESCRIPTION("command.configure.description"),
CONFIGURE_USAGE("command.configure.usage"),
CONFIGURE_EXAMPLE("command.configure.example"),
CONFIGURE_EXTRA("command.configure.extra"),
CONFIGURE_PROMPT_CREATIVE("command.configure.prompt-creative"),
CONFIGURE_TITLE_MAIN("command.configure.title.main"),
CONFIGURE_TITLE_GENERAL("command.configure.title.general"),
CONFIGURE_TITLE_BLOCKS("command.configure.title.blocks"),
CONFIGURE_TITLE_LOOT("command.configure.title.loot"),
CONFIGURE_TITLE_INVENTORY("command.configure.title.inventory"),
CONFIGURE_BACK("command.configure.back"),
CONFIGURE_DESTRUCTIBLE("command.configure.blocks.destructible.name"),
CONFIGURE_DESTRUCTIBLE_DESC("command.configure.blocks.destructible.description"),
CONFIGURE_INDESTRUCTIBLE("command.configure.blocks.indestructible.name"),
CONFIGURE_INDESTRUCTIBLE_DESC("command.configure.blocks.indestructible.description"),
CONFIGURE_PASS_KEEP("command.configure.blocks.pass-keep.name"),
CONFIGURE_PASS_KEEP_DESC("command.configure.blocks.pass-keep.description"),
CONFIGURE_PASS_DESTROY("command.configure.blocks.pass-destroy.name"),
CONFIGURE_PASS_DESTROY_DESC("command.configure.blocks.pass-destroy.description"),
CONFIGURE_LIVES("command.configure.general.lives"),
CONFIGURE_FUSE_TICKS("command.configure.general.fuse-ticks"),
CONFIGURE_FIRE_TICKS("command.configure.general.fire-ticks"),
CONFIGURE_IMMUNITY_TICKS("command.configure.general.immunity-ticks"),
CONFIGURE_TNT_BLOCK("command.configure.general.tnt-block"),
CONFIGURE_FIRE_ITEM("command.configure.general.fire-item"),
CONFIGURE_LOOT_SLOT("command.configure.loot.slot"),
CONFIGURE_LOOT_BLOCK("command.configure.loot.block"),
CONFIGURE_LOOT_ITEM("command.configure.loot.item"),
CONFIGURE_LOOT_WEIGHT("command.configure.loot.weight"),
CREATE_NAME("command.create.name"),
CREATE_DESCRIPTION("command.create.description"),
CREATE_USAGE("command.create.usage"),
CREATE_EXAMPLE("command.create.example"),
CREATE_EXTRA("command.create.extra"),
CREATE_GAME_EXISTS("command.create.game-exists"),
CREATE_GAME_FILE_CONFLICT("command.create.file-conflict"),
CREATE_GAME_FILE_NOT_FOUND("command.create.file-not-found"),
CREATE_NEED_SELECTION("command.create.need-selection"),
CREATE_SUCCESS("command.create.success"),
CREATE_ERROR("command.create.error"),
CREATE_SCHEMA_NOT_FOUND("command.create.schema-not-found"),
CREATE_FLAG_SCHEMA("command.create.flags.s.description"),
CREATE_FLAG_GAME("command.create.flags.g.description"),
CREATE_FLAG_TEMPLATE("command.create.flags.t.description"),
CREATE_FLAG_WAND("command.create.flags.w.description"),
CREATE_FLAG_SCHEMA_EXT("command.create.flags.s.ext"),
CREATE_FLAG_GAME_EXT("command.create.flags.t.ext"),
CREATE_FLAG_TEMPLATE_EXT("command.create.flags.g.ext"),
DELETE_NAME("command.delete.name"),
DELETE_DESCRIPTION("command.delete.description"),
DELETE_USAGE("command.delete.usage"),
DELETE_EXAMPLE("command.delete.example"),
DELETE_EXTRA("command.delete.extra"),
DELETE_SUCCESS("command.delete.success"),
UNDO_NAME("command.undo.name"),
UNDO_DESCRIPTION("command.undo.description"),
UNDO_USAGE("command.undo.usage"),
UNDO_EXAMPLE("command.undo.example"),
UNDO_EXTRA("command.undo.extra"),
UNDO_DELETED("command.undo.deleted"),
UNDO_SUCCESS("command.undo.success"),
UNDO_UNKNOWN_GAME("command.undo.unknown-game"),
GAMELIST_NAME("command.list.name"),
GAMELIST_DESCRIPTION("command.list.description"),
GAMELIST_USAGE("command.list.usage"),
GAMELIST_EXAMPLE("command.list.example"),
GAMELIST_EXTRA("command.list.extra"),
GAMELIST_GAMES("command.list.games"),
INFO_NAME("command.info.name"),
INFO_DESCRIPTION("command.info.description"),
INFO_USAGE("command.info.usage"),
INFO_EXAMPLE("command.info.example"),
INFO_EXTRA("command.info.extra"),
INFO_DETAILS("command.info.details"),
JOIN_NAME("command.join.name"),
JOIN_DESCRIPTION("command.join.description"),
JOIN_USAGE("command.join.usage"),
JOIN_EXAMPLE("command.join.example"),
JOIN_EXTRA("command.join.extra"),
JOIN_FLAG_TARGET("command.join.flags.t.description"),
JOIN_FLAG_TARGET_EXT("command.join.flags.t.ext"),
JOIN_GAME_STARTED("command.join.game-started"),
JOIN_ALREADY_JOINED("command.join.already-joined"),
JOIN_GAME_FULL("command.join.game-full"),
JOIN_SUCCESS("command.join.success"),
LEAVE_NAME("command.leave.name"),
LEAVE_DESCRIPTION("command.leave.description"),
LEAVE_USAGE("command.leave.usage"),
LEAVE_EXAMPLE("command.leave.example"),
LEAVE_EXTRA("command.leave.extra"),
LEAVE_FLAG_TARGET("command.leave.flags.t.description"),
LEAVE_FLAG_TARGET_EXT("command.leave.flags.t.ext"),
LEAVE_SUCCESS("command.leave.success"),
LEAVE_NOT_JOINED("command.leave.not-joined");
private final String text;
Text(String path) {
String serverValue = YAMLLanguage.server.getString(path);
if (serverValue != null) {
this.text = serverValue;
return;
}
String fallback = YAMLLanguage.builtin.getString(path);
if (fallback == null) {
throw new RuntimeException("No default message for text: " + path);
}
this.text = fallback;
}
@Override
public Formattable applyModifier(Message arg) {
return this;
}
@Override
public Message format(Context context) {
return Expander.expand(text, context.plus(YAMLLanguage.textContext));
}
private static final class YAMLLanguage {
private static final YamlConfiguration builtin;
private static final YamlConfiguration server;
private static final Context textContext;
static {
Reader reader = new BufferedReader(new InputStreamReader(
Text.class.getClassLoader().getResourceAsStream("default_messages.yml")
));
builtin = new YamlConfiguration();
server = new YamlConfiguration();
try {
builtin.load(reader);
if (Bomberman.instance != null) {
Path custom = Bomberman.instance.language();
if (Files.exists(custom)) {
try (Reader fileReader = Files.newBufferedReader(custom)) {
server.load(fileReader);
}
}
}
} catch (IOException | InvalidConfigurationException e) {
e.printStackTrace();
}
textContext = new Context(true).addFunctions(key -> {
String result = server.getString(key);
return result != null ? result : builtin.getString(key);
});
}
}
}

View File

@@ -0,0 +1,28 @@
package io.github.mviper.bomberman.messaging;
public class TitleExpander implements Formattable {
private final Formattable delegate = new RequiredArg(title ->
new DefaultArg(Message.empty, subtitle ->
new DefaultArg(Message.of(0), argFadeIn ->
new DefaultArg(Message.of(20), argDuration ->
new DefaultArg(Message.of(0), argFadeOut -> {
int fadeIn = Integer.parseInt(argFadeIn.toString());
int duration = Integer.parseInt(argDuration.toString());
int fadeOut = Integer.parseInt(argFadeOut.toString());
return Message.title(title, subtitle, fadeIn, duration, fadeOut);
})
)
)
)
);
@Override
public Formattable applyModifier(Message arg) {
return delegate.applyModifier(arg);
}
@Override
public Message format(Context context) {
return delegate.format(context);
}
}

View File

@@ -0,0 +1,4 @@
@ParametersAreNonnullByDefault
package io.github.mviper.bomberman;
import javax.annotation.ParametersAreNonnullByDefault;

View File

@@ -0,0 +1,104 @@
package io.github.mviper.bomberman.utils;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.entity.Entity;
import javax.annotation.CheckReturnValue;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Stream;
@CheckReturnValue
public class Box {
public final World world;
public final Dim p1;
public final Dim p2;
public Box(World world, Dim p1, Dim p2) {
this.world = world;
this.p1 = p1;
this.p2 = p2;
}
public Box(World world, int x, int y, int z, int xSize, int ySize, int zSize) {
this(world, new Dim(x, y, z), new Dim(x + xSize - 1, y + ySize - 1, z + zSize - 1));
}
public Box(Location location, int xSize, int ySize, int zSize) {
this(location.getWorld(), location.getBlockX(), location.getBlockY(), location.getBlockZ(), xSize, ySize, zSize);
}
public Dim getSize() {
return new Dim(p2.x - p1.x + 1, p2.y - p1.y + 1, p2.z - p1.z + 1);
}
public boolean contains(Location location) {
return contains(location.getWorld(), location.getBlockX(), location.getBlockY(), location.getBlockZ());
}
public boolean contains(World world, int x, int y, int z) {
if (world != this.world) {
return false;
}
return x >= p1.x && x <= p2.x
&& y >= p1.y && y <= p2.y
&& z >= p1.z && z <= p2.z;
}
public List<Entity> getEntities() {
List<Entity> entities = new ArrayList<>();
Dim size = getSize();
// the "+ 16" is to make sure the chunks at the edge are also included
int i = p1.x;
while (i < p1.x + size.x + 16) {
int k = p1.z;
while (k < p1.z + size.z + 16) {
var chunk = world.getBlockAt(i, 1, k).getChunk();
for (Entity entity : chunk.getEntities()) {
if (contains(entity.getLocation())) {
entities.add(entity);
}
}
k += 16;
}
i += 16;
}
return entities;
}
public Stream<Location> stream() {
Stream.Builder<Location> builder = Stream.builder();
for (int x = p1.x; x <= p2.x; x++) {
for (int y = p1.y; y <= p2.y; y++) {
for (int z = p1.z; z <= p2.z; z++) {
builder.add(new Location(world, x, y, z));
}
}
}
return builder.build();
}
@Override
public String toString() {
return "Box " + p1 + " - " + p2 + " : (" + getSize().volume() + " blocks)";
}
@Override
public int hashCode() {
int hash = 31;
hash = hash * 31 + world.hashCode();
hash = hash * 31 + p1.hashCode();
hash = hash * 31 + p2.hashCode();
return hash;
}
@Override
public boolean equals(Object other) {
if (!(other instanceof Box)) {
return false;
}
Box box = (Box) other;
return box.world == world && box.p1.equals(p1) && box.p2.equals(p2);
}
}

View File

@@ -0,0 +1,65 @@
package io.github.mviper.bomberman.utils;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.World;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.PotionMeta;
import org.bukkit.potion.PotionData;
import org.bukkit.potion.PotionType;
import org.bukkit.util.BoundingBox;
import javax.annotation.CheckReturnValue;
public final class BukkitUtils {
private BukkitUtils() {
}
@CheckReturnValue
public static Location boxLoc1(Box box) {
return new Location(box.world, box.p1.x, box.p1.y, box.p1.z);
}
@CheckReturnValue
public static Location boxLoc2(Box box) {
return new Location(box.world, box.p2.x, box.p2.y, box.p2.z);
}
@CheckReturnValue
public static Location asLoc(World world, Dim dim) {
return new Location(world, dim.x, dim.y, dim.z);
}
public static BoundingBox convert(Box box) {
return new BoundingBox(
box.p1.x, box.p1.y, box.p1.z,
box.p2.x + 1, box.p2.y + 1, box.p2.z + 1
);
}
@CheckReturnValue
public static ItemStack makePotion(PotionType type, int qty, boolean extend, boolean upgraded) {
ItemStack stack = new ItemStack(Material.POTION, qty);
PotionMeta meta = (PotionMeta) stack.getItemMeta();
if (meta != null) {
meta.setBasePotionData(new PotionData(type, extend, upgraded));
stack.setItemMeta(meta);
}
return stack;
}
@CheckReturnValue
public static ItemStack makePotion(PotionType type) {
return makePotion(type, 1, false, false);
}
@CheckReturnValue
public static ItemStack makePotion(PotionType type, int qty) {
return makePotion(type, qty, false, false);
}
@CheckReturnValue
public static Location blockLoc(Location loc) {
return new Location(loc.getWorld(), loc.getBlockX(), loc.getBlockY(), loc.getBlockZ());
}
}

View File

@@ -0,0 +1,28 @@
package io.github.mviper.bomberman.utils;
import org.bukkit.configuration.serialization.ConfigurationSerializable;
import java.util.Collections;
import java.util.Map;
/**
* Stores the data passed into it from a configuration section.
* Useful for retrieving data from classes that do not exist anymore.
*/
public class DataRestorer implements ConfigurationSerializable {
private final Map<String, Object> data;
@RefectAccess
public DataRestorer(Map<String, Object> data) {
this.data = data;
}
public Map<String, Object> getData() {
return data;
}
@Override
public Map<String, Object> serialize() {
return Collections.emptyMap();
}
}

View File

@@ -0,0 +1,43 @@
package io.github.mviper.bomberman.utils;
import javax.annotation.CheckReturnValue;
@CheckReturnValue
public class Dim {
public final int x;
public final int y;
public final int z;
public Dim(int x, int y, int z) {
this.x = x;
this.y = y;
this.z = z;
}
public int volume() {
return x * y * z;
}
@Override
public int hashCode() {
int hash = 31;
hash = hash * 31 + x;
hash = hash * 31 + y;
hash = hash * 31 + z;
return hash;
}
@Override
public boolean equals(Object other) {
if (!(other instanceof Dim)) {
return false;
}
Dim dim = (Dim) other;
return dim.x == x && dim.y == y && dim.z == z;
}
@Override
public String toString() {
return "Dim{" + x + ", " + y + ", " + z + "}";
}
}

View File

@@ -0,0 +1,11 @@
package io.github.mviper.bomberman.utils;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/**
* States that the item is designed to be accessed by reflection. Can be used to tell the IDE warnings to shut up
*/
@Retention(value = RetentionPolicy.SOURCE)
public @interface RefectAccess {
}

View File

@@ -0,0 +1,47 @@
package io.github.mviper.bomberman.utils;
import com.sk89q.worldedit.bukkit.BukkitAdapter;
import com.sk89q.worldedit.extent.clipboard.Clipboard;
import com.sk89q.worldedit.math.BlockVector3;
import com.sk89q.worldedit.regions.CuboidRegion;
import com.sk89q.worldedit.regions.Region;
import org.bukkit.Location;
public final class WorldEditUtils {
private WorldEditUtils() {
}
public static CuboidRegion convert(Box box) {
return new CuboidRegion(
BukkitAdapter.adapt(box.world),
BlockVector3.at(box.p1.x, box.p1.y, box.p1.z),
BlockVector3.at(box.p2.x, box.p2.y, box.p2.z)
);
}
public static BlockVector3 convert(Dim dim) {
return BlockVector3.at(dim.x, dim.y, dim.z);
}
public static Dim convert(BlockVector3 vec) {
return new Dim(vec.getBlockX(), vec.getBlockY(), vec.getBlockZ());
}
public static Box pastedBounds(Location pasteLocation, Clipboard clipboard) {
BlockVector3 pasteVec = BlockVector3.at(
pasteLocation.getBlockX(),
pasteLocation.getBlockY(),
pasteLocation.getBlockZ()
);
BlockVector3 delta = pasteVec.subtract(clipboard.getOrigin());
BlockVector3 min = clipboard.getMinimumPoint().add(delta);
BlockVector3 max = clipboard.getMaximumPoint().add(delta);
return new Box(pasteLocation.getWorld(), convert(min), convert(max));
}
public static Box selectionBounds(Region region) {
BlockVector3 min = region.getMinimumPoint();
BlockVector3 max = region.getMaximumPoint();
return new Box(BukkitAdapter.adapt(region.getWorld()), convert(min), convert(max));
}
}

View File

@@ -0,0 +1,14 @@
# -------------------------------------------------------------------- #
# #
# BOMBERMAN CONFIG #
# #
# Refer to the Bomberman Wiki for help on configuration: #
# https://github.com/mviper/bomberman/wiki #
# #
# There is nothing to configure in this file. All configuration #
# is done on a per-game basis. #
# #
# This file is automatically generated and will be reset on next #
# server reload. #
# #
# -------------------------------------------------------------------- #

View File

@@ -0,0 +1,356 @@
# -------------------------------------------------------------------- #
# #
# BOMBERMAN DEFAULT MESSAGES #
# #
# This file contains the default messages. #
# You can overwrite anything by setting the value in messages.yml #
# #
# Refer to the Bomberman wiki for help on configuring messages: #
# https://github.com/mviper/bomberman/wiki/Localisation #
# #
# This file is automatically generated and will be reset on next #
# server reload. #
# #
# -------------------------------------------------------------------- #
format:
# How a basic message looks
# {message}: the message being sent
message: '{#green|[Bomberman]} {message}'
# How to format an item by default
# {item}: the item to format
item: '{item|amount}x{item|type}'
# How to format a heading
# {arg0}: The title
heading: '{#raw}{#yellow|--------} {arg0} {#yellow|---------------}'
# How to format a list of key-value pairs
# {arg0}: the maps key
# {arg1} the map's value
map: "{#raw}{#switch|{arg1}||| {#gold|{arg0}:} {arg1}}"
# How to display a list
# {arg0}: the value of the list
list: "{#raw}{#switch|{arg0}||| {#gold|*} {arg0}}"
# How code/commands are formatted
# {arg0}: the code
code: '{#gray|{#italic|{arg0}}}'
# How errors are displayed
# {arg0}: the error
error: '{#red|{arg0}}'
# How names of things are displayed
# {arg0}: the name
name: '{#italic|{arg0}}'
# How quotes should be formatted
# {arg0}: the code
quote: "'{arg0}'"
# Format shortcodes
heading: '{#|format.heading|{arg0}}'
map: '{#|format.map|{arg0}|{arg1}}'
list: '{#|format.list|{arg0}}'
code: '{#|format.code|{arg0}}'
error: '{#|format.error|{arg0}}'
name: '{#|format.name|{arg0}}'
quote: '{#|format.quote|{arg0}}'
game-play:
# Informs all the other players not involved that a player died
# {player}: the player who died
player-killed: '{#|name|{player}} is out!'
# The winning text message
# {player}: the winner
player-won: '{#title|{#gold|WIN!}||0|60|20}'
# Sent as the game timer counts down. Called every second
# {time}: the time till the game begins
count: '{#title|{#yellow|{time}}||0|17|2}'
# Sent when the game starts
started: '{#title|{#white|GO!}||0|20|2}'
# All messages that can be displayed when executing a command.
# Most messages have a {command} argument (TODO: which ones?)
command:
# How to display command help
# {command}: The command
help: |-
{#|heading|Help: {command|name}}
{command|description}
{#|map|Usage|{#|code|{command|usage}}}
{#|map|Extra|{command|extra}}
{#|map|Example|{command|example}}
{#|map|Flags|{#switch|{command|flags|length}|0|| }}
{command|flags|map|{!#|map| -{it|name}{#switch|{it|ext}|||={it|ext}}|{it|description}}|join|
}
# Shown when a player tries to run a command they do not have permission to use
deny-permission: '{#|error|You do not have permission to use {#|code|/{command|path}}}'
# Shown when the player uses a command incorrectly
# {attempt}: the list of arguments that the player typed
incorrect-usage: '{#|error|Incorrect usage. For help, type: {#|code|/{command|path} -?}}'
# Shown when a player types a command that does not exist
# {attempt}: the cmd that the player typed
unknown-command: '{#|error|Unknown command: {#|quote|{attempt}}}'
# Shown to the console when it tries to run a command that needs to be done as a player
must-be-player: You must be a player
# Shown when a command uses a target selector, but the selecting is invalid
# {selector}: the string of what they typed
invalid-target-selector: '{#|error|{#|quote|{selector}} is invalid}'
# Shown when a command needs a number but they typed something else
# {number}: the string of what they typed (this will _not_ be a number)
invalid-number: '{#|error|{#|quote|{number}} is not a valid number}'
# Called when a command is cancelled for an unknown reason
cancelled: Operation cancelled
# The base command details
bomberman:
description: Main command for Bomberman
# Details for commands for the base Bomberman command
group:
# Help instructions
# {sender}: the player (or console) requesting help
help: |-
{#|heading|Help: {command|name}}
{command|children|sort|{!it|name}|filter|{!sender|haspermission|{it|permission}}|map|{!#|map|{it|name}|{#switch|{=|{#len|{it|description}}>40}|1|{#sub|{it|description}|0|40}...|{it|description}}}|join|
}
usage: '/{command|path} <subcommand>'
example: ''
extra: ''
# Commands relating to '/bm start'
start:
name: start
description: Start a game
usage: /{command|path} <game>
example: ''
extra: ''
flags:
d:
description: 'The delay in seconds (default=3)'
ext: '<delay>'
o:
description: 'Override existing timers (default=false)'
# Message when a player starts an already started arena
# {game}: the game attempted to be started
already-started: Game {#|name|{game}} already started
# Called if game start was called when a timer was already running
# {game}: the game attempted to be started
# {time}: the time remaining
already-counting: Game {#|name|{game}} already counting
# Called if game start was called when no players had joined
# {game}: the game attempted to be started
no-players: Game {#|name|{game}} has no players
# The game has been started
# {game}: the game that has been started
success: Game {#|name|{game}} starting
# Messages relating to '/bm stop'
stop:
name: stop
description: Stop a game
usage: /{command|path} <game>
example: ''
extra: ''
# The game hasn't been started so the game cannot be stopped
# {game}: the game
not-started: Game {#|name|{game}} hasn't started
# The game has stopped
# {game}: the game
success: Game {#|name|{game}} stopped
# The timer stopped
# {game}: the game
# {time}: time before it would have started
timer-stopped: Timer cancelled
# Commands Relating to /bm reload
reload:
name: reload
description: Reloads the config file and resets the arena
usage: /{command|path} <game>
example: ''
extra: ''
# The game has been reloaded
# {game}: the game
success: Game {#|name|{game}} reloaded
# Error while loading
# {game}: the old game's name (not an actual game)
cannot-load: Error reloading the game's save file
configure:
name: configure
description: Configure game's settings
usage: /{command|path} <game>
example: ''
extra: '{#gray|{#italic|Creative users only}}'
# If player tries to edit when not in creative
# {player}: the player
prompt-creative: '{#|error|You must be in creative}'
# Various labels in the pop-up menu (no args)
title:
main: 'Game Settings'
general: 'General Settings'
blocks: 'Block Settings'
loot: 'Block Loot Table'
inventory: 'Starting Kit'
back: 'Menu'
blocks:
destructible:
name: 'Destructible'
description: 'Semi-solid blocks'
indestructible:
name: 'Indestructible'
description: 'Solid blocks default'
pass-destroy:
name: 'Pass Destroy'
description: 'Non solid blocks default'
pass-keep:
name: 'Pass keep'
description: 'Fire passes through without changing'
general:
lives: Lives
fuse-ticks: Fuse ticks
fire-ticks: Fire ticks
immunity-ticks: Immunity ticks
tnt-block: TNT Block
fire-item: Fire Item
loot:
slot: "Block Group {=|{slot}+1}"
block: Block Types
item: Drop
weight: Weighting
# Messages relating to '/bm create'
create:
name: create
description: |-
Builds a Bomberman game. Use one of {#|code|-g}, {#|code|-t}, {#|code|-s} or {#|code|-w} to select what to build.
usage: /{command|path} <name> [options]
example: /{command|path} mygame -t=purple
extra: 'Custom schematics need all spawns marked by writing {#|code|[spawn]} on signs.'
flags:
s:
description: Build using a World Edit schematic
ext: '<file>'
t:
description: Specify a template game to duplicate
ext: '<file>'
g:
description: Specify an existing game to copy
ext: '<game>'
w:
description: Uses the current World Edit wand selection.
# The game's name is already used
# {game}: the game that has the shared name
game-exists: Game {#|name|{game}} already exists
# The game's file name conflicts with an already existing game
# {game}: the name of the game that tried to be created
# {file}: the filename in conflict
file-conflict: Game {#|name|{game}} conflicts with file {#|quote|{file}}
# The referenced file name could not be found
# {file}: the file (including path) that does not exist
# {filename}: just the filename
file-not-found: Can not find {#|quote|{file}}
# The game was built successfully
# {game}: the game that was built
success: Game {#|name|{game}} created{#switch|{game|spawns}|0| but has no spawns. Did you mark spawns with "[spawn]"?}
# An exception was thrown while building
# {error}: the error message
error: |-
{#|error|Build Error: {#|quote|{error}}
See console for details. Game creation may be incomplete.
Was the schematic made in a different WorldEdit version?}
# Player needs to use WorldEdit to select something to create the game from
need-selection: Use wooden axe to select a region first
# The requested schematic was not found
# {schema}: the file name that was attempted
schema-not-found: '{#red|Schematic not found: {#italic|{schema}}}'
# Messages relating to '/bm delete'
delete:
name: delete
description: Deletes a game
usage: /{command|path} <game>
example: ''
extra: ''
# The game has been deleted
# {game}: the game that was deleted (this is still a game object)
success: Game {#|name|{game}} destroyed
# Messages relating to '/bm undo'
undo:
name: undo
description: Reverts the schematic to its pre-create state and deletes the game (if not already deleted)
usage: /{command|path} <game>
example: ''
extra: Can only undo games built in the last 10 minutes. Does not work for games built with a wand.
# The game arena was restored to previous state
# {game}: the game name that was reset (NOT a valid object; it is just the name)
success: Game {#|name|{game}} schematic reverted
# The game was deleted
# {game}: the game that was deleted (this is a game object)
deleted: Game {#|name|{game}} deleted
# Game was not in the memory
# {game}: the game name requested (NOT a valid game)
unknown-game: No previous state of {#|name|{game}} stored - was it created more than 10 minutes ago or by using a wand?
# Messages relating to '/bm list'
list:
name: list
description: Shows all existing games
usage: /{command|path}
example: ''
extra: ''
# The displayed message
# {games}: a list of games
games: |-
{#|heading|Games}
{#switch|{games|length}|0|{#red|No games present}|{games|sort|{!it|name}|map|{!#|map|{it|name}|{it|players}/{it|spawns} ({#switch|{it|running}|true|Running|false|Waiting|{#red|?}})}|join|
}}
# Messages relating to '/bm info'
info:
name: info
description: Show information about a game
usage: /{command|path} <game>
example: ''
extra: ''
# What info to display about a game
# {game}: the game
details: |-
{#|heading|Info: {game|name}}
{#|map|Status|{#switch|{game|running}|true|Playing|false|Waiting|{#red|?}}}
{#|map|Players|{game|players}/{game|spawns}}
{#|map|Init lives|{game|lives}}
{#|map|Init bombs|{game|bombs}}
{#|map|Init power|{game|power}}
{#|map|Location|({game|x}, {game|y}, {game|z})}
{#|map|Schema|{game|schema|name}}
{#|map|Size|[{game|schema|xsize}x{game|schema|ysize}x{game|schema|zsize}]}
# Commands relating to '/bm join'. Also see 'join.xxx' messages
join:
name: join
description: Join a game
usage: /{command|path} <game>
example: ''
extra: ''
flags:
t:
description: Target selector to apply command to. All standard minecraft selectors work, but only players can be selected. This flag requires additional permissions.
ext: "<selector>"
# The game has no spare spawn points
# {game}: the game attempted to be joined
game-full: Game {#|name|{game}} is full
# Cannot join because the game has already started
# {game}: the game that couldn't be joined
game-started: 'Game has already started'
# Player tried to join a game when they are already playing a game
# {game}: the game that couldn't be joined
# {player}: the player joining
already-joined: 'You are already part of a game'
# A player has joined the game. Sent to all observers of a game
# {game}: the game joined
# {player}: the player who joined
success: '{#|name|{player}} joined game {#|name|{game}}{#switch|{=|{game|players}=={game|spawns}}|1|{#exec|bm start {game} -d=10}}'
# Messages relating to '/bm leave'
leave:
name: leave
description: Leave the game
usage: /{command|path}
example: ''
extra: ''
flags:
t:
description: Target selector to apply command to. All standard minecraft selectors work, but only players can be selected. This flag requires additional permissions.
ext: "<selector>"
# The player is not part of any game
# {player}: player who isn't joined
not-joined: You're not part of a game
# Player left successfully
# {player}: the player that left
success: '{#gray|{#italic|{#|name|{player}} left game}}{#switch|{=|{game|players}==0}|1|{#exec|bm stop {game}}}'

View File

@@ -0,0 +1,203 @@
# -------------------------------------------------------------------- #
# #
# BOMBERMAN GAMES DIRECTORY #
# #
# This directory contains all games that will be loaded. Each #
# game has a single zip file called <name>.game.zip. The game #
# file contains the schema and all configuration. #
# #
# DO NOT UNZIP GAME FILES - Unzipped files will not be loaded. #
# #
# To share a game, simple share the game's zip file. Others can #
# download the file into `games/templates` and create copies with #
# \bm create <name> -schem=t:<file>.game.zip #
# #
# Most configuration of a game can be done using: #
# \bm configure <name> #
# Advanced configuration can be done by opening .game.zip, and #
# editing settings.yml. This file contains the default settings #
# and explains each setting. #
# #
# This file is automatically generated and will be reset on next #
# server reload. #
# #
# -------------------------------------------------------------------- #
settings:
==: io.github.mviper.bomberman.game.GameSettings
# How many lives each player gets
lives: 3
# The item type to use for bomb size calculations
power: minecraft:gunpowder
# The block type which explodes
bomb: minecraft:tnt
# The block type used for flames from tnt
fire: minecraft:fire
# What each player spawns in the game with
# The item order is preserved in players inventory; pad with nulls
initial-items:
- ==: org.bukkit.inventory.ItemStack
v: 2230
type: TNT
amount: 3
# How long before tnt explodes
fuse-ticks: 40
# The duration that tnt flames should remain for
fire-ticks: 20
# How long before a player can take a second hit
immunity-ticks: 21
# What each block type should drop.
# Each set of materials has a list of Item stacks mapped to a weighting.
# Materials without an entry will not drop anything.
loot-table:
- blocks:
- minecraft:snow_block
- minecraft:dirt
- minecraft:sand
loot:
- weight: 4 # The relative weighting of choosing this item
item: # What item to drop
==: org.bukkit.inventory.ItemStack
v: 2230
type: TNT
- weight: 100
item:
==: org.bukkit.inventory.ItemStack
v: 2230
type: AIR
amount: 0 # amount 0 means "no drop"
- weight: 3
item:
==: org.bukkit.inventory.ItemStack
v: 2230
type: GUNPOWDER
- weight: 1
item:
==: org.bukkit.inventory.ItemStack
v: 2230
type: POTION
meta:
==: ItemMeta
meta-type: POTION
potion-type: minecraft:healing
- weight: 1
item:
==: org.bukkit.inventory.ItemStack
v: 2230
type: POTION
meta:
==: ItemMeta
meta-type: POTION
potion-type: minecraft:strong_swiftness
- weight: 1
item:
==: org.bukkit.inventory.ItemStack
v: 2230
type: POTION
meta:
==: ItemMeta
meta-type: POTION
potion-type: minecraft:invisibility
# Below is a second set of blocks with different loot.
# Add as many sets as you want
- blocks:
- minecraft:gravel
loot:
- item:
==: org.bukkit.inventory.ItemStack
v: 2230
type: TNT
weight: 1.0
- item:
==: org.bukkit.inventory.ItemStack
v: 2230
type: AIR
amount: 0
weight: 10.0
# By default, bomberman players cannot be hurt by anything except TNT
# By configuring the below, specific damage causes can be allowed
# The possible damage causes are listed here https://hub.spigotmc.org/javadocs/spigot/org/bukkit/event/entity/EntityDamageEvent.DamageCause.html
# Values are specified as a regex and not case-sensitive.
#
# Each damage cause can be altered with different modifiers listed here: https://hub.spigotmc.org/javadocs/bukkit/org/bukkit/event/entity/EntityDamageEvent.DamageModifier.html
# Modifiers can be matched by regex (case-insensitive). Modifiers not matched will not be altered.
# Modifiers that reduce damage taken will be negative.
# If the value is specified on the damage cause, only the 'base' modifier will be altered.
#
# A special 'cancel' value can be specified that will cancel the event if a non-zero number is given.
#
# The damage specified is evaluated as a localisation message with the following arguments:
# - game: the current game data
# - player: the player affected
# - cause: the damage cause
# - final: the total damage that will be dealt
# - base: the damage done by the base modifier
# - damage: the modifiers damage to do (not applicable for 'cancel')
# - modifier: the modifier being altered (not applicable for 'cancel')
damage-source:
CUSTOM: {} # damage from bomberman explosions. 'Custom' is the only cause enabled by default
# SUICIDE: {} # Allow use of the /kill command
# HOT_FLOOR|CONTACT: 1 # Take one heart base damage from magma blocks and cactus and such
# ENTITY_ATTACK: # Allow players to attack each other with tools only (hands deal 1.5 max). Each hit will deal 1 heart
# base: '{#=|{damage}>=2}'
# (?!base).+: 0 # Disable other modifiers (e.g. armour)
# cancel: '{=|!{final}}' # Cancel the event when not damaged
# '*': {} # Allow everything
# Materials listed here will not be pasted when building the schematic.
# This is useful in irregular shaped buildings to keep e.g. the ground material.
# It could also be used to keep some blocks from being reset each game.
source-mask: []
# Sets the block that surrounds spawn points
cage-block: minecraft:white_stained_glass
# Bomberman has four types of blocks:
# 1. indestructible: tnt will not affect the block
# 2. destructible: tnt will destroy the block, but progress no further
# 3. pass-keep: tnt will pass through the block without changing the blocks state.
# Players inside pass-keep blocks will still be hit
# 4. pass-destroy: tnt will pass through the block, ignite the block, then change into air
#
# Unless set otherwise:
# * All solid blocks are indestructible
# * All non-solid blocks are pass-destroy.
# A solid block is defined as a block that has any hitbox that blocks the players movement
#
# All types will drop loot when destroyed if they are assigned in loot-table
#
# The assigned tnt fire type and air types are always pass-destroy and cannot be altered
#
# Note: The in-game configuration gui will "smart add" similar material ids (e.g wall_sign and sign).
# You have to add similar ids manually here
# Blocks tnt will explode the first block of
# Generally, blocks that are in the loot-table should be destructible
# Removing tnt from destructible means tnt explosions will not chain together
destructible:
- minecraft:tnt
- minecraft:snow_block
- minecraft:dirt
- minecraft:sand
- minecraft:gravel
# Blocks tnt cannot touch.
# All solid blocks included by default
indestructible: []
# Blocks tnt will pass through, but not alter.
# You might like to put your sensitive redstone, tripwires, signs, etc here
pass-keep: []
# Blocks tnt will pass through, ignite, and destroy
# All non-solid blocks included by default
# Will always include air types and the tnt fire type
pass-destroy: []

View File

@@ -0,0 +1,19 @@
# -------------------------------------------------------------------- #
# #
# BOMBERMAN TEMPLATE DIRECTORY #
# #
# This directory contains all games that can be used as a #
# template in `/bm create`. Games copied into this directory will #
# not be loaded. Template files should end in `.game.zip` #
# #
# To make a copy of a templated game, run: #
# \bm create <name> -schem=t:<file>.game.zip #
# #
# A templated game is the exact same file as a normal game, #
# except that it is stored in this folder. Thus, to share a game #
# you've created, simply copy the game's zip file. #
# #
# This file is automatically generated and will be reset on next #
# server reload. #
# #
# -------------------------------------------------------------------- #

Binary file not shown.

View File

@@ -0,0 +1,14 @@
# -------------------------------------------------------------------- #
# #
# BOMBERMAN MESSAGES #
# #
# This file is where overrides to messages are placed. #
# #
# A list of default messages and parameters are specified in #
# default_messages.yml. #
# #
# For changes to take effect, the server must be reloaded. #
# #
# -------------------------------------------------------------------- #
# Add overrides here

View File

@@ -0,0 +1,101 @@
name: Bomberman
main: io.github.mviper.bomberman.Bomberman
version: ${version}
api-version: 1.21
load: POSTWORLD
author: M_Viper
website: https://github.com/mviper/bomberman
commands:
bomberman:
description: Main command for Bomberman
usage: bomberman <more commands>
aliases: bm
permission: bomberman.bm
depend: [WorldEdit]
# SoftDepend Multiverse so Bomberman is loaded after the Multiverse worlds get loaded
softdepend: [Multiverse-Core, PlaceholderAPI]
permissions:
bomberman.*:
description: Access to all Bomberman commands
children:
bomberman.bm: true
bomberman.dictator: true
bomberman.operator: true
bomberman.player: true
bomberman.remote: true
default: op
bomberman.player:
description: Allows join/leave/info
children:
bomberman.join: true
bomberman.leave: true
bomberman.info: true
bomberman.list: true
default: true
bomberman.operator:
description: Allows control of games (start/stop)
children:
bomberman.start: true
bomberman.stop: true
bomberman.reload: true
default: op
bomberman.dictator:
description: Allows building/configuring games
children:
bomberman.create: true
bomberman.delete: true
bomberman.undo: true
bomberman.configure: true
default: op
bomberman.remote:
description: Allows executing commands on behalf of other players
children:
bomberman.join.remote: true
bomberman.leave.remote: true
default: op
bomberman.bm:
description: Root bomberman command (/bm)
default: true
bomberman.join:
description: /bm join
default: false
bomberman.join.remote:
description: /bm join -t=...
default: false
bomberman.leave:
description: /bm join
default: false
bomberman.leave.remote:
description: /bm leave -t=...
default: false
bomberman.info:
description: /bm info
default: false
bomberman.list:
description: /bm list
default: false
bomberman.start:
description: /bm start
default: false
bomberman.stop:
description: /bm stop
default: false
bomberman.reload:
description: /bm reload
default: false
bomberman.create:
description: /bm create
default: false
bomberman.delete:
description: /bm delete
default: false
bomberman.undo:
description: /bm undo
default: false
bomberman.configure:
description: /bm configure
default: false

View File

@@ -0,0 +1,21 @@
# -------------------------------------------------------------------- #
# #
# BOMBERMAN TEMPORARY DIRECTORY #
# #
# This directory contains temporary data for Bomberman's #
# operation. #
# #
# Two directories exist: #
# - game: #
# Data cached for a game. This decreases the server #
# restart time as less data needs to be read and processed #
# - players: #
# Data about the status of a player before they joined a #
# Bomberman game. This stores e.g. inventory, health, #
# location, etc. The file is used in-case the server #
# crashes or shutdowns un-expectantly. #
# #
# This file is automatically generated and will be reset on next #
# server reload. #
# #
# -------------------------------------------------------------------- #

View File

@@ -0,0 +1,102 @@
package io.github.mviper.bomberman.commands;
import io.github.mviper.bomberman.messaging.Message;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.junit.jupiter.api.Test;
import java.util.List;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.*;
class BaseCommandTest {
@Test
void testBaseCommandFindsCorrectChildAndGivesCorrectArguments() {
BaseCommand group = new BaseCommand(false);
Cmd child = mock(Cmd.class, "child");
group.addChildren(child);
when(child.name()).thenReturn(Message.of("Child"));
when(child.permission()).thenReturn(Permissions.CREATE);
when(child.run(any(), eq(List.of("hello", "world")), eq(Map.of()))).thenReturn(true);
Cmd child2 = mock(Cmd.class, "child2");
group.addChildren(child2);
when(child2.name()).thenReturn(Message.of("child2"));
when(child2.permission()).thenReturn(Permissions.CREATE);
CommandSender sender = mock(CommandSender.class, "sender");
when(sender.hasPermission(anyString())).thenReturn(true);
group.run(sender, List.of("chIlD", "hello", "world"), Map.of());
verify(child).run(sender, List.of("hello", "world"), Map.of());
verify(child2, atMostOnce()).name();
verifyNoMoreInteractions(child2);
verify(sender, never()).sendMessage(anyString());
}
@Test
void testBaseCommandTabCompletesCommands() {
BaseCommand group = new BaseCommand(false);
CommandSender sender = mock(CommandSender.class, "sender");
when(sender.hasPermission(anyString())).thenReturn(true);
Permission permission = mock(Permission.class);
when(permission.isAllowedBy(any())).thenReturn(true);
Cmd child = mock(Cmd.class, "child");
group.addChildren(child);
when(child.name()).thenReturn(Message.of("child"));
when(child.permission()).thenReturn(permission);
when(child.options(sender, List.of("hello", "world", ""))).thenReturn(List.of("all", "good"));
Cmd child2 = mock(Cmd.class, "child2");
group.addChildren(child2);
when(child2.name()).thenReturn(Message.of("child2"));
when(child2.permission()).thenReturn(permission);
List<String> result = group.onTabComplete(sender, mock(Command.class), "", new String[]{"chIlD", "hello", "world", ""});
verify(child, atLeastOnce()).name();
verify(child).permission();
verify(child).options(sender, List.of("hello", "world", ""));
verify(child2, atMostOnce()).name();
verify(child2, atMostOnce()).permission();
verifyNoMoreInteractions(child2);
assertEquals(List.of("all", "good"), result);
verify(sender, never()).sendMessage(anyString());
}
@Test
void testBaseCommandTabCompletesFlagOptions() {
CommandSender sender = mock(CommandSender.class, "sender");
when(sender.hasPermission(anyString())).thenReturn(true);
Permission permission = mock(Permission.class);
when(permission.isAllowedBy(any())).thenReturn(true);
Cmd subCommand = mock(Cmd.class);
when(subCommand.name()).thenReturn(Message.of("dummy"));
when(subCommand.permission()).thenReturn(permission);
when(subCommand.flags(any())).thenReturn(List.of("a", "b").stream().collect(java.util.stream.Collectors.toSet()));
when(subCommand.flagExtension("a")).thenReturn(Message.of("<something>"));
when(subCommand.flagExtension("b")).thenReturn(Message.of(""));
BaseCommand base = new BaseCommand(false);
base.addChildren(subCommand);
List<String> result = base.onTabComplete(sender, mock(Command.class), "", new String[]{"dummy", "-"});
assertArrayEquals(new String[]{"-?", "-a", "-b"}, result.stream().sorted().toArray(String[]::new));
}
}

View File

@@ -0,0 +1,50 @@
package io.github.mviper.bomberman.game;
import org.junit.jupiter.api.Test;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
class ExplosionTest {
@Test
void lootSelectWithSingleItemAlwaysGetThatItem() {
for (int i = 0; i <= 99; i++) {
Map<String, Integer> loot = Map.of("diamond", 1);
Set<String> got = Explosion.lootSelect(loot);
assertEquals(Set.of("diamond"), got);
}
}
@Test
void lootSelectWithNoLootGetsNothing() {
for (int i = 0; i <= 99; i++) {
Map<String, Integer> loot = Map.of();
Set<String> got = Explosion.lootSelect(loot);
assertEquals(Set.of(), got);
}
}
@Test
void lootSelectIsWeightedUnbiased() {
AtomicInteger diamonds = new AtomicInteger();
AtomicInteger dirt = new AtomicInteger();
Map<AtomicInteger, Integer> loot = Map.of(
diamonds, 1,
dirt, 3
);
for (int i = 0; i <= 999; i++) {
Set<AtomicInteger> got = Explosion.lootSelect(loot);
got.forEach(AtomicInteger::incrementAndGet);
}
int diamondsCount = diamonds.get();
int dirtCount = dirt.get();
assertTrue(diamondsCount >= 150 && diamondsCount <= 350, "Diamonds count out of expected range: " + diamondsCount);
assertTrue(dirtCount >= 650 && dirtCount <= 850, "Dirt count out of expected range: " + dirtCount);
}
}

Some files were not shown because too many files have changed in this diff Show More