commit c0eb94cf6d793dbbde19c997c84ea976b0cd2e2f Author: HerozDotExe Date: Sun Feb 8 12:12:44 2026 +0100 chore: init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d5f737e --- /dev/null +++ b/.gitignore @@ -0,0 +1,119 @@ +# User-specific stuff +.idea/ + +*.iml +*.ipr +*.iws + +# IntelliJ +out/ +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Compiled class file +*.class + +# Log file +*.log + +# BlueJ files +*.ctxt + +# Package Files # +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* + +*~ + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* + +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +# Windows thumbnail cache files +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +.gradle +build/ + +# Ignore Gradle GUI config +gradle-app.setting + +# Cache of project +.gradletasknamecache + +**/build/ + +# Common working directory +run/ +runs/ + +# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) +!gradle-wrapper.jar diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..4434e6c --- /dev/null +++ b/build.gradle @@ -0,0 +1,58 @@ +plugins { + id 'java' + id("xyz.jpenilla.run-paper") version "2.3.1" +} + +group = 'dev.hrz0' +version = '1.0-SNAPSHOT' + +repositories { + mavenCentral() + maven { + name = "papermc-repo" + url = "https://repo.papermc.io/repository/maven-public/" + } +} + +dependencies { + compileOnly("io.papermc.paper:paper-api:1.21.8-R0.1-SNAPSHOT") + implementation("com.google.code.gson:gson:2.13.2") + implementation("net.kyori:adventure-api:4.26.1") + implementation("net.kyori:adventure-text-minimessage:4.26.1") +} + +tasks { + runServer { + // Configure the Minecraft version for our task. + // This is the only required configuration besides applying the plugin. + // Your plugin's jar (or shadowJar if present) will be used automatically. + minecraftVersion("1.21.8") + } +} + +def targetJavaVersion = 21 +java { + def javaVersion = JavaVersion.toVersion(targetJavaVersion) + sourceCompatibility = javaVersion + targetCompatibility = javaVersion + if (JavaVersion.current() < javaVersion) { + toolchain.languageVersion = JavaLanguageVersion.of(targetJavaVersion) + } +} + +tasks.withType(JavaCompile).configureEach { + options.encoding = 'UTF-8' + + if (targetJavaVersion >= 10 || JavaVersion.current().isJava10Compatible()) { + options.release.set(targetJavaVersion) + } +} + +processResources { + def props = [version: version] + inputs.properties props + filteringCharset 'UTF-8' + filesMatching('plugin.yml') { + expand props + } +} diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..e69de29 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..0d8ab51 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1 @@ +distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..e3eb2b8 --- /dev/null +++ b/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'smp4' diff --git a/src/main/java/dev/hrz0/smp4/ELO.java b/src/main/java/dev/hrz0/smp4/ELO.java new file mode 100644 index 0000000..015517e --- /dev/null +++ b/src/main/java/dev/hrz0/smp4/ELO.java @@ -0,0 +1,50 @@ +package dev.hrz0.smp4; + +import java.lang.Math; + +public class ELO { + // Function to calculate the Probability + public static double Probability(double rating1, double rating2) { + // Calculate and return the expected score + return 1.0 / (1 + Math.pow(10, (rating1 - rating2) / 400.0)); + } + + // Function to calculate Elo rating + // K is a constant. + // outcome determines the outcome: 1 for Player A win, 0 for Player B win, 0.5 for draw. + public static int[] EloRating(double Ra, double Rb, int K, double outcome) { + System.out.println("Original Ratings:-"); + System.out.println("Ra = " + Ra + " Rb = " + Rb); + // Calculate the Winning Probability of Player B + double Pb = Probability(Ra, Rb); + + // Calculate the Winning Probability of Player A + double Pa = Probability(Rb, Ra); + + // Update the Elo Ratings + Ra = Ra + K * (outcome - Pa); + Rb = Rb + K * ((1 - outcome) - Pb); + + // Print updated ratings + System.out.println("Updated Ratings:-"); + System.out.println("Ra = " + Ra + " Rb = " + Rb); + + return new int[] { + (int) Ra, (int) Rb + }; + } + +// public static void main(String[] args) { +// // Current ELO ratings +// double Ra = 1200, Rb = 1000; +// +// // K is a constant +// int K = 30; +// +// // Outcome: 1 for Player A win, 0 for Player B win, 0.5 for draw +// double outcome = 1; +// +// // Function call +// EloRating(Ra, Rb, K, outcome); +// } +} \ No newline at end of file diff --git a/src/main/java/dev/hrz0/smp4/LeaderboardCommand.java b/src/main/java/dev/hrz0/smp4/LeaderboardCommand.java new file mode 100644 index 0000000..501be41 --- /dev/null +++ b/src/main/java/dev/hrz0/smp4/LeaderboardCommand.java @@ -0,0 +1,49 @@ +package dev.hrz0.smp4; + +import dev.hrz0.smp4.States.States; +import io.papermc.paper.command.brigadier.BasicCommand; +import io.papermc.paper.command.brigadier.CommandSourceStack; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.minimessage.MiniMessage; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +import java.util.*; + +@NullMarked +public class LeaderboardCommand implements BasicCommand { + States states; + + public LeaderboardCommand(States states) { + this.states = states; + } + + @Override + public void execute(CommandSourceStack source, String[] args) { + MiniMessage mm = MiniMessage.miniMessage(); + + StringBuilder leaderboard = new StringBuilder("[ELO] ELO Leaderboards:\n"); + + ArrayList> players = new ArrayList<>(states.rankings.entrySet()); + + players.sort(Collections.reverseOrder(Map.Entry.comparingByValue())); + + int index = 0; + for (Map.Entry player : players) { + String key = player.getKey(); + Object value = player.getValue(); + index++; + + leaderboard.append(String.format("%s. %s (%s) \n", index, states.usernameCache.getPlayerUsername(UUID.fromString(key)), value)); + } + + Component msg = mm.deserialize(leaderboard.toString()); + + source.getSender().sendMessage(msg); + } + + @Override + public @Nullable String permission() { + return "smp4.rankings"; + } +} \ No newline at end of file diff --git a/src/main/java/dev/hrz0/smp4/Listeners/DeathListener.java b/src/main/java/dev/hrz0/smp4/Listeners/DeathListener.java new file mode 100644 index 0000000..b8927c3 --- /dev/null +++ b/src/main/java/dev/hrz0/smp4/Listeners/DeathListener.java @@ -0,0 +1,62 @@ +package dev.hrz0.smp4.Listeners; + +import dev.hrz0.smp4.ELO; +import dev.hrz0.smp4.States.States; +import dev.hrz0.smp4.Utilities; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.minimessage.MiniMessage; +import org.bukkit.entity.EntityType; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.entity.PlayerDeathEvent; + +import java.util.logging.Level; +import java.util.logging.Logger; + +public class DeathListener implements Listener { + private final States states; + private final Logger logger; + + public DeathListener(States states, Logger logger) { + this.states = states; + this.logger = logger; + } + + /** + * Modify the player death message if they are holding the egg when they die + */ + @EventHandler(priority = EventPriority.HIGHEST) + public void onPlayerDeath(PlayerDeathEvent event) { + Player killed = event.getEntity().getPlayer(); + Player killer = event.getEntity().getKiller(); + + if (killed != null && killer != null && killer.getType().equals(EntityType.PLAYER)) { + logger.log(Level.INFO, String.format("%s killed %s", killer, killed)); + + int killedRanking = states.rankings.getPlayerRanking(killed); + int killerRanking = states.rankings.getPlayerRanking(killer); + int newKilledRanking = states.rankings.getPlayerRanking(killed); + int newKillerRanking = states.rankings.getPlayerRanking(killer); + + int[] results = ELO.EloRating(newKilledRanking, newKillerRanking, 200, 0); + + states.rankings.setPlayerRanking(killed, results[0]); + states.rankings.setPlayerRanking(killer, results[1]); + + MiniMessage mm = MiniMessage.miniMessage(); + + Component msg = mm.deserialize(String.format(""" + [ELO] Updated rankings: + %s's ranking increased by %s (%s->%s) + %s's ranking decreased by %s (%s->%s)""", + killer.getName(), results[1] - killerRanking, killerRanking, results[1], killed.getName(), killedRanking - results[0], killedRanking, results[0])); + + Utilities.setScore(killed, results[0]); + Utilities.setScore(killer, results[1]); + + Utilities.announce(msg); + } + } +} diff --git a/src/main/java/dev/hrz0/smp4/Listeners/JoinListener.java b/src/main/java/dev/hrz0/smp4/Listeners/JoinListener.java new file mode 100644 index 0000000..a9586e1 --- /dev/null +++ b/src/main/java/dev/hrz0/smp4/Listeners/JoinListener.java @@ -0,0 +1,42 @@ +package dev.hrz0.smp4.Listeners; + +import dev.hrz0.smp4.States.States; +import dev.hrz0.smp4.Utilities; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerJoinEvent; + +import java.util.logging.Logger; + + +public class JoinListener implements Listener { + private final States states; + private final Logger logger; + + public JoinListener(States states, Logger logger) { + this.states = states; + this.logger = logger; + } + + /** + * Modify the player death message if they are holding the egg when they die + */ + @EventHandler(priority = EventPriority.HIGHEST) + public void onPlayerJoin(PlayerJoinEvent event) { + Player player = event.getPlayer(); + + Integer rank = states.rankings.getPlayerRanking(player); + + if (rank == null) { + this.states.rankings.setPlayerRanking(player, 1000); + rank = 1000; + } else { + logger.info(String.format("%s has a ranking of %s", player.getName(), rank)); + } + + Utilities.setScore(player, rank); + states.usernameCache.cachePlayerUsername(player.getUniqueId(), player.getName()); + } +} diff --git a/src/main/java/dev/hrz0/smp4/Listeners/WorldSaveListener.java b/src/main/java/dev/hrz0/smp4/Listeners/WorldSaveListener.java new file mode 100644 index 0000000..f87bd48 --- /dev/null +++ b/src/main/java/dev/hrz0/smp4/Listeners/WorldSaveListener.java @@ -0,0 +1,25 @@ +package dev.hrz0.smp4.Listeners; + +import dev.hrz0.smp4.States.States; +import dev.hrz0.smp4.States.StatesParser; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.world.WorldSaveEvent; +import org.bukkit.plugin.java.JavaPlugin; + +import java.io.IOException; + +public class WorldSaveListener implements Listener { + States states; + JavaPlugin plugin; + + public WorldSaveListener(JavaPlugin plugin, States states) { + this.states = states; + this.plugin = plugin; + } + + @EventHandler + public void onAutoSave(WorldSaveEvent event) throws IOException { + StatesParser.save(StatesParser.getStatesPath(plugin), states); + } +} diff --git a/src/main/java/dev/hrz0/smp4/SetRankingCommand.java b/src/main/java/dev/hrz0/smp4/SetRankingCommand.java new file mode 100644 index 0000000..981aafa --- /dev/null +++ b/src/main/java/dev/hrz0/smp4/SetRankingCommand.java @@ -0,0 +1,41 @@ +package dev.hrz0.smp4; + +import dev.hrz0.smp4.States.States; +import io.papermc.paper.command.brigadier.BasicCommand; +import io.papermc.paper.command.brigadier.CommandSourceStack; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +@NullMarked +public class SetRankingCommand implements BasicCommand { + States states; + + public SetRankingCommand(States states) { + this.states = states; + } + + @Override + public void execute(CommandSourceStack source, String[] args) { + if (args.length < 2) { + source.getSender().sendRichMessage("Not enough arguments!"); + return; + } + + Player target = Bukkit.getPlayer(args[0]); + int ranking = Integer.parseInt(args[1]); + + if (target != null) { + states.rankings.setPlayerRanking(target, ranking); + source.getSender().sendRichMessage(" Ranking changed!"); + } else { + source.getSender().sendRichMessage("Player not found!"); + } + } + + @Override + public @Nullable String permission() { + return "smp4.setranking"; + } +} \ No newline at end of file diff --git a/src/main/java/dev/hrz0/smp4/Smp4.java b/src/main/java/dev/hrz0/smp4/Smp4.java new file mode 100644 index 0000000..d27a81d --- /dev/null +++ b/src/main/java/dev/hrz0/smp4/Smp4.java @@ -0,0 +1,61 @@ +package dev.hrz0.smp4; + +import dev.hrz0.smp4.Listeners.DeathListener; +import dev.hrz0.smp4.Listeners.JoinListener; +import dev.hrz0.smp4.Listeners.WorldSaveListener; +import dev.hrz0.smp4.States.States; +import dev.hrz0.smp4.States.StatesParser; +import org.bukkit.plugin.PluginManager; +import org.bukkit.plugin.java.JavaPlugin; + +import java.io.IOException; +import java.util.logging.Logger; + +public final class Smp4 extends JavaPlugin { + Logger logger; + States states; + + @Override + public void onEnable() { + // Plugin startup logic + logger = getLogger(); + PluginManager manager = getServer().getPluginManager(); + + logger.info("Initializing SMP4 Plugin..."); + + try { + states = States.init(this, logger); + } catch (IOException e) { + throw new RuntimeException(e); + } + + WorldSaveListener worldSaveListener = new WorldSaveListener(this, states); + manager.registerEvents(worldSaveListener, this); + DeathListener deathListener = new DeathListener(states, logger); + manager.registerEvents(deathListener, this); + JoinListener joinListener = new JoinListener(states, logger); + manager.registerEvents(joinListener, this); + + registerCommand( + "setranking", + new SetRankingCommand(states) + ); + + registerCommand( + "rankings", + new LeaderboardCommand(states) + ); + + Utilities.initScore(); + } + + @Override + public void onDisable() { + // Plugin shutdown logic + try { + StatesParser.save(StatesParser.getStatesPath(this), states); + } catch (IOException e) { + throw new RuntimeException(e); + } + } +} diff --git a/src/main/java/dev/hrz0/smp4/States/Rankings.java b/src/main/java/dev/hrz0/smp4/States/Rankings.java new file mode 100644 index 0000000..d714b00 --- /dev/null +++ b/src/main/java/dev/hrz0/smp4/States/Rankings.java @@ -0,0 +1,20 @@ +package dev.hrz0.smp4.States; + +import org.bukkit.entity.Player; + +import java.util.HashMap; + +public class Rankings extends HashMap { + public Integer getPlayerRanking(Player player) { + if (this.containsKey(player.getUniqueId().toString())) { + return get(player.getUniqueId().toString()); + } else { + put(player.getUniqueId().toString(), 1000); + return 1000; + } + } + + public void setPlayerRanking(Player player, int ranking) { + put(player.getUniqueId().toString(), ranking); + } +} diff --git a/src/main/java/dev/hrz0/smp4/States/States.java b/src/main/java/dev/hrz0/smp4/States/States.java new file mode 100644 index 0000000..6259d32 --- /dev/null +++ b/src/main/java/dev/hrz0/smp4/States/States.java @@ -0,0 +1,40 @@ +package dev.hrz0.smp4.States; + +import org.bukkit.plugin.java.JavaPlugin; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class States { + + public Rankings rankings; + public UsernameCache usernameCache; + + public States(JavaPlugin plugin, Logger logger) throws IOException { + Path statesPath = StatesParser.getStatesPath(plugin); + + this.rankings = new Rankings(); + this.usernameCache = new UsernameCache(); + + logger.log(Level.INFO, "Saving empty states"); + StatesParser.save(statesPath, this); + } + + public static States init(JavaPlugin plugin, Logger logger) throws IOException { + Path statesPath = StatesParser.getStatesPath(plugin); + + if (!Files.exists(statesPath)) { + if (!Files.exists(statesPath.getParent())) { + Files.createDirectory(statesPath.getParent()); + } + + return new States(plugin, logger); + } else { + logger.log(Level.INFO, "Loading states from disk"); + return StatesParser.deserialize(StatesParser.load(statesPath)); + } + } +} diff --git a/src/main/java/dev/hrz0/smp4/States/StatesParser.java b/src/main/java/dev/hrz0/smp4/States/StatesParser.java new file mode 100644 index 0000000..97a9921 --- /dev/null +++ b/src/main/java/dev/hrz0/smp4/States/StatesParser.java @@ -0,0 +1,33 @@ +package dev.hrz0.smp4.States; + +import com.google.gson.Gson; +import org.bukkit.plugin.java.JavaPlugin; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +public class StatesParser { + private static final Gson gson = new Gson(); + + public static String serialize(Object data) { + return gson.toJson(data); + } + + public static States deserialize(String src) { + return gson.fromJson(src, States.class); + } + + public static Path getStatesPath(JavaPlugin plugin) { + return Paths.get(plugin.getDataPath().toString(), "states.yml"); + } + + public static void save(Path path, Object data) throws IOException { + Files.writeString(path, serialize(data)); + } + + public static String load(Path path) throws IOException { + return Files.readString(path); + } +} \ No newline at end of file diff --git a/src/main/java/dev/hrz0/smp4/States/UsernameCache.java b/src/main/java/dev/hrz0/smp4/States/UsernameCache.java new file mode 100644 index 0000000..5b6dbea --- /dev/null +++ b/src/main/java/dev/hrz0/smp4/States/UsernameCache.java @@ -0,0 +1,14 @@ +package dev.hrz0.smp4.States; + +import java.util.HashMap; +import java.util.UUID; + +public class UsernameCache extends HashMap { + public String getPlayerUsername(UUID uuid) { + return get(uuid.toString()); + } + + public void cachePlayerUsername(UUID uuid, String username) { + put(uuid.toString(), username); + } +} diff --git a/src/main/java/dev/hrz0/smp4/Utilities.java b/src/main/java/dev/hrz0/smp4/Utilities.java new file mode 100644 index 0000000..c98091c --- /dev/null +++ b/src/main/java/dev/hrz0/smp4/Utilities.java @@ -0,0 +1,37 @@ +package dev.hrz0.smp4; + +import net.kyori.adventure.text.Component; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import org.bukkit.scoreboard.*; + +import java.util.ArrayList; +import java.util.List; + +public class Utilities { + static ScoreboardManager sm = Bukkit.getScoreboardManager(); + static Scoreboard s = sm.getMainScoreboard(); + static Objective elo = s.getObjective("elo"); + + public static void announce(Component message) { + List players = new ArrayList<>(Bukkit.getOnlinePlayers()); + + for (Player player : players) { + player.sendMessage(message); + } + } + + public static void initScore() { + if (elo == null) { + s.registerNewObjective("elo", Criteria.DUMMY, Component.text("ELO"), RenderType.INTEGER); + elo = s.getObjective("elo"); + } + + elo.setDisplaySlot(DisplaySlot.BELOW_NAME); + } + + public static void setScore(Player player, int value) { + Score score = elo.getScore(player); + score.setScore(value); + } +} diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml new file mode 100644 index 0000000..09cf849 --- /dev/null +++ b/src/main/resources/plugin.yml @@ -0,0 +1,29 @@ +name: smp4 +version: '1.0-SNAPSHOT' +main: dev.hrz0.smp4.Smp4 +api-version: '1.21' + +commands: + setranking: + description: Set the ranking of a player to a another value + usage: / + permission: smp4.setranking + permission-message: You don't have the permission to do this. + rankings: + description: Show rankings of players + usage: / + permission: smp4.rankings + permission-message: You don't have the permission to do this. + +permissions: + smp4.*: + children: + smp4.setranking: true + smp4.rankings: true + default: false + + smp4.setranking: + default: true + + smp4.rankings: + default: true \ No newline at end of file