Update from Git Manager GUI

This commit is contained in:
2026-03-17 11:21:25 +01:00
parent a6e674b5fb
commit a665f216ae
8 changed files with 355 additions and 25 deletions

View File

@@ -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);
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);

View File

@@ -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<Player> players = new HashSet<>();
private boolean running = false;
private boolean countingDown = false;
private final YamlConfiguration tempData;
private Box box;
private Set<Location> 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<Location> searchSpawns() {
PLUGIN.getLogger().info("Searching for spawns...");
Set<Location> 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);

View File

@@ -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) {

View File

@@ -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,31 +99,100 @@ public class GameSave {
try {
loadGame(file);
} catch (Exception 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) {
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);
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<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);
@@ -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) {

View File

@@ -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();
}
}

View File

@@ -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. #

View File

@@ -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. #

View File

@@ -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: