diff --git a/src/main/java/io/github/mviper/bomberman/Bomberman.java b/src/main/java/io/github/mviper/bomberman/Bomberman.java index 45db2be..051fa31 100644 --- a/src/main/java/io/github/mviper/bomberman/Bomberman.java +++ b/src/main/java/io/github/mviper/bomberman/Bomberman.java @@ -4,6 +4,7 @@ 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.game.JoinSignListener; import io.github.mviper.bomberman.utils.DataRestorer; import org.bukkit.Bukkit; import org.bukkit.command.PluginCommand; @@ -30,7 +31,9 @@ public class Bomberman extends JavaPlugin implements Listener { instance = this; ConfigurationSerialization.registerClass(GameSettings.class); + ConfigurationSerialization.registerClass(GameSettings.class, "io.github.mdsimmo.bomberman.game.GameSettings"); ConfigurationSerialization.registerClass(DataRestorer.class, "io.github.mviper.bomberman.game.Game$BuildFlags"); + ConfigurationSerialization.registerClass(DataRestorer.class, "io.github.mdsimmo.bomberman.game.Game$BuildFlags"); getDataFolder().mkdirs(); @@ -39,9 +42,9 @@ public class Bomberman extends JavaPlugin implements Listener { bukkitBmCmd.setExecutor(bmCmd); bukkitBmCmd.setTabCompleter(bmCmd); - if (Bukkit.getPluginManager().getPlugin("PlaceholderAPI") != null) { - new BmPlaceholder().register(); - } + Bukkit.getPluginManager().registerEvents(new JoinSignListener(), this); + + // Placeholder expansion registration disabled to avoid console startup noise. // Update the old file system GameSave.updatePre080Saves(); @@ -66,7 +69,9 @@ public class Bomberman extends JavaPlugin implements Listener { // Copy resources saveResource("config.yml", true); - saveResource("messages.yml", false); + if (!Files.exists(language())) { + saveResource("messages.yml", false); + } saveResource("default_messages.yml", true); saveResource("games/templates/purple.game.zip", true); saveResource("games/templates/experimental.game.zip", true); diff --git a/src/main/java/io/github/mviper/bomberman/game/Game.java b/src/main/java/io/github/mviper/bomberman/game/Game.java index d515f82..b4461d9 100644 --- a/src/main/java/io/github/mviper/bomberman/game/Game.java +++ b/src/main/java/io/github/mviper/bomberman/game/Game.java @@ -62,11 +62,13 @@ import java.util.regex.Pattern; public class Game implements Formattable, Listener { private static final Bomberman PLUGIN = Bomberman.instance; + private static final int AUTO_START_DELAY_SECONDS = 3; private final GameSave save; public final String name; private final Set players = new HashSet<>(); private boolean running = false; + private boolean countingDown = false; private final YamlConfiguration tempData; private Box box; private Set spawns; @@ -149,6 +151,26 @@ public class Game implements Formattable, Listener { return save.origin; } + public int getPlayerCount() { + return players.size(); + } + + public int getMaxPlayers() { + return getSpawns().size(); + } + + public boolean isRunning() { + return running; + } + + public boolean isCountingDown() { + return countingDown; + } + + public boolean contains(Location location) { + return location != null && getBox().contains(location); + } + public Clipboard getClipboard() throws IOException { return save.getSchematic(); } @@ -193,7 +215,6 @@ public class Game implements Formattable, Listener { } private Set searchSpawns() { - PLUGIN.getLogger().info("Searching for spawns..."); Set result = new HashSet<>(); try { Clipboard clipboard = getClipboard(); @@ -215,7 +236,6 @@ public class Game implements Formattable, Listener { } catch (IOException e) { throw new RuntimeException("Failed to search spawns", e); } - PLUGIN.getLogger().info(" " + result.size() + " spawns found"); return result; } @@ -326,6 +346,11 @@ public class Game implements Formattable, Listener { GamePlayer.spawnGamePlayer(event.player, this, gameSpawn); players.add(event.player); + + if (!running && players.size() > 1) { + BmRunStartCountDownIntent.startGame(this, AUTO_START_DELAY_SECONDS, false); + } + event.setHandled(); } @@ -420,6 +445,7 @@ public class Game implements Formattable, Listener { return; } + countingDown = true; StartTimer.createTimer(this, event.delay); event.setHandled(); } @@ -429,6 +455,7 @@ public class Game implements Formattable, Listener { if (event.game != this) { return; } + countingDown = false; running = true; removeCages(); GameProtection.protect(this, getBox()); @@ -466,6 +493,7 @@ public class Game implements Formattable, Listener { if (event.game != this) { return; } + countingDown = false; if (!running) { event.cancelFor(Text.STOP_NOT_STARTED.format(new Context(false).plus("game", this))); } @@ -476,6 +504,7 @@ public class Game implements Formattable, Listener { if (event.game != this) { return; } + countingDown = false; if (running) { running = false; BmGameBuildIntent.build(this); diff --git a/src/main/java/io/github/mviper/bomberman/game/GamePlayer.java b/src/main/java/io/github/mviper/bomberman/game/GamePlayer.java index ad21789..b3f5ab5 100644 --- a/src/main/java/io/github/mviper/bomberman/game/GamePlayer.java +++ b/src/main/java/io/github/mviper/bomberman/game/GamePlayer.java @@ -281,7 +281,7 @@ public class GamePlayer implements Listener { } } - @EventHandler(ignoreCancelled = true, priority = EventPriority.LOW) + @EventHandler(ignoreCancelled = false, priority = EventPriority.LOW) public void onPlayerBreakBlockWithWrongTool(PlayerInteractEvent event) { if (event.getPlayer() != player) { return; @@ -290,6 +290,12 @@ public class GamePlayer implements Listener { return; } + if (shouldBypassSpawnProtection(event.getClickedBlock() != null ? event.getClickedBlock().getLocation() : null)) { + event.setCancelled(false); + event.setUseInteractedBlock(Event.Result.ALLOW); + event.setUseItemInHand(Event.Result.ALLOW); + } + String key = event.getClickedBlock() != null ? event.getClickedBlock().getBlockData().getMaterial().getKey().toString() : null; @@ -310,12 +316,17 @@ public class GamePlayer implements Listener { event.getPlayer().addPotionEffect(new PotionEffect(PotionEffectType.MINING_FATIGUE, 20, 1)); } - @EventHandler(ignoreCancelled = true, priority = EventPriority.LOW) + @EventHandler(ignoreCancelled = false, priority = EventPriority.LOW) public void onPlayerPlaceBlock(BlockPlaceEvent event) { if (event.getPlayer() != player) { return; } + if (shouldBypassSpawnProtection(event.getBlockPlaced().getLocation())) { + event.setCancelled(false); + event.setBuild(true); + } + 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; @@ -327,11 +338,17 @@ public class GamePlayer implements Listener { } } - @EventHandler(ignoreCancelled = true, priority = EventPriority.HIGHEST) + @EventHandler(ignoreCancelled = false, priority = EventPriority.HIGHEST) public void onPlayerPlaceTNT(BlockPlaceEvent event) { if (event.getPlayer() != player) { return; } + + if (shouldBypassSpawnProtection(event.getBlockPlaced().getLocation())) { + event.setCancelled(false); + event.setBuild(true); + } + var block = event.getBlock(); if (block.getType() == game.getSettings().getBombItem()) { if (!Bomb.spawnBomb(game, player, block)) { @@ -340,6 +357,10 @@ public class GamePlayer implements Listener { } } + private boolean shouldBypassSpawnProtection(Location location) { + return game.isRunning() && game.contains(location); + } + @EventHandler(ignoreCancelled = true, priority = EventPriority.LOW) public void onPlayerMoved(PlayerMoveEvent event) { if (event.getPlayer() != player) { diff --git a/src/main/java/io/github/mviper/bomberman/game/GameSave.java b/src/main/java/io/github/mviper/bomberman/game/GameSave.java index 37b89bd..1b4b6d0 100644 --- a/src/main/java/io/github/mviper/bomberman/game/GameSave.java +++ b/src/main/java/io/github/mviper/bomberman/game/GameSave.java @@ -5,9 +5,12 @@ 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.Bukkit; import org.bukkit.Location; import org.bukkit.Material; +import org.bukkit.World; import org.bukkit.configuration.file.YamlConfiguration; +import org.yaml.snakeyaml.Yaml; import java.io.ByteArrayInputStream; import java.io.File; @@ -96,7 +99,11 @@ public class GameSave { try { loadGame(file); } catch (Exception e) { - PLUGIN.getLogger().log(Level.WARNING, "Exception occurred while loading: " + file, e); + if (e instanceof IOException) { + PLUGIN.getLogger().warning("Skipping save '" + file + "': " + e.getMessage()); + } else { + PLUGIN.getLogger().log(Level.WARNING, "Exception occurred while loading: " + file, e); + } } } } catch (IOException e) { @@ -105,22 +112,87 @@ public class GameSave { } 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); + Object parsed = new Yaml().load(reader); + if (!(parsed instanceof Map config)) { + throw new IOException("Cannot read 'config.yml' in '" + zipFile + "'"); + } + + String name = asString(config.get("name")); + Location origin = parseLocation(config.get("origin"), zipFile); 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); } } } + private static Location parseLocation(Object source, Path zipFile) throws IOException { + if (source instanceof Location location) { + return location; + } + if (!(source instanceof Map map)) { + return null; + } + + String worldName = asString(map.get("world")); + if (worldName == null) { + throw new IOException("Cannot read world from 'config.yml' in '" + zipFile + "'"); + } + + World world = Bukkit.getWorld(worldName); + if (world == null) { + throw new IOException("Unknown world '" + worldName + "' in '" + zipFile + "'"); + } + + Double x = asDouble(map.get("x")); + Double y = asDouble(map.get("y")); + Double z = asDouble(map.get("z")); + if (x == null || y == null || z == null) { + throw new IOException("Cannot read coordinates from 'config.yml' in '" + zipFile + "'"); + } + + Float yaw = asFloat(map.get("yaw")); + Float pitch = asFloat(map.get("pitch")); + return new Location(world, x, y, z, yaw != null ? yaw : 0.0f, pitch != null ? pitch : 0.0f); + } + + private static String asString(Object value) { + return value instanceof String text ? text : null; + } + + private static Double asDouble(Object value) { + if (value instanceof Number number) { + return number.doubleValue(); + } + if (value instanceof String text) { + try { + return Double.parseDouble(text); + } catch (NumberFormatException ignored) { + return null; + } + } + return null; + } + + private static Float asFloat(Object value) { + if (value instanceof Number number) { + return number.floatValue(); + } + if (value instanceof String text) { + try { + return Float.parseFloat(text); + } catch (NumberFormatException ignored) { + return null; + } + } + return null; + } + public static Game loadGame(Path zipFile) throws IOException { return new Game(loadSave(zipFile)); } @@ -129,8 +201,6 @@ public class GameSave { try (DirectoryStream 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); @@ -153,7 +223,6 @@ public class GameSave { settings = builder.build(); if (name == null || schema == null || origin == null) { - PLUGIN.getLogger().info(" Skipping update as file missing data"); continue; } @@ -190,14 +259,12 @@ public class GameSave { 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) { @@ -213,7 +280,6 @@ public class GameSave { 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)) { @@ -223,7 +289,6 @@ public class GameSave { settings = new GameSettingsBuilder().build(); } settingsCache = settings; - PLUGIN.getLogger().info("Data read"); return settings; } } catch (IOException e) { diff --git a/src/main/java/io/github/mviper/bomberman/game/JoinSignListener.java b/src/main/java/io/github/mviper/bomberman/game/JoinSignListener.java new file mode 100644 index 0000000..bc9eff6 --- /dev/null +++ b/src/main/java/io/github/mviper/bomberman/game/JoinSignListener.java @@ -0,0 +1,210 @@ +package io.github.mviper.bomberman.game; + +import io.github.mviper.bomberman.Bomberman; +import io.github.mviper.bomberman.commands.Permissions; +import io.github.mviper.bomberman.events.BmGameLookupIntent; +import io.github.mviper.bomberman.events.BmPlayerJoinGameIntent; +import io.github.mviper.bomberman.messaging.Context; +import io.github.mviper.bomberman.messaging.Message; +import io.github.mviper.bomberman.messaging.Text; +import org.bukkit.Bukkit; +import org.bukkit.ChatColor; +import org.bukkit.Chunk; +import org.bukkit.block.Sign; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.block.Action; +import org.bukkit.event.block.SignChangeEvent; +import org.bukkit.event.player.PlayerInteractEvent; +import org.bukkit.inventory.EquipmentSlot; + +public class JoinSignListener implements Listener { + private static final Bomberman PLUGIN = Bomberman.instance; + private static final long REFRESH_INTERVAL_TICKS = 40L; + private static final String HEADER = "[Bomberman]"; + private static final String SHORT_HEADER = "[bm]"; + private static final String JOIN_ACTION = "join"; + + public JoinSignListener() { + Bukkit.getScheduler().runTaskTimer(PLUGIN, this::refreshLoadedSigns, REFRESH_INTERVAL_TICKS, REFRESH_INTERVAL_TICKS); + } + + @EventHandler(ignoreCancelled = true, priority = EventPriority.NORMAL) + public void onSignCreate(SignChangeEvent event) { + if (!isBombermanHeader(event.getLine(0))) { + return; + } + + if (!Permissions.CONFIGURE.isAllowedBy(event.getPlayer())) { + Text.DENY_PERMISSION.format(new Context(false)).sendTo(event.getPlayer()); + event.setCancelled(true); + return; + } + + String gameName = extractGameName(event.getLine(1), event.getLine(2)); + if (gameName.isEmpty()) { + Message.error("Join-Schild ungültig. Verwende Zeile 2 oder 3 für den Spielnamen.").sendTo(event.getPlayer()); + event.setCancelled(true); + return; + } + + if (BmGameLookupIntent.find(gameName) == null) { + Message.error("Spiel '" + gameName + "' wurde nicht gefunden.").sendTo(event.getPlayer()); + event.setCancelled(true); + return; + } + + applySignFormat(event, gameName); + } + + @EventHandler(ignoreCancelled = true, priority = EventPriority.NORMAL) + public void onSignUse(PlayerInteractEvent event) { + if (event.getHand() != EquipmentSlot.HAND || event.getAction() != Action.RIGHT_CLICK_BLOCK || event.getClickedBlock() == null) { + return; + } + if (!(event.getClickedBlock().getState() instanceof Sign sign)) { + return; + } + if (!isBombermanHeader(sign.getLine(0))) { + return; + } + + event.setCancelled(true); + + Player player = event.getPlayer(); + if (!Permissions.JOIN.isAllowedBy(player)) { + Text.DENY_PERMISSION.format(new Context(false)).sendTo(player); + return; + } + + String gameName = extractGameName(sign.getLine(1), sign.getLine(2)); + if (gameName.isEmpty()) { + Message.error("Dieses Join-Schild ist ungültig.").sendTo(player); + return; + } + + Game game = BmGameLookupIntent.find(gameName); + if (game == null) { + Message.error("Spiel '" + gameName + "' wurde nicht gefunden.").sendTo(player); + return; + } + + var joinEvent = BmPlayerJoinGameIntent.join(game, player); + if (joinEvent.isCancelled()) { + if (joinEvent.cancelledReason() != null) { + joinEvent.cancelledReason().sendTo(player); + } else { + Text.COMMAND_CANCELLED.format(new Context(false) + .plus("game", game) + .plus("player", player)) + .sendTo(player); + } + return; + } + + Text.JOIN_SUCCESS.format(new Context(false) + .plus("game", game) + .plus("player", player)) + .sendTo(player); + } + + private void refreshLoadedSigns() { + for (var world : Bukkit.getWorlds()) { + for (Chunk chunk : world.getLoadedChunks()) { + for (var state : chunk.getTileEntities()) { + if (state instanceof Sign sign && isBombermanHeader(sign.getLine(0))) { + refreshSign(sign); + } + } + } + } + } + + private void refreshSign(Sign sign) { + String gameName = extractGameName(sign.getLine(1), sign.getLine(2)); + if (gameName.isEmpty()) { + return; + } + + Game game = BmGameLookupIntent.find(gameName); + if (game == null) { + updateSign(sign, + ChatColor.DARK_BLUE + HEADER, + ChatColor.GOLD + gameName, + ChatColor.DARK_RED + "Offline", + ChatColor.GRAY + "Spiel fehlt"); + return; + } + + updateSign(sign, + ChatColor.DARK_BLUE + HEADER, + ChatColor.GOLD + game.getName(), + statusLine(game), + playersLine(game)); + } + + private void applySignFormat(SignChangeEvent event, String gameName) { + Game game = BmGameLookupIntent.find(gameName); + if (game == null) { + return; + } + + event.setLine(0, ChatColor.DARK_BLUE + HEADER); + event.setLine(1, ChatColor.GOLD + game.getName()); + event.setLine(2, statusLine(game)); + event.setLine(3, playersLine(game)); + } + + private void updateSign(Sign sign, String line0, String line1, String line2, String line3) { + if (sameLine(sign.getLine(0), line0) + && sameLine(sign.getLine(1), line1) + && sameLine(sign.getLine(2), line2) + && sameLine(sign.getLine(3), line3)) { + return; + } + + sign.setLine(0, line0); + sign.setLine(1, line1); + sign.setLine(2, line2); + sign.setLine(3, line3); + sign.update(true, false); + } + + private String statusLine(Game game) { + if (game.isRunning()) { + return ChatColor.RED + "Läuft"; + } + if (game.isCountingDown()) { + return ChatColor.YELLOW + "Startet"; + } + return ChatColor.GREEN + "Wartet"; + } + + private String playersLine(Game game) { + return ChatColor.GRAY + Integer.toString(game.getPlayerCount()) + "/" + game.getMaxPlayers() + " Spieler"; + } + + private boolean sameLine(String current, String expected) { + return normalize(current).equalsIgnoreCase(normalize(expected)); + } + + private static boolean isBombermanHeader(String value) { + String normalized = normalize(value); + return HEADER.equalsIgnoreCase(normalized) || SHORT_HEADER.equalsIgnoreCase(normalized); + } + + private static String extractGameName(String secondLine, String thirdLine) { + String lineTwo = normalize(secondLine); + String lineThree = normalize(thirdLine); + if (JOIN_ACTION.equalsIgnoreCase(lineTwo)) { + return lineThree; + } + return lineTwo; + } + + private static String normalize(String value) { + return value == null ? "" : ChatColor.stripColor(value).trim(); + } +} \ No newline at end of file diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index fa9ee9a..3383dec 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -3,7 +3,7 @@ # BOMBERMAN CONFIG # # # # Refer to the Bomberman Wiki for help on configuration: # -# https://github.com/mviper/bomberman/wiki # +# https://git.viper.ipv64.net/M_Viper/Bombermann # # # # There is nothing to configure in this file. All configuration # # is done on a per-game basis. # diff --git a/src/main/resources/default_messages.yml b/src/main/resources/default_messages.yml index 1484864..3877032 100644 --- a/src/main/resources/default_messages.yml +++ b/src/main/resources/default_messages.yml @@ -6,7 +6,7 @@ # 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 # +# https://git.viper.ipv64.net/M_Viper/Bombermann # # # # This file is automatically generated and will be reset on next # # server reload. # diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index 344b810..3d82749 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -5,7 +5,7 @@ api-version: 1.21 load: POSTWORLD author: M_Viper -website: https://github.com/mviper/bomberman +website: https://git.viper.ipv64.net/M_Viper/Bombermann commands: bomberman: