Ready. Set. Go!

This commit is contained in:
apio 2023-04-07 22:39:31 +02:00
commit 8e38aac816
Signed by: apio
GPG Key ID: B8A7D06E42258954
12 changed files with 559 additions and 0 deletions

12
.gitignore vendored Normal file
View File

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

9
LICENSE Normal file
View File

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

23
README.md Normal file
View File

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

40
pom.xml Normal file
View File

@ -0,0 +1,40 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>eu.cloudapio.deathswap</groupId>
<artifactId>deathswap</artifactId>
<packaging>jar</packaging>
<version>1.0-SNAPSHOT</version>
<name>deathswap</name>
<url>http://maven.apache.org</url>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>
<repositories>
<repository>
<id>spigot-repo</id>
<url>https://hub.spigotmc.org/nexus/content/repositories/snapshots/</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>org.spigotmc</groupId>
<artifactId>spigot-api</artifactId>
<version>1.19.4-R0.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<sourceDirectory>${project.basedir}/src/main/java</sourceDirectory>
<resources>
<resource>
<directory>${project.basedir}/src/main/resources</directory>
<includes>
<include>plugin.yml</include>
</includes>
</resource>
</resources>
</build>
</project>

View File

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

View File

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

View File

@ -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<Player> 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<Player>();
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<Advancement> 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<Player>(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<Location> spawnLocations = new ArrayList<Location>(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<Location> locations = new ArrayList<Location>(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;
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -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: /<command> [overworld|nether]
permission: deathswap.dswap