From 8e38aac816a1ecbd94956436259342ce41f39d1d Mon Sep 17 00:00:00 2001 From: apio Date: Fri, 7 Apr 2023 22:39:31 +0200 Subject: [PATCH] Ready. Set. Go! --- .gitignore | 12 + LICENSE | 9 + README.md | 23 ++ pom.xml | 40 +++ .../eu/cloudapio/deathswap/DeathSwap.java | 33 ++ .../cloudapio/deathswap/DeathSwapCommand.java | 30 ++ .../eu/cloudapio/deathswap/DeathSwapGame.java | 314 ++++++++++++++++++ .../eu/cloudapio/deathswap/EventListener.java | 22 ++ .../eu/cloudapio/deathswap/SwapNotifier.java | 15 + .../eu/cloudapio/deathswap/SwapRunner.java | 21 ++ .../cloudapio/deathswap/SwapTimeNotifier.java | 31 ++ src/main/resources/plugin.yml | 9 + 12 files changed, 559 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 pom.xml create mode 100644 src/main/java/eu/cloudapio/deathswap/DeathSwap.java create mode 100644 src/main/java/eu/cloudapio/deathswap/DeathSwapCommand.java create mode 100644 src/main/java/eu/cloudapio/deathswap/DeathSwapGame.java create mode 100644 src/main/java/eu/cloudapio/deathswap/EventListener.java create mode 100644 src/main/java/eu/cloudapio/deathswap/SwapNotifier.java create mode 100644 src/main/java/eu/cloudapio/deathswap/SwapRunner.java create mode 100644 src/main/java/eu/cloudapio/deathswap/SwapTimeNotifier.java create mode 100644 src/main/resources/plugin.yml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..85d60a8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +target/ +pom.xml.tag +pom.xml.releaseBackup +pom.xml.versionsBackup +pom.xml.next +release.properties +dependency-reduced-pom.xml +buildNumber.properties +.mvn/timing.properties +.mvn/wrapper/maven-wrapper.jar +.project +.classpath \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..5620b08 --- /dev/null +++ b/LICENSE @@ -0,0 +1,9 @@ +Copyright (c) 2023, apio + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..576c6bf --- /dev/null +++ b/README.md @@ -0,0 +1,23 @@ +# death-swap + +DeathSwap plugin for 1.19 Minecraft Java servers (Spigot/Paper) + +## WARNING + +This plugin is not configurable and makes several assumptions (as it was coded for a specific use case). These assumptions are: + +- That the server the plugin is running on serves as a lobby for the game, and nothing else. This is because when starting a DeathSwap game all players on the server are automatically added to the game. This plugin is intended for multi-world networks (using BungeeCord for example) where one server would be the DeathSwap server and other servers would have other minigames/worlds/etc. + +- That the default gamemode on said lobby is Adventure Mode. The plugin puts all players in Adventure Mode when returning them to the lobby. + +## Usage + +Build the plugin with Maven or download a .jar from the Releases section, and copy it to your server's plugins/ folder. Then, to start a game (2+ players required) run the command `/dswap overworld` (or `/dswap nether` to play in the Nether). This can be automated further using clickable signs/NPCs. + +Every 30 seconds - 2:30 minutes, a swap will occur. There is a small chance that a swap will be canceled (around 1/6). A timer is displayed above the hotbar showing how much time has passed since the last swap. + +Whenever a player dies/leaves the game, they will be transported back to the lobby. When only one player is remaining, that player will win and the game will end. + +## License + +Free and open-source software under the [BSD-2-Clause](LICENSE) license. \ No newline at end of file diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..2d3a209 --- /dev/null +++ b/pom.xml @@ -0,0 +1,40 @@ + + 4.0.0 + eu.cloudapio.deathswap + deathswap + jar + 1.0-SNAPSHOT + deathswap + http://maven.apache.org + + UTF-8 + 1.8 + 1.8 + + + + spigot-repo + https://hub.spigotmc.org/nexus/content/repositories/snapshots/ + + + + + org.spigotmc + spigot-api + 1.19.4-R0.1-SNAPSHOT + provided + + + + ${project.basedir}/src/main/java + + + ${project.basedir}/src/main/resources + + plugin.yml + + + + + diff --git a/src/main/java/eu/cloudapio/deathswap/DeathSwap.java b/src/main/java/eu/cloudapio/deathswap/DeathSwap.java new file mode 100644 index 0000000..b0d8638 --- /dev/null +++ b/src/main/java/eu/cloudapio/deathswap/DeathSwap.java @@ -0,0 +1,33 @@ +package eu.cloudapio.deathswap; + +import org.bukkit.plugin.Plugin; +import org.bukkit.plugin.java.JavaPlugin; + +public class DeathSwap extends JavaPlugin { + public static DeathSwapGame game; + + public static JavaPlugin plugin; + + public static SwapNotifier notifier; + public static SwapRunner runner; + public static SwapTimeNotifier timeNotifier; + + @Override + public void onEnable() { + plugin = (JavaPlugin)this; + + game = new DeathSwapGame(); + + notifier = new SwapNotifier(); + timeNotifier = new SwapTimeNotifier(); + runner = new SwapRunner(); + + this.getCommand("dswap").setExecutor(new DeathSwapCommand()); + + getServer().getPluginManager().registerEvents(new EventListener(), (Plugin)this); + } + + @Override + public void onDisable() { + } +} diff --git a/src/main/java/eu/cloudapio/deathswap/DeathSwapCommand.java b/src/main/java/eu/cloudapio/deathswap/DeathSwapCommand.java new file mode 100644 index 0000000..40e190b --- /dev/null +++ b/src/main/java/eu/cloudapio/deathswap/DeathSwapCommand.java @@ -0,0 +1,30 @@ +package eu.cloudapio.deathswap; + +import org.bukkit.ChatColor; +import org.bukkit.command.*; +import org.bukkit.entity.*; + +public class DeathSwapCommand implements CommandExecutor { + @Override + public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { + if(args.length != 1) + { + return false; + } + + if (sender instanceof Player) { + Player player = (Player) sender; + + int status = DeathSwap.game.start(args[0].equals("nether")); + if(status == -1) + { + player.sendMessage(ChatColor.RED + "Not enough players to start a game."); + } else if(status == -2) + { + player.sendMessage(ChatColor.RED + "A DeathSwap game is already in progress!"); + } + } + + return true; + } +} diff --git a/src/main/java/eu/cloudapio/deathswap/DeathSwapGame.java b/src/main/java/eu/cloudapio/deathswap/DeathSwapGame.java new file mode 100644 index 0000000..8b5f6ce --- /dev/null +++ b/src/main/java/eu/cloudapio/deathswap/DeathSwapGame.java @@ -0,0 +1,314 @@ +package eu.cloudapio.deathswap; + +import java.io.File; +import java.util.*; +import java.util.logging.Level; + +import org.bukkit.Bukkit; +import org.bukkit.ChatColor; +import org.bukkit.Difficulty; +import org.bukkit.GameMode; +import org.bukkit.GameRule; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.World; +import org.bukkit.WorldCreator; +import org.bukkit.World.Environment; +import org.bukkit.advancement.Advancement; +import org.bukkit.advancement.AdvancementProgress; +import org.bukkit.entity.Player; + +public class DeathSwapGame { + ArrayList players; + WorldCreator wc; + World world; + Random randomizer; + String worldName; + + boolean isPlayingGame = false; + + int minDeathSwapPlayers = 2; + int minSwapTime = 30; + int maxSwapTime = 150; + double swapCancelProbability = 0.15; + + long lastSwapTime; + + public DeathSwapGame() + { + randomizer = new Random(System.currentTimeMillis()); + players = new ArrayList(); + worldName = "death-swap"; + wc = new WorldCreator(worldName); + + getMainWorld().setPVP(false); + } + + private World getMainWorld() + { + return Bukkit.getServer().getWorlds().get(0); + } + + private boolean isUnspawnableNetherLocation(World world, Location loc) + { + if(world.getBlockAt(loc).getType() == Material.AIR) return true; + if(world.getBlockAt(loc).getType() == Material.BEDROCK) return true; + + if(world.getBlockAt(loc.add(0, 1, 0)).getType() != Material.AIR) return true; + if(world.getBlockAt(loc.add(0, 2, 0)).getType() != Material.AIR) return true; + + return false; + } + + // Selects a spawn location not in the air, not in water and not on top of bedrock (nether roof). + private Location randomSpawnLocation(World world, boolean isNether) + { + while(true) + { + int x = randomizer.nextInt(-1000000, 1000000); + int z = randomizer.nextInt(-1000000, 1000000); + + if(Math.abs(x) < 500) continue; + if(Math.abs(z) < 500) continue; + + int y = 0; + + if(isNether) { + y = 95; + while(isUnspawnableNetherLocation(world, new Location(world, x, y, z))) + { + y--; + if(y == 0) break; + } + if(y == 0) continue; + } + else y = world.getHighestBlockYAt(x, z); + + if(world.getBlockAt(x, y, z).getType() == Material.WATER) continue; + if(world.getBlockAt(x, y, z).getType() == Material.LAVA) continue; + + return new Location(world, x, y, z); + } + + } + + private void revokeAdvancements(Player p) + { + Iterator iterator = Bukkit.getServer().advancementIterator(); + while (iterator.hasNext()) + { + AdvancementProgress progress = p.getAdvancementProgress(iterator.next()); + for (String criteria : progress.getAwardedCriteria()) + progress.revokeCriteria(criteria); + } + } + + private void resetPlayer(Player p) + { + p.getInventory().clear(); + p.setHealth(20); + p.setFoodLevel(20); + p.setFireTicks(0); + p.setTotalExperience(0); + p.setFreezeTicks(0); + p.setFallDistance(0); + } + + public int start(boolean isNether) + { + if(Bukkit.getServer().getOnlinePlayers().size() < minDeathSwapPlayers) + { + return -1; + } + + if(isPlayingGame) + { + return -2; + } + + players = new ArrayList(Bukkit.getServer().getOnlinePlayers()); + + Bukkit.broadcastMessage(ChatColor.YELLOW + "Starting DeathSwap game..."); + + isPlayingGame = true; + + Bukkit.getServer().unloadWorld(world, false); + + deleteWorld(worldName); + + wc.copy(Bukkit.getServer().getWorlds().get(0)); + wc.seed(randomizer.nextLong()); + if(isNether) + { + wc.environment(Environment.NETHER); + } + world = wc.createWorld(); + world.setKeepSpawnInMemory(false); + world.setDifficulty(Difficulty.NORMAL); + world.setPVP(false); + world.setGameRule(GameRule.DO_IMMEDIATE_RESPAWN, true); + + ArrayList spawnLocations = new ArrayList(players.size()); + for(int i = 0; i < players.size(); i++) + { + spawnLocations.add(randomSpawnLocation(world, isNether)); + } + + for(int i = 0; i < players.size(); i++) + { + Player p = players.get(i); + revokeAdvancements(p); + resetPlayer(p); + p.setGameMode(GameMode.SURVIVAL); + p.teleport(spawnLocations.get(i)); + } + + int swapTime = getSwapTime(); + int tickTime = swapTime * 20; + Bukkit.getLogger().log(Level.INFO, String.format("Next swap in %d seconds", swapTime)); + Bukkit.getServer().getScheduler().runTaskLater(DeathSwap.plugin, DeathSwap.notifier, tickTime - 60); + Bukkit.getServer().getScheduler().runTaskLater(DeathSwap.plugin, DeathSwap.runner, tickTime); + Bukkit.getServer().getScheduler().runTaskLater(DeathSwap.plugin, DeathSwap.timeNotifier, 20); + + lastSwapTime = world.getFullTime(); + + return 0; + } + + public void teleportToWorldSpawn(Player p) + { + Location worldSpawn = getMainWorld().getSpawnLocation(); + + p.setGameMode(GameMode.ADVENTURE); + resetPlayer(p); + p.teleport(worldSpawn); + } + + private boolean hasPlayer(Player player) + { + for(Player p : players) + { + if(p.getUniqueId().equals(player.getUniqueId())) + { + return true; + } + } + + return false; + } + + private void checkGameEnd() + { + if(players.size() < minDeathSwapPlayers) + { + if(players.size() == 0) + { + Bukkit.broadcastMessage(ChatColor.translateAlternateColorCodes('&', "&eNo one won &f(&cis this a bug?&f)")); + } + else { + Player winner = players.get(0); + Bukkit.broadcastMessage(ChatColor.translateAlternateColorCodes('&', + String.format("&3%s &6won the game!", winner.getName()))); + } + + Bukkit.getScheduler().cancelTasks(DeathSwap.plugin); + + isPlayingGame = false; + + for(Player p : players) + { + teleportToWorldSpawn(p); + } + } + } + + public void playerDied(Player player) + { + if(!isPlayingGame) return; + + if(hasPlayer(player)) + { + players.remove(player); + player.setGameMode(GameMode.ADVENTURE); + + Bukkit.broadcastMessage(ChatColor.translateAlternateColorCodes('&', + String.format("&3%s &chas died. &9%d players &fremaining!", player.getName(), players.size()))); + + checkGameEnd(); + } + } + + public void playerLeft(Player player) + { + if(!isPlayingGame) return; + + if(hasPlayer(player)) + { + players.remove(player); + player.setGameMode(GameMode.ADVENTURE); + teleportToWorldSpawn(player); + + Bukkit.broadcastMessage(ChatColor.translateAlternateColorCodes('&', + String.format("&3%s &cleft. &9%d players &fremaining!", player.getName(), players.size()))); + + checkGameEnd(); + } + } + + private void deleteWorld(String worldName) + { + File container = Bukkit.getServer().getWorldContainer(); + + for(File f : container.listFiles()) + { + if(f.getName().equals(worldName)) + { + deleteFolder(f); + } + } + } + + private void deleteFolder(File folder) + { + if(folder.isDirectory()) + { + for(File f : folder.listFiles()) + { + deleteFolder(f); + } + } + folder.delete(); + } + + public void swap() + { + lastSwapTime = world.getFullTime(); + + if(randomizer.nextDouble() < swapCancelProbability) { + Bukkit.broadcastMessage(ChatColor.GOLD + "Swap cancelled!"); + return; + } + + ArrayList locations = new ArrayList(players.size() + 1); + for(Player p : players) + { + locations.add(p.getLocation()); + } + locations.add(locations.get(0)); + + for(int i = 0; i < players.size(); i++) + { + players.get(i).teleport(locations.get(i+1)); + } + } + + public int getSwapTime() + { + return randomizer.nextInt(minSwapTime, maxSwapTime); + } + + public long getTicksSinceLastSwap() + { + return world.getFullTime() - lastSwapTime; + } +} diff --git a/src/main/java/eu/cloudapio/deathswap/EventListener.java b/src/main/java/eu/cloudapio/deathswap/EventListener.java new file mode 100644 index 0000000..41e84ef --- /dev/null +++ b/src/main/java/eu/cloudapio/deathswap/EventListener.java @@ -0,0 +1,22 @@ +package eu.cloudapio.deathswap; + +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.entity.PlayerDeathEvent; +import org.bukkit.event.player.PlayerQuitEvent; + +public class EventListener implements Listener { + @EventHandler public void onPlayerDeath(PlayerDeathEvent event) + { + if(event.getEntity() instanceof Player) + { + DeathSwap.game.playerDied((Player)event.getEntity()); + } + } + + @EventHandler public void onPlayerLeave(PlayerQuitEvent event) + { + DeathSwap.game.playerLeft((Player)event.getPlayer()); + } +} diff --git a/src/main/java/eu/cloudapio/deathswap/SwapNotifier.java b/src/main/java/eu/cloudapio/deathswap/SwapNotifier.java new file mode 100644 index 0000000..2dd13e1 --- /dev/null +++ b/src/main/java/eu/cloudapio/deathswap/SwapNotifier.java @@ -0,0 +1,15 @@ +package eu.cloudapio.deathswap; + +import org.bukkit.Bukkit; +import org.bukkit.ChatColor; + +public class SwapNotifier implements Runnable { + + @Override + public void run() { + if(DeathSwap.game.isPlayingGame){ + Bukkit.broadcastMessage(ChatColor.GOLD + "Swapping in 3 seconds."); + } + } + +} diff --git a/src/main/java/eu/cloudapio/deathswap/SwapRunner.java b/src/main/java/eu/cloudapio/deathswap/SwapRunner.java new file mode 100644 index 0000000..3799d5f --- /dev/null +++ b/src/main/java/eu/cloudapio/deathswap/SwapRunner.java @@ -0,0 +1,21 @@ +package eu.cloudapio.deathswap; + +import java.util.logging.Level; + +import org.bukkit.Bukkit; + +public class SwapRunner implements Runnable { + + @Override + public void run() { + if(DeathSwap.game.isPlayingGame){ + DeathSwap.game.swap(); + + int swapTime = DeathSwap.game.getSwapTime(); + int tickTime = swapTime * 20; + Bukkit.getLogger().log(Level.INFO, String.format("Next swap in %d seconds", swapTime)); + Bukkit.getServer().getScheduler().runTaskLater(DeathSwap.plugin, DeathSwap.notifier, tickTime - 60); + Bukkit.getServer().getScheduler().runTaskLater(DeathSwap.plugin, DeathSwap.runner, tickTime); + } + } +} diff --git a/src/main/java/eu/cloudapio/deathswap/SwapTimeNotifier.java b/src/main/java/eu/cloudapio/deathswap/SwapTimeNotifier.java new file mode 100644 index 0000000..0a4309a --- /dev/null +++ b/src/main/java/eu/cloudapio/deathswap/SwapTimeNotifier.java @@ -0,0 +1,31 @@ +package eu.cloudapio.deathswap; + +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; + +import net.md_5.bungee.api.ChatColor; +import net.md_5.bungee.api.ChatMessageType; +import net.md_5.bungee.api.chat.TextComponent; + +public class SwapTimeNotifier implements Runnable { + + @Override + public void run() + { + if(DeathSwap.game.isPlayingGame) { + long secondsSinceLastSwap = DeathSwap.game.getTicksSinceLastSwap() / 20; + boolean safe = secondsSinceLastSwap < DeathSwap.game.minSwapTime; + + TextComponent tc = new TextComponent(String.format("[%s] Time since last swap: %d s", safe ? "safe" : "unsafe", secondsSinceLastSwap)); + tc.setColor(safe ? ChatColor.GREEN : ChatColor.RED); + + for(Player p : DeathSwap.game.players) + { + p.spigot().sendMessage(ChatMessageType.ACTION_BAR, tc); + } + } + + Bukkit.getServer().getScheduler().runTaskLater(DeathSwap.plugin, DeathSwap.timeNotifier, 20); + } + +} diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml new file mode 100644 index 0000000..8986498 --- /dev/null +++ b/src/main/resources/plugin.yml @@ -0,0 +1,9 @@ +main: eu.cloudapio.deathswap.DeathSwap +name: death-swap +version: 0.1 +api-version: 1.19 +commands: + dswap: + description: Death Swap + usage: / [overworld|nether] + permission: deathswap.dswap \ No newline at end of file