├── .gitignore ├── LICENSE ├── README.md ├── pom.xml └── src └── main ├── java └── net │ └── badlion │ └── timers │ ├── TimerPlugin.java │ ├── api │ ├── Timer.java │ ├── TimerApi.java │ └── TimerApiImpl.java │ ├── impl │ ├── NmsManager.java │ └── TimerImpl.java │ └── listeners │ └── TimerListener.java └── resources └── plugin.yml /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .idea/ 3 | target/ 4 | .project 5 | .classpath -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018-2020 ESL Gaming Online, Inc 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Badlion Client Timer API 2 | 3 | This repository explains how to use Badlion Client Timer Api. 4 | 5 | It allows the server to display timers in the Badlion Client. 6 | This plugin is an API and you need to call it from your own plugins for it to work. 7 | 8 | ![Timer Mod Example](https://i.gyazo.com/76f560b0be9f9585bd0afd737cdfb084.png) 9 | 10 | ### Installation 11 | 12 | How to install the Badlion Client Timer API on your server. 13 | 14 | #### Quick Installation 15 | 16 | 1. Download the latest bukkit plugin from our releases : https://github.com/BadlionNetwork/BadlionClientTimerAPI/releases 17 | 2. Place the downloaded plugin into your `plugins` directory on your server. 18 | 3. Turn on the Bukkit server 19 | 20 | ### API Usage 21 | 22 | Below is an example plugin with the four different timers types implemented. 23 | 24 | ```java 25 | import net.badlion.timers.api.Timer; 26 | import net.badlion.timers.api.TimerApi; 27 | import org.bukkit.Material; 28 | import org.bukkit.event.EventHandler; 29 | import org.bukkit.event.Listener; 30 | import org.bukkit.event.player.PlayerJoinEvent; 31 | import org.bukkit.event.player.PlayerQuitEvent; 32 | import org.bukkit.inventory.ItemStack; 33 | import org.bukkit.plugin.java.JavaPlugin; 34 | 35 | import java.util.concurrent.TimeUnit; 36 | 37 | public class ExamplePlugin extends JavaPlugin implements Listener { 38 | 39 | private TimerApi timerApi; 40 | 41 | private Timer tickTimer; // 1 minute tick timer 42 | private Timer repeatingTickTimer; // 1 minute repeating tick timer 43 | private Timer timeTimer; // 1 minute time timer 44 | private Timer repeatingTimeTimer; // 1 minute repeating time timer 45 | 46 | @Override 47 | public void onEnable() { 48 | 49 | // Get the timer api instancce 50 | this.timerApi = TimerApi.getInstance(); 51 | 52 | // Create the timers 53 | this.tickTimer = this.timerApi.createTickTimer("Tick Timer", new ItemStack(Material.IRON_INGOT), false, 1200L); 54 | this.repeatingTickTimer = this.timerApi.createTickTimer("Repeating Tick Timer", new ItemStack(Material.GOLD_INGOT), true, 1200L); 55 | this.timeTimer = this.timerApi.createTimeTimer("Time Timer", new ItemStack(Material.DIAMOND), false, 1L, TimeUnit.MINUTES); 56 | this.repeatingTimeTimer = this.timerApi.createTimeTimer("Repeating Tick Timer", new ItemStack(Material.EMERALD), true, 1L, TimeUnit.MINUTES); 57 | 58 | // Register the listener 59 | this.getServer().getPluginManager().registerEvents(this, this); 60 | } 61 | 62 | @Override 63 | public void onDisable() { 64 | 65 | // Remove the timers 66 | this.timerApi.removeTimer(this.tickTimer); 67 | this.timerApi.removeTimer(this.repeatingTickTimer); 68 | this.timerApi.removeTimer(this.timeTimer); 69 | this.timerApi.removeTimer(this.repeatingTimeTimer); 70 | } 71 | 72 | @EventHandler 73 | public void onLogin(PlayerJoinEvent event) { 74 | 75 | // Add the player to the timers 76 | this.tickTimer.addReceiver(event.getPlayer()); 77 | this.repeatingTickTimer.addReceiver(event.getPlayer()); 78 | this.timeTimer.addReceiver(event.getPlayer()); 79 | this.repeatingTimeTimer.addReceiver(event.getPlayer()); 80 | } 81 | 82 | @EventHandler 83 | public void onLogout(PlayerQuitEvent event) { 84 | 85 | // Remove the player from the timers 86 | this.tickTimer.removeReceiver(event.getPlayer()); 87 | this.repeatingTickTimer.removeReceiver(event.getPlayer()); 88 | this.timeTimer.removeReceiver(event.getPlayer()); 89 | this.repeatingTimeTimer.removeReceiver(event.getPlayer()); 90 | } 91 | } 92 | ``` -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | net.badlion 8 | timer-api 9 | 1.2.2 10 | 11 | jar 12 | 13 | 14 | 15 | spigot-repo 16 | https://hub.spigotmc.org/nexus/content/repositories/snapshots/ 17 | 18 | 19 | 20 | 21 | 22 | org.spigotmc 23 | spigot-api 24 | 1.8.8-R0.1-SNAPSHOT 25 | provided 26 | 27 | 28 | 29 | 30 | clean install 31 | badlionclienttimerapi 32 | src/main/java 33 | 34 | 35 | . 36 | true 37 | src/main/resources/ 38 | 39 | *.yml 40 | 41 | 42 | 43 | 44 | 45 | 46 | org.apache.maven.plugins 47 | maven-compiler-plugin 48 | 3.8.0 49 | 50 | 51 | 1.7 52 | 1.7 53 | UTF-8 54 | 55 | 56 | 57 | 58 | org.apache.maven.plugins 59 | maven-resources-plugin 60 | 3.1.0 61 | 62 | 63 | UTF-8 64 | 65 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /src/main/java/net/badlion/timers/TimerPlugin.java: -------------------------------------------------------------------------------- 1 | package net.badlion.timers; 2 | 3 | import net.badlion.timers.api.TimerApiImpl; 4 | import net.badlion.timers.impl.NmsManager; 5 | import net.badlion.timers.listeners.TimerListener; 6 | import org.bukkit.plugin.java.JavaPlugin; 7 | 8 | import java.nio.charset.Charset; 9 | 10 | public class TimerPlugin extends JavaPlugin { 11 | 12 | public static final String CHANNEL_NAME = "badlion:timers"; 13 | public static final Charset UTF_8_CHARSET = Charset.forName("UTF-8"); // Do not use Guava because of 1.7 14 | 15 | private TimerApiImpl timerApi; 16 | 17 | @Override 18 | public void onEnable() { 19 | 20 | NmsManager.init(this); 21 | 22 | this.timerApi = new TimerApiImpl(this); 23 | 24 | this.getServer().getMessenger().registerOutgoingPluginChannel(this, TimerPlugin.CHANNEL_NAME); 25 | 26 | this.getServer().getPluginManager().registerEvents(new TimerListener(this), this); 27 | 28 | this.getServer().getScheduler().runTaskTimer(this, new Runnable() { 29 | @Override 30 | public void run() { 31 | TimerPlugin.this.timerApi.tickTimers(); 32 | } 33 | }, 1L, 1L); 34 | 35 | this.getServer().getScheduler().runTaskTimer(this, new Runnable() { 36 | @Override 37 | public void run() { 38 | TimerPlugin.this.timerApi.syncTimers(); 39 | } 40 | }, 60L, 60L); 41 | } 42 | 43 | @Override 44 | public void onDisable() { 45 | 46 | } 47 | 48 | public TimerApiImpl getTimerApi() { 49 | return this.timerApi; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/net/badlion/timers/api/Timer.java: -------------------------------------------------------------------------------- 1 | package net.badlion.timers.api; 2 | 3 | import org.bukkit.entity.Player; 4 | import org.bukkit.inventory.ItemStack; 5 | 6 | import java.util.Collection; 7 | import java.util.concurrent.TimeUnit; 8 | 9 | public interface Timer { 10 | 11 | // Getters and setters 12 | 13 | /** 14 | * Get the timer's id 15 | * 16 | * @return Id as unique long value 17 | */ 18 | long getId(); 19 | 20 | /** 21 | * Get the timer's name 22 | * 23 | * @return Name as string 24 | */ 25 | String getName(); 26 | 27 | /** 28 | * Set the timer's name 29 | * Will be the text displayed in the client 30 | * 31 | * @param name Name as string 32 | */ 33 | void setName(String name); 34 | 35 | /** 36 | * Get the item displayed in the client 37 | * 38 | * @return Item as a Bukkit ItemStack 39 | */ 40 | ItemStack getItem(); 41 | 42 | /** 43 | * Set the item displayed in the client 44 | * Cannot be a null value 45 | *

46 | * Note : Item metas are currently not implemented, 47 | * but enchantments should work fine 48 | * 49 | * @param item Item as a Bukkit ItemStack 50 | */ 51 | void setItem(ItemStack item); 52 | 53 | /** 54 | * Get if the timer is repeating or not 55 | * 56 | * @return Repeating value as a boolean 57 | */ 58 | boolean isRepeating(); 59 | 60 | /** 61 | * Set whether the timer should repeat when reaching 0 or not 62 | * 63 | * @param repeating {@code true} if repeating, {@code false} otherwise 64 | */ 65 | void setRepeating(boolean repeating); 66 | 67 | /** 68 | * Get the timer countdown time 69 | * 70 | * @return Timer countdown time as a long number of ticks 71 | */ 72 | long getTime(); 73 | 74 | /** 75 | * Set the timer countdown time 76 | * Note : This implies a call to {@link Timer#reset()} 77 | * 78 | * @param time Timer countdown time as a long number of ticks 79 | */ 80 | void setTime(long time); 81 | 82 | /** 83 | * Get the timer countdown time 84 | * 85 | * @return Timer countdown time in milliseconds 86 | */ 87 | long getMillis(); 88 | 89 | /** 90 | * Set the timer countdown time 91 | * Note : This implies a call to {@link Timer#reset()} 92 | * 93 | * @param time Timer countdown time 94 | * @param timeUnit Timer countdown time unit 95 | */ 96 | void setTime(long time, TimeUnit timeUnit); 97 | 98 | // Player functions 99 | 100 | /** 101 | * Add a receiver to the timer 102 | * Note : A disconnecting player will automatically 103 | * be removed from the timer 104 | * 105 | * @param player Player instance to add 106 | */ 107 | void addReceiver(Player player); 108 | 109 | /** 110 | * Manually remove a receiver form the timer 111 | * 112 | * @param player Player instance to remove 113 | */ 114 | void removeReceiver(Player player); 115 | 116 | /** 117 | * Clear all players receiving this timer 118 | */ 119 | void clearReceivers(); 120 | 121 | /** 122 | * Get all the players that are receiving this timer 123 | * 124 | * @return Collection of receivers as a thread-safe collection 125 | */ 126 | Collection getReceivers(); 127 | 128 | // Other functions 129 | 130 | /** 131 | * Reset the current countdown to the {@link Timer#getTime()} value 132 | */ 133 | void reset(); 134 | } 135 | -------------------------------------------------------------------------------- /src/main/java/net/badlion/timers/api/TimerApi.java: -------------------------------------------------------------------------------- 1 | package net.badlion.timers.api; 2 | 3 | import org.bukkit.entity.Player; 4 | import org.bukkit.inventory.ItemStack; 5 | 6 | import java.util.concurrent.TimeUnit; 7 | 8 | public abstract class TimerApi { 9 | static TimerApi instance; 10 | 11 | /** 12 | * Get the API instance. 13 | * 14 | * @return The API instance 15 | */ 16 | public static TimerApi getInstance() { 17 | return TimerApi.instance; 18 | } 19 | 20 | /** 21 | * Create a new timer and register it into the API. 22 | *

23 | * A timer will automatically handle synchronizing with its receivers, 24 | * and will repeat itself if it's mark as repeating. If not, it'll be 25 | * automatically removed from the API. 26 | * 27 | * @param item Item to show in the client 28 | * @param repeating {@code true} if the timer is repeating, {@code false} otherwise 29 | * @param time Countdown time, in ticks (20 per seconds) 30 | * @return The new timer instance 31 | */ 32 | public abstract Timer createTickTimer(ItemStack item, boolean repeating, long time); 33 | 34 | /** 35 | * Create a new timer and register it into the API. 36 | *

37 | * A timer will automatically handle synchronizing with its receivers, 38 | * and will repeat itself if it's mark as repeating. If not, it'll be 39 | * automatically removed from the API. 40 | * 41 | * @param name Name to show in the client 42 | * @param item Item to show in the client 43 | * @param repeating {@code true} if the timer is repeating, {@code false} otherwise 44 | * @param time Countdown time, in ticks (20 per seconds) 45 | * @return The new timer instance 46 | */ 47 | public abstract Timer createTickTimer(String name, ItemStack item, boolean repeating, long time); 48 | 49 | /** 50 | * Create a new timer and register it into the API. 51 | *

52 | * A timer will automatically handle synchronizing with its receivers, 53 | * and will repeat itself if it's mark as repeating. If not, it'll be 54 | * automatically removed from the API. 55 | * 56 | * @param item Item to show in the client 57 | * @param repeating {@code true} if the timer is repeating, {@code false} otherwise 58 | * @param time Countdown time 59 | * @param timeUnit Countdown time unit 60 | * @return The new timer instance 61 | */ 62 | public abstract Timer createTimeTimer(ItemStack item, boolean repeating, long time, TimeUnit timeUnit); 63 | 64 | /** 65 | * Create a new timer and register it into the API. 66 | *

67 | * A timer will automatically handle synchronizing with its receivers, 68 | * and will repeat itself if it's mark as repeating. If not, it'll be 69 | * automatically removed from the API. 70 | * 71 | * @param name Name to show in the client 72 | * @param item Item to show in the client 73 | * @param repeating {@code true} if the timer is repeating, {@code false} otherwise 74 | * @param time Countdown time 75 | * @param timeUnit Countdown time unit 76 | * @return The new timer instance 77 | */ 78 | public abstract Timer createTimeTimer(String name, ItemStack item, boolean repeating, long time, TimeUnit timeUnit); 79 | 80 | /** 81 | * Remove a timer from the API, disabling all API features about it. 82 | * 83 | * @param timer The timer instance to remove 84 | */ 85 | public abstract void removeTimer(Timer timer); 86 | 87 | /** 88 | * Clear all timers for a player. 89 | * 90 | * @param player The player instance to remove 91 | */ 92 | public abstract void clearTimers(Player player); 93 | } 94 | -------------------------------------------------------------------------------- /src/main/java/net/badlion/timers/api/TimerApiImpl.java: -------------------------------------------------------------------------------- 1 | package net.badlion.timers.api; 2 | 3 | import net.badlion.timers.TimerPlugin; 4 | import net.badlion.timers.impl.NmsManager; 5 | import net.badlion.timers.impl.TimerImpl; 6 | import org.bukkit.entity.Player; 7 | import org.bukkit.inventory.ItemStack; 8 | 9 | import java.util.Collections; 10 | import java.util.Set; 11 | import java.util.concurrent.ConcurrentHashMap; 12 | import java.util.concurrent.TimeUnit; 13 | import java.util.concurrent.atomic.AtomicInteger; 14 | 15 | public class TimerApiImpl extends TimerApi { 16 | 17 | private final TimerPlugin plugin; 18 | private final AtomicInteger idGenerator; 19 | private final Set allTimers; 20 | 21 | public TimerApiImpl(TimerPlugin plugin) { 22 | this.plugin = plugin; 23 | this.idGenerator = new AtomicInteger(1); 24 | this.allTimers = Collections.newSetFromMap(new ConcurrentHashMap()); 25 | 26 | TimerApi.instance = this; 27 | } 28 | 29 | @Override 30 | public Timer createTickTimer(ItemStack item, boolean repeating, long time) { 31 | return this.createTickTimer(null, item, repeating, time); 32 | } 33 | 34 | @Override 35 | public Timer createTickTimer(String name, ItemStack item, boolean repeating, long time) { 36 | TimerImpl timer = new TimerImpl(this.plugin, this.idGenerator.getAndIncrement(), name, item, repeating, time); 37 | 38 | this.allTimers.add(timer); 39 | 40 | return timer; 41 | } 42 | 43 | @Override 44 | public Timer createTimeTimer(ItemStack item, boolean repeating, long time, TimeUnit timeUnit) { 45 | return this.createTimeTimer(null, item, repeating, time, timeUnit); 46 | } 47 | 48 | @Override 49 | public Timer createTimeTimer(String name, ItemStack item, boolean repeating, long time, TimeUnit timeUnit) { 50 | TimerImpl timer = new TimerImpl(this.plugin, this.idGenerator.getAndIncrement(), name, item, repeating, time, timeUnit); 51 | 52 | this.allTimers.add(timer); 53 | 54 | return timer; 55 | } 56 | 57 | @Override 58 | public void removeTimer(Timer timer) { 59 | // Failsafe 60 | if (timer instanceof TimerImpl) { 61 | TimerImpl timerImpl = (TimerImpl) timer; 62 | 63 | this.allTimers.remove(timerImpl); 64 | } 65 | 66 | timer.clearReceivers(); 67 | } 68 | 69 | @Override 70 | public void clearTimers(Player player) { 71 | for (TimerImpl timer : this.allTimers) { 72 | timer.getReceivers().remove(player); 73 | } 74 | 75 | NmsManager.sendPluginMessage(player, TimerPlugin.CHANNEL_NAME, "REMOVE_ALL_TIMERS|{}".getBytes(TimerPlugin.UTF_8_CHARSET)); 76 | } 77 | 78 | public void tickTimers() { 79 | for (TimerImpl timer : this.allTimers) { 80 | timer.tick(); 81 | } 82 | } 83 | 84 | public void syncTimers() { 85 | for (TimerImpl timer : this.allTimers) { 86 | timer.syncTimer(); 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/main/java/net/badlion/timers/impl/NmsManager.java: -------------------------------------------------------------------------------- 1 | package net.badlion.timers.impl; 2 | 3 | import net.badlion.timers.TimerPlugin; 4 | import org.bukkit.entity.Player; 5 | 6 | import java.lang.reflect.Constructor; 7 | import java.lang.reflect.Field; 8 | import java.lang.reflect.Method; 9 | import java.util.Arrays; 10 | 11 | public class NmsManager { 12 | private static TimerPlugin plugin; 13 | 14 | private static String versionSuffix; 15 | 16 | private static Method getHandleMethod; 17 | 18 | private static Field playerConnectionField; 19 | 20 | private static Method sendPacketMethod; 21 | 22 | private static Constructor packetPlayOutCustomPayloadConstructor; 23 | 24 | // Bukkit 1.8+ support 25 | private static Class packetDataSerializerClass; 26 | private static Constructor packetDataSerializerConstructor; 27 | 28 | // Bukkit 1.13+ support 29 | private static Class minecraftKeyClass; 30 | private static Constructor minecraftKeyConstructor; 31 | 32 | private static Method wrappedBufferMethod; 33 | 34 | public static void init(TimerPlugin plugin) { 35 | 36 | NmsManager.plugin = plugin; 37 | 38 | // Get the v1_X_Y from the end of the package name, e.g. v_1_7_R4 or v_1_12_R1 39 | String packageName = plugin.getServer().getClass().getPackage().getName(); 40 | String[] parts = packageName.split("\\."); 41 | 42 | if (parts.length > 0) { 43 | String suffix = parts[parts.length - 1]; 44 | if (!suffix.startsWith("v")) { 45 | throw new RuntimeException("Failed to find version for running Minecraft server, got suffix " + suffix); 46 | } 47 | 48 | NmsManager.versionSuffix = suffix; 49 | 50 | plugin.getLogger().info("Found version " + NmsManager.versionSuffix); 51 | } 52 | 53 | // We need to use reflection because Bukkit by default handles plugin messages in a really silly way 54 | // Reflection stuff 55 | Class craftPlayerClass = NmsManager.getClass("org.bukkit.craftbukkit." + NmsManager.versionSuffix + ".entity.CraftPlayer"); 56 | if (craftPlayerClass == null) { 57 | throw new RuntimeException("Failed to find CraftPlayer class"); 58 | } 59 | 60 | Class nmsPlayerClass = NmsManager.getClass("net.minecraft.server." + NmsManager.versionSuffix + ".EntityPlayer"); 61 | if (nmsPlayerClass == null) { 62 | throw new RuntimeException("Failed to find EntityPlayer class"); 63 | } 64 | 65 | Class playerConnectionClass = NmsManager.getClass("net.minecraft.server." + NmsManager.versionSuffix + ".PlayerConnection"); 66 | if (playerConnectionClass == null) { 67 | throw new RuntimeException("Failed to find PlayerConnection class"); 68 | } 69 | 70 | Class packetPlayOutCustomPayloadClass = NmsManager.getClass("net.minecraft.server." + NmsManager.versionSuffix + ".PacketPlayOutCustomPayload"); 71 | if (packetPlayOutCustomPayloadClass == null) { 72 | throw new RuntimeException("Failed to find PacketPlayOutCustomPayload class"); 73 | } 74 | 75 | NmsManager.packetPlayOutCustomPayloadConstructor = NmsManager.getConstructor(packetPlayOutCustomPayloadClass, String.class, byte[].class); 76 | if (NmsManager.packetPlayOutCustomPayloadConstructor == null) { 77 | // Newer versions of Minecraft use a different custom packet system 78 | NmsManager.packetDataSerializerClass = NmsManager.getClass("net.minecraft.server." + NmsManager.versionSuffix + ".PacketDataSerializer"); 79 | if (NmsManager.packetDataSerializerClass == null) { 80 | throw new RuntimeException("Failed to find PacketPlayOutCustomPayload constructor or PacketDataSerializer class"); 81 | } 82 | 83 | // Netty classes used by newer 1.8 and newer 84 | Class byteBufClass = NmsManager.getClass("io.netty.buffer.ByteBuf"); 85 | if (byteBufClass == null) { 86 | throw new RuntimeException("Failed to find PacketPlayOutCustomPayload constructor or ByteBuf class"); 87 | } 88 | 89 | NmsManager.packetDataSerializerConstructor = NmsManager.getConstructor(NmsManager.packetDataSerializerClass, byteBufClass); 90 | if (NmsManager.packetDataSerializerConstructor == null) { 91 | throw new RuntimeException("Failed to find PacketPlayOutCustomPayload constructor or PacketDataSerializer constructor"); 92 | } 93 | 94 | Class unpooledClass = NmsManager.getClass("io.netty.buffer.Unpooled"); 95 | if (unpooledClass == null) { 96 | throw new RuntimeException("Failed to find PacketPlayOutCustomPayload constructor or Unpooled class"); 97 | } 98 | 99 | NmsManager.wrappedBufferMethod = NmsManager.getMethod(unpooledClass, "wrappedBuffer", byte[].class); 100 | if (NmsManager.wrappedBufferMethod == null) { 101 | throw new RuntimeException("Failed to find PacketPlayOutCustomPayload constructor or wrappedBuffer()"); 102 | } 103 | 104 | // If we made it this far in theory we are on at least 1.8 105 | NmsManager.packetPlayOutCustomPayloadConstructor = NmsManager.getConstructor(packetPlayOutCustomPayloadClass, String.class, NmsManager.packetDataSerializerClass); 106 | if (NmsManager.packetPlayOutCustomPayloadConstructor == null) { 107 | // Ok we are in 1.13 or higher now... 108 | NmsManager.minecraftKeyClass = NmsManager.getClass("net.minecraft.server." + NmsManager.versionSuffix + ".MinecraftKey"); 109 | if (NmsManager.minecraftKeyClass == null) { 110 | throw new RuntimeException("Failed to find PacketPlayOutCustomPayload constructor or MinecraftKey class"); 111 | } 112 | 113 | NmsManager.minecraftKeyConstructor = NmsManager.getConstructor(NmsManager.minecraftKeyClass, String.class, String.class); 114 | if (NmsManager.minecraftKeyConstructor == null) { 115 | throw new RuntimeException("Failed to find PacketPlayOutCustomPayload constructor or MinecraftKey constructor"); 116 | } 117 | 118 | // If we still can't find this...unknown version 119 | NmsManager.packetPlayOutCustomPayloadConstructor = NmsManager.getConstructor(packetPlayOutCustomPayloadClass, NmsManager.minecraftKeyClass, NmsManager.packetDataSerializerClass); 120 | if (NmsManager.packetPlayOutCustomPayloadConstructor == null) { 121 | throw new RuntimeException("Failed to find PacketPlayOutCustomPayload constructor"); 122 | } 123 | } 124 | } 125 | 126 | NmsManager.getHandleMethod = NmsManager.getMethod(craftPlayerClass, "getHandle"); 127 | if (NmsManager.getHandleMethod == null) { 128 | throw new RuntimeException("Failed to find CraftPlayer.getHandle()"); 129 | } 130 | 131 | NmsManager.playerConnectionField = NmsManager.getField(nmsPlayerClass, "playerConnection"); 132 | if (NmsManager.playerConnectionField == null) { 133 | throw new RuntimeException("Failed to find EntityPlayer.playerConnection"); 134 | } 135 | 136 | NmsManager.sendPacketMethod = NmsManager.getMethod(playerConnectionClass, "sendPacket"); 137 | if (NmsManager.sendPacketMethod == null) { 138 | throw new RuntimeException("Failed to find PlayerConnection.sendPacket()"); 139 | } 140 | } 141 | 142 | public static void sendPluginMessage(Player player, String channel, byte[] message) { 143 | try { 144 | Object packet; 145 | 146 | // 1.13+ 147 | if (NmsManager.minecraftKeyClass != null) { 148 | Object minecraftKey = NmsManager.minecraftKeyConstructor.newInstance("badlion", "timers"); 149 | Object byteBuf = NmsManager.wrappedBufferMethod.invoke(null, (Object) message); 150 | Object packetDataSerializer = NmsManager.packetDataSerializerConstructor.newInstance(byteBuf); 151 | 152 | packet = NmsManager.packetPlayOutCustomPayloadConstructor.newInstance(minecraftKey, packetDataSerializer); 153 | } else if (NmsManager.packetDataSerializerClass != null) { // 1.8+ 154 | Object byteBuf = NmsManager.wrappedBufferMethod.invoke(null, (Object) message); 155 | Object packetDataSerializer = NmsManager.packetDataSerializerConstructor.newInstance(byteBuf); 156 | 157 | packet = NmsManager.packetPlayOutCustomPayloadConstructor.newInstance(channel, packetDataSerializer); 158 | } else { // 1.7 159 | // Work our magic to make the packet 160 | packet = NmsManager.packetPlayOutCustomPayloadConstructor.newInstance(channel, message); 161 | } 162 | 163 | // Work our magic to send the packet 164 | Object nmsPlayer = NmsManager.getHandleMethod.invoke(player); 165 | Object playerConnection = NmsManager.playerConnectionField.get(nmsPlayer); 166 | NmsManager.sendPacketMethod.invoke(playerConnection, packet); 167 | } catch (Exception ex) { 168 | NmsManager.plugin.getLogger().severe("Failed to send BLC Timer packet"); 169 | ex.printStackTrace(); 170 | } 171 | } 172 | 173 | private static Class getClass(String className) { 174 | try { 175 | return Class.forName(className); 176 | } catch (ClassNotFoundException e) { 177 | return null; 178 | } 179 | } 180 | 181 | private static Constructor getConstructor(Class clazz, Class... params) { 182 | for (final Constructor constructor : clazz.getDeclaredConstructors()) { 183 | if (Arrays.equals(constructor.getParameterTypes(), params)) { 184 | constructor.setAccessible(true); 185 | return constructor; 186 | } 187 | } 188 | 189 | return null; 190 | } 191 | 192 | private static Method getMethod(Class clazz, String methodName, Class... params) { 193 | for (final Method method : clazz.getDeclaredMethods()) { 194 | if (method.getName().equals(methodName)) { 195 | if (params.length > 0) { 196 | if (Arrays.equals(method.getParameterTypes(), params)) { 197 | method.setAccessible(true); 198 | return method; 199 | } 200 | } else { 201 | method.setAccessible(true); 202 | return method; 203 | } 204 | } 205 | } 206 | 207 | return null; 208 | } 209 | 210 | private static Field getField(Class clazz, String fieldName) { 211 | for (final Field field : clazz.getDeclaredFields()) { 212 | if (field.getName().equals(fieldName)) { 213 | field.setAccessible(true); 214 | return field; 215 | } 216 | } 217 | 218 | return null; 219 | } 220 | } 221 | -------------------------------------------------------------------------------- /src/main/java/net/badlion/timers/impl/TimerImpl.java: -------------------------------------------------------------------------------- 1 | package net.badlion.timers.impl; 2 | 3 | import com.google.gson.Gson; 4 | import com.google.gson.GsonBuilder; 5 | import com.google.gson.JsonElement; 6 | import com.google.gson.JsonNull; 7 | import com.google.gson.JsonObject; 8 | import com.google.gson.JsonPrimitive; 9 | import com.google.gson.JsonSerializationContext; 10 | import com.google.gson.JsonSerializer; 11 | import net.badlion.timers.TimerPlugin; 12 | import net.badlion.timers.api.Timer; 13 | import org.bukkit.entity.Player; 14 | import org.bukkit.inventory.ItemStack; 15 | 16 | import java.lang.reflect.Type; 17 | import java.util.Collection; 18 | import java.util.Collections; 19 | import java.util.Set; 20 | import java.util.concurrent.ConcurrentHashMap; 21 | import java.util.concurrent.TimeUnit; 22 | import java.util.concurrent.atomic.AtomicBoolean; 23 | 24 | public class TimerImpl implements Timer { 25 | 26 | private static final Gson GSON = new GsonBuilder() 27 | .registerTypeAdapter(TimerImpl.class, new TimerSerializer()) 28 | .create(); 29 | 30 | private final TimerPlugin plugin; 31 | 32 | private final long id; 33 | // Optimization 34 | private final RemoveReceiverRequest removeReceiverRequest; 35 | private String name; 36 | private ItemStack item; 37 | private boolean repeating; 38 | private long time; 39 | private long millis; 40 | private AtomicBoolean updated; 41 | private Set receivers; 42 | private long currentTime; 43 | private long lastTick; 44 | 45 | public TimerImpl(TimerPlugin plugin, long id, String name, ItemStack item, boolean repeating, long time) { 46 | this.plugin = plugin; 47 | this.id = id; 48 | this.name = name; 49 | this.item = item; 50 | this.repeating = repeating; 51 | this.time = time; 52 | this.millis = -1; 53 | this.currentTime = time; 54 | this.lastTick = System.currentTimeMillis(); 55 | 56 | this.updated = new AtomicBoolean(false); 57 | this.receivers = Collections.newSetFromMap(new ConcurrentHashMap()); 58 | 59 | this.removeReceiverRequest = new RemoveReceiverRequest(); 60 | this.removeReceiverRequest.id = id; 61 | } 62 | 63 | public TimerImpl(TimerPlugin plugin, int id, String name, ItemStack item, boolean repeating, long time, TimeUnit timeUnit) { 64 | this.plugin = plugin; 65 | this.id = id; 66 | this.name = name; 67 | this.item = item; 68 | this.repeating = repeating; 69 | this.time = -1; 70 | this.millis = timeUnit.toMillis(time); 71 | this.currentTime = this.millis; 72 | this.lastTick = System.currentTimeMillis(); 73 | 74 | this.updated = new AtomicBoolean(false); 75 | this.receivers = Collections.newSetFromMap(new ConcurrentHashMap()); 76 | 77 | this.removeReceiverRequest = new RemoveReceiverRequest(); 78 | this.removeReceiverRequest.id = id; 79 | } 80 | 81 | @Override 82 | public long getId() { 83 | return this.id; 84 | } 85 | 86 | @Override 87 | public String getName() { 88 | return this.name; 89 | } 90 | 91 | @Override 92 | public void setName(String name) { 93 | String old = this.name; 94 | 95 | this.name = name; 96 | 97 | if (!old.equals(name)) { 98 | this.updated.set(true); 99 | } 100 | } 101 | 102 | @Override 103 | public ItemStack getItem() { 104 | return this.item; 105 | } 106 | 107 | @Override 108 | public void setItem(ItemStack item) { 109 | ItemStack old = this.item; 110 | 111 | this.item = item; 112 | 113 | if (old != item) { 114 | this.updated.set(true); 115 | } 116 | } 117 | 118 | @Override 119 | public boolean isRepeating() { 120 | return this.repeating; 121 | } 122 | 123 | @Override 124 | public void setRepeating(boolean repeating) { 125 | boolean old = this.repeating; 126 | 127 | this.repeating = repeating; 128 | 129 | if (old != repeating) { 130 | this.updated.set(true); 131 | } 132 | } 133 | 134 | @Override 135 | public long getTime() { 136 | return this.time; 137 | } 138 | 139 | @Override 140 | public void setTime(long time) { 141 | this.time = time; 142 | this.millis = -1L; 143 | this.updated.set(true); 144 | this.reset(); 145 | } 146 | 147 | @Override 148 | public long getMillis() { 149 | return this.millis; 150 | } 151 | 152 | @Override 153 | public void setTime(long time, TimeUnit timeUnit) { 154 | this.time = -1L; 155 | this.millis = timeUnit.toMillis(time); 156 | this.updated.set(true); 157 | this.reset(); 158 | } 159 | 160 | @Override 161 | public void addReceiver(Player player) { 162 | if (this.receivers.add(player)) { 163 | this.send(player, "ADD_TIMER", this); 164 | } 165 | } 166 | 167 | @Override 168 | public void removeReceiver(Player player) { 169 | if (this.receivers.remove(player)) { 170 | this.send(player, "REMOVE_TIMER", this.removeReceiverRequest); 171 | } 172 | } 173 | 174 | @Override 175 | public void clearReceivers() { 176 | this.send(this.receivers, "REMOVE_TIMER", this.removeReceiverRequest); 177 | 178 | this.receivers.clear(); 179 | } 180 | 181 | @Override 182 | public Collection getReceivers() { 183 | return this.receivers; 184 | } 185 | 186 | @Override 187 | public void reset() { 188 | this.currentTime = this.time != -1L ? this.time : this.millis; 189 | 190 | this.syncTimer(); 191 | } 192 | 193 | public void tick() { 194 | 195 | long currentMillis = System.currentTimeMillis(); 196 | 197 | if (this.time != -1L) { 198 | if (--this.currentTime <= 0) { 199 | if (!this.repeating) { 200 | this.plugin.getTimerApi().removeTimer(this); 201 | return; 202 | } else { 203 | this.currentTime = this.time; 204 | } 205 | } 206 | } else { 207 | long diff = currentMillis - this.lastTick; 208 | 209 | if ((this.currentTime -= diff) <= 0) { 210 | if (!this.repeating) { 211 | this.plugin.getTimerApi().removeTimer(this); 212 | return; 213 | } else { 214 | this.currentTime += this.millis; 215 | } 216 | } 217 | } 218 | 219 | this.lastTick = currentMillis; 220 | 221 | if (this.updated.compareAndSet(true, false)) { 222 | this.send(this.receivers, "UPDATE_TIMER", this); 223 | } 224 | } 225 | 226 | public void syncTimer() { 227 | SyncTimerRequest syncTimerRequest = new SyncTimerRequest(); 228 | syncTimerRequest.id = this.id; 229 | syncTimerRequest.time = this.currentTime; 230 | 231 | this.send(this.receivers, "SYNC_TIMERS", syncTimerRequest); 232 | } 233 | 234 | private void send(Player player, String requestName, T request) { 235 | NmsManager.sendPluginMessage(player, TimerPlugin.CHANNEL_NAME, (requestName + "|" + TimerImpl.GSON.toJson(request)).getBytes(TimerPlugin.UTF_8_CHARSET)); 236 | } 237 | 238 | private void send(Collection players, String requestName, T request) { 239 | byte[] data = (requestName + "|" + TimerImpl.GSON.toJson(request)).getBytes(TimerPlugin.UTF_8_CHARSET); 240 | 241 | for (Player player : players) { 242 | NmsManager.sendPluginMessage(player, TimerPlugin.CHANNEL_NAME, data); 243 | } 244 | } 245 | 246 | private static class TimerSerializer implements JsonSerializer { 247 | @Override 248 | public JsonElement serialize(TimerImpl timer, Type type, JsonSerializationContext jsonSerializationContext) { 249 | JsonObject jsonObject = new JsonObject(); 250 | 251 | jsonObject.add("id", new JsonPrimitive(timer.id)); 252 | jsonObject.add("name", timer.name == null ? JsonNull.INSTANCE : new JsonPrimitive(timer.name)); 253 | jsonObject.add("item", TimerImpl.GSON.toJsonTree(timer.item.serialize())); 254 | jsonObject.add("repeating", new JsonPrimitive(timer.repeating)); 255 | jsonObject.add("time", new JsonPrimitive(timer.time)); 256 | jsonObject.add("millis", new JsonPrimitive(timer.millis)); 257 | jsonObject.add("currentTime", new JsonPrimitive(timer.currentTime)); 258 | 259 | return jsonObject; 260 | } 261 | } 262 | 263 | private static class RemoveReceiverRequest { 264 | private long id; 265 | } 266 | 267 | private static class SyncTimerRequest { 268 | private long id; 269 | private long time; 270 | } 271 | } 272 | -------------------------------------------------------------------------------- /src/main/java/net/badlion/timers/listeners/TimerListener.java: -------------------------------------------------------------------------------- 1 | package net.badlion.timers.listeners; 2 | 3 | import net.badlion.timers.TimerPlugin; 4 | import net.badlion.timers.impl.NmsManager; 5 | import org.bukkit.event.EventHandler; 6 | import org.bukkit.event.EventPriority; 7 | import org.bukkit.event.Listener; 8 | import org.bukkit.event.player.PlayerJoinEvent; 9 | import org.bukkit.event.player.PlayerQuitEvent; 10 | import org.bukkit.event.player.PlayerRespawnEvent; 11 | import org.bukkit.event.player.PlayerTeleportEvent; 12 | 13 | public class TimerListener implements Listener { 14 | 15 | private final TimerPlugin plugin; 16 | 17 | public TimerListener(TimerPlugin plugin) { 18 | this.plugin = plugin; 19 | } 20 | 21 | @EventHandler(priority = EventPriority.LOWEST) 22 | public void onJoin(PlayerJoinEvent event) { 23 | NmsManager.sendPluginMessage(event.getPlayer(), TimerPlugin.CHANNEL_NAME, "REGISTER|{}".getBytes(TimerPlugin.UTF_8_CHARSET)); 24 | NmsManager.sendPluginMessage(event.getPlayer(), TimerPlugin.CHANNEL_NAME, "CHANGE_WORLD|{}".getBytes(TimerPlugin.UTF_8_CHARSET)); 25 | } 26 | 27 | @EventHandler 28 | public void onDisconnect(PlayerQuitEvent event) { 29 | this.plugin.getTimerApi().clearTimers(event.getPlayer()); 30 | } 31 | 32 | @EventHandler(ignoreCancelled = true, priority = EventPriority.MONITOR) 33 | public void onTeleport(PlayerTeleportEvent event) { 34 | if (!event.getFrom().getWorld().equals(event.getTo().getWorld())) { 35 | NmsManager.sendPluginMessage(event.getPlayer(), TimerPlugin.CHANNEL_NAME, "CHANGE_WORLD|{}".getBytes(TimerPlugin.UTF_8_CHARSET)); 36 | } 37 | } 38 | 39 | @EventHandler(ignoreCancelled = true, priority = EventPriority.MONITOR) 40 | public void onRespawn(PlayerRespawnEvent event) { 41 | NmsManager.sendPluginMessage(event.getPlayer(), TimerPlugin.CHANNEL_NAME, "CHANGE_WORLD|{}".getBytes(TimerPlugin.UTF_8_CHARSET)); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/resources/plugin.yml: -------------------------------------------------------------------------------- 1 | main: net.badlion.timers.TimerPlugin 2 | version: 1.2.2 3 | name: BadlionClientTimerAPI 4 | --------------------------------------------------------------------------------