diff --git a/.idea/artifacts/MCTPAudio_jar.xml b/.idea/artifacts/MCTPAudio_jar.xml index e6e74de..5b631ee 100644 --- a/.idea/artifacts/MCTPAudio_jar.xml +++ b/.idea/artifacts/MCTPAudio_jar.xml @@ -4,6 +4,7 @@ + \ No newline at end of file diff --git a/.idea/uiDesigner.xml b/.idea/uiDesigner.xml new file mode 100644 index 0000000..e96534f --- /dev/null +++ b/.idea/uiDesigner.xml @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/MCTPAudio.iml b/MCTPAudio.iml index bb5f66f..6c4466f 100644 --- a/MCTPAudio.iml +++ b/MCTPAudio.iml @@ -16,20 +16,9 @@ - - - - - - - - - - - - - + + @@ -38,7 +27,7 @@ - + @@ -47,7 +36,7 @@ - + @@ -56,5 +45,14 @@ + + + + + + + + + \ No newline at end of file diff --git a/src/me/mctp/Main.java b/src/me/mctp/Main.java index 0edbcf3..22abd86 100644 --- a/src/me/mctp/Main.java +++ b/src/me/mctp/Main.java @@ -1,8 +1,11 @@ package me.mctp; +import me.mctp.commands.MCTPAudioCMD; +import me.mctp.commands.MCTPShowCMD; import me.mctp.listener.LogoutListener; import me.mctp.listener.WGListener; import me.mctp.managers.WGManager; +import me.mctp.radio.Playlist; import me.mctp.socket.Client; import org.bukkit.Bukkit; import org.bukkit.ChatColor; @@ -14,6 +17,7 @@ public class Main extends JavaPlugin implements Listener { private static Plugin pl; private static Client client; + private static Playlist playlist; public static String prefix = (ChatColor.GOLD + "[" + ChatColor.YELLOW + "MCTP" + ChatColor.GOLD + "] " + ChatColor.GRAY); @@ -29,6 +33,7 @@ public class Main extends JavaPlugin implements Listener { } getConfig().addDefault("Regions.demosound", "https://audiopagina.mcthemeparks.eu/gallery/voletarium.mp3"); + getConfig().addDefault("HueRegions.demosound", "255_0_0_254"); getConfig().options().copyDefaults(true); saveConfig(); @@ -37,12 +42,15 @@ public class Main extends JavaPlugin implements Listener { client = new Client("ws://144.91.100.169:30217"); client.connect(); - getCommand("audio").setExecutor(new MCTPAudioCmd()); - getCommand("mctpaudio").setExecutor(new MCTPAudioCmd()); + getCommand("audio").setExecutor(new MCTPAudioCMD()); + getCommand("mctpaudio").setExecutor(new MCTPAudioCMD()); + getCommand("mctpshow").setExecutor(new MCTPShowCMD()); Bukkit.getPluginManager().registerEvents(new WGListener(), this); Bukkit.getPluginManager().registerEvents(new LogoutListener(), this); + playlist = new Playlist(); + Bukkit.getLogger().info(prefix + " __ __ ____ _____ ____ _ _ _ "); Bukkit.getLogger().info(prefix + " | \\/ |/ ___|_ _| _ \\ / \\ _ _ __| (_) ___ "); Bukkit.getLogger().info(prefix + " | |\\/| | | | | | |_) / _ \\| | | |/ _` | |/ _ \\ "); @@ -75,4 +83,8 @@ public class Main extends JavaPlugin implements Listener { public static Client getClient() { return client; } + + public static Playlist getPlaylist() { + return playlist; + } } diff --git a/src/me/mctp/api/maps/Playlist.java b/src/me/mctp/api/maps/Playlist.java new file mode 100644 index 0000000..c4524e3 --- /dev/null +++ b/src/me/mctp/api/maps/Playlist.java @@ -0,0 +1,28 @@ +package me.mctp.api.maps; + +import java.util.*; + +public class Playlist extends ArrayList { + public void shuffle() { + Random random = new Random(); + + for (int index = 0; index < this.size(); index++) { + int secondIndex = random.nextInt(this.size()); + swap(index, secondIndex); + } + } + + public E getRandom() { + Random random = new Random(); + + return this.get(random.nextInt(this.size())); + } + + private void swap(int firstIndex, int secondIndex) { + E first = this.get(firstIndex); + E second = this.get(secondIndex); + + this.set(secondIndex, first); + this.set(firstIndex, second); + } +} \ No newline at end of file diff --git a/src/me/mctp/MCTPAudioCmd.java b/src/me/mctp/commands/MCTPAudioCMD.java similarity index 81% rename from src/me/mctp/MCTPAudioCmd.java rename to src/me/mctp/commands/MCTPAudioCMD.java index e06e969..a6758ba 100644 --- a/src/me/mctp/MCTPAudioCmd.java +++ b/src/me/mctp/commands/MCTPAudioCMD.java @@ -1,8 +1,10 @@ -package me.mctp; +package me.mctp.commands; +import me.mctp.Main; import me.mctp.api.AudioType; import me.mctp.api.HueType; import me.mctp.managers.PinManager; +import me.mctp.utils.HeadUtil; import me.mctp.utils.SpigotPlayerSelector; import net.md_5.bungee.api.chat.ClickEvent; import net.md_5.bungee.api.chat.TextComponent; @@ -15,11 +17,13 @@ import org.bukkit.entity.Player; import org.json.simple.JSONObject; import java.util.ArrayList; +import java.util.List; -public class MCTPAudioCmd implements CommandExecutor { +public class MCTPAudioCMD implements CommandExecutor { public static String prefix = (ChatColor.GOLD + "[" + ChatColor.YELLOW + "MCTP" + ChatColor.GOLD + "] " + ChatColor.GRAY); + @Override public boolean onCommand(CommandSender sender, Command cmd, String commandlabel, String[] args) { if (cmd.getName().equalsIgnoreCase("mctpaudio")) { if (!sender.hasPermission("mctp.audio")) { @@ -27,9 +31,24 @@ public class MCTPAudioCmd implements CommandExecutor { return true; } - // /mctpaudio play SELECTOR TYPE URL - // /mctpaudio stop SELECTOR TYPE - if (args.length == 4 && args[0].equalsIgnoreCase("play")) { + if (args.length == 1 && args[0].equalsIgnoreCase("toggleradio")) { + if (Main.getPlaylist().isRunning()) { + Main.getPlaylist().stop(); + sender.sendMessage(prefix + "De auto radio is stopgezet. Zodra het huidige nummer is afgelopen, gebeurt er niks meer."); + } else { + Main.getPlaylist().init(); + sender.sendMessage(prefix + "De auto radio is weer gestart."); + } + return true; + } else if (args.length == 2 && args[0].equalsIgnoreCase("addsong")) { + List urls = Main.getPlugin().getConfig().getStringList("RadioSongs"); + urls.add(args[1]); + Main.getPlugin().getConfig().set("RadioSongs", urls); + Main.getPlugin().saveConfig(); + Main.getPlaylist().addSong(args[1]); + sender.sendMessage(prefix + "Nummer toegevoegd aan de lijst."); + return true; + } else if (args.length == 4 && args[0].equalsIgnoreCase("play")) { AudioType type; try { type = AudioType.valueOf(args[2].toUpperCase()); @@ -116,6 +135,16 @@ public class MCTPAudioCmd implements CommandExecutor { players.addAll(selector.getPlayers(sender)); } + //CHECK FOR THE REGION SELECTOR -> Then save + if (args[1].startsWith("@a") && HeadUtil.getArgument(args[1], "region").length() != 0) { + String regionID = HeadUtil.getArgument(args[1], "region"); + String data = r + "_" + g + "_" + b + "_" + type.name(); + if (brightness != null) data += "_" + brightness; + + Main.getPlugin().getConfig().set("HueRegions." + regionID, data); + Main.getPlugin().saveConfig(); + } + JSONObject data; for (Player p : players) { data = new JSONObject(); diff --git a/src/me/mctp/commands/MCTPShowCMD.java b/src/me/mctp/commands/MCTPShowCMD.java new file mode 100644 index 0000000..fcb8f06 --- /dev/null +++ b/src/me/mctp/commands/MCTPShowCMD.java @@ -0,0 +1,81 @@ +package me.mctp.commands; + +import me.mctp.Main; +import me.mctp.utils.Laser; +import org.bukkit.ChatColor; +import org.bukkit.Sound; +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; +import org.bukkit.scheduler.BukkitRunnable; + +import java.util.HashMap; +import java.util.Map; + +public class MCTPShowCMD implements CommandExecutor { + public static String prefix = (ChatColor.GOLD + "[" + ChatColor.YELLOW + "MCTP" + ChatColor.GOLD + "] " + ChatColor.GRAY); + + private Map lasers =new HashMap<>(); + + @Override + public boolean onCommand(CommandSender sender, Command cmd, String commandlabel, String[] args) { + if (cmd.getName().equalsIgnoreCase("mctpshow")) { + if (!sender.hasPermission("mctp.show")) { + sender.sendMessage(prefix + "You don't have the permission to do this."); + return true; + } + + if (args.length == 1 && args[0].equalsIgnoreCase("demo")) { + Player p = (Player) sender; + + if (lasers.containsKey(p)){ + lasers.get(p).cancel(); + }else { + try{ + lasers.put(p, new LaserRunnable(p)); + lasers.get(p).runTaskTimer(Main.getPlugin(), 5, 1); + }catch (ReflectiveOperationException e){ + e.printStackTrace(); + } + } + } + } + return true; + } + + public class LaserRunnable extends BukkitRunnable { + public static final byte LOADING_TIME = 30; + public static final byte RANGE = 10; + + private final Laser laser; + private final Player p; + + public byte loading = 0; + + public LaserRunnable(Player p) throws ReflectiveOperationException{ + this.p = p; + this.laser = new Laser(p.getLocation(), p.getLocation().add(0, 1, 0), -1, 50); + this.laser.start(Main.getPlugin()); + } + + public void run(){ + if (loading != LOADING_TIME){ + loading++; + p.getWorld().playSound(p.getLocation(), Sound.BLOCK_NOTE_BLOCK_PLING, 0.7f, loading == LOADING_TIME ? 1.5f : 0.2f); + } + try{ + laser.moveStart(p.getLocation().add(0, 0.8, 0)); + laser.moveEnd(p.getLocation().add(0, 1.2, 0).add(p.getLocation().getDirection().multiply(loading == LOADING_TIME ? RANGE : loading / (LOADING_TIME / RANGE * 1.3)))); + }catch (ReflectiveOperationException e){ + e.printStackTrace(); + } + } + + public synchronized void cancel() throws IllegalStateException{ + laser.stop(); + lasers.remove(p); + super.cancel(); + } + } +} diff --git a/src/me/mctp/listener/WGListener.java b/src/me/mctp/listener/WGListener.java index 6460cf7..6d128ef 100644 --- a/src/me/mctp/listener/WGListener.java +++ b/src/me/mctp/listener/WGListener.java @@ -1,69 +1,105 @@ package me.mctp.listener; +import com.sk89q.worldguard.protection.regions.ProtectedRegion; import me.mctp.Main; import me.mctp.managers.PinManager; +import me.mctp.managers.WGManager; import net.raidstone.wgevents.events.RegionsEnteredEvent; -import net.raidstone.wgevents.events.RegionsLeftEvent; import org.bukkit.event.EventHandler; import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerMoveEvent; import org.json.simple.JSONObject; -import java.util.Collections; -import java.util.Objects; -import java.util.Optional; -import java.util.Set; +import java.util.*; +import java.util.stream.Collectors; public class WGListener implements Listener { + /** Music detection */ @EventHandler - public void onRegionEnter(RegionsEnteredEvent e) { - Set list = Main.getPlugin().getConfig().getConfigurationSection("Regions").getKeys(false); - Set list2 = Main.getPlugin().getConfig().getConfigurationSection("HueRegions").getKeys(false); + public void onMove(PlayerMoveEvent e) { + if (e.getTo() == null || e.getFrom().getWorld() == null || e.getTo().getWorld() == null) return; - if (!Collections.disjoint(list, e.getRegionsNames())) { - //One element is the same -> In a region - - Optional name = e.getRegionsNames().stream().filter(list::contains).findFirst(); - if (!name.isPresent()) return; - String regionURL = Main.getPlugin().getConfig().getString("Regions." + name.get()); - - JSONObject data = new JSONObject(); + if (e.getFrom().getBlockX() != e.getTo().getBlockX() || e.getFrom().getBlockY() != e.getTo().getBlockY() || e.getFrom().getBlockZ() != e.getTo().getBlockZ()) { + Set list = Main.getPlugin().getConfig().getConfigurationSection("Regions").getKeys(false); if (!PinManager.hasPin(Objects.requireNonNull(e.getPlayer()).getUniqueId())) return; - data.put("task", "MUSIC"); - data.put("path", regionURL); - data.put("uuid", e.getPlayer().getUniqueId().toString().replace("-", "")); - Main.getClient().sendData(data); - } + List regions = WGManager.getRegionsIn(e.getFrom()); + List regionNames = regions.stream().map(ProtectedRegion::getId).collect(Collectors.toList()); + List regions2 = WGManager.getRegionsIn(e.getTo()); + List regionNames2 = regions2.stream().map(ProtectedRegion::getId).collect(Collectors.toList()); - if (!Collections.disjoint(list2, e.getRegionsNames())) { - //One element is the same -> In a region + if ((Collections.disjoint(list, regionNames) && !Collections.disjoint(list, regionNames2)) || (!Collections.disjoint(list, regionNames) && !Collections.disjoint(list, regionNames2))) { + //Walked in a region - //TODO Add hue support + if (!Collections.disjoint(list, regionNames) && !Collections.disjoint(list, regionNames2)) { + Optional name = regionNames.stream().filter(list::contains).findFirst(); + Optional name2 = regionNames2.stream().filter(list::contains).findFirst(); + if (name.isPresent() && name2.isPresent() && name.get().equals(name2.get())) { + return; + } + + if (name.isPresent() && name2.isPresent()) { + String regionURL = Main.getPlugin().getConfig().getString("Regions." + name.get()); + String regionURL2 = Main.getPlugin().getConfig().getString("Regions." + name2.get()); + + if (regionURL.equals(regionURL2)) { + return; + } + } + } + + Optional name = regionNames2.stream().filter(list::contains).findFirst(); + if (!name.isPresent()) return; + String regionURL = Main.getPlugin().getConfig().getString("Regions." + name.get()); + + JSONObject data = new JSONObject(); + data.put("task", "MUSIC"); + data.put("path", regionURL); + data.put("uuid", e.getPlayer().getUniqueId().toString().replace("-", "")); + Main.getClient().sendData(data); + } else if (!Collections.disjoint(list, regionNames) && Collections.disjoint(list, regionNames2)) { + //Not in a region, stop... + JSONObject data = new JSONObject(); + data.put("task", "MUSIC"); + data.put("path", ""); + data.put("uuid", e.getPlayer().getUniqueId().toString().replace("-", "")); + Main.getClient().sendData(data); + } } } + /** Hue detection */ @EventHandler - public void onRegionExit(RegionsLeftEvent e) { - Set list = Main.getPlugin().getConfig().getConfigurationSection("Regions").getKeys(false); + public void onRegionEnter(RegionsEnteredEvent e) { Set list2 = Main.getPlugin().getConfig().getConfigurationSection("HueRegions").getKeys(false); - if (!Collections.disjoint(list, e.getRegionsNames())) { - //One element is the same -> In a region - JSONObject data = new JSONObject(); - - if (!PinManager.hasPin(Objects.requireNonNull(e.getPlayer()).getUniqueId())) return; - - data.put("task", "MUSIC"); - data.put("path", ""); - data.put("uuid", e.getPlayer().getUniqueId().toString().replace("-", "")); - Main.getClient().sendData(data); - } - if (!Collections.disjoint(list2, e.getRegionsNames())) { //One element is the same -> In a region - //TODO Add hue support + Optional name = e.getRegionsNames().stream().filter(list2::contains).findFirst(); + if (!name.isPresent()) return; + String configData = Main.getPlugin().getConfig().getString("HueRegions." + name.get()); + + if (configData == null) return; + String[] configDataSplit = configData.split("_"); + + int r = Integer.parseInt(configDataSplit[0]); + int g = Integer.parseInt(configDataSplit[1]); + int b = Integer.parseInt(configDataSplit[2]); + String type = configDataSplit[3]; + Integer brightness = null; + if (configDataSplit.length == 5) brightness = Integer.parseInt(configDataSplit[4]); + + if (!PinManager.hasPin(Objects.requireNonNull(e.getPlayer()).getUniqueId())) return; + + JSONObject data = new JSONObject(); + data.put("task", "HUE"); + data.put("rgb", r + ":" + g + ":" + b); + data.put("type", type); + if (brightness != null) data.put("brightness", brightness); + data.put("uuid", e.getPlayer().getUniqueId().toString().replace("-", "")); + Main.getClient().sendData(data); } } } diff --git a/src/me/mctp/radio/Playlist.java b/src/me/mctp/radio/Playlist.java new file mode 100644 index 0000000..8a36597 --- /dev/null +++ b/src/me/mctp/radio/Playlist.java @@ -0,0 +1,114 @@ +package me.mctp.radio; + +import com.mpatric.mp3agic.InvalidDataException; +import com.mpatric.mp3agic.UnsupportedTagException; +import me.mctp.Main; +import me.mctp.managers.PinManager; +import me.mctp.utils.HeadUtil; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import org.bukkit.scheduler.BukkitTask; +import org.json.simple.JSONObject; + +import java.io.IOException; + +public class Playlist { + private me.mctp.api.maps.Playlist songList = new me.mctp.api.maps.Playlist<>(); + private boolean running = false; + private BukkitTask currentTimer; + + /** + * Init, load all the songs from the data and start first + */ + public Playlist() { + init(); + } + + /** + * Load the songs into the system and start + */ + public void init() { + for (String URL : Main.getPlugin().getConfig().getStringList("RadioSongs")) { + addSong(URL); + } + + Bukkit.getScheduler().runTaskAsynchronously(Main.getPlugin(), this::nextSong); + + running = true; + } + + /** + * Stop the playlist (clears the queue) + */ + public void stop() { + songList.clear(); + + if (currentTimer != null) { + currentTimer.cancel(); + currentTimer = null; + } + + running = false; + } + + /** + * Add a song by the url + * @param url The song url (mp3) + */ + public void addSong(String url) { + songList.add(url); + songList.shuffle(); + } + + /** + * Go to the next song + */ + public void nextSong() { + if (currentTimer != null) return; + + //Get song + String nextURL = songList.getRandom(); + if (nextURL == null) return; + + JSONObject data; + for (Player p : Bukkit.getOnlinePlayers()) { + data = new JSONObject(); + + if (!PinManager.hasPin(p.getUniqueId())) continue; + + data.put("task", "RADIO"); + data.put("path", nextURL); + data.put("uuid", p.getUniqueId().toString().replace("-", "")); + Main.getClient().sendData(data); + } + + int ticks; + try { + ticks = HeadUtil.getTicksOfFile(nextURL); + } catch (IOException | InvalidDataException | UnsupportedTagException e) { + e.printStackTrace(); + nextSong(); + return; + } + + if (ticks == 0) { + Bukkit.getLogger().info("0 ticks"); + nextSong(); + return; + } + + Bukkit.getLogger().info("Started song with duration: " + ticks/20 + " sec."); + + currentTimer = Bukkit.getScheduler().runTaskLaterAsynchronously(Main.getPlugin(), () -> { + currentTimer = null; + nextSong(); + }, ticks); + + //Shuffle playlist again + songList.shuffle(); + } + + public boolean isRunning() { + return running; + } +} diff --git a/src/me/mctp/socket/Client.java b/src/me/mctp/socket/Client.java index 656d3e7..b2f3189 100644 --- a/src/me/mctp/socket/Client.java +++ b/src/me/mctp/socket/Client.java @@ -1,6 +1,6 @@ package me.mctp.socket; -import me.mctp.MCTPAudioCmd; +import me.mctp.commands.MCTPAudioCMD; import me.mctp.Main; import me.mctp.managers.PinManager; import org.bukkit.Bukkit; @@ -73,7 +73,7 @@ public class Client { Player p = Bukkit.getPlayer(pUUID); if (p != null && p.isOnline()) { - p.sendMessage(MCTPAudioCmd.prefix + "You are now connected with the audioclient."); + p.sendMessage(MCTPAudioCMD.prefix + "You are now connected with the audioclient."); } JSONObject reply = new JSONObject(); @@ -88,7 +88,7 @@ public class Client { UUID pUUID = JSONUtil.formatFromInput(uuid); Player p = Bukkit.getPlayer(pUUID); if (p != null && p.isOnline()) { - p.sendMessage(MCTPAudioCmd.prefix + "You are now disconnected from the audioclient."); + p.sendMessage(MCTPAudioCMD.prefix + "You are now disconnected from the audioclient."); } } } diff --git a/src/me/mctp/utils/HeadUtil.java b/src/me/mctp/utils/HeadUtil.java new file mode 100644 index 0000000..ff0eac0 --- /dev/null +++ b/src/me/mctp/utils/HeadUtil.java @@ -0,0 +1,84 @@ +package me.mctp.utils; + +import com.mpatric.mp3agic.InvalidDataException; +import com.mpatric.mp3agic.Mp3File; +import com.mpatric.mp3agic.UnsupportedTagException; +import org.bukkit.craftbukkit.libs.org.apache.commons.io.FilenameUtils; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.net.URLConnection; + +public class HeadUtil { + public static String getArgument(String selector, String key) { + StringBuilder result = new StringBuilder(); + String[] arguments = selector.split(key + "="); + if (arguments.length == 1) return ""; + for (byte type : arguments[1].getBytes()) { + char element = (char) type; + if (element == ',' || element == ']') { + return result.toString(); + } else { + result.append(element); + } + } + + return result.toString().replaceAll(".", ""); + } + + private static String downloadFromUrl(URL url, String localFilename) throws IOException { + InputStream is = null; + FileOutputStream fos = null; + + String tempDir = System.getProperty("java.io.tmpdir"); + String outputPath = tempDir + "/" + localFilename; + + try { + //connect + URLConnection urlConn = url.openConnection(); + + //get inputstream from connection + is = urlConn.getInputStream(); + fos = new FileOutputStream(outputPath); + + // 4KB buffer + byte[] buffer = new byte[4096]; + int length; + + // read from source and write into local file + while ((length = is.read(buffer)) > 0) { + fos.write(buffer, 0, length); + } + return outputPath; + } finally { + try { + if (is != null) { + is.close(); + } + } finally { + if (fos != null) { + fos.close(); + } + } + } + } + + public static int getTicksOfFile(String input) throws IOException, InvalidDataException, UnsupportedTagException { + URL url = new URL(input); + + String result = downloadFromUrl(url, FilenameUtils.getName(url.getPath())); + File file = new File(result); + + if (!file.exists()) return 0; + + Mp3File mp3File = new Mp3File(file); + long sec = mp3File.getLengthInSeconds(); + + file.delete(); + + return (int) (sec * 20); + } +} diff --git a/src/me/mctp/utils/Laser.java b/src/me/mctp/utils/Laser.java new file mode 100644 index 0000000..75e4f1f --- /dev/null +++ b/src/me/mctp/utils/Laser.java @@ -0,0 +1,376 @@ +package me.mctp.utils; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.Collection; +import java.util.HashSet; +import java.util.UUID; + +import org.apache.commons.lang.Validate; +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.entity.Player; +import org.bukkit.plugin.Plugin; +import org.bukkit.scheduler.BukkitRunnable; + +/** + * A whole class to create Guardian Beams by reflection
+ * Inspired by the API GuardianBeamAPI
+ * 1.9 -> 1.15 + * + * @see GitHub page + * @author SkytAsul + */ +public class Laser { + private final int duration; + private final int distanceSquared; + private Location start; + private Location end; + + private final Object createGuardianPacket; + private final Object createSquidPacket; + private final Object teamAddPacket; + private final Object destroyPacket; + private final Object metadataPacketGuardian; + private final Object metadataPacketSquid; + private final Object fakeGuardianDataWatcher; + + private final int squid; + private final UUID squidUUID; + private final int guardian; + private final UUID guardianUUID; + + private BukkitRunnable run; + private HashSet show = new HashSet<>(); + + /** + * Create a Laser instance + * @param start Location where laser will starts + * @param end Location where laser will ends + * @param duration Duration of laser in seconds (-1 if infinite) + * @param distance Distance where laser will be visible + */ + public Laser(Location start, Location end, int duration, int distance) throws ReflectiveOperationException { + this.start = start; + this.end = end; + this.duration = duration; + distanceSquared = distance * distance; + + createSquidPacket = Packets.createPacketSquidSpawn(end); + squid = (int) Packets.getField(Packets.packetSpawn, "a", createSquidPacket); + squidUUID = (UUID) Packets.getField(Packets.packetSpawn, "b", createSquidPacket); + metadataPacketSquid = Packets.createPacketMetadata(squid, Packets.fakeSquidWatcher); + Packets.setDirtyWatcher(Packets.fakeSquidWatcher); + + fakeGuardianDataWatcher = Packets.createFakeDataWatcher(); + createGuardianPacket = Packets.createPacketGuardianSpawn(start, fakeGuardianDataWatcher, squid); + guardian = (int) Packets.getField(Packets.packetSpawn, "a", createGuardianPacket); + guardianUUID = (UUID) Packets.getField(Packets.packetSpawn, "b", createGuardianPacket); + metadataPacketGuardian = Packets.createPacketMetadata(guardian, fakeGuardianDataWatcher); + + teamAddPacket = Packets.createPacketTeamAddEntities(squidUUID, guardianUUID); + destroyPacket = Packets.createPacketRemoveEntities(squid, guardian); + } + + public void start(Plugin plugin) { + Validate.isTrue(run == null, "Task already started"); + run = new BukkitRunnable() { + int time = duration; + + @Override + public void run() { + try { + if (time == 0) { + cancel(); + return; + } + for (Player p : start.getWorld().getPlayers()) { + if (isCloseEnough(p.getLocation())) { + if (!show.contains(p)) { + sendStartPackets(p); + show.add(p); + } + }else if (show.contains(p)) { + Packets.sendPacket(p, destroyPacket); + show.remove(p); + } + } + if (time != -1) time--; + }catch (ReflectiveOperationException e) { + e.printStackTrace(); + } + } + + @Override + public synchronized void cancel() throws IllegalStateException { + super.cancel(); + try { + for (Player p : show) { + Packets.sendPacket(p, destroyPacket); + } + }catch (ReflectiveOperationException e) { + e.printStackTrace(); + } + run = null; + } + }; + run.runTaskTimerAsynchronously(plugin, 0L, 20L); + } + + public void stop() { + Validate.isTrue(run != null, "Task not started"); + run.cancel(); + } + + public void moveStart(Location location) throws ReflectiveOperationException { + this.start = location; + Object packet = Packets.createPacketMoveEntity(start, guardian); + for (Player p : show) { + Packets.sendPacket(p, packet); + } + } + + public Location getStart() { + return start; + } + + public void moveEnd(Location location) throws ReflectiveOperationException { + this.end = location; + Object packet = Packets.createPacketMoveEntity(end, squid); + for (Player p : show) { + Packets.sendPacket(p, packet); + } + } + + public Location getEnd() { + return end; + } + + public void callColorChange() throws ReflectiveOperationException{ + for (Player p : show) { + Packets.sendPacket(p, metadataPacketGuardian); + } + } + + public boolean isStarted() { + return run != null; + } + + private void sendStartPackets(Player p) throws ReflectiveOperationException { + Packets.sendPacket(p, createSquidPacket); + Packets.sendPacket(p, createGuardianPacket); + if (Packets.version > 14) { + Packets.sendPacket(p, metadataPacketSquid); + Packets.sendPacket(p, metadataPacketGuardian); + } + Packets.sendPacket(p, Packets.packetTeamCreate); + Packets.sendPacket(p, teamAddPacket); + } + + private boolean isCloseEnough(Location location) { + return start.distanceSquared(location) <= distanceSquared || + end.distanceSquared(location) <= distanceSquared; + } + + + + private static class Packets { + private static int lastIssuedEID = 2000000000; + + static int generateEID() { + return lastIssuedEID++; + } + + private static int version = Integer.parseInt(Bukkit.getServer().getClass().getPackage().getName().replace(".", ",").split(",")[3].substring(1).split("_")[1]); + private static String npack = "net.minecraft.server." + Bukkit.getServer().getClass().getPackage().getName().replace(".", ",").split(",")[3] + "."; + private static String cpack = Bukkit.getServer().getClass().getPackage().getName() + "."; + private static Object packetTeamCreate; + private static Constructor watcherConstructor; + private static Method watcherSet; + private static Method watcherRegister; + private static Method watcherDirty; + private static Class packetSpawn; + private static Class packetRemove; + private static Class packetTeleport; + private static Class packetTeam; + private static Class packetMetadata; + private static Object watcherObject1; // invisilibity + private static Object watcherObject2; // spikes + private static Object watcherObject3; // attack id + private static int squidID; + private static int guardianID; + + private static Object fakeSquid; + private static Object fakeSquidWatcher; + + static { + try { + String watcherName1 = null, watcherName2 = null, watcherName3 = null; + if (version < 13) { + watcherName1 = "Z"; + watcherName2 = "bA"; + watcherName3 = "bB"; + squidID = 94; + guardianID = 68; + }else if (version == 13) { + watcherName1 = "ac"; + watcherName2 = "bF"; + watcherName3 = "bG"; + squidID = 70; + guardianID = 28; + }else if (version == 14) { + watcherName1 = "W"; + watcherName2 = "b"; + watcherName3 = "bD"; + squidID = 73; + guardianID = 30; + }else if (version > 14) { + watcherName1 = "T"; + watcherName2 = "b"; + watcherName3 = "bA"; + squidID = 74; + guardianID = 31; + } + watcherObject1 = getField(Class.forName(npack + "Entity"), watcherName1, null); + watcherObject2 = getField(Class.forName(npack + "EntityGuardian"), watcherName2, null); + watcherObject3 = getField(Class.forName(npack + "EntityGuardian"), watcherName3, null); + + watcherConstructor = Class.forName(npack + "DataWatcher").getDeclaredConstructor(Class.forName(npack + "Entity")); + watcherSet = getMethod(Class.forName(npack + "DataWatcher"), "set"); + watcherRegister = getMethod(Class.forName(npack + "DataWatcher"), "register"); + if (version >= 15) watcherDirty = getMethod(Class.forName(npack + "DataWatcher"), "markDirty"); + packetSpawn = Class.forName(npack + "PacketPlayOutSpawnEntityLiving"); + packetRemove = Class.forName(npack + "PacketPlayOutEntityDestroy"); + packetTeleport = Class.forName(npack + "PacketPlayOutEntityTeleport"); + packetTeam = Class.forName(npack + "PacketPlayOutScoreboardTeam"); + packetMetadata = Class.forName(npack + "PacketPlayOutEntityMetadata"); + + packetTeamCreate = packetTeam.newInstance(); + setField(packetTeamCreate, "a", "noclip"); + setField(packetTeamCreate, "i", 0); + setField(packetTeamCreate, "f", "never"); + + Object world = Class.forName(cpack + "CraftWorld").getDeclaredMethod("getHandle").invoke(Bukkit.getWorlds().get(0)); + Object[] entityConstructorParams = version < 14 ? new Object[] { world } : new Object[] { Class.forName(npack + "EntityTypes").getDeclaredField("SQUID").get(null), world }; + fakeSquid = getMethod(Class.forName(cpack + "entity.CraftSquid"), "getHandle").invoke(Class.forName(cpack + "entity.CraftSquid").getDeclaredConstructors()[0].newInstance( + null, Class.forName(npack + "EntitySquid").getDeclaredConstructors()[0].newInstance( + entityConstructorParams))); + fakeSquidWatcher = createFakeDataWatcher(); + tryWatcherSet(fakeSquidWatcher, watcherObject1, (byte) 32); + }catch (ReflectiveOperationException e) { + e.printStackTrace(); + } + } + + public static void sendPacket(Player p, Object packet) throws ReflectiveOperationException { + Object entityPlayer = Class.forName(cpack + "entity.CraftPlayer").getDeclaredMethod("getHandle").invoke(p); + Object playerConnection = entityPlayer.getClass().getDeclaredField("playerConnection").get(entityPlayer); + playerConnection.getClass().getDeclaredMethod("sendPacket", Class.forName(npack + "Packet")).invoke(playerConnection, packet); + } + + public static Object createFakeDataWatcher() throws ReflectiveOperationException { + Object watcher = watcherConstructor.newInstance(fakeSquid); + if (version > 13) setField(watcher, "registrationLocked", false); + return watcher; + } + + public static void setDirtyWatcher(Object watcher) throws ReflectiveOperationException { + if (version >= 15) watcherDirty.invoke(watcher, watcherObject1); + } + + public static Object createPacketSquidSpawn(Location location) throws ReflectiveOperationException { + Object packet = packetSpawn.newInstance(); + setField(packet, "a", generateEID()); + setField(packet, "b", UUID.randomUUID()); + setField(packet, "c", squidID); + setField(packet, "d", location.getX()); + setField(packet, "e", location.getY()); + setField(packet, "f", location.getZ()); + setField(packet, "j", (byte) (location.getYaw() * 256.0F / 360.0F)); + setField(packet, "k", (byte) (location.getPitch() * 256.0F / 360.0F)); + if (version <= 14) setField(packet, "m", fakeSquidWatcher); + return packet; + } + + public static Object createPacketGuardianSpawn(Location location, Object watcher, int squidId) throws ReflectiveOperationException { + Object packet = packetSpawn.newInstance(); + setField(packet, "a", generateEID()); + setField(packet, "b", UUID.randomUUID()); + setField(packet, "c", guardianID); + setField(packet, "d", location.getX()); + setField(packet, "e", location.getY()); + setField(packet, "f", location.getZ()); + setField(packet, "j", (byte) (location.getYaw() * 256.0F / 360.0F)); + setField(packet, "k", (byte) (location.getPitch() * 256.0F / 360.0F)); + tryWatcherSet(watcher, watcherObject1, (byte) 32); + tryWatcherSet(watcher, watcherObject2, false); + tryWatcherSet(watcher, watcherObject3, squidId); + if (version <= 14) setField(packet, "m", watcher); + return packet; + } + + public static Object createPacketRemoveEntities(int squidId, int guardianId) throws ReflectiveOperationException { + Object packet = packetRemove.newInstance(); + setField(packet, "a", new int[] { squidId, guardianId }); + return packet; + } + + public static Object createPacketMoveEntity(Location location, int entityId) throws ReflectiveOperationException { + Object packet = packetTeleport.newInstance(); + setField(packet, "a", entityId); + setField(packet, "b", location.getX()); + setField(packet, "c", location.getY()); + setField(packet, "d", location.getZ()); + setField(packet, "e", (byte) (location.getYaw() * 256.0F / 360.0F)); + setField(packet, "f", (byte) (location.getPitch() * 256.0F / 360.0F)); + setField(packet, "g", true); + return packet; + } + + public static Object createPacketTeamAddEntities(UUID squidUUID, UUID guardianUUID) throws ReflectiveOperationException { + Object packet = packetTeam.newInstance(); + setField(packet, "a", "noclip"); + setField(packet, "i", 3); + Collection players = (Collection) getField(packetTeam, "h", packet); + players.add(squidUUID.toString()); + players.add(guardianUUID.toString()); + return packet; + } + + private static Object createPacketMetadata(int entityId, Object watcher) throws ReflectiveOperationException { + return packetMetadata.getConstructor(int.class, watcher.getClass(), boolean.class).newInstance(entityId, watcher, false); + } + + private static void tryWatcherSet(Object watcher, Object watcherObject, Object watcherData) throws ReflectiveOperationException { + try { + watcherSet.invoke(watcher, watcherObject, watcherData); + }catch (InvocationTargetException ex) { + watcherRegister.invoke(watcher, watcherObject, watcherData); + if (version >= 15) watcherDirty.invoke(watcher, watcherObject); + } + } + + /* Reflection utils */ + private static Method getMethod(Class clazz, String name) { + for (Method m : clazz.getDeclaredMethods()) { + if (m.getName().equals(name)) return m; + } + return null; + } + + private static void setField(Object instance, String name, Object value) throws ReflectiveOperationException { + Validate.notNull(instance); + Field field = instance.getClass().getDeclaredField(name); + field.setAccessible(true); + field.set(instance, value); + } + + private static Object getField(Class clazz, String name, Object instance) throws ReflectiveOperationException { + Field field = clazz.getDeclaredField(name); + field.setAccessible(true); + return field.get(instance); + } + } +} \ No newline at end of file diff --git a/src/plugin.yml b/src/plugin.yml index 5410f17..fb02282 100644 --- a/src/plugin.yml +++ b/src/plugin.yml @@ -1,6 +1,6 @@ main: me.mctp.Main name: MCTPAudio -version: 1.0 +version: 1.2 api-version: 1.14 author: MaybeFromNL_SBDeveloper description: Copyright MaybeFromNL & SBDeveloper @@ -9,6 +9,8 @@ depend: ['WorldGuard'] commands: mctpaudio: description: Main command + mctpshow: + description: Show command audio: description: Connect command