├── .gitattributes ├── .gitignore ├── LICENSE ├── README.md ├── pom.xml └── src └── main ├── java └── ua │ └── coolboy │ └── f3name │ ├── api │ └── F3NameAPI.java │ ├── bukkit │ ├── BukkitConfigParser.java │ ├── BukkitF3Runnable.java │ ├── BukkitLoggerUtil.java │ ├── F3MessageListener.java │ ├── F3NameBukkit.java │ ├── hooks │ │ ├── PAPIHook.java │ │ └── VaultHook.java │ └── packet │ │ └── ReflectionPayloadPacket.java │ ├── bungee │ ├── BungeeConfigParser.java │ ├── BungeeEventListener.java │ ├── BungeeF3Runnable.java │ ├── BungeeLoggerUtil.java │ ├── F3NameBungee.java │ ├── F3NameCommand.java │ └── messenger │ │ └── BungeeMessenger.java │ ├── core │ ├── ConfigParser.java │ ├── F3Group.java │ ├── F3Name.java │ ├── F3Runnable.java │ ├── LoggerUtil.java │ ├── PacketSerializer.java │ └── hooks │ │ ├── ILuckPermsHook.java │ │ ├── LP5.java │ │ └── LuckPermsHook.java │ ├── hooks │ └── BungeePlaceholders.java │ ├── metrics │ ├── BukkitMetrics.java │ └── BungeeMetrics.java │ └── spiget │ ├── SpigetUpdateBukkit.java │ ├── SpigetUpdateBungee.java │ └── updater │ ├── ResourceFile.java │ ├── ResourceInfo.java │ ├── ResourceVersion.java │ ├── SpigetUpdateAbstract.java │ ├── UpdateCallback.java │ ├── comparator │ └── VersionComparator.java │ └── download │ ├── DownloadCallback.java │ └── UpdateDownloader.java └── resources ├── bungee.yml ├── bungee_config.yml ├── config.yml └── plugin.yml /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .metadata 2 | bin/ 3 | *.project 4 | *.classpath 5 | tmp/ 6 | *.tmp 7 | *.bak 8 | *.swp 9 | *~.nib 10 | local.properties 11 | .settings/ 12 | .loadpath 13 | .recommenders 14 | .externalToolBuilders/ 15 | *.launch 16 | .factorypath 17 | .buildpath 18 | .target 19 | 20 | nbproject/ 21 | target/ 22 | build/ 23 | nbbuild/ 24 | dist/ 25 | nbdist/ 26 | .nb-gradle/ 27 | build.xml 28 | manifest.mf 29 | *.jar 30 | 31 | dependency-reduced-pom.xml 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # F3Name 2 | ## Edit server brand in debug screen 3 | 4 | # [`SpigotMC page`](https://www.spigotmc.org/resources/f3name-edit-your-server-brand-in-debug-screen.58997/) 5 | 6 | ### Info 7 | 8 | With release of Minecraft 1.13, server owners got an additional place to show information to players. And this place is... debug screen! 9 | With this plugin you can easily edit server brand from default ("Spigot", or very long Bungee version) to whatever you want. 10 | 11 | ### Features 12 | * Edit server brand in debug screen (F3) 13 | * Colors and animation 14 | * Easy configuration 15 | * Both BungeeCord and Spigot 16 | * Supports PlaceholderAPI 17 | * Vault (basic) or LuckPerms (advanced) group support 18 | * Built-in placeholders for BungeeCord 19 | 20 | ### Screenshots 21 | ![Colorful](https://www.spigotmc.org/attachments/ezgif-4-1206f344dd-gif.357306/) 22 | ![Local world time](https://www.spigotmc.org/attachments/ezgif-4-dda45a2c9f-gif.357307/) 23 | 24 | ## Compiling 25 | Clone or download this repository and import it like `Maven project` into your IDE. Then build `F3Name` project, and plugin jar will appear in `target` folder. 26 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | ua.coolboy 5 | f3name 6 | 3.3.0 7 | F3Name 8 | https://www.spigotmc.org/resources/f3name-rename-your-server-in-debug-screen.58997/ 9 | jar 10 | 11 | ${project.version} 12 | UTF-8 13 | 1.8 14 | 1.8 15 | 16 | 17 | 18 | 19 | spigot-repo 20 | https://hub.spigotmc.org/nexus/content/repositories/snapshots/ 21 | 22 | 23 | placeholderapi 24 | https://repo.extendedclip.com/content/repositories/placeholderapi/ 25 | 26 | 27 | vault 28 | http://nexus.hc.to/content/repositories/pub_releases/ 29 | 30 | 31 | 32 | 33 | 34 | org.spigotmc 35 | spigot-api 36 | 1.16.5-R0.1-SNAPSHOT 37 | provided 38 | 39 | 40 | me.clip 41 | placeholderapi 42 | 2.10.9 43 | provided 44 | 45 | 46 | net.luckperms 47 | api 48 | 5.2 49 | provided 50 | 51 | 52 | net.milkbowl.vault 53 | VaultAPI 54 | 1.7 55 | provided 56 | 57 | 58 | net.md-5 59 | bungeecord-api 60 | 1.16-R0.4 61 | provided 62 | 63 | 64 | 65 | 66 | 67 | F3Name 68 | 69 | 70 | src/main/resources 71 | true 72 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /src/main/java/ua/coolboy/f3name/api/F3NameAPI.java: -------------------------------------------------------------------------------- 1 | package ua.coolboy.f3name.api; 2 | 3 | import ua.coolboy.f3name.core.F3Name; 4 | 5 | /** 6 | * API for F3Name 7 | *
8 | * Use {@link #getInstance()} 9 | * @author Cool_boy 10 | */ 11 | public class F3NameAPI { 12 | 13 | private static F3Name plugin; 14 | 15 | public F3NameAPI(F3Name plugin) { 16 | if(this.plugin != null) throw new IllegalAccessError("Plugin already set!"); 17 | this.plugin = plugin; 18 | } 19 | 20 | /** 21 | * Gets plugin API 22 | * @return Instance of plugin main class 23 | */ 24 | public static F3Name getInstance() { 25 | return plugin; 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/ua/coolboy/f3name/bukkit/BukkitConfigParser.java: -------------------------------------------------------------------------------- 1 | package ua.coolboy.f3name.bukkit; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | import org.bukkit.configuration.ConfigurationSection; 6 | import org.bukkit.configuration.file.FileConfiguration; 7 | import ua.coolboy.f3name.core.ConfigParser; 8 | import ua.coolboy.f3name.core.F3Group; 9 | 10 | public class BukkitConfigParser implements ConfigParser { 11 | 12 | private FileConfiguration config; 13 | 14 | private boolean coloredConsole, bungeecord, checkForUpdates, autoupdate; 15 | 16 | private List groups; 17 | 18 | public BukkitConfigParser(FileConfiguration config) { 19 | this.config = config; 20 | 21 | coloredConsole = config.getBoolean("colored-console", true); 22 | bungeecord = config.getBoolean("bungeecord-as-primary", false); 23 | checkForUpdates = config.getBoolean("check-for-updates", true); 24 | autoupdate = config.getBoolean("auto-update", true); 25 | 26 | groups = new ArrayList<>(); 27 | for (String key : config.getConfigurationSection("groups").getKeys(false)) { 28 | ConfigurationSection section = config.getConfigurationSection("groups." + key); 29 | List messages = section.getStringList("f3names"); 30 | int updateTime = section.getInt("update-time", 200); 31 | boolean shuffle = section.getBoolean("shuffle", false); 32 | groups.add(new F3Group(key, messages, updateTime, shuffle)); 33 | } 34 | } 35 | 36 | @Override 37 | public boolean isColoredConsole() { 38 | return coloredConsole; 39 | } 40 | 41 | @Override 42 | public boolean checkForUpdates() { 43 | return checkForUpdates; 44 | } 45 | 46 | public boolean isAutoUpdate() { 47 | return autoupdate; 48 | } 49 | 50 | public boolean isBungeeCord() { 51 | return bungeecord; 52 | } 53 | 54 | @Override 55 | public F3Group getF3Group(String name) { 56 | for (F3Group gds : groups) { 57 | if (gds.getGroupName().equals(name)) { 58 | return gds; 59 | } 60 | } 61 | return null; 62 | } 63 | 64 | @Override 65 | public List getF3GroupList() { 66 | return groups; 67 | } 68 | 69 | } 70 | -------------------------------------------------------------------------------- /src/main/java/ua/coolboy/f3name/bukkit/BukkitF3Runnable.java: -------------------------------------------------------------------------------- 1 | package ua.coolboy.f3name.bukkit; 2 | 3 | import com.google.common.collect.ImmutableList; 4 | import java.util.ArrayList; 5 | import java.util.List; 6 | import java.util.Random; 7 | import java.util.concurrent.CopyOnWriteArrayList; 8 | import org.bukkit.ChatColor; 9 | import org.bukkit.entity.Player; 10 | import org.bukkit.scheduler.BukkitRunnable; 11 | import ua.coolboy.f3name.core.F3Group; 12 | import ua.coolboy.f3name.core.F3Runnable; 13 | 14 | public class BukkitF3Runnable extends BukkitRunnable implements F3Runnable { 15 | 16 | private List names; 17 | private int current; 18 | private F3NameBukkit plugin; 19 | private F3Group group; 20 | 21 | private static final Random random = new Random(); 22 | 23 | private List players; 24 | 25 | public BukkitF3Runnable(F3NameBukkit plugin, F3Group group) { 26 | this.plugin = plugin; 27 | //I don't trust this line 28 | this.players = new CopyOnWriteArrayList<>(); 29 | this.group = group; 30 | 31 | if (group.getNamesList() == null || group.getNamesList().isEmpty()) { 32 | throw new IllegalArgumentException("List must contain at least one string!"); 33 | } 34 | 35 | this.names = new ArrayList<>(); 36 | 37 | for (String string : group.getNamesList()) { 38 | this.names.add(ChatColor.translateAlternateColorCodes('&',string) + ChatColor.RESET); 39 | } 40 | 41 | plugin.getLoggerUtil().info(this.names); 42 | 43 | current = -1; 44 | } 45 | 46 | @Override 47 | public String getCurrentString() { 48 | return current < 0 ? names.get(0) : names.get(current); 49 | } 50 | 51 | @Override 52 | public List getStrings() { 53 | return names; 54 | } 55 | 56 | public List getPlayers() { 57 | return ImmutableList.copyOf(players); //clone list 58 | } 59 | 60 | @Override 61 | public F3Group getGroup() { 62 | return group; 63 | } 64 | 65 | public void addPlayer(Player player) { 66 | players.add(player); 67 | } 68 | 69 | public void removePlayer(Player player) { 70 | players.remove(player); 71 | } 72 | 73 | @Override 74 | public void run() { 75 | if (group.isShuffle()) { 76 | current = random.nextInt(names.size()); 77 | } else { 78 | current++; 79 | } 80 | if (names.size() <= current) { 81 | current = 0; 82 | } 83 | for(Player player : players) { 84 | if (!player.isOnline()) { 85 | continue; 86 | } 87 | plugin.send(player, names.get(current)); 88 | } 89 | players.removeIf((Player t) -> !t.isOnline()); 90 | } 91 | 92 | } 93 | -------------------------------------------------------------------------------- /src/main/java/ua/coolboy/f3name/bukkit/BukkitLoggerUtil.java: -------------------------------------------------------------------------------- 1 | package ua.coolboy.f3name.bukkit; 2 | 3 | import java.io.PrintWriter; 4 | import java.io.StringWriter; 5 | import org.bukkit.Bukkit; 6 | import org.bukkit.ChatColor; 7 | import org.bukkit.command.ConsoleCommandSender; 8 | import ua.coolboy.f3name.core.F3Name; 9 | import ua.coolboy.f3name.core.LoggerUtil; 10 | 11 | public class BukkitLoggerUtil implements LoggerUtil { 12 | 13 | private boolean coloredConsole; 14 | private ConsoleCommandSender console; 15 | 16 | public BukkitLoggerUtil() { 17 | this(true); 18 | } 19 | 20 | public BukkitLoggerUtil(boolean coloredConsole) { 21 | this.coloredConsole = coloredConsole; 22 | console = Bukkit.getConsoleSender(); 23 | } 24 | 25 | @Override 26 | public void info(Object obj) { 27 | console.sendMessage(getMessage(obj, ChatColor.GOLD)); 28 | } 29 | 30 | @Override 31 | public void error(Object obj) { 32 | console.sendMessage(getMessage(obj, ChatColor.DARK_RED)); 33 | } 34 | 35 | @Override 36 | public void error(Object obj, Throwable t) { 37 | console.sendMessage(getMessage(obj + "\n" + t.getMessage(), ChatColor.DARK_RED)); 38 | } 39 | 40 | @Override 41 | public void printStacktrace(Exception ex) { 42 | StringWriter outError = new StringWriter(); 43 | ex.printStackTrace(new PrintWriter(outError)); 44 | console.sendMessage(getMessage(outError, ChatColor.GRAY)); 45 | } 46 | 47 | private String getMessage(Object obj, ChatColor color) { 48 | String message = F3Name.PREFIX + color + obj + ChatColor.RESET; 49 | if (!coloredConsole) { 50 | message = ChatColor.stripColor(message); 51 | } 52 | return message; 53 | } 54 | 55 | @Override 56 | public void setColoredConsole(boolean colored) { 57 | coloredConsole = colored; 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /src/main/java/ua/coolboy/f3name/bukkit/F3MessageListener.java: -------------------------------------------------------------------------------- 1 | package ua.coolboy.f3name.bukkit; 2 | 3 | import com.google.common.io.ByteArrayDataInput; 4 | import com.google.common.io.ByteArrayDataOutput; 5 | import com.google.common.io.ByteStreams; 6 | import org.bukkit.Bukkit; 7 | import org.bukkit.entity.Player; 8 | import org.bukkit.plugin.messaging.PluginMessageListener; 9 | 10 | public class F3MessageListener implements PluginMessageListener { 11 | 12 | private F3NameBukkit plugin; 13 | 14 | public F3MessageListener(F3NameBukkit plugin) { 15 | this.plugin = plugin; 16 | 17 | Bukkit.getMessenger().registerOutgoingPluginChannel(plugin, F3NameBukkit.PLUGIN_CHANNEL); 18 | Bukkit.getMessenger().registerIncomingPluginChannel(plugin, F3NameBukkit.PLUGIN_CHANNEL, this); 19 | } 20 | 21 | /* 22 | Local API 23 | check - checking server for plugin 24 | message - get formatted message from plugin 25 | group - get player group 26 | */ 27 | @Override 28 | public void onPluginMessageReceived(String string, Player player, byte[] bytes) { 29 | ByteArrayDataInput in = ByteStreams.newDataInput(bytes); 30 | String code = in.readUTF(); 31 | ByteArrayDataOutput out = ByteStreams.newDataOutput(); 32 | switch (code) { 33 | case "check": 34 | out.writeUTF("check"); 35 | out.writeBoolean(plugin.getConfigParser().isBungeeCord()); 36 | player.sendPluginMessage(plugin, F3NameBukkit.PLUGIN_CHANNEL, out.toByteArray()); 37 | break; 38 | case "ok": 39 | plugin.setBungeePlugin(); 40 | plugin.getLoggerUtil().info("Found BungeeCord plugin, using it."); 41 | break; 42 | case "message": 43 | String name = in.readUTF(); 44 | 45 | Player pl = Bukkit.getPlayer(name); 46 | if (pl == null) { 47 | return; 48 | } 49 | plugin.send(pl, in.readUTF()); 50 | break; 51 | case "group": 52 | out.writeUTF(plugin.getPlayerGroup(player)); 53 | player.sendPluginMessage(plugin, F3NameBukkit.PLUGIN_CHANNEL, out.toByteArray()); 54 | break; 55 | } 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /src/main/java/ua/coolboy/f3name/bukkit/F3NameBukkit.java: -------------------------------------------------------------------------------- 1 | package ua.coolboy.f3name.bukkit; 2 | 3 | import ua.coolboy.f3name.spiget.SpigetUpdateBukkit; 4 | import ua.coolboy.f3name.metrics.BukkitMetrics; 5 | import com.google.common.io.ByteArrayDataOutput; 6 | import com.google.common.io.ByteStreams; 7 | import java.io.File; 8 | import java.lang.reflect.Field; 9 | import java.util.ArrayList; 10 | import java.util.Collection; 11 | import java.util.HashMap; 12 | import java.util.List; 13 | import java.util.Map; 14 | import java.util.UUID; 15 | 16 | import org.bukkit.Bukkit; 17 | import org.bukkit.ChatColor; 18 | import org.bukkit.command.Command; 19 | import org.bukkit.command.CommandSender; 20 | import org.bukkit.entity.Player; 21 | import org.bukkit.event.EventHandler; 22 | import org.bukkit.event.Listener; 23 | import org.bukkit.event.player.PlayerJoinEvent; 24 | import org.bukkit.event.player.PlayerQuitEvent; 25 | import org.bukkit.plugin.Plugin; 26 | import org.bukkit.plugin.java.JavaPlugin; 27 | import org.bukkit.scheduler.BukkitRunnable; 28 | import ua.coolboy.f3name.api.F3NameAPI; 29 | import ua.coolboy.f3name.core.F3Group; 30 | import ua.coolboy.f3name.core.F3Name; 31 | import ua.coolboy.f3name.core.LoggerUtil; 32 | import ua.coolboy.f3name.bukkit.hooks.PAPIHook; 33 | import ua.coolboy.f3name.bukkit.hooks.VaultHook; 34 | import ua.coolboy.f3name.bukkit.packet.ReflectionPayloadPacket; 35 | import ua.coolboy.f3name.core.F3Runnable; 36 | import ua.coolboy.f3name.spiget.updater.UpdateCallback; 37 | import ua.coolboy.f3name.spiget.updater.comparator.VersionComparator; 38 | import ua.coolboy.f3name.core.hooks.ILuckPermsHook; 39 | import ua.coolboy.f3name.core.hooks.LuckPermsHook; 40 | 41 | public class F3NameBukkit extends JavaPlugin implements Listener, F3Name { 42 | 43 | private static F3NameBukkit plugin; 44 | 45 | private HashMap runnables; 46 | private HashMap players; 47 | 48 | private String serverVersion = Bukkit.getServer().getClass().getPackage().getName().split("\\.")[3]; 49 | 50 | private static BukkitConfigParser parser; 51 | 52 | private BukkitMetrics metrics; 53 | 54 | private ILuckPermsHook lpHook; 55 | private VaultHook vaultHook; 56 | private PAPIHook papiHook; 57 | 58 | private boolean bungeePlugin; 59 | 60 | private F3MessageListener messageListener; 61 | 62 | private static final List HOOKS = new ArrayList<>(); 63 | 64 | private LoggerUtil logger; 65 | 66 | @Override 67 | public void onEnable() { 68 | F3NameBukkit.plugin = this; 69 | logger = new BukkitLoggerUtil(); 70 | 71 | new F3NameAPI(this); 72 | 73 | File file = new File(getDataFolder(), "config.yml"); 74 | if (!file.exists()) { 75 | saveDefaultConfig(); 76 | } 77 | 78 | parser = new BukkitConfigParser(getConfig()); 79 | logger.setColoredConsole(parser.isColoredConsole()); 80 | 81 | logger.info("Starting Bukkit(" + serverVersion + ") version..."); 82 | 83 | messageListener = new F3MessageListener(plugin); 84 | 85 | bungeePlugin = false; 86 | check(); 87 | 88 | //TODO rewrite to use direct plugin messages on 1.13.2+ 89 | /*if (serverVersion.equals("v1_13_R1")) { 90 | logger.error("Update to 1.13.2!"); 91 | Bukkit.getPluginManager().disablePlugin(this); 92 | return; 93 | }*/ 94 | /*try { 95 | this.getServer().getMessenger().registerOutgoingPluginChannel(this, BRAND_CHANNEL); 96 | } catch (Exception ex) { 97 | logger.error("Couldn't initialize messaging channel! Plugin is not working on versions lower than 1.13!"); 98 | }*/ 99 | runnables = new HashMap<>(); 100 | players = new HashMap<>(); 101 | 102 | searchHooks(); 103 | 104 | if (!parser.isBungeeCord()) { 105 | startRunnables(); 106 | } 107 | 108 | Bukkit.getPluginManager().registerEvents(this, this); 109 | 110 | setupMetrics(); 111 | logger.info("Plugin enabled!"); 112 | 113 | if(parser.checkForUpdates()) { 114 | checkUpdate(); 115 | } 116 | } 117 | 118 | @Override 119 | public void onDisable() { 120 | //Hacky way to send PluginMessage onDisable 121 | try { 122 | Field field = this.getClass().getField("isEnabled"); 123 | field.setAccessible(true); 124 | field.set(this, true); 125 | 126 | if (Bukkit.getOnlinePlayers().isEmpty()) { 127 | return; 128 | } 129 | ByteArrayDataOutput out = ByteStreams.newDataOutput(); 130 | out.writeUTF("off"); 131 | Bukkit.getOnlinePlayers().iterator().next().sendPluginMessage(plugin, PLUGIN_CHANNEL, out.toByteArray()); 132 | 133 | field.set(this, false); 134 | } catch (Exception ex) { 135 | //Seems broken, let's just silent it :D 136 | //logger.error("Failed to notify BungeeCord!", ex); 137 | } 138 | 139 | } 140 | 141 | @Override 142 | public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { 143 | if (sender.hasPermission("f3name.reload") && args.length == 1 && args[0].equals("reload")) { 144 | runnables.values().stream().forEach(BukkitF3Runnable::cancel); 145 | 146 | reload(); 147 | 148 | sender.sendMessage(PREFIX + ChatColor.GOLD + "Reloaded configuration!"); 149 | } else { 150 | sender.sendMessage(PREFIX + ChatColor.GOLD + "v" + getDescription().getVersion()); 151 | } 152 | return true; 153 | } 154 | 155 | private void check() { 156 | if (Bukkit.getOnlinePlayers().size() > 0) { 157 | ByteArrayDataOutput out = ByteStreams.newDataOutput(); 158 | out.writeUTF("check"); 159 | out.writeBoolean(parser.isBungeeCord()); 160 | Bukkit.getOnlinePlayers().iterator().next().sendPluginMessage(plugin, PLUGIN_CHANNEL, out.toByteArray()); 161 | } 162 | } 163 | 164 | @EventHandler 165 | public void onJoin(PlayerJoinEvent e) { 166 | BukkitF3Runnable runnable = addPlayer(e.getPlayer()); 167 | if (runnable != null) { 168 | send(e.getPlayer().getUniqueId(), runnable.getCurrentString()); 169 | } 170 | //check bungeecord when player connects to empty server 171 | if (Bukkit.getOnlinePlayers().size() == 1) { 172 | new BukkitRunnable() { 173 | @Override 174 | public void run() { 175 | check(); 176 | } 177 | }.runTaskLater(plugin, 20); 178 | } 179 | } 180 | 181 | public boolean hasBungeePlugin() { 182 | return bungeePlugin; 183 | } 184 | 185 | protected void setBungeePlugin() { 186 | bungeePlugin = true; 187 | 188 | } 189 | 190 | @EventHandler 191 | public void onLeave(PlayerQuitEvent e) { 192 | removePlayer(e.getPlayer()); 193 | } 194 | 195 | @Override 196 | public Collection getRunnables() { 197 | return runnables.values(); 198 | } 199 | 200 | public Map getRunnablesMap() { 201 | return runnables; 202 | } 203 | 204 | public String getPlayerGroup(Player player) { 205 | if (isHooked("LP")) { 206 | return lpHook.getBestPlayerGroup(player.getUniqueId()); 207 | } else if (isHooked("Vault")) { 208 | return vaultHook.getBestPlayerGroup(player); 209 | } else { 210 | return F3Group.DEFAULT_GROUP; 211 | } 212 | } 213 | 214 | public static boolean isHooked(String name) { 215 | return HOOKS.contains(name); 216 | } 217 | 218 | private void reload() { 219 | File file = new File(getDataFolder(), "config.yml"); 220 | if (!file.exists()) { 221 | saveDefaultConfig(); 222 | } 223 | 224 | reloadConfig(); 225 | parser = new BukkitConfigParser(getConfig()); 226 | 227 | Bukkit.getMessenger().unregisterIncomingPluginChannel(this); 228 | messageListener = new F3MessageListener(plugin); 229 | 230 | bungeePlugin = false; 231 | check(); 232 | 233 | searchHooks(); 234 | //stopping in case if we are missing something 235 | for (BukkitF3Runnable runnable : runnables.values()) { 236 | if (runnable == null) { 237 | continue; 238 | } 239 | runnable.cancel(); 240 | } 241 | 242 | if (!parser.isBungeeCord()) { 243 | startRunnables(); 244 | } 245 | 246 | } 247 | 248 | public BukkitF3Runnable addPlayer(Player player) { 249 | BukkitF3Runnable current = players.get(player); 250 | if (current != null) { 251 | current.removePlayer(player); 252 | } 253 | BukkitF3Runnable toAdd = runnables.get(getPlayerGroup(player)); 254 | if (toAdd != null) { 255 | toAdd.addPlayer(player); 256 | players.put(player, toAdd); 257 | } 258 | return toAdd; 259 | } 260 | 261 | public BukkitF3Runnable removePlayer(Player player) { 262 | BukkitF3Runnable current = players.get(player); 263 | if (current != null) { 264 | current.removePlayer(player); 265 | players.remove(player); 266 | } 267 | return current; 268 | } 269 | 270 | private void setupMetrics() { 271 | metrics = new BukkitMetrics(plugin, 2920); 272 | addHookPie("placeholderapi", Bukkit.getPluginManager().getPlugin("PlaceholderAPI")); 273 | addHookPie("luckperms", Bukkit.getPluginManager().getPlugin("LuckPerms")); 274 | addHookPie("vault", Bukkit.getPluginManager().getPlugin("Vault")); 275 | } 276 | 277 | private void addHookPie(String charid, Plugin plugin) { 278 | metrics.addCustomChart(new BukkitMetrics.AdvancedPie(charid, () -> { 279 | Map map = new HashMap<>(); 280 | if (plugin != null) { 281 | map.put(plugin.getDescription().getVersion(), 1); 282 | } else { 283 | map.put("Not using", 1); 284 | } 285 | return map; 286 | })); 287 | } 288 | 289 | private void checkUpdate() { 290 | final SpigetUpdateBukkit updater = new SpigetUpdateBukkit(this, RESOURCE_ID); 291 | updater.setVersionComparator(VersionComparator.SEM_VER); 292 | updater.checkForUpdate(new UpdateCallback() { 293 | @Override 294 | public void updateAvailable(String newVersion, String downloadUrl, boolean hasDirectDownload) { 295 | if (hasDirectDownload) { 296 | if (parser.isAutoUpdate() && updater.downloadUpdate()) { 297 | logger.info("Downloaded update! It will be applied after restart"); 298 | } 299 | } 300 | } 301 | 302 | @Override 303 | public void upToDate() { 304 | } 305 | }); 306 | } 307 | 308 | private void startRunnables() { 309 | runnables.clear(); 310 | players.clear(); 311 | for (F3Group group : parser.getF3GroupList()) { 312 | BukkitF3Runnable runnable = new BukkitF3Runnable(plugin, group); 313 | //idk is it safe, let's test 314 | runnable.runTaskTimerAsynchronously(plugin, 0, group.getUpdateTime()); 315 | runnables.put(group.getGroupName(), runnable); 316 | } 317 | 318 | Bukkit.getOnlinePlayers().forEach(p -> addPlayer(p)); 319 | } 320 | 321 | private void searchHooks() { 322 | HOOKS.clear(); 323 | if (Bukkit.getPluginManager().isPluginEnabled("PlaceholderAPI")) { 324 | HOOKS.add("PAPI"); 325 | papiHook = new PAPIHook(Bukkit.getPluginManager().getPlugin("PlaceholderAPI")); 326 | logger.info("Found PlaceholderAPI! Using it for placeholders."); 327 | } 328 | 329 | if (Bukkit.getPluginManager().isPluginEnabled("LuckPerms")) { 330 | HOOKS.add("LP"); 331 | lpHook = LuckPermsHook.get(parser.getF3GroupList()); 332 | if (lpHook != null) { 333 | logger.info("Found LuckPerms! Using it for groups."); 334 | } else { 335 | logger.error("Problem with obtaining LuckPerms instance!"); 336 | HOOKS.remove("LP"); 337 | } 338 | } else if (getServer().getPluginManager().isPluginEnabled("Vault")) { 339 | HOOKS.add("Vault"); 340 | vaultHook = new VaultHook(parser.getF3GroupList()); 341 | logger.info("Found Vault! Using it for groups."); 342 | } 343 | } 344 | 345 | @Override 346 | public LoggerUtil getLoggerUtil() { 347 | return logger; 348 | } 349 | 350 | public static F3NameBukkit getInstance() { 351 | return plugin; 352 | } 353 | 354 | @Override 355 | public ServerType getServerType() { 356 | return ServerType.BUKKIT; 357 | } 358 | 359 | public void send(Player player, String brand) { 360 | sendRaw(player, PAPIHook.getPAPIString(player, brand)); 361 | } 362 | 363 | public void sendRaw(Player player, String brand) { 364 | if (player == null) { 365 | return; 366 | } 367 | //player.sendPluginMessage(plugin, BRAND_CHANNEL, new PacketSerializer(brand).toArray()); 368 | new ReflectionPayloadPacket(plugin).send(player, brand); 369 | } 370 | 371 | @Override 372 | public void send(UUID uuid, String brand) { 373 | Player player = Bukkit.getPlayer(uuid); 374 | send(player, brand); 375 | } 376 | 377 | @Override 378 | public void sendRaw(UUID uuid, String brand) { 379 | sendRaw(Bukkit.getPlayer(uuid), brand); 380 | } 381 | 382 | @Override 383 | public BukkitConfigParser getConfigParser() { 384 | return parser; 385 | } 386 | 387 | } 388 | -------------------------------------------------------------------------------- /src/main/java/ua/coolboy/f3name/bukkit/hooks/PAPIHook.java: -------------------------------------------------------------------------------- 1 | package ua.coolboy.f3name.bukkit.hooks; 2 | 3 | import org.bukkit.OfflinePlayer; 4 | import org.bukkit.entity.Player; 5 | import org.bukkit.plugin.Plugin; 6 | 7 | public class PAPIHook { 8 | private static boolean isHooked; 9 | 10 | public PAPIHook(Plugin papi) { 11 | isHooked = papi != null; 12 | } 13 | 14 | public static String getPAPIString(Player player, String string) { 15 | if(!isHooked) return string; 16 | return me.clip.placeholderapi.PlaceholderAPI.setPlaceholders((OfflinePlayer) player, string); 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/ua/coolboy/f3name/bukkit/hooks/VaultHook.java: -------------------------------------------------------------------------------- 1 | package ua.coolboy.f3name.bukkit.hooks; 2 | 3 | import java.util.List; 4 | import java.util.stream.Collectors; 5 | import net.milkbowl.vault.permission.Permission; 6 | import org.bukkit.Bukkit; 7 | import org.bukkit.entity.Player; 8 | import org.bukkit.plugin.RegisteredServiceProvider; 9 | import ua.coolboy.f3name.core.F3Group; 10 | 11 | public class VaultHook { 12 | 13 | private Permission perms; 14 | private List groups; 15 | 16 | public VaultHook(List groups) { 17 | RegisteredServiceProvider rsp = Bukkit.getServicesManager().getRegistration(Permission.class); 18 | perms = rsp.getProvider(); 19 | this.groups = groups.stream().map(F3Group::getGroupName).collect(Collectors.toList()); 20 | } 21 | 22 | public String getBestPlayerGroup(Player player) { 23 | String group = perms.getPrimaryGroup(player); 24 | return groups.contains(group) ? group : F3Group.DEFAULT_GROUP; 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/ua/coolboy/f3name/bukkit/packet/ReflectionPayloadPacket.java: -------------------------------------------------------------------------------- 1 | package ua.coolboy.f3name.bukkit.packet; 2 | 3 | import java.lang.reflect.Field; 4 | import java.lang.reflect.Method; 5 | import java.util.Set; 6 | import org.apache.commons.lang.Validate; 7 | import org.bukkit.Bukkit; 8 | import org.bukkit.entity.Player; 9 | import org.bukkit.plugin.Plugin; 10 | import org.bukkit.plugin.messaging.Messenger; 11 | import ua.coolboy.f3name.bukkit.F3NameBukkit; 12 | import ua.coolboy.f3name.core.PacketSerializer; 13 | import ua.coolboy.f3name.bukkit.hooks.PAPIHook; 14 | 15 | public class ReflectionPayloadPacket { 16 | 17 | private F3NameBukkit plugin; 18 | 19 | public ReflectionPayloadPacket(F3NameBukkit plugin) { 20 | this.plugin = plugin; 21 | 22 | Messenger messenger = Bukkit.getMessenger(); 23 | try { 24 | Method method = messenger.getClass().getDeclaredMethod("addToOutgoing", Plugin.class, String.class); 25 | method.setAccessible(true); 26 | method.invoke(messenger, plugin, F3NameBukkit.BRAND_CHANNEL); 27 | } catch (Exception ex) { 28 | plugin.getLoggerUtil().error("Failed to register channel!", ex); 29 | } 30 | } 31 | 32 | public void send(Player player, String string) { 33 | sendRaw(player, PAPIHook.getPAPIString(player, string)); 34 | } 35 | 36 | public void sendRaw(Player player, String brand) { 37 | Validate.notNull(player, "Player is null!"); 38 | Validate.notNull(brand, "Server brand is null!"); 39 | 40 | checkPlayerChannels(player); 41 | 42 | player.sendPluginMessage(plugin, F3NameBukkit.BRAND_CHANNEL, new PacketSerializer(brand).toArray()); 43 | } 44 | 45 | public Object getHandle() { 46 | throw new UnsupportedOperationException("Not implemented in ReflectionPayloadPacket!"); 47 | } 48 | 49 | //Less efficient than direct usе of NMS 50 | private void checkPlayerChannels(Player player) { 51 | try { 52 | Field playerChannels = player.getClass().getDeclaredField("channels"); 53 | playerChannels.setAccessible(true); 54 | Set channels = (Set) playerChannels.get(player); 55 | if(!channels.contains(F3NameBukkit.BRAND_CHANNEL)) { 56 | channels.add(F3NameBukkit.BRAND_CHANNEL); 57 | } 58 | } catch (Exception ex) { 59 | plugin.getLoggerUtil().error("Failed to add channel to player!",ex); 60 | } 61 | } 62 | 63 | } -------------------------------------------------------------------------------- /src/main/java/ua/coolboy/f3name/bungee/BungeeConfigParser.java: -------------------------------------------------------------------------------- 1 | package ua.coolboy.f3name.bungee; 2 | 3 | import com.google.common.collect.ImmutableList; 4 | import java.io.File; 5 | import java.io.IOException; 6 | import java.io.InputStream; 7 | import java.nio.file.Files; 8 | import java.util.ArrayList; 9 | import java.util.List; 10 | 11 | import net.md_5.bungee.api.plugin.Plugin; 12 | import net.md_5.bungee.config.Configuration; 13 | import net.md_5.bungee.config.ConfigurationProvider; 14 | import net.md_5.bungee.config.YamlConfiguration; 15 | 16 | import ua.coolboy.f3name.core.ConfigParser; 17 | import ua.coolboy.f3name.core.F3Group; 18 | 19 | public class BungeeConfigParser implements ConfigParser { 20 | 21 | private Configuration config; 22 | 23 | private List groups; 24 | 25 | private List excludedServers; 26 | 27 | private boolean coloredConsole, onlyapi, checkForUpdates; 28 | 29 | public BungeeConfigParser(Plugin plugin) throws IOException { 30 | if (!plugin.getDataFolder().exists()) { 31 | plugin.getDataFolder().mkdir(); 32 | } 33 | 34 | File file = new File(plugin.getDataFolder(), "config.yml"); 35 | 36 | if (!file.exists()) { 37 | InputStream in = plugin.getResourceAsStream("bungee_config.yml"); 38 | Files.copy(in, file.toPath()); 39 | } 40 | config = ConfigurationProvider.getProvider(YamlConfiguration.class).load(file); 41 | 42 | coloredConsole = config.getBoolean("colored-console", true); 43 | onlyapi = config.getBoolean("only-api", false); 44 | checkForUpdates = config.getBoolean("check-for-updates", true); 45 | 46 | excludedServers = config.getStringList("excluded-servers"); 47 | 48 | groups = new ArrayList<>(); 49 | for (String key : config.getSection("groups").getKeys()) { 50 | Configuration section = config.getSection("groups." + key); 51 | List messages = section.getStringList("f3names"); 52 | int updateTime = section.getInt("update-time", 200); 53 | boolean shuffle = section.getBoolean("shuffle", false); 54 | groups.add(new F3Group(key, messages, updateTime, shuffle)); 55 | } 56 | } 57 | 58 | public List getExcludedServers() { 59 | return ImmutableList.copyOf(excludedServers); 60 | } 61 | 62 | public boolean isOnlyAPI() { 63 | return onlyapi; 64 | } 65 | 66 | @Override 67 | public boolean checkForUpdates() { 68 | return checkForUpdates; 69 | } 70 | 71 | public void excludeServer(String name) { 72 | excludedServers.add(name); 73 | } 74 | 75 | public void removeExcludedServer(String name) { 76 | excludedServers.remove(name); 77 | } 78 | 79 | @Override 80 | public boolean isColoredConsole() { 81 | return coloredConsole; 82 | } 83 | 84 | @Override 85 | public F3Group getF3Group(String name) { 86 | for (F3Group gds : groups) { 87 | if (gds.getGroupName().equals(name)) { 88 | return gds; 89 | } 90 | } 91 | return null; 92 | } 93 | 94 | @Override 95 | public List getF3GroupList() { 96 | return groups; 97 | } 98 | 99 | } 100 | -------------------------------------------------------------------------------- /src/main/java/ua/coolboy/f3name/bungee/BungeeEventListener.java: -------------------------------------------------------------------------------- 1 | package ua.coolboy.f3name.bungee; 2 | 3 | import net.md_5.bungee.api.connection.ProxiedPlayer; 4 | import net.md_5.bungee.api.event.PlayerDisconnectEvent; 5 | import net.md_5.bungee.api.event.PluginMessageEvent; 6 | import net.md_5.bungee.api.event.ServerConnectedEvent; 7 | import net.md_5.bungee.api.event.ServerSwitchEvent; 8 | import net.md_5.bungee.api.plugin.Listener; 9 | import net.md_5.bungee.event.EventHandler; 10 | 11 | public class BungeeEventListener implements Listener { 12 | 13 | private F3NameBungee plugin; 14 | 15 | public BungeeEventListener(F3NameBungee plugin) { 16 | this.plugin = plugin; 17 | plugin.getProxy().getPluginManager().registerListener(plugin, this); 18 | } 19 | 20 | @EventHandler 21 | public void onJoin(ServerConnectedEvent e) { 22 | ProxiedPlayer player = e.getPlayer(); 23 | if (plugin.getHookedServers().contains(e.getServer().getInfo().getName())) { 24 | plugin.getPlayerGroup(player); 25 | return; 26 | } 27 | if (!plugin.getConfigParser().getExcludedServers().contains(e.getServer().getInfo().getName())) { 28 | BungeeF3Runnable runnable = plugin.addPlayer(player); 29 | if (runnable != null) { 30 | plugin.send(player, runnable.getCurrentString(), true); 31 | } 32 | } 33 | } 34 | 35 | @EventHandler 36 | public void onLeave(PlayerDisconnectEvent e) { 37 | plugin.removePlayer(e.getPlayer()); 38 | } 39 | 40 | public void onSwitch(ServerSwitchEvent e) { 41 | ProxiedPlayer player = e.getPlayer(); 42 | if (plugin.getHookedServers().contains(player.getServer().getInfo().getName())) { 43 | plugin.getPlayerGroup(player); //ask method to get player group 44 | } 45 | } 46 | 47 | //Forward server brand through bungee 48 | @EventHandler 49 | public void onPluginMessage(PluginMessageEvent e) { 50 | if(e.getTag().equals(F3NameBungee.BRAND_CHANNEL) && e.getReceiver() instanceof ProxiedPlayer) { 51 | if(e.isCancelled()) return; 52 | ProxiedPlayer player = (ProxiedPlayer) e.getReceiver(); 53 | e.setCancelled(true); 54 | player.sendData(F3NameBungee.BRAND_CHANNEL, e.getData()); 55 | } 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /src/main/java/ua/coolboy/f3name/bungee/BungeeF3Runnable.java: -------------------------------------------------------------------------------- 1 | package ua.coolboy.f3name.bungee; 2 | 3 | import com.google.common.collect.ImmutableList; 4 | import java.util.ArrayList; 5 | import java.util.List; 6 | import java.util.Random; 7 | import java.util.concurrent.TimeUnit; 8 | 9 | import net.md_5.bungee.api.ChatColor; 10 | import net.md_5.bungee.api.ProxyServer; 11 | import net.md_5.bungee.api.connection.ProxiedPlayer; 12 | import net.md_5.bungee.api.connection.Server; 13 | import net.md_5.bungee.api.plugin.Plugin; 14 | import net.md_5.bungee.api.scheduler.ScheduledTask; 15 | import net.md_5.bungee.protocol.ProtocolConstants; 16 | 17 | import ua.coolboy.f3name.core.F3Group; 18 | import ua.coolboy.f3name.core.F3Runnable; 19 | 20 | public class BungeeF3Runnable implements Runnable, F3Runnable { 21 | 22 | private F3Group group; 23 | private List names; 24 | private int current; 25 | 26 | private static final Random random = new Random(); 27 | 28 | private List players; 29 | private ProxyServer server; 30 | 31 | private F3NameBungee plugin; 32 | private ScheduledTask task; 33 | 34 | public BungeeF3Runnable(F3NameBungee plugin, F3Group group) { 35 | this.plugin = plugin; 36 | this.group = group; 37 | this.players = new ArrayList<>(); 38 | server = ProxyServer.getInstance(); 39 | 40 | if (group.getNamesList() == null || group.getNamesList().isEmpty()) { 41 | throw new IllegalArgumentException("List must contain at least one string!"); 42 | } 43 | 44 | this.names = new ArrayList<>(); 45 | 46 | for (String string : group.getNamesList()) { 47 | this.names.add(ChatColor.translateAlternateColorCodes('&', string) + ChatColor.RESET); 48 | } 49 | 50 | current = -1; 51 | } 52 | 53 | @Override 54 | public void run() { 55 | if (group.isShuffle()) { 56 | current = random.nextInt(names.size()); 57 | } else { 58 | current++; 59 | } 60 | if (names.size() <= current) { 61 | current = 0; 62 | } 63 | for (ProxiedPlayer player : players) { 64 | if (!player.isConnected()) { 65 | //players.remove(player); 66 | continue; 67 | } 68 | 69 | if (player.getPendingConnection().getVersion() < ProtocolConstants.MINECRAFT_1_13 70 | || isExcludedServer(player.getServer())) { 71 | continue; 72 | } 73 | 74 | if (isHookedServer(player.getServer())) { 75 | plugin.getMessenger().getMessage(player.getName(), names.get(current), (String msg, Throwable ex) -> { 76 | plugin.send(player, msg, true); 77 | }); 78 | } 79 | 80 | plugin.send(player, names.get(current), true); 81 | } 82 | } 83 | 84 | private boolean isExcludedServer(Server server) { 85 | if (server == null) { 86 | return true; 87 | } 88 | return plugin.getConfigParser().getExcludedServers().contains(server.getInfo().getName()); 89 | } 90 | 91 | private boolean isHookedServer(Server server) { 92 | if (server == null) { 93 | return false; 94 | } 95 | return plugin.getHookedServers().contains(server.getInfo().getName()); 96 | } 97 | 98 | @Override 99 | public F3Group getGroup() { 100 | return group; 101 | } 102 | 103 | public List getPlayers() { 104 | return ImmutableList.copyOf(players); //clone list 105 | } 106 | 107 | public void addPlayer(ProxiedPlayer player) { 108 | players.add(player); 109 | } 110 | 111 | public void removePlayer(ProxiedPlayer player) { 112 | players.remove(player); 113 | } 114 | 115 | @Override 116 | public String getCurrentString() { 117 | return current < 0 ? names.get(0) : names.get(current); 118 | } 119 | 120 | @Override 121 | public List getStrings() { 122 | return names; 123 | } 124 | 125 | public ScheduledTask runTaskTimer(Plugin plugin, int delay, int period) { 126 | return task = plugin.getProxy().getScheduler().schedule(plugin, this, delay / 20, period / 20, TimeUnit.SECONDS); 127 | } 128 | 129 | public ScheduledTask getTask() { 130 | return task; 131 | } 132 | 133 | } 134 | -------------------------------------------------------------------------------- /src/main/java/ua/coolboy/f3name/bungee/BungeeLoggerUtil.java: -------------------------------------------------------------------------------- 1 | package ua.coolboy.f3name.bungee; 2 | 3 | import java.io.PrintWriter; 4 | import java.io.StringWriter; 5 | import net.md_5.bungee.api.ChatColor; 6 | import net.md_5.bungee.api.CommandSender; 7 | import net.md_5.bungee.api.ProxyServer; 8 | import net.md_5.bungee.api.chat.BaseComponent; 9 | import net.md_5.bungee.api.chat.TextComponent; 10 | 11 | import ua.coolboy.f3name.core.F3Name; 12 | import ua.coolboy.f3name.core.LoggerUtil; 13 | 14 | public class BungeeLoggerUtil implements LoggerUtil { 15 | 16 | private boolean coloredConsole; 17 | private CommandSender console; 18 | 19 | public BungeeLoggerUtil() { 20 | this(true); 21 | } 22 | 23 | public BungeeLoggerUtil(boolean coloredConsole) { 24 | this.coloredConsole = coloredConsole; 25 | console = ProxyServer.getInstance().getConsole(); 26 | } 27 | 28 | @Override 29 | public void info(Object obj) { 30 | console.sendMessage(getMessage(obj, ChatColor.GOLD)); 31 | } 32 | 33 | @Override 34 | public void error(Object obj) { 35 | console.sendMessage(getMessage(obj, ChatColor.DARK_RED)); 36 | } 37 | 38 | @Override 39 | public void error(Object obj, Throwable t) { 40 | console.sendMessage(getMessage(obj + "\n" + t.getMessage(), ChatColor.DARK_RED)); 41 | } 42 | 43 | @Override 44 | public void printStacktrace(Exception ex) { 45 | StringWriter outError = new StringWriter(); 46 | ex.printStackTrace(new PrintWriter(outError)); 47 | console.sendMessage(getMessage(outError, ChatColor.GRAY)); 48 | } 49 | 50 | @Override 51 | public void setColoredConsole(boolean bool) { 52 | coloredConsole = bool; 53 | } 54 | 55 | private BaseComponent[] getMessage(Object obj, ChatColor color) { 56 | String message = F3Name.PREFIX + color + obj + ChatColor.RESET; 57 | if (!coloredConsole) { 58 | message = ChatColor.stripColor(message); 59 | } 60 | return TextComponent.fromLegacyText(message); 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /src/main/java/ua/coolboy/f3name/bungee/F3NameBungee.java: -------------------------------------------------------------------------------- 1 | package ua.coolboy.f3name.bungee; 2 | 3 | import ua.coolboy.f3name.core.hooks.bungee.BungeePlaceholders; 4 | import ua.coolboy.f3name.spiget.SpigetUpdateBungee; 5 | import ua.coolboy.f3name.metrics.BungeeMetrics; 6 | import com.google.common.collect.ImmutableList; 7 | import java.io.IOException; 8 | import java.util.ArrayList; 9 | import java.util.Collection; 10 | import java.util.HashMap; 11 | import java.util.List; 12 | import java.util.Map; 13 | import java.util.Objects; 14 | import java.util.UUID; 15 | import java.util.concurrent.TimeUnit; 16 | 17 | import net.md_5.bungee.api.ChatColor; 18 | import net.md_5.bungee.api.config.ServerInfo; 19 | import net.md_5.bungee.api.connection.ProxiedPlayer; 20 | import net.md_5.bungee.api.plugin.Plugin; 21 | import net.md_5.bungee.api.scheduler.ScheduledTask; 22 | import ua.coolboy.f3name.api.F3NameAPI; 23 | import ua.coolboy.f3name.bungee.messenger.BungeeMessenger; 24 | 25 | import ua.coolboy.f3name.core.F3Group; 26 | import ua.coolboy.f3name.core.F3Name; 27 | import ua.coolboy.f3name.core.F3Runnable; 28 | import ua.coolboy.f3name.core.LoggerUtil; 29 | import ua.coolboy.f3name.core.PacketSerializer; 30 | import ua.coolboy.f3name.spiget.updater.UpdateCallback; 31 | import ua.coolboy.f3name.spiget.updater.comparator.VersionComparator; 32 | import ua.coolboy.f3name.core.hooks.ILuckPermsHook; 33 | import ua.coolboy.f3name.core.hooks.LuckPermsHook; 34 | 35 | public class F3NameBungee extends Plugin implements F3Name { 36 | 37 | private BungeeLoggerUtil logger; 38 | private BungeeConfigParser parser; 39 | 40 | private BungeeMessenger messenger; 41 | 42 | private BungeeMetrics metrics; 43 | 44 | private ILuckPermsHook lpHook; 45 | 46 | private static final List HOOKS = new ArrayList<>(); 47 | 48 | private List hookedServers; 49 | 50 | private Map runnables; 51 | private Map players; 52 | 53 | @Override 54 | public void onEnable() { 55 | logger = new BungeeLoggerUtil(); 56 | 57 | try { 58 | parser = new BungeeConfigParser(this); 59 | } catch (IOException ex) { 60 | logger.error("Failed to load config file!", ex); 61 | return; 62 | } 63 | 64 | logger.setColoredConsole(parser.isColoredConsole()); 65 | 66 | logger.info("Starting BungeeCord version..."); 67 | 68 | new F3NameAPI(this); 69 | 70 | messenger = new BungeeMessenger(this); 71 | 72 | new BungeeEventListener(this); 73 | 74 | getProxy().registerChannel(PLUGIN_CHANNEL); 75 | 76 | getProxy().getPluginManager().registerCommand(this, new F3NameCommand(this)); 77 | 78 | if (getProxy().getPluginManager().getPlugin("LuckPerms") != null) { 79 | HOOKS.add("LP"); 80 | lpHook = LuckPermsHook.get(parser.getF3GroupList()); 81 | logger.info("Found LuckPerms! Using it for groups."); 82 | } 83 | 84 | hookedServers = new ArrayList<>(); 85 | 86 | runnables = new HashMap<>(); 87 | players = new HashMap<>(); 88 | 89 | if (!parser.isOnlyAPI()) { 90 | startRunnables(); 91 | } 92 | 93 | setupMetrics(); 94 | 95 | logger.info("Plugin enabled!"); 96 | 97 | if(parser.checkForUpdates()) { 98 | checkUpdate(); 99 | } 100 | } 101 | 102 | private void startRunnables() { 103 | for (F3Group group : parser.getF3GroupList()) { 104 | BungeeF3Runnable runnable = new BungeeF3Runnable(this, group); 105 | getProxy().getScheduler().schedule(this, runnable, 0, group.getUpdateTime() / 20, TimeUnit.SECONDS); 106 | runnables.put(group.getGroupName(), runnable); 107 | } 108 | 109 | getProxy().getPlayers().forEach(p -> addPlayer(p)); 110 | } 111 | 112 | public BungeeMessenger getMessenger() { 113 | return messenger; 114 | } 115 | 116 | public String getPlayerGroup(ProxiedPlayer player) { 117 | if (player.getServer() != null && hookedServers.contains(player.getServer().getInfo().getName())) { 118 | messenger.getPlayerGroup(player, (String group, Throwable error) -> { 119 | if (group.equals(F3Group.DEFAULT_GROUP)) { 120 | group = getPlayerGroupLocally(player); 121 | } 122 | addPlayer(player, group); 123 | }); 124 | return F3Group.DEFAULT_GROUP; 125 | } 126 | 127 | return getPlayerGroupLocally(player); 128 | } 129 | 130 | public String getPlayerGroupLocally(ProxiedPlayer player) { 131 | if (isHooked("LP")) { 132 | return lpHook.getBestPlayerGroup(player.getUniqueId()); 133 | } else { 134 | Collection groups = player.getGroups(); 135 | for (F3Group group : parser.getF3GroupList()) { 136 | if (groups.contains(group.getGroupName())) { 137 | return group.getGroupName(); 138 | } 139 | } 140 | return F3Group.DEFAULT_GROUP; 141 | } 142 | } 143 | 144 | public BungeeF3Runnable addPlayer(ProxiedPlayer player, String group) { 145 | if (runnables == null || runnables.isEmpty()) { 146 | return null; 147 | } 148 | 149 | BungeeF3Runnable current = players.get(player); 150 | if (current != null) { 151 | current.removePlayer(player); 152 | } 153 | BungeeF3Runnable toAdd = runnables.get(group); 154 | if (toAdd == null) { 155 | toAdd = runnables.get(F3Group.DEFAULT_GROUP); 156 | } 157 | toAdd.addPlayer(player); 158 | players.put(player, toAdd); 159 | return toAdd; 160 | } 161 | 162 | public BungeeF3Runnable addPlayer(ProxiedPlayer player) { 163 | return addPlayer(player, getPlayerGroup(player)); 164 | } 165 | 166 | public BungeeF3Runnable removePlayer(ProxiedPlayer player) { 167 | BungeeF3Runnable current = players.get(player); 168 | if (current != null) { 169 | current.removePlayer(player); 170 | players.remove(player); 171 | } 172 | return current; 173 | } 174 | 175 | public boolean isHooked(String string) { 176 | return HOOKS.contains(string); 177 | } 178 | 179 | @Override 180 | public Collection getRunnables() { 181 | return runnables.values(); 182 | } 183 | 184 | public Map getRunnablesMap() { 185 | return runnables; 186 | } 187 | 188 | public List getHookedServers() { 189 | return ImmutableList.copyOf(hookedServers); 190 | } 191 | 192 | public void addHookedServer(String name) { 193 | hookedServers.add(name); 194 | } 195 | 196 | public void removeHookedServer(String name) { 197 | hookedServers.remove(name); 198 | } 199 | 200 | protected void reload() { 201 | //trying to suddenly not kill bStats runnable 202 | runnables.values().stream() 203 | .filter(Objects::nonNull) //idk how it can be null, but better be prepared 204 | .map(BungeeF3Runnable::getTask) 205 | .filter(Objects::nonNull) //fix for NullPointer 206 | .forEach(ScheduledTask::cancel); 207 | 208 | runnables.clear(); 209 | players.clear(); 210 | hookedServers.clear(); 211 | 212 | checkServers(); 213 | 214 | try { 215 | parser = new BungeeConfigParser(this); 216 | } catch (IOException ex) { 217 | logger.error("Failed to load config file!", ex); 218 | return; 219 | } 220 | if (!parser.isOnlyAPI()) { 221 | startRunnables(); 222 | } 223 | 224 | if (isHooked("LP")) { 225 | lpHook = LuckPermsHook.get(parser.getF3GroupList()); 226 | } 227 | } 228 | 229 | private void checkServers() { 230 | logger.info("Checking servers for F3Name plugin..."); 231 | for (ServerInfo info : getProxy().getServers().values()) { 232 | checkServer(info); 233 | } 234 | } 235 | 236 | public void checkServer(ServerInfo info) { 237 | if (!info.getPlayers().isEmpty()) { 238 | //clear known data about server 239 | hookedServers.remove(info.getName()); 240 | getConfigParser().removeExcludedServer(info.getName()); 241 | 242 | messenger.checkServer(info, (Boolean result, Throwable error) -> { 243 | if (result) { 244 | addHookedServer(info.getName()); 245 | } else { 246 | getConfigParser().excludeServer(info.getName()); 247 | } 248 | }); 249 | } 250 | } 251 | 252 | private void setupMetrics() { 253 | metrics = new BungeeMetrics(this, 3004); 254 | addHookPie("luckperms", getProxy().getPluginManager().getPlugin("LuckPerms")); 255 | } 256 | 257 | private void addHookPie(String charid, Plugin plugin) { 258 | metrics.addCustomChart(new BungeeMetrics.AdvancedPie(charid, () -> { 259 | Map map = new HashMap<>(); 260 | if (plugin != null) { 261 | map.put(plugin.getDescription().getVersion(), 1); 262 | } else { 263 | map.put("Not using", 1); 264 | } 265 | return map; 266 | })); 267 | } 268 | 269 | private void checkUpdate() { 270 | final SpigetUpdateBungee updater = new SpigetUpdateBungee(this, RESOURCE_ID); 271 | updater.setVersionComparator(VersionComparator.SEM_VER); 272 | updater.checkForUpdate(new UpdateCallback() { 273 | @Override 274 | public void updateAvailable(String newVersion, String downloadUrl, boolean hasDirectDownload) { 275 | logger.info("Update available: " + newVersion + "! Download link: " + downloadUrl); 276 | } 277 | 278 | @Override 279 | public void upToDate() { 280 | } 281 | }); 282 | } 283 | 284 | @Override 285 | public LoggerUtil getLoggerUtil() { 286 | return logger; 287 | } 288 | 289 | @Override 290 | public ServerType getServerType() { 291 | return ServerType.BUNGEE; 292 | } 293 | 294 | @Override 295 | public BungeeConfigParser getConfigParser() { 296 | return parser; 297 | } 298 | 299 | public void send(ProxiedPlayer player, String brand, boolean placeholders) { 300 | messenger.getMessage(player.getName(), brand, (String message, Throwable error) -> { 301 | if (brand.equals(message)) { 302 | message = placeholders ? BungeePlaceholders.setPlaceholders(player, brand) : message; 303 | } 304 | player.sendData(BRAND_CHANNEL, new PacketSerializer(message + ChatColor.RESET).toArray()); 305 | }); 306 | } 307 | 308 | public void sendLocally(ProxiedPlayer player, String brand) { 309 | player.sendData(BRAND_CHANNEL, new PacketSerializer(brand + ChatColor.RESET).toArray()); 310 | } 311 | 312 | @Override 313 | public void send(UUID uuid, String brand) { 314 | ProxiedPlayer player = getProxy().getPlayer(uuid); 315 | if (player == null) { 316 | throw new IllegalArgumentException("Can't find player with UUID " + uuid); 317 | } 318 | send(player, brand, true); 319 | } 320 | 321 | @Override 322 | public void sendRaw(UUID uuid, String brand) { 323 | ProxiedPlayer player = getProxy().getPlayer(uuid); 324 | if (player == null) { 325 | throw new IllegalArgumentException("Can't find player with UUID " + uuid); 326 | } 327 | send(player, brand, false); 328 | } 329 | 330 | } 331 | -------------------------------------------------------------------------------- /src/main/java/ua/coolboy/f3name/bungee/F3NameCommand.java: -------------------------------------------------------------------------------- 1 | package ua.coolboy.f3name.bungee; 2 | 3 | import net.md_5.bungee.api.ChatColor; 4 | import net.md_5.bungee.api.CommandSender; 5 | import net.md_5.bungee.api.chat.TextComponent; 6 | import net.md_5.bungee.api.plugin.Command; 7 | 8 | public class F3NameCommand extends Command { 9 | 10 | private F3NameBungee plugin; 11 | 12 | public F3NameCommand(F3NameBungee plugin) { 13 | super("f3namebungee", "f3name.reload", "f3nameb"); 14 | this.plugin = plugin; 15 | } 16 | 17 | @Override 18 | public void execute(CommandSender sender, String[] args) { 19 | if (args.length == 1 && args[0].equals("reload")) { 20 | 21 | plugin.reload(); 22 | 23 | sender.sendMessage(TextComponent.fromLegacyText( 24 | F3NameBungee.PREFIX + ChatColor.GOLD + "Reloaded configuration!" 25 | )); 26 | } else { 27 | sender.sendMessage(TextComponent.fromLegacyText( 28 | F3NameBungee.PREFIX + ChatColor.GOLD + "v" 29 | + plugin.getDescription().getVersion() + " by Cool_boy (aka prettydude)" 30 | )); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/ua/coolboy/f3name/bungee/messenger/BungeeMessenger.java: -------------------------------------------------------------------------------- 1 | package ua.coolboy.f3name.bungee.messenger; 2 | 3 | import com.google.common.io.ByteArrayDataInput; 4 | import com.google.common.io.ByteArrayDataOutput; 5 | import com.google.common.io.ByteStreams; 6 | import java.util.HashMap; 7 | import java.util.Map; 8 | import net.md_5.bungee.api.Callback; 9 | import net.md_5.bungee.api.config.ServerInfo; 10 | import net.md_5.bungee.api.connection.ProxiedPlayer; 11 | import net.md_5.bungee.api.connection.Server; 12 | import net.md_5.bungee.api.event.PluginMessageEvent; 13 | import net.md_5.bungee.api.plugin.Listener; 14 | import net.md_5.bungee.event.EventHandler; 15 | import ua.coolboy.f3name.bungee.F3NameBungee; 16 | import ua.coolboy.f3name.core.F3Group; 17 | import ua.coolboy.f3name.core.F3Name; 18 | 19 | public class BungeeMessenger implements Listener { 20 | 21 | private F3NameBungee plugin; 22 | private Map> groupCallback; 23 | private Map> messageCallback; 24 | private Map> checkCallback; 25 | 26 | public BungeeMessenger(F3NameBungee plugin) { 27 | this.plugin = plugin; 28 | plugin.getProxy().getPluginManager().registerListener(plugin, this); 29 | 30 | groupCallback = new HashMap<>(); 31 | messageCallback = new HashMap<>(); 32 | checkCallback = new HashMap<>(); 33 | } 34 | 35 | public void getPlayerGroup(String playername, Callback callback) { 36 | ProxiedPlayer player = plugin.getProxy().getPlayer(playername); 37 | if (player == null) { 38 | callback.done(F3Group.DEFAULT_GROUP, new IllegalArgumentException("Player not found!")); 39 | return; 40 | } 41 | getPlayerGroup(player, callback); 42 | } 43 | 44 | public void getPlayerGroup(ProxiedPlayer player, Callback callback) { 45 | Server server = player.getServer(); 46 | if (server == null || !plugin.getHookedServers().contains(server.getInfo().getName())) { 47 | callback.done(F3Group.DEFAULT_GROUP, null); 48 | return; 49 | } 50 | ByteArrayDataOutput out = getNewOutput(); 51 | out.writeUTF(Actions.GROUP.getSubchannel()); 52 | out.writeUTF(player.getName()); 53 | server.sendData(F3Name.PLUGIN_CHANNEL, out.toByteArray()); 54 | groupCallback.put(player.getName(), callback); 55 | } 56 | 57 | //Callback never fires if server don't have plugin 58 | public void checkServer(String server, Callback callback) { 59 | ServerInfo info = plugin.getProxy().getServerInfo(server); 60 | if (info == null) { 61 | callback.done(false, new IllegalArgumentException("Server not found!")); 62 | return; 63 | } 64 | checkServer(info, callback); 65 | } 66 | 67 | //Callback never fires if server don't have plugin 68 | public void checkServer(ServerInfo server, Callback callback) { 69 | ByteArrayDataOutput out = getNewOutput(); 70 | out.writeUTF(Actions.CHECK.getSubchannel()); 71 | server.sendData(F3Name.PLUGIN_CHANNEL, out.toByteArray()); 72 | checkCallback.put(server.getName().toLowerCase(), callback); 73 | } 74 | 75 | public void getMessage(String playername, String message, Callback callback) { 76 | ProxiedPlayer player = plugin.getProxy().getPlayer(playername); 77 | 78 | if (player == null || player.getServer() == null) { 79 | callback.done(message, new IllegalArgumentException("Player not found!")); 80 | return; 81 | } 82 | //returns message if server isn't hooked 83 | if (!plugin.getHookedServers().contains(player.getServer().getInfo().getName())) { 84 | callback.done(message, new IllegalAccessError("Server don't have plugin")); //IllegalAccess? Why not? 85 | return; 86 | } 87 | 88 | ByteArrayDataOutput out = getNewOutput(); 89 | out.writeUTF(Actions.MESSAGE.getSubchannel()); 90 | out.writeUTF(player.getName()); 91 | out.writeUTF(message); 92 | 93 | player.getServer().sendData(F3Name.PLUGIN_CHANNEL, out.toByteArray()); 94 | 95 | messageCallback.put(player.getName(), callback); 96 | } 97 | 98 | @EventHandler 99 | public void onPluginMessage(PluginMessageEvent e) { 100 | if (e.getTag().equals(F3NameBungee.PLUGIN_CHANNEL)) { 101 | if (!(e.getReceiver() instanceof ProxiedPlayer)) { 102 | return; 103 | } 104 | ProxiedPlayer player = (ProxiedPlayer) e.getReceiver(); 105 | ByteArrayDataInput in = ByteStreams.newDataInput(e.getData()); 106 | String code = in.readUTF(); 107 | switch (code) { 108 | case "check": 109 | boolean bungee = in.readBoolean(); 110 | String servername = player.getServer().getInfo().getName(); 111 | 112 | plugin.getLoggerUtil().info("Found plugin on " + servername); 113 | 114 | Callback b = checkCallback.get(servername); 115 | if (b != null) { 116 | b.done(bungee, null); 117 | checkCallback.remove(servername); 118 | } else if (bungee) { 119 | plugin.addHookedServer(servername); 120 | } else { 121 | plugin.getConfigParser().excludeServer(servername); 122 | } 123 | 124 | //auto response 125 | ByteArrayDataOutput out = ByteStreams.newDataOutput(); 126 | out.writeUTF("ok"); 127 | player.getServer().sendData(F3NameBungee.PLUGIN_CHANNEL, out.toByteArray()); 128 | break; 129 | case "group": 130 | String group = in.readUTF(); 131 | Callback s = groupCallback.get(player.getName()); 132 | if (s != null) { 133 | s.done(group, null); 134 | } 135 | groupCallback.remove(player.getName()); 136 | break; 137 | case "message": 138 | String text = in.readUTF(); 139 | Callback msg = messageCallback.get(player.getName()); 140 | 141 | if (msg != null) { 142 | msg.done(text, null); 143 | } else { 144 | plugin.sendLocally(player, text); 145 | } 146 | messageCallback.remove(player.getName()); 147 | break; 148 | case "off": 149 | plugin.removeHookedServer(player.getServer().getInfo().getName()); 150 | break; 151 | } 152 | } 153 | } 154 | 155 | private ByteArrayDataOutput getNewOutput() { 156 | return ByteStreams.newDataOutput(); 157 | } 158 | 159 | private ByteArrayDataInput getInput(byte[] bytes) { 160 | return ByteStreams.newDataInput(bytes); 161 | } 162 | 163 | public enum Actions { 164 | CHECK, GROUP, MESSAGE; 165 | 166 | public String getSubchannel() { 167 | return this.toString().toLowerCase(); 168 | } 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /src/main/java/ua/coolboy/f3name/core/ConfigParser.java: -------------------------------------------------------------------------------- 1 | package ua.coolboy.f3name.core; 2 | 3 | import java.util.List; 4 | 5 | public interface ConfigParser { 6 | 7 | public boolean isColoredConsole(); 8 | 9 | public boolean checkForUpdates(); 10 | 11 | public F3Group getF3Group(String name); 12 | 13 | public List getF3GroupList(); 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/ua/coolboy/f3name/core/F3Group.java: -------------------------------------------------------------------------------- 1 | package ua.coolboy.f3name.core; 2 | 3 | import java.util.List; 4 | 5 | public class F3Group { 6 | 7 | private String group; 8 | private List messages; 9 | private boolean shuffle; 10 | private int updateTime; 11 | 12 | public static final String DEFAULT_GROUP = "everyone"; 13 | 14 | //GroupDebugScreen 15 | public F3Group(String group, List messages, int updateTime, boolean shuffle) { 16 | this.group = group; 17 | this.messages = messages; 18 | this.updateTime = updateTime; 19 | this.shuffle = shuffle; 20 | } 21 | 22 | public String getGroupName() { 23 | return group; 24 | } 25 | 26 | public List getNamesList() { 27 | return messages; 28 | } 29 | 30 | public boolean isShuffle() { 31 | return shuffle; 32 | } 33 | 34 | public int getUpdateTime() { 35 | return updateTime; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/ua/coolboy/f3name/core/F3Name.java: -------------------------------------------------------------------------------- 1 | package ua.coolboy.f3name.core; 2 | 3 | import java.util.Collection; 4 | import java.util.UUID; 5 | 6 | public interface F3Name { 7 | 8 | /** 9 | * Gets plugin {@link LoggerUtil} 10 | * @return LoggerUtil for logging on behalf of the plugin 11 | */ 12 | public LoggerUtil getLoggerUtil(); 13 | 14 | /** 15 | * Gets {@link ServerType} 16 | * @return ServerType to understand on what platform plugin is loaded 17 | */ 18 | public ServerType getServerType(); 19 | 20 | /** 21 | * Gets plugin {@link ConfigParser} 22 | * @return ConfigParser that used to parse config 23 | */ 24 | public ConfigParser getConfigParser(); 25 | 26 | /** 27 | * Gets {@link F3Runnable} list 28 | * @return List of F3Runnable, each runnable serves one group 29 | */ 30 | public Collection getRunnables(); 31 | 32 | /** 33 | * Replaces placeholders and sends server brand to player 34 | * @param uuid player UUID 35 | * @param brand string to send 36 | */ 37 | public void send(UUID uuid, String brand); 38 | 39 | /** 40 | * Sends server brand to player without editing text 41 | * @param uuid player UUID 42 | * @param brand string to send 43 | */ 44 | public void sendRaw(UUID uuid, String brand); 45 | 46 | /** 47 | * Logger prefix 48 | */ 49 | public static final String PREFIX = "§3[F3Name] §r"; 50 | /** 51 | * Resource ID for updationg 52 | */ 53 | public static final int RESOURCE_ID = 58997; 54 | 55 | /** 56 | * Minecraft brand channel 57 | */ 58 | public static final String BRAND_CHANNEL = "minecraft:brand"; 59 | 60 | /** 61 | * Plugin channel 62 | */ 63 | public static final String PLUGIN_CHANNEL = "bukkit:f3name"; 64 | 65 | public enum ServerType { 66 | /** 67 | * Represents Bukkit server 68 | */ 69 | BUKKIT, 70 | /** 71 | * Represents BungeeCord server 72 | */ 73 | BUNGEE; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/main/java/ua/coolboy/f3name/core/F3Runnable.java: -------------------------------------------------------------------------------- 1 | package ua.coolboy.f3name.core; 2 | 3 | import java.util.List; 4 | 5 | public interface F3Runnable extends Runnable{ 6 | 7 | public F3Group getGroup(); 8 | 9 | public String getCurrentString(); 10 | 11 | public List getStrings(); 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/ua/coolboy/f3name/core/LoggerUtil.java: -------------------------------------------------------------------------------- 1 | package ua.coolboy.f3name.core; 2 | 3 | public interface LoggerUtil { 4 | public void info(Object obj); 5 | public void error(Object obj); 6 | public void error(Object obj, Throwable t); 7 | public void printStacktrace(Exception ex); 8 | public void setColoredConsole(boolean colored); 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/ua/coolboy/f3name/core/PacketSerializer.java: -------------------------------------------------------------------------------- 1 | package ua.coolboy.f3name.core; 2 | 3 | import io.netty.buffer.ByteBuf; 4 | import io.netty.buffer.Unpooled; 5 | import java.nio.charset.StandardCharsets; 6 | 7 | public class PacketSerializer { 8 | 9 | private ByteBuf buf; 10 | private byte[] result; 11 | 12 | public PacketSerializer(String string) { 13 | buf = Unpooled.buffer(); 14 | /*byte[] str = string.getBytes(StandardCharsets.UTF_8); 15 | buf.writeByte(str.length); 16 | buf.writeBytes(str);*/ 17 | writeString(string, buf); 18 | result = buf.array(); 19 | buf.release(); 20 | } 21 | 22 | //wiki.vg methods 23 | private void writeString(String s, ByteBuf buf) { 24 | if (s.length() > Short.MAX_VALUE) { 25 | throw new IllegalArgumentException(String.format("Cannot send string longer than Short.MAX_VALUE (got %s characters)", s.length())); 26 | } 27 | 28 | byte[] b = s.getBytes(StandardCharsets.UTF_8); 29 | writeVarInt(b.length, buf); 30 | buf.writeBytes(b); 31 | 32 | } 33 | 34 | private void writeVarInt(int value, ByteBuf output) { 35 | int part; 36 | while (true) { 37 | part = value & 0x7F; 38 | 39 | value >>>= 7; 40 | if (value != 0) { 41 | part |= 0x80; 42 | } 43 | 44 | output.writeByte(part); 45 | 46 | if (value == 0) { 47 | break; 48 | } 49 | } 50 | } 51 | 52 | public byte[] toArray() { 53 | return result; 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/ua/coolboy/f3name/core/hooks/ILuckPermsHook.java: -------------------------------------------------------------------------------- 1 | package ua.coolboy.f3name.core.hooks; 2 | 3 | import java.util.UUID; 4 | 5 | public interface ILuckPermsHook { 6 | /* 7 | Better than Vault, we can get best group from already existed in config with sort by weight 8 | */ 9 | public String getBestPlayerGroup(UUID uuid); 10 | 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/ua/coolboy/f3name/core/hooks/LP5.java: -------------------------------------------------------------------------------- 1 | package ua.coolboy.f3name.core.hooks; 2 | 3 | import java.util.Comparator; 4 | import java.util.List; 5 | import java.util.Objects; 6 | import java.util.Optional; 7 | import java.util.OptionalInt; 8 | import java.util.UUID; 9 | import java.util.stream.Collectors; 10 | import net.luckperms.api.LuckPerms; 11 | import net.luckperms.api.LuckPermsProvider; 12 | import net.luckperms.api.model.group.Group; 13 | import net.luckperms.api.model.user.User; 14 | import net.luckperms.api.node.NodeType; 15 | import ua.coolboy.f3name.core.F3Group; 16 | 17 | public class LP5 implements ILuckPermsHook{ 18 | 19 | private LuckPerms api; 20 | private List groups; 21 | 22 | public LP5(List groups) { 23 | this.api = LuckPermsProvider.get(); 24 | this.groups = groups.stream().map(F3Group::getGroupName).collect(Collectors.toList()); 25 | } 26 | 27 | public static Comparator GROUP_COMPARATOR = (Group o1, Group o2) -> { 28 | OptionalInt obj1 = o1 == null ? OptionalInt.empty() : o1.getWeight(); 29 | OptionalInt obj2 = o2 == null ? OptionalInt.empty() : o2.getWeight(); 30 | if (obj1.isPresent() && obj2.isPresent()) { 31 | return obj1.getAsInt() - obj2.getAsInt(); 32 | } else if (obj1.isPresent()) { 33 | return -1; 34 | } else if (obj2.isPresent()) { 35 | return 1; 36 | } else { 37 | return 0; 38 | } 39 | }; 40 | 41 | @Override 42 | public String getBestPlayerGroup(UUID uuid) { 43 | User user = api.getUserManager().getUser(uuid); 44 | if (user == null) { 45 | return F3Group.DEFAULT_GROUP; 46 | } 47 | //Wrong sorting fixed, thanks to runescapejohn 48 | Optional group = user.getNodes().stream() 49 | .filter(NodeType.INHERITANCE::matches) 50 | .map(NodeType.INHERITANCE::cast) 51 | .map(node -> api.getGroupManager().getGroup(node.getGroupName())) 52 | //Filter non-existing groups 53 | .filter(Objects::nonNull) 54 | //getting only groups in config 55 | .filter(n -> groups.contains(n.getName())) 56 | //max by weight 57 | .max(GROUP_COMPARATOR); 58 | 59 | if (!group.isPresent()) { 60 | return F3Group.DEFAULT_GROUP; 61 | } 62 | return group.get().getName(); 63 | } 64 | 65 | } 66 | -------------------------------------------------------------------------------- /src/main/java/ua/coolboy/f3name/core/hooks/LuckPermsHook.java: -------------------------------------------------------------------------------- 1 | package ua.coolboy.f3name.core.hooks; 2 | 3 | import java.util.List; 4 | import java.util.logging.Logger; 5 | import ua.coolboy.f3name.core.F3Group; 6 | import ua.coolboy.f3name.core.F3Name; 7 | 8 | public abstract class LuckPermsHook { 9 | 10 | public final static ILuckPermsHook get(List groups) { 11 | try{ 12 | Class.forName("net.luckperms.api.LuckPerms"); 13 | return new LP5(groups); 14 | } catch(ClassNotFoundException ex) {} 15 | 16 | try{ 17 | Class.forName("me.lucko.luckperms.LuckPerms"); 18 | Logger.getLogger(F3Name.PREFIX).severe(F3Name.PREFIX + "Found old LuckPerms 4.0. Groups would not work!"); 19 | } catch(ClassNotFoundException ex) {} 20 | return null; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/ua/coolboy/f3name/hooks/BungeePlaceholders.java: -------------------------------------------------------------------------------- 1 | package ua.coolboy.f3name.core.hooks.bungee; 2 | 3 | import net.md_5.bungee.api.connection.ProxiedPlayer; 4 | import net.md_5.bungee.api.connection.Server; 5 | 6 | //meh... i don't think that this is a hook, but this package is better for it 7 | public class BungeePlaceholders { 8 | 9 | public static String setPlaceholders(ProxiedPlayer player, String string) { 10 | string = string.replace("%player_name%", catchNull(player.getName())); 11 | string = string.replace("%player_displayname%", catchNull(player.getDisplayName())); 12 | string = string.replace("%player_uuid%", catchNull(player.getUniqueId().toString())); 13 | 14 | string = string.replace("%player_ping%", catchNull(Integer.toString(player.getPing()))); 15 | Server server = player.getServer(); 16 | if (server != null) { 17 | string = string.replace("%server_name%", catchNull(server.getInfo().getName())); 18 | string = string.replace("%server_motd%", catchNull(server.getInfo().getMotd())); 19 | } 20 | return string; 21 | } 22 | 23 | private static String catchNull(String string) { 24 | return string == null ? "null" : string; 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/ua/coolboy/f3name/metrics/BukkitMetrics.java: -------------------------------------------------------------------------------- 1 | package ua.coolboy.f3name.metrics; 2 | 3 | import java.io.BufferedReader; 4 | import java.io.ByteArrayOutputStream; 5 | import java.io.DataOutputStream; 6 | import java.io.File; 7 | import java.io.IOException; 8 | import java.io.InputStreamReader; 9 | import java.lang.reflect.Method; 10 | import java.net.URL; 11 | import java.nio.charset.StandardCharsets; 12 | import java.util.Arrays; 13 | import java.util.Collection; 14 | import java.util.HashSet; 15 | import java.util.Map; 16 | import java.util.Objects; 17 | import java.util.Set; 18 | import java.util.UUID; 19 | import java.util.concurrent.Callable; 20 | import java.util.concurrent.Executors; 21 | import java.util.concurrent.ScheduledExecutorService; 22 | import java.util.concurrent.TimeUnit; 23 | import java.util.function.BiConsumer; 24 | import java.util.function.Consumer; 25 | import java.util.function.Supplier; 26 | import java.util.logging.Level; 27 | import java.util.stream.Collectors; 28 | import java.util.zip.GZIPOutputStream; 29 | import javax.net.ssl.HttpsURLConnection; 30 | import org.bukkit.Bukkit; 31 | import org.bukkit.configuration.file.YamlConfiguration; 32 | import org.bukkit.entity.Player; 33 | import org.bukkit.plugin.Plugin; 34 | import org.bukkit.plugin.java.JavaPlugin; 35 | 36 | public class BukkitMetrics { 37 | 38 | private final Plugin plugin; 39 | 40 | private final MetricsBase metricsBase; 41 | 42 | /** 43 | * Creates a new Metrics instance. 44 | * 45 | * @param plugin Your plugin instance. 46 | * @param serviceId The id of the service. It can be found at What is my plugin id? 48 | */ 49 | public BukkitMetrics(JavaPlugin plugin, int serviceId) { 50 | this.plugin = plugin; 51 | // Get the config file 52 | File bStatsFolder = new File(plugin.getDataFolder().getParentFile(), "bStats"); 53 | File configFile = new File(bStatsFolder, "config.yml"); 54 | YamlConfiguration config = YamlConfiguration.loadConfiguration(configFile); 55 | if (!config.isSet("serverUuid")) { 56 | config.addDefault("enabled", true); 57 | config.addDefault("serverUuid", UUID.randomUUID().toString()); 58 | config.addDefault("logFailedRequests", false); 59 | config.addDefault("logSentData", false); 60 | config.addDefault("logResponseStatusText", false); 61 | // Inform the server owners about bStats 62 | config 63 | .options() 64 | .header( 65 | "bStats (https://bStats.org) collects some basic information for plugin authors, like how\n" 66 | + "many people use their plugin and their total player count. It's recommended to keep bStats\n" 67 | + "enabled, but if you're not comfortable with this, you can turn this setting off. There is no\n" 68 | + "performance penalty associated with having metrics enabled, and data sent to bStats is fully\n" 69 | + "anonymous.") 70 | .copyDefaults(true); 71 | try { 72 | config.save(configFile); 73 | } catch (IOException ignored) { 74 | } 75 | } 76 | // Load the data 77 | boolean enabled = config.getBoolean("enabled", true); 78 | String serverUUID = config.getString("serverUuid"); 79 | boolean logErrors = config.getBoolean("logFailedRequests", false); 80 | boolean logSentData = config.getBoolean("logSentData", false); 81 | boolean logResponseStatusText = config.getBoolean("logResponseStatusText", false); 82 | metricsBase = 83 | new MetricsBase( 84 | "bukkit", 85 | serverUUID, 86 | serviceId, 87 | enabled, 88 | this::appendPlatformData, 89 | this::appendServiceData, 90 | submitDataTask -> Bukkit.getScheduler().runTask(plugin, submitDataTask), 91 | plugin::isEnabled, 92 | (message, error) -> this.plugin.getLogger().log(Level.WARNING, message, error), 93 | (message) -> this.plugin.getLogger().log(Level.INFO, message), 94 | logErrors, 95 | logSentData, 96 | logResponseStatusText); 97 | } 98 | 99 | /** 100 | * Adds a custom chart. 101 | * 102 | * @param chart The chart to add. 103 | */ 104 | public void addCustomChart(CustomChart chart) { 105 | metricsBase.addCustomChart(chart); 106 | } 107 | 108 | private void appendPlatformData(JsonObjectBuilder builder) { 109 | builder.appendField("playerAmount", getPlayerAmount()); 110 | builder.appendField("onlineMode", Bukkit.getOnlineMode() ? 1 : 0); 111 | builder.appendField("bukkitVersion", Bukkit.getVersion()); 112 | builder.appendField("bukkitName", Bukkit.getName()); 113 | builder.appendField("javaVersion", System.getProperty("java.version")); 114 | builder.appendField("osName", System.getProperty("os.name")); 115 | builder.appendField("osArch", System.getProperty("os.arch")); 116 | builder.appendField("osVersion", System.getProperty("os.version")); 117 | builder.appendField("coreCount", Runtime.getRuntime().availableProcessors()); 118 | } 119 | 120 | private void appendServiceData(JsonObjectBuilder builder) { 121 | builder.appendField("pluginVersion", plugin.getDescription().getVersion()); 122 | } 123 | 124 | private int getPlayerAmount() { 125 | try { 126 | // Around MC 1.8 the return type was changed from an array to a collection, 127 | // This fixes java.lang.NoSuchMethodError: 128 | // org.bukkit.Bukkit.getOnlinePlayers()Ljava/util/Collection; 129 | Method onlinePlayersMethod = Class.forName("org.bukkit.Server").getMethod("getOnlinePlayers"); 130 | return onlinePlayersMethod.getReturnType().equals(Collection.class) 131 | ? ((Collection) onlinePlayersMethod.invoke(Bukkit.getServer())).size() 132 | : ((Player[]) onlinePlayersMethod.invoke(Bukkit.getServer())).length; 133 | } catch (Exception e) { 134 | // Just use the new method if the reflection failed 135 | return Bukkit.getOnlinePlayers().size(); 136 | } 137 | } 138 | 139 | public static class MetricsBase { 140 | 141 | /** The version of the Metrics class. */ 142 | public static final String METRICS_VERSION = "2.2.1"; 143 | 144 | private static final ScheduledExecutorService scheduler = 145 | Executors.newScheduledThreadPool(1, task -> new Thread(task, "bStats-Metrics")); 146 | 147 | private static final String REPORT_URL = "https://bStats.org/api/v2/data/%s"; 148 | 149 | private final String platform; 150 | 151 | private final String serverUuid; 152 | 153 | private final int serviceId; 154 | 155 | private final Consumer appendPlatformDataConsumer; 156 | 157 | private final Consumer appendServiceDataConsumer; 158 | 159 | private final Consumer submitTaskConsumer; 160 | 161 | private final Supplier checkServiceEnabledSupplier; 162 | 163 | private final BiConsumer errorLogger; 164 | 165 | private final Consumer infoLogger; 166 | 167 | private final boolean logErrors; 168 | 169 | private final boolean logSentData; 170 | 171 | private final boolean logResponseStatusText; 172 | 173 | private final Set customCharts = new HashSet<>(); 174 | 175 | private final boolean enabled; 176 | 177 | /** 178 | * Creates a new MetricsBase class instance. 179 | * 180 | * @param platform The platform of the service. 181 | * @param serviceId The id of the service. 182 | * @param serverUuid The server uuid. 183 | * @param enabled Whether or not data sending is enabled. 184 | * @param appendPlatformDataConsumer A consumer that receives a {@code JsonObjectBuilder} and 185 | * appends all platform-specific data. 186 | * @param appendServiceDataConsumer A consumer that receives a {@code JsonObjectBuilder} and 187 | * appends all service-specific data. 188 | * @param submitTaskConsumer A consumer that takes a runnable with the submit task. This can be 189 | * used to delegate the data collection to a another thread to prevent errors caused by 190 | * concurrency. Can be {@code null}. 191 | * @param checkServiceEnabledSupplier A supplier to check if the service is still enabled. 192 | * @param errorLogger A consumer that accepts log message and an error. 193 | * @param infoLogger A consumer that accepts info log messages. 194 | * @param logErrors Whether or not errors should be logged. 195 | * @param logSentData Whether or not the sent data should be logged. 196 | * @param logResponseStatusText Whether or not the response status text should be logged. 197 | */ 198 | public MetricsBase( 199 | String platform, 200 | String serverUuid, 201 | int serviceId, 202 | boolean enabled, 203 | Consumer appendPlatformDataConsumer, 204 | Consumer appendServiceDataConsumer, 205 | Consumer submitTaskConsumer, 206 | Supplier checkServiceEnabledSupplier, 207 | BiConsumer errorLogger, 208 | Consumer infoLogger, 209 | boolean logErrors, 210 | boolean logSentData, 211 | boolean logResponseStatusText) { 212 | this.platform = platform; 213 | this.serverUuid = serverUuid; 214 | this.serviceId = serviceId; 215 | this.enabled = enabled; 216 | this.appendPlatformDataConsumer = appendPlatformDataConsumer; 217 | this.appendServiceDataConsumer = appendServiceDataConsumer; 218 | this.submitTaskConsumer = submitTaskConsumer; 219 | this.checkServiceEnabledSupplier = checkServiceEnabledSupplier; 220 | this.errorLogger = errorLogger; 221 | this.infoLogger = infoLogger; 222 | this.logErrors = logErrors; 223 | this.logSentData = logSentData; 224 | this.logResponseStatusText = logResponseStatusText; 225 | checkRelocation(); 226 | if (enabled) { 227 | startSubmitting(); 228 | } 229 | } 230 | 231 | public void addCustomChart(CustomChart chart) { 232 | this.customCharts.add(chart); 233 | } 234 | 235 | private void startSubmitting() { 236 | final Runnable submitTask = 237 | () -> { 238 | if (!enabled || !checkServiceEnabledSupplier.get()) { 239 | // Submitting data or service is disabled 240 | scheduler.shutdown(); 241 | return; 242 | } 243 | if (submitTaskConsumer != null) { 244 | submitTaskConsumer.accept(this::submitData); 245 | } else { 246 | this.submitData(); 247 | } 248 | }; 249 | // Many servers tend to restart at a fixed time at xx:00 which causes an uneven distribution 250 | // of requests on the 251 | // bStats backend. To circumvent this problem, we introduce some randomness into the initial 252 | // and second delay. 253 | // WARNING: You must not modify and part of this Metrics class, including the submit delay or 254 | // frequency! 255 | // WARNING: Modifying this code will get your plugin banned on bStats. Just don't do it! 256 | long initialDelay = (long) (1000 * 60 * (3 + Math.random() * 3)); 257 | long secondDelay = (long) (1000 * 60 * (Math.random() * 30)); 258 | scheduler.schedule(submitTask, initialDelay, TimeUnit.MILLISECONDS); 259 | scheduler.scheduleAtFixedRate( 260 | submitTask, initialDelay + secondDelay, 1000 * 60 * 30, TimeUnit.MILLISECONDS); 261 | } 262 | 263 | private void submitData() { 264 | final JsonObjectBuilder baseJsonBuilder = new JsonObjectBuilder(); 265 | appendPlatformDataConsumer.accept(baseJsonBuilder); 266 | final JsonObjectBuilder serviceJsonBuilder = new JsonObjectBuilder(); 267 | appendServiceDataConsumer.accept(serviceJsonBuilder); 268 | JsonObjectBuilder.JsonObject[] chartData = 269 | customCharts.stream() 270 | .map(customChart -> customChart.getRequestJsonObject(errorLogger, logErrors)) 271 | .filter(Objects::nonNull) 272 | .toArray(JsonObjectBuilder.JsonObject[]::new); 273 | serviceJsonBuilder.appendField("id", serviceId); 274 | serviceJsonBuilder.appendField("customCharts", chartData); 275 | baseJsonBuilder.appendField("service", serviceJsonBuilder.build()); 276 | baseJsonBuilder.appendField("serverUUID", serverUuid); 277 | baseJsonBuilder.appendField("metricsVersion", METRICS_VERSION); 278 | JsonObjectBuilder.JsonObject data = baseJsonBuilder.build(); 279 | scheduler.execute( 280 | () -> { 281 | try { 282 | // Send the data 283 | sendData(data); 284 | } catch (Exception e) { 285 | // Something went wrong! :( 286 | if (logErrors) { 287 | errorLogger.accept("Could not submit bStats metrics data", e); 288 | } 289 | } 290 | }); 291 | } 292 | 293 | private void sendData(JsonObjectBuilder.JsonObject data) throws Exception { 294 | if (logSentData) { 295 | infoLogger.accept("Sent bStats metrics data: " + data.toString()); 296 | } 297 | String url = String.format(REPORT_URL, platform); 298 | HttpsURLConnection connection = (HttpsURLConnection) new URL(url).openConnection(); 299 | // Compress the data to save bandwidth 300 | byte[] compressedData = compress(data.toString()); 301 | connection.setRequestMethod("POST"); 302 | connection.addRequestProperty("Accept", "application/json"); 303 | connection.addRequestProperty("Connection", "close"); 304 | connection.addRequestProperty("Content-Encoding", "gzip"); 305 | connection.addRequestProperty("Content-Length", String.valueOf(compressedData.length)); 306 | connection.setRequestProperty("Content-Type", "application/json"); 307 | connection.setRequestProperty("User-Agent", "Metrics-Service/1"); 308 | connection.setDoOutput(true); 309 | try (DataOutputStream outputStream = new DataOutputStream(connection.getOutputStream())) { 310 | outputStream.write(compressedData); 311 | } 312 | StringBuilder builder = new StringBuilder(); 313 | try (BufferedReader bufferedReader = 314 | new BufferedReader(new InputStreamReader(connection.getInputStream()))) { 315 | String line; 316 | while ((line = bufferedReader.readLine()) != null) { 317 | builder.append(line); 318 | } 319 | } 320 | if (logResponseStatusText) { 321 | infoLogger.accept("Sent data to bStats and received response: " + builder); 322 | } 323 | } 324 | 325 | /** Checks that the class was properly relocated. */ 326 | private void checkRelocation() { 327 | // You can use the property to disable the check in your test environment 328 | if (System.getProperty("bstats.relocatecheck") == null 329 | || !System.getProperty("bstats.relocatecheck").equals("false")) { 330 | // Maven's Relocate is clever and changes strings, too. So we have to use this little 331 | // "trick" ... :D 332 | final String defaultPackage = 333 | new String(new byte[] {'o', 'r', 'g', '.', 'b', 's', 't', 'a', 't', 's'}); 334 | final String examplePackage = 335 | new String(new byte[] {'y', 'o', 'u', 'r', '.', 'p', 'a', 'c', 'k', 'a', 'g', 'e'}); 336 | // We want to make sure no one just copy & pastes the example and uses the wrong package 337 | // names 338 | if (MetricsBase.class.getPackage().getName().startsWith(defaultPackage) 339 | || MetricsBase.class.getPackage().getName().startsWith(examplePackage)) { 340 | throw new IllegalStateException("bStats Metrics class has not been relocated correctly!"); 341 | } 342 | } 343 | } 344 | 345 | /** 346 | * Gzips the given string. 347 | * 348 | * @param str The string to gzip. 349 | * @return The gzipped string. 350 | */ 351 | private static byte[] compress(final String str) throws IOException { 352 | if (str == null) { 353 | return null; 354 | } 355 | ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); 356 | try (GZIPOutputStream gzip = new GZIPOutputStream(outputStream)) { 357 | gzip.write(str.getBytes(StandardCharsets.UTF_8)); 358 | } 359 | return outputStream.toByteArray(); 360 | } 361 | } 362 | 363 | public static class AdvancedBarChart extends CustomChart { 364 | 365 | private final Callable> callable; 366 | 367 | /** 368 | * Class constructor. 369 | * 370 | * @param chartId The id of the chart. 371 | * @param callable The callable which is used to request the chart data. 372 | */ 373 | public AdvancedBarChart(String chartId, Callable> callable) { 374 | super(chartId); 375 | this.callable = callable; 376 | } 377 | 378 | @Override 379 | protected JsonObjectBuilder.JsonObject getChartData() throws Exception { 380 | JsonObjectBuilder valuesBuilder = new JsonObjectBuilder(); 381 | Map map = callable.call(); 382 | if (map == null || map.isEmpty()) { 383 | // Null = skip the chart 384 | return null; 385 | } 386 | boolean allSkipped = true; 387 | for (Map.Entry entry : map.entrySet()) { 388 | if (entry.getValue().length == 0) { 389 | // Skip this invalid 390 | continue; 391 | } 392 | allSkipped = false; 393 | valuesBuilder.appendField(entry.getKey(), entry.getValue()); 394 | } 395 | if (allSkipped) { 396 | // Null = skip the chart 397 | return null; 398 | } 399 | return new JsonObjectBuilder().appendField("values", valuesBuilder.build()).build(); 400 | } 401 | } 402 | 403 | public static class SimpleBarChart extends CustomChart { 404 | 405 | private final Callable> callable; 406 | 407 | /** 408 | * Class constructor. 409 | * 410 | * @param chartId The id of the chart. 411 | * @param callable The callable which is used to request the chart data. 412 | */ 413 | public SimpleBarChart(String chartId, Callable> callable) { 414 | super(chartId); 415 | this.callable = callable; 416 | } 417 | 418 | @Override 419 | protected JsonObjectBuilder.JsonObject getChartData() throws Exception { 420 | JsonObjectBuilder valuesBuilder = new JsonObjectBuilder(); 421 | Map map = callable.call(); 422 | if (map == null || map.isEmpty()) { 423 | // Null = skip the chart 424 | return null; 425 | } 426 | for (Map.Entry entry : map.entrySet()) { 427 | valuesBuilder.appendField(entry.getKey(), new int[] {entry.getValue()}); 428 | } 429 | return new JsonObjectBuilder().appendField("values", valuesBuilder.build()).build(); 430 | } 431 | } 432 | 433 | public static class MultiLineChart extends CustomChart { 434 | 435 | private final Callable> callable; 436 | 437 | /** 438 | * Class constructor. 439 | * 440 | * @param chartId The id of the chart. 441 | * @param callable The callable which is used to request the chart data. 442 | */ 443 | public MultiLineChart(String chartId, Callable> callable) { 444 | super(chartId); 445 | this.callable = callable; 446 | } 447 | 448 | @Override 449 | protected JsonObjectBuilder.JsonObject getChartData() throws Exception { 450 | JsonObjectBuilder valuesBuilder = new JsonObjectBuilder(); 451 | Map map = callable.call(); 452 | if (map == null || map.isEmpty()) { 453 | // Null = skip the chart 454 | return null; 455 | } 456 | boolean allSkipped = true; 457 | for (Map.Entry entry : map.entrySet()) { 458 | if (entry.getValue() == 0) { 459 | // Skip this invalid 460 | continue; 461 | } 462 | allSkipped = false; 463 | valuesBuilder.appendField(entry.getKey(), entry.getValue()); 464 | } 465 | if (allSkipped) { 466 | // Null = skip the chart 467 | return null; 468 | } 469 | return new JsonObjectBuilder().appendField("values", valuesBuilder.build()).build(); 470 | } 471 | } 472 | 473 | public static class AdvancedPie extends CustomChart { 474 | 475 | private final Callable> callable; 476 | 477 | /** 478 | * Class constructor. 479 | * 480 | * @param chartId The id of the chart. 481 | * @param callable The callable which is used to request the chart data. 482 | */ 483 | public AdvancedPie(String chartId, Callable> callable) { 484 | super(chartId); 485 | this.callable = callable; 486 | } 487 | 488 | @Override 489 | protected JsonObjectBuilder.JsonObject getChartData() throws Exception { 490 | JsonObjectBuilder valuesBuilder = new JsonObjectBuilder(); 491 | Map map = callable.call(); 492 | if (map == null || map.isEmpty()) { 493 | // Null = skip the chart 494 | return null; 495 | } 496 | boolean allSkipped = true; 497 | for (Map.Entry entry : map.entrySet()) { 498 | if (entry.getValue() == 0) { 499 | // Skip this invalid 500 | continue; 501 | } 502 | allSkipped = false; 503 | valuesBuilder.appendField(entry.getKey(), entry.getValue()); 504 | } 505 | if (allSkipped) { 506 | // Null = skip the chart 507 | return null; 508 | } 509 | return new JsonObjectBuilder().appendField("values", valuesBuilder.build()).build(); 510 | } 511 | } 512 | 513 | public abstract static class CustomChart { 514 | 515 | private final String chartId; 516 | 517 | protected CustomChart(String chartId) { 518 | if (chartId == null) { 519 | throw new IllegalArgumentException("chartId must not be null"); 520 | } 521 | this.chartId = chartId; 522 | } 523 | 524 | public JsonObjectBuilder.JsonObject getRequestJsonObject( 525 | BiConsumer errorLogger, boolean logErrors) { 526 | JsonObjectBuilder builder = new JsonObjectBuilder(); 527 | builder.appendField("chartId", chartId); 528 | try { 529 | JsonObjectBuilder.JsonObject data = getChartData(); 530 | if (data == null) { 531 | // If the data is null we don't send the chart. 532 | return null; 533 | } 534 | builder.appendField("data", data); 535 | } catch (Throwable t) { 536 | if (logErrors) { 537 | errorLogger.accept("Failed to get data for custom chart with id " + chartId, t); 538 | } 539 | return null; 540 | } 541 | return builder.build(); 542 | } 543 | 544 | protected abstract JsonObjectBuilder.JsonObject getChartData() throws Exception; 545 | } 546 | 547 | public static class SingleLineChart extends CustomChart { 548 | 549 | private final Callable callable; 550 | 551 | /** 552 | * Class constructor. 553 | * 554 | * @param chartId The id of the chart. 555 | * @param callable The callable which is used to request the chart data. 556 | */ 557 | public SingleLineChart(String chartId, Callable callable) { 558 | super(chartId); 559 | this.callable = callable; 560 | } 561 | 562 | @Override 563 | protected JsonObjectBuilder.JsonObject getChartData() throws Exception { 564 | int value = callable.call(); 565 | if (value == 0) { 566 | // Null = skip the chart 567 | return null; 568 | } 569 | return new JsonObjectBuilder().appendField("value", value).build(); 570 | } 571 | } 572 | 573 | public static class SimplePie extends CustomChart { 574 | 575 | private final Callable callable; 576 | 577 | /** 578 | * Class constructor. 579 | * 580 | * @param chartId The id of the chart. 581 | * @param callable The callable which is used to request the chart data. 582 | */ 583 | public SimplePie(String chartId, Callable callable) { 584 | super(chartId); 585 | this.callable = callable; 586 | } 587 | 588 | @Override 589 | protected JsonObjectBuilder.JsonObject getChartData() throws Exception { 590 | String value = callable.call(); 591 | if (value == null || value.isEmpty()) { 592 | // Null = skip the chart 593 | return null; 594 | } 595 | return new JsonObjectBuilder().appendField("value", value).build(); 596 | } 597 | } 598 | 599 | public static class DrilldownPie extends CustomChart { 600 | 601 | private final Callable>> callable; 602 | 603 | /** 604 | * Class constructor. 605 | * 606 | * @param chartId The id of the chart. 607 | * @param callable The callable which is used to request the chart data. 608 | */ 609 | public DrilldownPie(String chartId, Callable>> callable) { 610 | super(chartId); 611 | this.callable = callable; 612 | } 613 | 614 | @Override 615 | public JsonObjectBuilder.JsonObject getChartData() throws Exception { 616 | JsonObjectBuilder valuesBuilder = new JsonObjectBuilder(); 617 | Map> map = callable.call(); 618 | if (map == null || map.isEmpty()) { 619 | // Null = skip the chart 620 | return null; 621 | } 622 | boolean reallyAllSkipped = true; 623 | for (Map.Entry> entryValues : map.entrySet()) { 624 | JsonObjectBuilder valueBuilder = new JsonObjectBuilder(); 625 | boolean allSkipped = true; 626 | for (Map.Entry valueEntry : map.get(entryValues.getKey()).entrySet()) { 627 | valueBuilder.appendField(valueEntry.getKey(), valueEntry.getValue()); 628 | allSkipped = false; 629 | } 630 | if (!allSkipped) { 631 | reallyAllSkipped = false; 632 | valuesBuilder.appendField(entryValues.getKey(), valueBuilder.build()); 633 | } 634 | } 635 | if (reallyAllSkipped) { 636 | // Null = skip the chart 637 | return null; 638 | } 639 | return new JsonObjectBuilder().appendField("values", valuesBuilder.build()).build(); 640 | } 641 | } 642 | 643 | /** 644 | * An extremely simple JSON builder. 645 | * 646 | *

While this class is neither feature-rich nor the most performant one, it's sufficient enough 647 | * for its use-case. 648 | */ 649 | public static class JsonObjectBuilder { 650 | 651 | private StringBuilder builder = new StringBuilder(); 652 | 653 | private boolean hasAtLeastOneField = false; 654 | 655 | public JsonObjectBuilder() { 656 | builder.append("{"); 657 | } 658 | 659 | /** 660 | * Appends a null field to the JSON. 661 | * 662 | * @param key The key of the field. 663 | * @return A reference to this object. 664 | */ 665 | public JsonObjectBuilder appendNull(String key) { 666 | appendFieldUnescaped(key, "null"); 667 | return this; 668 | } 669 | 670 | /** 671 | * Appends a string field to the JSON. 672 | * 673 | * @param key The key of the field. 674 | * @param value The value of the field. 675 | * @return A reference to this object. 676 | */ 677 | public JsonObjectBuilder appendField(String key, String value) { 678 | if (value == null) { 679 | throw new IllegalArgumentException("JSON value must not be null"); 680 | } 681 | appendFieldUnescaped(key, "\"" + escape(value) + "\""); 682 | return this; 683 | } 684 | 685 | /** 686 | * Appends an integer field to the JSON. 687 | * 688 | * @param key The key of the field. 689 | * @param value The value of the field. 690 | * @return A reference to this object. 691 | */ 692 | public JsonObjectBuilder appendField(String key, int value) { 693 | appendFieldUnescaped(key, String.valueOf(value)); 694 | return this; 695 | } 696 | 697 | /** 698 | * Appends an object to the JSON. 699 | * 700 | * @param key The key of the field. 701 | * @param object The object. 702 | * @return A reference to this object. 703 | */ 704 | public JsonObjectBuilder appendField(String key, JsonObject object) { 705 | if (object == null) { 706 | throw new IllegalArgumentException("JSON object must not be null"); 707 | } 708 | appendFieldUnescaped(key, object.toString()); 709 | return this; 710 | } 711 | 712 | /** 713 | * Appends a string array to the JSON. 714 | * 715 | * @param key The key of the field. 716 | * @param values The string array. 717 | * @return A reference to this object. 718 | */ 719 | public JsonObjectBuilder appendField(String key, String[] values) { 720 | if (values == null) { 721 | throw new IllegalArgumentException("JSON values must not be null"); 722 | } 723 | String escapedValues = 724 | Arrays.stream(values) 725 | .map(value -> "\"" + escape(value) + "\"") 726 | .collect(Collectors.joining(",")); 727 | appendFieldUnescaped(key, "[" + escapedValues + "]"); 728 | return this; 729 | } 730 | 731 | /** 732 | * Appends an integer array to the JSON. 733 | * 734 | * @param key The key of the field. 735 | * @param values The integer array. 736 | * @return A reference to this object. 737 | */ 738 | public JsonObjectBuilder appendField(String key, int[] values) { 739 | if (values == null) { 740 | throw new IllegalArgumentException("JSON values must not be null"); 741 | } 742 | String escapedValues = 743 | Arrays.stream(values).mapToObj(String::valueOf).collect(Collectors.joining(",")); 744 | appendFieldUnescaped(key, "[" + escapedValues + "]"); 745 | return this; 746 | } 747 | 748 | /** 749 | * Appends an object array to the JSON. 750 | * 751 | * @param key The key of the field. 752 | * @param values The integer array. 753 | * @return A reference to this object. 754 | */ 755 | public JsonObjectBuilder appendField(String key, JsonObject[] values) { 756 | if (values == null) { 757 | throw new IllegalArgumentException("JSON values must not be null"); 758 | } 759 | String escapedValues = 760 | Arrays.stream(values).map(JsonObject::toString).collect(Collectors.joining(",")); 761 | appendFieldUnescaped(key, "[" + escapedValues + "]"); 762 | return this; 763 | } 764 | 765 | /** 766 | * Appends a field to the object. 767 | * 768 | * @param key The key of the field. 769 | * @param escapedValue The escaped value of the field. 770 | */ 771 | private void appendFieldUnescaped(String key, String escapedValue) { 772 | if (builder == null) { 773 | throw new IllegalStateException("JSON has already been built"); 774 | } 775 | if (key == null) { 776 | throw new IllegalArgumentException("JSON key must not be null"); 777 | } 778 | if (hasAtLeastOneField) { 779 | builder.append(","); 780 | } 781 | builder.append("\"").append(escape(key)).append("\":").append(escapedValue); 782 | hasAtLeastOneField = true; 783 | } 784 | 785 | /** 786 | * Builds the JSON string and invalidates this builder. 787 | * 788 | * @return The built JSON string. 789 | */ 790 | public JsonObject build() { 791 | if (builder == null) { 792 | throw new IllegalStateException("JSON has already been built"); 793 | } 794 | JsonObject object = new JsonObject(builder.append("}").toString()); 795 | builder = null; 796 | return object; 797 | } 798 | 799 | /** 800 | * Escapes the given string like stated in https://www.ietf.org/rfc/rfc4627.txt. 801 | * 802 | *

This method escapes only the necessary characters '"', '\'. and '\u0000' - '\u001F'. 803 | * Compact escapes are not used (e.g., '\n' is escaped as "\u000a" and not as "\n"). 804 | * 805 | * @param value The value to escape. 806 | * @return The escaped value. 807 | */ 808 | private static String escape(String value) { 809 | final StringBuilder builder = new StringBuilder(); 810 | for (int i = 0; i < value.length(); i++) { 811 | char c = value.charAt(i); 812 | if (c == '"') { 813 | builder.append("\\\""); 814 | } else if (c == '\\') { 815 | builder.append("\\\\"); 816 | } else if (c <= '\u000F') { 817 | builder.append("\\u000").append(Integer.toHexString(c)); 818 | } else if (c <= '\u001F') { 819 | builder.append("\\u00").append(Integer.toHexString(c)); 820 | } else { 821 | builder.append(c); 822 | } 823 | } 824 | return builder.toString(); 825 | } 826 | 827 | /** 828 | * A super simple representation of a JSON object. 829 | * 830 | *

This class only exists to make methods of the {@link JsonObjectBuilder} type-safe and not 831 | * allow a raw string inputs for methods like {@link JsonObjectBuilder#appendField(String, 832 | * JsonObject)}. 833 | */ 834 | public static class JsonObject { 835 | 836 | private final String value; 837 | 838 | private JsonObject(String value) { 839 | this.value = value; 840 | } 841 | 842 | @Override 843 | public String toString() { 844 | return value; 845 | } 846 | } 847 | } 848 | } -------------------------------------------------------------------------------- /src/main/java/ua/coolboy/f3name/metrics/BungeeMetrics.java: -------------------------------------------------------------------------------- 1 | package ua.coolboy.f3name.metrics; 2 | 3 | import java.io.BufferedReader; 4 | import java.io.BufferedWriter; 5 | import java.io.ByteArrayOutputStream; 6 | import java.io.DataOutputStream; 7 | import java.io.File; 8 | import java.io.FileWriter; 9 | import java.io.IOException; 10 | import java.io.InputStreamReader; 11 | import java.net.URL; 12 | import java.nio.charset.StandardCharsets; 13 | import java.util.Arrays; 14 | import java.util.HashSet; 15 | import java.util.Map; 16 | import java.util.Objects; 17 | import java.util.Set; 18 | import java.util.UUID; 19 | import java.util.concurrent.Callable; 20 | import java.util.concurrent.Executors; 21 | import java.util.concurrent.ScheduledExecutorService; 22 | import java.util.concurrent.TimeUnit; 23 | import java.util.function.BiConsumer; 24 | import java.util.function.Consumer; 25 | import java.util.function.Supplier; 26 | import java.util.logging.Level; 27 | import java.util.stream.Collectors; 28 | import java.util.zip.GZIPOutputStream; 29 | import javax.net.ssl.HttpsURLConnection; 30 | import net.md_5.bungee.api.plugin.Plugin; 31 | import net.md_5.bungee.config.Configuration; 32 | import net.md_5.bungee.config.ConfigurationProvider; 33 | import net.md_5.bungee.config.YamlConfiguration; 34 | 35 | public class BungeeMetrics { 36 | 37 | private final Plugin plugin; 38 | 39 | private final MetricsBase metricsBase; 40 | 41 | private boolean enabled; 42 | 43 | private String serverUUID; 44 | 45 | private boolean logErrors = false; 46 | 47 | private boolean logSentData; 48 | 49 | private boolean logResponseStatusText; 50 | 51 | /** 52 | * Creates a new Metrics instance. 53 | * 54 | * @param plugin Your plugin instance. 55 | * @param serviceId The id of the service. It can be found at What is my plugin id? 57 | */ 58 | public BungeeMetrics(Plugin plugin, int serviceId) { 59 | this.plugin = plugin; 60 | try { 61 | loadConfig(); 62 | } catch (IOException e) { 63 | // Failed to load configuration 64 | plugin.getLogger().log(Level.WARNING, "Failed to load bStats config!", e); 65 | metricsBase = null; 66 | return; 67 | } 68 | metricsBase = 69 | new MetricsBase( 70 | "bungeecord", 71 | serverUUID, 72 | serviceId, 73 | enabled, 74 | this::appendPlatformData, 75 | this::appendServiceData, 76 | null, 77 | () -> true, 78 | (message, error) -> this.plugin.getLogger().log(Level.WARNING, message, error), 79 | (message) -> this.plugin.getLogger().log(Level.INFO, message), 80 | logErrors, 81 | logSentData, 82 | logResponseStatusText); 83 | } 84 | 85 | /** Loads the bStats configuration. */ 86 | private void loadConfig() throws IOException { 87 | File bStatsFolder = new File(plugin.getDataFolder().getParentFile(), "bStats"); 88 | bStatsFolder.mkdirs(); 89 | File configFile = new File(bStatsFolder, "config.yml"); 90 | if (!configFile.exists()) { 91 | writeFile( 92 | configFile, 93 | "# bStats (https://bStats.org) collects some basic information for plugin authors, like how", 94 | "# many people use their plugin and their total player count. It's recommended to keep bStats", 95 | "# enabled, but if you're not comfortable with this, you can turn this setting off. There is no", 96 | "# performance penalty associated with having metrics enabled, and data sent to bStats is fully", 97 | "# anonymous.", 98 | "enabled: true", 99 | "serverUuid: \"" + UUID.randomUUID() + "\"", 100 | "logFailedRequests: false", 101 | "logSentData: false", 102 | "logResponseStatusText: false"); 103 | } 104 | Configuration configuration = 105 | ConfigurationProvider.getProvider(YamlConfiguration.class).load(configFile); 106 | // Load configuration 107 | enabled = configuration.getBoolean("enabled", true); 108 | serverUUID = configuration.getString("serverUuid"); 109 | logErrors = configuration.getBoolean("logFailedRequests", false); 110 | logSentData = configuration.getBoolean("logSentData", false); 111 | logResponseStatusText = configuration.getBoolean("logResponseStatusText", false); 112 | } 113 | 114 | private void writeFile(File file, String... lines) throws IOException { 115 | try (BufferedWriter bufferedWriter = new BufferedWriter(new FileWriter(file))) { 116 | for (String line : lines) { 117 | bufferedWriter.write(line); 118 | bufferedWriter.newLine(); 119 | } 120 | } 121 | } 122 | 123 | /** 124 | * Adds a custom chart. 125 | * 126 | * @param chart The chart to add. 127 | */ 128 | public void addCustomChart(CustomChart chart) { 129 | metricsBase.addCustomChart(chart); 130 | } 131 | 132 | private void appendPlatformData(JsonObjectBuilder builder) { 133 | builder.appendField("playerAmount", plugin.getProxy().getOnlineCount()); 134 | builder.appendField("managedServers", plugin.getProxy().getServers().size()); 135 | builder.appendField("onlineMode", plugin.getProxy().getConfig().isOnlineMode() ? 1 : 0); 136 | builder.appendField("bungeecordVersion", plugin.getProxy().getVersion()); 137 | builder.appendField("javaVersion", System.getProperty("java.version")); 138 | builder.appendField("osName", System.getProperty("os.name")); 139 | builder.appendField("osArch", System.getProperty("os.arch")); 140 | builder.appendField("osVersion", System.getProperty("os.version")); 141 | builder.appendField("coreCount", Runtime.getRuntime().availableProcessors()); 142 | } 143 | 144 | private void appendServiceData(JsonObjectBuilder builder) { 145 | builder.appendField("pluginVersion", plugin.getDescription().getVersion()); 146 | } 147 | 148 | public static class MetricsBase { 149 | 150 | /** The version of the Metrics class. */ 151 | public static final String METRICS_VERSION = "2.2.1"; 152 | 153 | private static final ScheduledExecutorService scheduler = 154 | Executors.newScheduledThreadPool(1, task -> new Thread(task, "bStats-Metrics")); 155 | 156 | private static final String REPORT_URL = "https://bStats.org/api/v2/data/%s"; 157 | 158 | private final String platform; 159 | 160 | private final String serverUuid; 161 | 162 | private final int serviceId; 163 | 164 | private final Consumer appendPlatformDataConsumer; 165 | 166 | private final Consumer appendServiceDataConsumer; 167 | 168 | private final Consumer submitTaskConsumer; 169 | 170 | private final Supplier checkServiceEnabledSupplier; 171 | 172 | private final BiConsumer errorLogger; 173 | 174 | private final Consumer infoLogger; 175 | 176 | private final boolean logErrors; 177 | 178 | private final boolean logSentData; 179 | 180 | private final boolean logResponseStatusText; 181 | 182 | private final Set customCharts = new HashSet<>(); 183 | 184 | private final boolean enabled; 185 | 186 | /** 187 | * Creates a new MetricsBase class instance. 188 | * 189 | * @param platform The platform of the service. 190 | * @param serviceId The id of the service. 191 | * @param serverUuid The server uuid. 192 | * @param enabled Whether or not data sending is enabled. 193 | * @param appendPlatformDataConsumer A consumer that receives a {@code JsonObjectBuilder} and 194 | * appends all platform-specific data. 195 | * @param appendServiceDataConsumer A consumer that receives a {@code JsonObjectBuilder} and 196 | * appends all service-specific data. 197 | * @param submitTaskConsumer A consumer that takes a runnable with the submit task. This can be 198 | * used to delegate the data collection to a another thread to prevent errors caused by 199 | * concurrency. Can be {@code null}. 200 | * @param checkServiceEnabledSupplier A supplier to check if the service is still enabled. 201 | * @param errorLogger A consumer that accepts log message and an error. 202 | * @param infoLogger A consumer that accepts info log messages. 203 | * @param logErrors Whether or not errors should be logged. 204 | * @param logSentData Whether or not the sent data should be logged. 205 | * @param logResponseStatusText Whether or not the response status text should be logged. 206 | */ 207 | public MetricsBase( 208 | String platform, 209 | String serverUuid, 210 | int serviceId, 211 | boolean enabled, 212 | Consumer appendPlatformDataConsumer, 213 | Consumer appendServiceDataConsumer, 214 | Consumer submitTaskConsumer, 215 | Supplier checkServiceEnabledSupplier, 216 | BiConsumer errorLogger, 217 | Consumer infoLogger, 218 | boolean logErrors, 219 | boolean logSentData, 220 | boolean logResponseStatusText) { 221 | this.platform = platform; 222 | this.serverUuid = serverUuid; 223 | this.serviceId = serviceId; 224 | this.enabled = enabled; 225 | this.appendPlatformDataConsumer = appendPlatformDataConsumer; 226 | this.appendServiceDataConsumer = appendServiceDataConsumer; 227 | this.submitTaskConsumer = submitTaskConsumer; 228 | this.checkServiceEnabledSupplier = checkServiceEnabledSupplier; 229 | this.errorLogger = errorLogger; 230 | this.infoLogger = infoLogger; 231 | this.logErrors = logErrors; 232 | this.logSentData = logSentData; 233 | this.logResponseStatusText = logResponseStatusText; 234 | checkRelocation(); 235 | if (enabled) { 236 | startSubmitting(); 237 | } 238 | } 239 | 240 | public void addCustomChart(CustomChart chart) { 241 | this.customCharts.add(chart); 242 | } 243 | 244 | private void startSubmitting() { 245 | final Runnable submitTask = 246 | () -> { 247 | if (!enabled || !checkServiceEnabledSupplier.get()) { 248 | // Submitting data or service is disabled 249 | scheduler.shutdown(); 250 | return; 251 | } 252 | if (submitTaskConsumer != null) { 253 | submitTaskConsumer.accept(this::submitData); 254 | } else { 255 | this.submitData(); 256 | } 257 | }; 258 | // Many servers tend to restart at a fixed time at xx:00 which causes an uneven distribution 259 | // of requests on the 260 | // bStats backend. To circumvent this problem, we introduce some randomness into the initial 261 | // and second delay. 262 | // WARNING: You must not modify and part of this Metrics class, including the submit delay or 263 | // frequency! 264 | // WARNING: Modifying this code will get your plugin banned on bStats. Just don't do it! 265 | long initialDelay = (long) (1000 * 60 * (3 + Math.random() * 3)); 266 | long secondDelay = (long) (1000 * 60 * (Math.random() * 30)); 267 | scheduler.schedule(submitTask, initialDelay, TimeUnit.MILLISECONDS); 268 | scheduler.scheduleAtFixedRate( 269 | submitTask, initialDelay + secondDelay, 1000 * 60 * 30, TimeUnit.MILLISECONDS); 270 | } 271 | 272 | private void submitData() { 273 | final JsonObjectBuilder baseJsonBuilder = new JsonObjectBuilder(); 274 | appendPlatformDataConsumer.accept(baseJsonBuilder); 275 | final JsonObjectBuilder serviceJsonBuilder = new JsonObjectBuilder(); 276 | appendServiceDataConsumer.accept(serviceJsonBuilder); 277 | JsonObjectBuilder.JsonObject[] chartData = 278 | customCharts.stream() 279 | .map(customChart -> customChart.getRequestJsonObject(errorLogger, logErrors)) 280 | .filter(Objects::nonNull) 281 | .toArray(JsonObjectBuilder.JsonObject[]::new); 282 | serviceJsonBuilder.appendField("id", serviceId); 283 | serviceJsonBuilder.appendField("customCharts", chartData); 284 | baseJsonBuilder.appendField("service", serviceJsonBuilder.build()); 285 | baseJsonBuilder.appendField("serverUUID", serverUuid); 286 | baseJsonBuilder.appendField("metricsVersion", METRICS_VERSION); 287 | JsonObjectBuilder.JsonObject data = baseJsonBuilder.build(); 288 | scheduler.execute( 289 | () -> { 290 | try { 291 | // Send the data 292 | sendData(data); 293 | } catch (Exception e) { 294 | // Something went wrong! :( 295 | if (logErrors) { 296 | errorLogger.accept("Could not submit bStats metrics data", e); 297 | } 298 | } 299 | }); 300 | } 301 | 302 | private void sendData(JsonObjectBuilder.JsonObject data) throws Exception { 303 | if (logSentData) { 304 | infoLogger.accept("Sent bStats metrics data: " + data.toString()); 305 | } 306 | String url = String.format(REPORT_URL, platform); 307 | HttpsURLConnection connection = (HttpsURLConnection) new URL(url).openConnection(); 308 | // Compress the data to save bandwidth 309 | byte[] compressedData = compress(data.toString()); 310 | connection.setRequestMethod("POST"); 311 | connection.addRequestProperty("Accept", "application/json"); 312 | connection.addRequestProperty("Connection", "close"); 313 | connection.addRequestProperty("Content-Encoding", "gzip"); 314 | connection.addRequestProperty("Content-Length", String.valueOf(compressedData.length)); 315 | connection.setRequestProperty("Content-Type", "application/json"); 316 | connection.setRequestProperty("User-Agent", "Metrics-Service/1"); 317 | connection.setDoOutput(true); 318 | try (DataOutputStream outputStream = new DataOutputStream(connection.getOutputStream())) { 319 | outputStream.write(compressedData); 320 | } 321 | StringBuilder builder = new StringBuilder(); 322 | try (BufferedReader bufferedReader = 323 | new BufferedReader(new InputStreamReader(connection.getInputStream()))) { 324 | String line; 325 | while ((line = bufferedReader.readLine()) != null) { 326 | builder.append(line); 327 | } 328 | } 329 | if (logResponseStatusText) { 330 | infoLogger.accept("Sent data to bStats and received response: " + builder); 331 | } 332 | } 333 | 334 | /** Checks that the class was properly relocated. */ 335 | private void checkRelocation() { 336 | // You can use the property to disable the check in your test environment 337 | if (System.getProperty("bstats.relocatecheck") == null 338 | || !System.getProperty("bstats.relocatecheck").equals("false")) { 339 | // Maven's Relocate is clever and changes strings, too. So we have to use this little 340 | // "trick" ... :D 341 | final String defaultPackage = 342 | new String(new byte[] {'o', 'r', 'g', '.', 'b', 's', 't', 'a', 't', 's'}); 343 | final String examplePackage = 344 | new String(new byte[] {'y', 'o', 'u', 'r', '.', 'p', 'a', 'c', 'k', 'a', 'g', 'e'}); 345 | // We want to make sure no one just copy & pastes the example and uses the wrong package 346 | // names 347 | if (MetricsBase.class.getPackage().getName().startsWith(defaultPackage) 348 | || MetricsBase.class.getPackage().getName().startsWith(examplePackage)) { 349 | throw new IllegalStateException("bStats Metrics class has not been relocated correctly!"); 350 | } 351 | } 352 | } 353 | 354 | /** 355 | * Gzips the given string. 356 | * 357 | * @param str The string to gzip. 358 | * @return The gzipped string. 359 | */ 360 | private static byte[] compress(final String str) throws IOException { 361 | if (str == null) { 362 | return null; 363 | } 364 | ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); 365 | try (GZIPOutputStream gzip = new GZIPOutputStream(outputStream)) { 366 | gzip.write(str.getBytes(StandardCharsets.UTF_8)); 367 | } 368 | return outputStream.toByteArray(); 369 | } 370 | } 371 | 372 | public static class AdvancedBarChart extends CustomChart { 373 | 374 | private final Callable> callable; 375 | 376 | /** 377 | * Class constructor. 378 | * 379 | * @param chartId The id of the chart. 380 | * @param callable The callable which is used to request the chart data. 381 | */ 382 | public AdvancedBarChart(String chartId, Callable> callable) { 383 | super(chartId); 384 | this.callable = callable; 385 | } 386 | 387 | @Override 388 | protected JsonObjectBuilder.JsonObject getChartData() throws Exception { 389 | JsonObjectBuilder valuesBuilder = new JsonObjectBuilder(); 390 | Map map = callable.call(); 391 | if (map == null || map.isEmpty()) { 392 | // Null = skip the chart 393 | return null; 394 | } 395 | boolean allSkipped = true; 396 | for (Map.Entry entry : map.entrySet()) { 397 | if (entry.getValue().length == 0) { 398 | // Skip this invalid 399 | continue; 400 | } 401 | allSkipped = false; 402 | valuesBuilder.appendField(entry.getKey(), entry.getValue()); 403 | } 404 | if (allSkipped) { 405 | // Null = skip the chart 406 | return null; 407 | } 408 | return new JsonObjectBuilder().appendField("values", valuesBuilder.build()).build(); 409 | } 410 | } 411 | 412 | public static class SimpleBarChart extends CustomChart { 413 | 414 | private final Callable> callable; 415 | 416 | /** 417 | * Class constructor. 418 | * 419 | * @param chartId The id of the chart. 420 | * @param callable The callable which is used to request the chart data. 421 | */ 422 | public SimpleBarChart(String chartId, Callable> callable) { 423 | super(chartId); 424 | this.callable = callable; 425 | } 426 | 427 | @Override 428 | protected JsonObjectBuilder.JsonObject getChartData() throws Exception { 429 | JsonObjectBuilder valuesBuilder = new JsonObjectBuilder(); 430 | Map map = callable.call(); 431 | if (map == null || map.isEmpty()) { 432 | // Null = skip the chart 433 | return null; 434 | } 435 | for (Map.Entry entry : map.entrySet()) { 436 | valuesBuilder.appendField(entry.getKey(), new int[] {entry.getValue()}); 437 | } 438 | return new JsonObjectBuilder().appendField("values", valuesBuilder.build()).build(); 439 | } 440 | } 441 | 442 | public static class MultiLineChart extends CustomChart { 443 | 444 | private final Callable> callable; 445 | 446 | /** 447 | * Class constructor. 448 | * 449 | * @param chartId The id of the chart. 450 | * @param callable The callable which is used to request the chart data. 451 | */ 452 | public MultiLineChart(String chartId, Callable> callable) { 453 | super(chartId); 454 | this.callable = callable; 455 | } 456 | 457 | @Override 458 | protected JsonObjectBuilder.JsonObject getChartData() throws Exception { 459 | JsonObjectBuilder valuesBuilder = new JsonObjectBuilder(); 460 | Map map = callable.call(); 461 | if (map == null || map.isEmpty()) { 462 | // Null = skip the chart 463 | return null; 464 | } 465 | boolean allSkipped = true; 466 | for (Map.Entry entry : map.entrySet()) { 467 | if (entry.getValue() == 0) { 468 | // Skip this invalid 469 | continue; 470 | } 471 | allSkipped = false; 472 | valuesBuilder.appendField(entry.getKey(), entry.getValue()); 473 | } 474 | if (allSkipped) { 475 | // Null = skip the chart 476 | return null; 477 | } 478 | return new JsonObjectBuilder().appendField("values", valuesBuilder.build()).build(); 479 | } 480 | } 481 | 482 | public static class AdvancedPie extends CustomChart { 483 | 484 | private final Callable> callable; 485 | 486 | /** 487 | * Class constructor. 488 | * 489 | * @param chartId The id of the chart. 490 | * @param callable The callable which is used to request the chart data. 491 | */ 492 | public AdvancedPie(String chartId, Callable> callable) { 493 | super(chartId); 494 | this.callable = callable; 495 | } 496 | 497 | @Override 498 | protected JsonObjectBuilder.JsonObject getChartData() throws Exception { 499 | JsonObjectBuilder valuesBuilder = new JsonObjectBuilder(); 500 | Map map = callable.call(); 501 | if (map == null || map.isEmpty()) { 502 | // Null = skip the chart 503 | return null; 504 | } 505 | boolean allSkipped = true; 506 | for (Map.Entry entry : map.entrySet()) { 507 | if (entry.getValue() == 0) { 508 | // Skip this invalid 509 | continue; 510 | } 511 | allSkipped = false; 512 | valuesBuilder.appendField(entry.getKey(), entry.getValue()); 513 | } 514 | if (allSkipped) { 515 | // Null = skip the chart 516 | return null; 517 | } 518 | return new JsonObjectBuilder().appendField("values", valuesBuilder.build()).build(); 519 | } 520 | } 521 | 522 | public abstract static class CustomChart { 523 | 524 | private final String chartId; 525 | 526 | protected CustomChart(String chartId) { 527 | if (chartId == null) { 528 | throw new IllegalArgumentException("chartId must not be null"); 529 | } 530 | this.chartId = chartId; 531 | } 532 | 533 | public JsonObjectBuilder.JsonObject getRequestJsonObject( 534 | BiConsumer errorLogger, boolean logErrors) { 535 | JsonObjectBuilder builder = new JsonObjectBuilder(); 536 | builder.appendField("chartId", chartId); 537 | try { 538 | JsonObjectBuilder.JsonObject data = getChartData(); 539 | if (data == null) { 540 | // If the data is null we don't send the chart. 541 | return null; 542 | } 543 | builder.appendField("data", data); 544 | } catch (Throwable t) { 545 | if (logErrors) { 546 | errorLogger.accept("Failed to get data for custom chart with id " + chartId, t); 547 | } 548 | return null; 549 | } 550 | return builder.build(); 551 | } 552 | 553 | protected abstract JsonObjectBuilder.JsonObject getChartData() throws Exception; 554 | } 555 | 556 | public static class SingleLineChart extends CustomChart { 557 | 558 | private final Callable callable; 559 | 560 | /** 561 | * Class constructor. 562 | * 563 | * @param chartId The id of the chart. 564 | * @param callable The callable which is used to request the chart data. 565 | */ 566 | public SingleLineChart(String chartId, Callable callable) { 567 | super(chartId); 568 | this.callable = callable; 569 | } 570 | 571 | @Override 572 | protected JsonObjectBuilder.JsonObject getChartData() throws Exception { 573 | int value = callable.call(); 574 | if (value == 0) { 575 | // Null = skip the chart 576 | return null; 577 | } 578 | return new JsonObjectBuilder().appendField("value", value).build(); 579 | } 580 | } 581 | 582 | public static class SimplePie extends CustomChart { 583 | 584 | private final Callable callable; 585 | 586 | /** 587 | * Class constructor. 588 | * 589 | * @param chartId The id of the chart. 590 | * @param callable The callable which is used to request the chart data. 591 | */ 592 | public SimplePie(String chartId, Callable callable) { 593 | super(chartId); 594 | this.callable = callable; 595 | } 596 | 597 | @Override 598 | protected JsonObjectBuilder.JsonObject getChartData() throws Exception { 599 | String value = callable.call(); 600 | if (value == null || value.isEmpty()) { 601 | // Null = skip the chart 602 | return null; 603 | } 604 | return new JsonObjectBuilder().appendField("value", value).build(); 605 | } 606 | } 607 | 608 | public static class DrilldownPie extends CustomChart { 609 | 610 | private final Callable>> callable; 611 | 612 | /** 613 | * Class constructor. 614 | * 615 | * @param chartId The id of the chart. 616 | * @param callable The callable which is used to request the chart data. 617 | */ 618 | public DrilldownPie(String chartId, Callable>> callable) { 619 | super(chartId); 620 | this.callable = callable; 621 | } 622 | 623 | @Override 624 | public JsonObjectBuilder.JsonObject getChartData() throws Exception { 625 | JsonObjectBuilder valuesBuilder = new JsonObjectBuilder(); 626 | Map> map = callable.call(); 627 | if (map == null || map.isEmpty()) { 628 | // Null = skip the chart 629 | return null; 630 | } 631 | boolean reallyAllSkipped = true; 632 | for (Map.Entry> entryValues : map.entrySet()) { 633 | JsonObjectBuilder valueBuilder = new JsonObjectBuilder(); 634 | boolean allSkipped = true; 635 | for (Map.Entry valueEntry : map.get(entryValues.getKey()).entrySet()) { 636 | valueBuilder.appendField(valueEntry.getKey(), valueEntry.getValue()); 637 | allSkipped = false; 638 | } 639 | if (!allSkipped) { 640 | reallyAllSkipped = false; 641 | valuesBuilder.appendField(entryValues.getKey(), valueBuilder.build()); 642 | } 643 | } 644 | if (reallyAllSkipped) { 645 | // Null = skip the chart 646 | return null; 647 | } 648 | return new JsonObjectBuilder().appendField("values", valuesBuilder.build()).build(); 649 | } 650 | } 651 | 652 | /** 653 | * An extremely simple JSON builder. 654 | * 655 | *

While this class is neither feature-rich nor the most performant one, it's sufficient enough 656 | * for its use-case. 657 | */ 658 | public static class JsonObjectBuilder { 659 | 660 | private StringBuilder builder = new StringBuilder(); 661 | 662 | private boolean hasAtLeastOneField = false; 663 | 664 | public JsonObjectBuilder() { 665 | builder.append("{"); 666 | } 667 | 668 | /** 669 | * Appends a null field to the JSON. 670 | * 671 | * @param key The key of the field. 672 | * @return A reference to this object. 673 | */ 674 | public JsonObjectBuilder appendNull(String key) { 675 | appendFieldUnescaped(key, "null"); 676 | return this; 677 | } 678 | 679 | /** 680 | * Appends a string field to the JSON. 681 | * 682 | * @param key The key of the field. 683 | * @param value The value of the field. 684 | * @return A reference to this object. 685 | */ 686 | public JsonObjectBuilder appendField(String key, String value) { 687 | if (value == null) { 688 | throw new IllegalArgumentException("JSON value must not be null"); 689 | } 690 | appendFieldUnescaped(key, "\"" + escape(value) + "\""); 691 | return this; 692 | } 693 | 694 | /** 695 | * Appends an integer field to the JSON. 696 | * 697 | * @param key The key of the field. 698 | * @param value The value of the field. 699 | * @return A reference to this object. 700 | */ 701 | public JsonObjectBuilder appendField(String key, int value) { 702 | appendFieldUnescaped(key, String.valueOf(value)); 703 | return this; 704 | } 705 | 706 | /** 707 | * Appends an object to the JSON. 708 | * 709 | * @param key The key of the field. 710 | * @param object The object. 711 | * @return A reference to this object. 712 | */ 713 | public JsonObjectBuilder appendField(String key, JsonObject object) { 714 | if (object == null) { 715 | throw new IllegalArgumentException("JSON object must not be null"); 716 | } 717 | appendFieldUnescaped(key, object.toString()); 718 | return this; 719 | } 720 | 721 | /** 722 | * Appends a string array to the JSON. 723 | * 724 | * @param key The key of the field. 725 | * @param values The string array. 726 | * @return A reference to this object. 727 | */ 728 | public JsonObjectBuilder appendField(String key, String[] values) { 729 | if (values == null) { 730 | throw new IllegalArgumentException("JSON values must not be null"); 731 | } 732 | String escapedValues = 733 | Arrays.stream(values) 734 | .map(value -> "\"" + escape(value) + "\"") 735 | .collect(Collectors.joining(",")); 736 | appendFieldUnescaped(key, "[" + escapedValues + "]"); 737 | return this; 738 | } 739 | 740 | /** 741 | * Appends an integer array to the JSON. 742 | * 743 | * @param key The key of the field. 744 | * @param values The integer array. 745 | * @return A reference to this object. 746 | */ 747 | public JsonObjectBuilder appendField(String key, int[] values) { 748 | if (values == null) { 749 | throw new IllegalArgumentException("JSON values must not be null"); 750 | } 751 | String escapedValues = 752 | Arrays.stream(values).mapToObj(String::valueOf).collect(Collectors.joining(",")); 753 | appendFieldUnescaped(key, "[" + escapedValues + "]"); 754 | return this; 755 | } 756 | 757 | /** 758 | * Appends an object array to the JSON. 759 | * 760 | * @param key The key of the field. 761 | * @param values The integer array. 762 | * @return A reference to this object. 763 | */ 764 | public JsonObjectBuilder appendField(String key, JsonObject[] values) { 765 | if (values == null) { 766 | throw new IllegalArgumentException("JSON values must not be null"); 767 | } 768 | String escapedValues = 769 | Arrays.stream(values).map(JsonObject::toString).collect(Collectors.joining(",")); 770 | appendFieldUnescaped(key, "[" + escapedValues + "]"); 771 | return this; 772 | } 773 | 774 | /** 775 | * Appends a field to the object. 776 | * 777 | * @param key The key of the field. 778 | * @param escapedValue The escaped value of the field. 779 | */ 780 | private void appendFieldUnescaped(String key, String escapedValue) { 781 | if (builder == null) { 782 | throw new IllegalStateException("JSON has already been built"); 783 | } 784 | if (key == null) { 785 | throw new IllegalArgumentException("JSON key must not be null"); 786 | } 787 | if (hasAtLeastOneField) { 788 | builder.append(","); 789 | } 790 | builder.append("\"").append(escape(key)).append("\":").append(escapedValue); 791 | hasAtLeastOneField = true; 792 | } 793 | 794 | /** 795 | * Builds the JSON string and invalidates this builder. 796 | * 797 | * @return The built JSON string. 798 | */ 799 | public JsonObject build() { 800 | if (builder == null) { 801 | throw new IllegalStateException("JSON has already been built"); 802 | } 803 | JsonObject object = new JsonObject(builder.append("}").toString()); 804 | builder = null; 805 | return object; 806 | } 807 | 808 | /** 809 | * Escapes the given string like stated in https://www.ietf.org/rfc/rfc4627.txt. 810 | * 811 | *

This method escapes only the necessary characters '"', '\'. and '\u0000' - '\u001F'. 812 | * Compact escapes are not used (e.g., '\n' is escaped as "\u000a" and not as "\n"). 813 | * 814 | * @param value The value to escape. 815 | * @return The escaped value. 816 | */ 817 | private static String escape(String value) { 818 | final StringBuilder builder = new StringBuilder(); 819 | for (int i = 0; i < value.length(); i++) { 820 | char c = value.charAt(i); 821 | if (c == '"') { 822 | builder.append("\\\""); 823 | } else if (c == '\\') { 824 | builder.append("\\\\"); 825 | } else if (c <= '\u000F') { 826 | builder.append("\\u000").append(Integer.toHexString(c)); 827 | } else if (c <= '\u001F') { 828 | builder.append("\\u00").append(Integer.toHexString(c)); 829 | } else { 830 | builder.append(c); 831 | } 832 | } 833 | return builder.toString(); 834 | } 835 | 836 | /** 837 | * A super simple representation of a JSON object. 838 | * 839 | *

This class only exists to make methods of the {@link JsonObjectBuilder} type-safe and not 840 | * allow a raw string inputs for methods like {@link JsonObjectBuilder#appendField(String, 841 | * JsonObject)}. 842 | */ 843 | public static class JsonObject { 844 | 845 | private final String value; 846 | 847 | private JsonObject(String value) { 848 | this.value = value; 849 | } 850 | 851 | @Override 852 | public String toString() { 853 | return value; 854 | } 855 | } 856 | } 857 | } -------------------------------------------------------------------------------- /src/main/java/ua/coolboy/f3name/spiget/SpigetUpdateBukkit.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 inventivetalent. All rights reserved. 3 | * 4 | * Redistribution and use in source and binary forms, with or without modification, are 5 | * permitted provided that the following conditions are met: 6 | * 7 | * 1. Redistributions of source code must retain the above copyright notice, this list of 8 | * conditions and the following disclaimer. 9 | * 10 | * 2. Redistributions in binary form must reproduce the above copyright notice, this list 11 | * of conditions and the following disclaimer in the documentation and/or other materials 12 | * provided with the distribution. 13 | * 14 | * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ''AS IS'' AND ANY EXPRESS OR IMPLIED 15 | * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND 16 | * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR 17 | * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 18 | * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 19 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 20 | * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 21 | * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF 22 | * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 23 | * 24 | * The views and conclusions contained in the software and documentation are those of the 25 | * authors and contributors and should not be interpreted as representing official policies, 26 | * either expressed or implied, of anybody else. 27 | */ 28 | 29 | package ua.coolboy.f3name.spiget; 30 | 31 | import org.bukkit.Bukkit; 32 | import org.bukkit.plugin.Plugin; 33 | import org.bukkit.plugin.java.JavaPlugin; 34 | 35 | import java.io.File; 36 | import java.io.FileReader; 37 | import java.io.FileWriter; 38 | import java.io.IOException; 39 | import java.lang.reflect.Method; 40 | import java.util.Properties; 41 | import java.util.logging.Level; 42 | 43 | import ua.coolboy.f3name.spiget.updater.SpigetUpdateAbstract; 44 | import ua.coolboy.f3name.spiget.updater.comparator.VersionComparator; 45 | import ua.coolboy.f3name.spiget.updater.download.DownloadCallback; 46 | import ua.coolboy.f3name.spiget.updater.download.UpdateDownloader; 47 | 48 | public class SpigetUpdateBukkit extends SpigetUpdateAbstract { 49 | 50 | protected final Plugin plugin; 51 | protected DownloadFailReason failReason = DownloadFailReason.UNKNOWN; 52 | 53 | public SpigetUpdateBukkit(Plugin plugin, int resourceId) { 54 | super(resourceId, plugin.getDescription().getVersion(), plugin.getLogger()); 55 | this.plugin = plugin; 56 | setUserAgent("SpigetResourceUpdater/Bukkit"); 57 | } 58 | 59 | @Override 60 | public SpigetUpdateBukkit setUserAgent(String userAgent) { 61 | super.setUserAgent(userAgent); 62 | return this; 63 | } 64 | 65 | @Override 66 | public SpigetUpdateBukkit setVersionComparator(VersionComparator comparator) { 67 | super.setVersionComparator(comparator); 68 | return this; 69 | } 70 | 71 | @Override 72 | protected void dispatch(Runnable runnable) { 73 | Bukkit.getScheduler().runTaskAsynchronously(plugin, runnable); 74 | } 75 | 76 | public boolean downloadUpdate() { 77 | if (latestResourceInfo == null) { 78 | failReason = DownloadFailReason.NOT_CHECKED; 79 | return false;// Update not yet checked 80 | } 81 | if (!isVersionNewer(currentVersion, latestResourceInfo.latestVersion.name)) { 82 | failReason = DownloadFailReason.NO_UPDATE; 83 | return false;// Version is no update 84 | } 85 | if (latestResourceInfo.external) { 86 | failReason = DownloadFailReason.NO_DOWNLOAD; 87 | return false;// No download available 88 | } 89 | 90 | File pluginFile = getPluginFile();// /plugins/XXX.jar 91 | if (pluginFile == null) { 92 | failReason = DownloadFailReason.NO_PLUGIN_FILE; 93 | return false; 94 | } 95 | File updateFolder = Bukkit.getUpdateFolderFile(); 96 | if (!updateFolder.exists()) { 97 | if (!updateFolder.mkdirs()) { 98 | failReason = DownloadFailReason.NO_UPDATE_FOLDER; 99 | return false; 100 | } 101 | } 102 | final File updateFile = new File(Bukkit.getUpdateFolderFile(), pluginFile.getName()); 103 | 104 | Properties properties = getUpdaterProperties(); 105 | boolean allowExternalDownload = properties != null && properties.containsKey("externalDownloads") && Boolean.valueOf(properties.getProperty("externalDownloads")); 106 | 107 | if (!allowExternalDownload && latestResourceInfo.external) { 108 | failReason = DownloadFailReason.EXTERNAL_DISALLOWED; 109 | return false; 110 | } 111 | 112 | log.info("[SpigetUpdate] Downloading update..."); 113 | dispatch(UpdateDownloader.downloadAsync(latestResourceInfo, updateFile, getUserAgent(), new DownloadCallback() { 114 | @Override 115 | public void finished() { 116 | //log.info("[SpigetUpdate] Update saved as " + updateFile.getPath()); mute Spiget, we have own message 117 | } 118 | 119 | @Override 120 | public void error(Exception exception) { 121 | log.log(Level.WARNING, "[SpigetUpdate] Could not download update", exception); 122 | } 123 | })); 124 | 125 | return true; 126 | } 127 | 128 | public DownloadFailReason getFailReason() { 129 | return failReason; 130 | } 131 | 132 | public Properties getUpdaterProperties() { 133 | File file = new File(Bukkit.getUpdateFolderFile(), "spiget.properties"); 134 | Properties properties = new Properties(); 135 | if (!file.exists()) { 136 | try { 137 | if (!file.createNewFile()) { return null; } 138 | properties.setProperty("externalDownloads", "false"); 139 | properties.store(new FileWriter(file), "Configuration for the Spiget auto-updater. https://spiget.org | https://github.com/InventivetalentDev/SpigetUpdater\n" 140 | + "Use 'externalDownloads' if you want to auto-download resources hosted on external sites\n" 141 | + ""); 142 | } catch (Exception ignored) { 143 | return null; 144 | } 145 | } 146 | try { 147 | properties.load(new FileReader(file)); 148 | } catch (IOException e) { 149 | return null; 150 | } 151 | return properties; 152 | } 153 | 154 | /** 155 | * Get the plugin's file name 156 | * 157 | * @return the plugin file name 158 | */ 159 | private File getPluginFile() { 160 | if (!(this.plugin instanceof JavaPlugin)) { return null; } 161 | try { 162 | Method method = JavaPlugin.class.getDeclaredMethod("getFile"); 163 | method.setAccessible(true); 164 | return (File) method.invoke(this.plugin); 165 | } catch (ReflectiveOperationException e) { 166 | throw new RuntimeException("Could not get plugin file", e); 167 | } 168 | } 169 | 170 | public enum DownloadFailReason { 171 | NOT_CHECKED, 172 | NO_UPDATE, 173 | NO_DOWNLOAD, 174 | NO_PLUGIN_FILE, 175 | NO_UPDATE_FOLDER, 176 | EXTERNAL_DISALLOWED, 177 | UNKNOWN; 178 | } 179 | 180 | } 181 | -------------------------------------------------------------------------------- /src/main/java/ua/coolboy/f3name/spiget/SpigetUpdateBungee.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 inventivetalent. All rights reserved. 3 | * 4 | * Redistribution and use in source and binary forms, with or without modification, are 5 | * permitted provided that the following conditions are met: 6 | * 7 | * 1. Redistributions of source code must retain the above copyright notice, this list of 8 | * conditions and the following disclaimer. 9 | * 10 | * 2. Redistributions in binary form must reproduce the above copyright notice, this list 11 | * of conditions and the following disclaimer in the documentation and/or other materials 12 | * provided with the distribution. 13 | * 14 | * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ''AS IS'' AND ANY EXPRESS OR IMPLIED 15 | * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND 16 | * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR 17 | * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 18 | * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 19 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 20 | * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 21 | * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF 22 | * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 23 | * 24 | * The views and conclusions contained in the software and documentation are those of the 25 | * authors and contributors and should not be interpreted as representing official policies, 26 | * either expressed or implied, of anybody else. 27 | */ 28 | 29 | package ua.coolboy.f3name.spiget; 30 | 31 | import net.md_5.bungee.api.ProxyServer; 32 | import net.md_5.bungee.api.plugin.Plugin; 33 | import ua.coolboy.f3name.spiget.updater.SpigetUpdateAbstract; 34 | import ua.coolboy.f3name.spiget.updater.comparator.VersionComparator; 35 | 36 | public class SpigetUpdateBungee extends SpigetUpdateAbstract { 37 | 38 | protected final Plugin plugin; 39 | 40 | public SpigetUpdateBungee(Plugin plugin, int resourceId) { 41 | super(resourceId, plugin.getDescription().getVersion(), plugin.getLogger()); 42 | this.plugin = plugin; 43 | setUserAgent("SpigetResourceUpdater/Bungee"); 44 | } 45 | 46 | @Override 47 | public SpigetUpdateBungee setUserAgent(String userAgent) { 48 | super.setUserAgent(userAgent); 49 | return this; 50 | } 51 | 52 | @Override 53 | public SpigetUpdateBungee setVersionComparator(VersionComparator comparator) { 54 | super.setVersionComparator(comparator); 55 | return this; 56 | } 57 | 58 | @Override 59 | protected void dispatch(Runnable runnable) { 60 | ProxyServer.getInstance().getScheduler().runAsync(plugin, runnable); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/main/java/ua/coolboy/f3name/spiget/updater/ResourceFile.java: -------------------------------------------------------------------------------- 1 | package ua.coolboy.f3name.spiget.updater; 2 | 3 | public class ResourceFile { 4 | 5 | public String type; 6 | public int ize; 7 | public String sizeUnit; 8 | public String url; 9 | 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/ua/coolboy/f3name/spiget/updater/ResourceInfo.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 inventivetalent. All rights reserved. 3 | * 4 | * Redistribution and use in source and binary forms, with or without modification, are 5 | * permitted provided that the following conditions are met: 6 | * 7 | * 1. Redistributions of source code must retain the above copyright notice, this list of 8 | * conditions and the following disclaimer. 9 | * 10 | * 2. Redistributions in binary form must reproduce the above copyright notice, this list 11 | * of conditions and the following disclaimer in the documentation and/or other materials 12 | * provided with the distribution. 13 | * 14 | * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ''AS IS'' AND ANY EXPRESS OR IMPLIED 15 | * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND 16 | * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR 17 | * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 18 | * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 19 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 20 | * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 21 | * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF 22 | * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 23 | * 24 | * The views and conclusions contained in the software and documentation are those of the 25 | * authors and contributors and should not be interpreted as representing official policies, 26 | * either expressed or implied, of anybody else. 27 | */ 28 | 29 | package ua.coolboy.f3name.spiget.updater; 30 | 31 | public class ResourceInfo { 32 | 33 | public int id; 34 | public boolean external; 35 | public ResourceFile file; 36 | 37 | public ResourceVersion latestVersion; 38 | 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/ua/coolboy/f3name/spiget/updater/ResourceVersion.java: -------------------------------------------------------------------------------- 1 | package ua.coolboy.f3name.spiget.updater; 2 | 3 | public class ResourceVersion { 4 | 5 | public int id; 6 | public String name; 7 | public long releaseDate; 8 | public String url; 9 | 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/ua/coolboy/f3name/spiget/updater/SpigetUpdateAbstract.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 inventivetalent. All rights reserved. 3 | * 4 | * Redistribution and use in source and binary forms, with or without modification, are 5 | * permitted provided that the following conditions are met: 6 | * 7 | * 1. Redistributions of source code must retain the above copyright notice, this list of 8 | * conditions and the following disclaimer. 9 | * 10 | * 2. Redistributions in binary form must reproduce the above copyright notice, this list 11 | * of conditions and the following disclaimer in the documentation and/or other materials 12 | * provided with the distribution. 13 | * 14 | * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ''AS IS'' AND ANY EXPRESS OR IMPLIED 15 | * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND 16 | * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR 17 | * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 18 | * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 19 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 20 | * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 21 | * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF 22 | * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 23 | * 24 | * The views and conclusions contained in the software and documentation are those of the 25 | * authors and contributors and should not be interpreted as representing official policies, 26 | * either expressed or implied, of anybody else. 27 | */ 28 | 29 | package ua.coolboy.f3name.spiget.updater; 30 | 31 | import com.google.gson.Gson; 32 | import com.google.gson.JsonObject; 33 | import com.google.gson.JsonParser; 34 | import ua.coolboy.f3name.spiget.updater.comparator.VersionComparator; 35 | 36 | import java.io.InputStreamReader; 37 | import java.net.HttpURLConnection; 38 | import java.net.URL; 39 | import java.util.logging.Level; 40 | import java.util.logging.Logger; 41 | 42 | public abstract class SpigetUpdateAbstract { 43 | 44 | public static final String RESOURCE_INFO = "http://api.spiget.org/v2/resources/%s?ut=%s"; 45 | public static final String RESOURCE_VERSION = "http://api.spiget.org/v2/resources/%s/versions/latest?ut=%s"; 46 | 47 | protected final int resourceId; 48 | protected final String currentVersion; 49 | protected final Logger log; 50 | protected String userAgent = "SpigetResourceUpdater"; 51 | protected VersionComparator versionComparator = VersionComparator.EQUAL; 52 | 53 | protected ResourceInfo latestResourceInfo; 54 | 55 | public SpigetUpdateAbstract(int resourceId, String currentVersion, Logger log) { 56 | this.resourceId = resourceId; 57 | this.currentVersion = currentVersion; 58 | this.log = log; 59 | } 60 | 61 | public SpigetUpdateAbstract setUserAgent(String userAgent) { 62 | this.userAgent = userAgent; 63 | return this; 64 | } 65 | 66 | public String getUserAgent() { 67 | return userAgent; 68 | } 69 | 70 | public SpigetUpdateAbstract setVersionComparator(VersionComparator comparator) { 71 | this.versionComparator = comparator; 72 | return this; 73 | } 74 | 75 | public ResourceInfo getLatestResourceInfo() { 76 | return latestResourceInfo; 77 | } 78 | 79 | protected abstract void dispatch(Runnable runnable); 80 | 81 | public boolean isVersionNewer(String oldVersion, String newVersion) { 82 | return versionComparator.isNewer(oldVersion, newVersion); 83 | } 84 | 85 | public void checkForUpdate(final UpdateCallback callback) { 86 | dispatch(new Runnable() { 87 | @Override 88 | public void run() { 89 | try { 90 | HttpURLConnection connection = (HttpURLConnection) new URL(String.format(RESOURCE_INFO, resourceId, System.currentTimeMillis())).openConnection(); 91 | connection.setRequestProperty("User-Agent", getUserAgent()); 92 | JsonObject jsonObject = new JsonParser().parse(new InputStreamReader(connection.getInputStream())).getAsJsonObject(); 93 | latestResourceInfo = new Gson().fromJson(jsonObject, ResourceInfo.class); 94 | 95 | connection = (HttpURLConnection) new URL(String.format(RESOURCE_VERSION, resourceId, System.currentTimeMillis())).openConnection(); 96 | connection.setRequestProperty("User-Agent", getUserAgent()); 97 | jsonObject = new JsonParser().parse(new InputStreamReader(connection.getInputStream())).getAsJsonObject(); 98 | latestResourceInfo.latestVersion = new Gson().fromJson(jsonObject, ResourceVersion.class); 99 | 100 | if (isVersionNewer(currentVersion, latestResourceInfo.latestVersion.name)) { 101 | callback.updateAvailable(latestResourceInfo.latestVersion.name, "https://spigotmc.org/" + latestResourceInfo.file.url, !latestResourceInfo.external); 102 | } else { 103 | callback.upToDate(); 104 | } 105 | } catch (Exception e) { 106 | log.log(Level.WARNING, "Failed to get resource info from spiget.org", e); 107 | } 108 | } 109 | }); 110 | } 111 | 112 | } 113 | -------------------------------------------------------------------------------- /src/main/java/ua/coolboy/f3name/spiget/updater/UpdateCallback.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 inventivetalent. All rights reserved. 3 | * 4 | * Redistribution and use in source and binary forms, with or without modification, are 5 | * permitted provided that the following conditions are met: 6 | * 7 | * 1. Redistributions of source code must retain the above copyright notice, this list of 8 | * conditions and the following disclaimer. 9 | * 10 | * 2. Redistributions in binary form must reproduce the above copyright notice, this list 11 | * of conditions and the following disclaimer in the documentation and/or other materials 12 | * provided with the distribution. 13 | * 14 | * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ''AS IS'' AND ANY EXPRESS OR IMPLIED 15 | * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND 16 | * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR 17 | * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 18 | * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 19 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 20 | * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 21 | * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF 22 | * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 23 | * 24 | * The views and conclusions contained in the software and documentation are those of the 25 | * authors and contributors and should not be interpreted as representing official policies, 26 | * either expressed or implied, of anybody else. 27 | */ 28 | 29 | package ua.coolboy.f3name.spiget.updater; 30 | 31 | public interface UpdateCallback { 32 | 33 | /** 34 | * Called when a new version was found 35 | *

36 | * Use {@link SpigetUpdateAbstract#getLatestResourceInfo()} to get all resource details 37 | * 38 | * @param newVersion the new version's name 39 | * @param downloadUrl URL to download the update 40 | * @param canAutoDownload whether this update can be downloaded automatically 41 | */ 42 | void updateAvailable(String newVersion, String downloadUrl, boolean canAutoDownload); 43 | 44 | /** 45 | * Called when no update was found 46 | */ 47 | void upToDate(); 48 | 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/ua/coolboy/f3name/spiget/updater/comparator/VersionComparator.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 inventivetalent. All rights reserved. 3 | * 4 | * Redistribution and use in source and binary forms, with or without modification, are 5 | * permitted provided that the following conditions are met: 6 | * 7 | * 1. Redistributions of source code must retain the above copyright notice, this list of 8 | * conditions and the following disclaimer. 9 | * 10 | * 2. Redistributions in binary form must reproduce the above copyright notice, this list 11 | * of conditions and the following disclaimer in the documentation and/or other materials 12 | * provided with the distribution. 13 | * 14 | * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ''AS IS'' AND ANY EXPRESS OR IMPLIED 15 | * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND 16 | * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR 17 | * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 18 | * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 19 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 20 | * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 21 | * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF 22 | * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 23 | * 24 | * The views and conclusions contained in the software and documentation are those of the 25 | * authors and contributors and should not be interpreted as representing official policies, 26 | * either expressed or implied, of anybody else. 27 | */ 28 | 29 | package ua.coolboy.f3name.spiget.updater.comparator; 30 | 31 | public abstract class VersionComparator { 32 | 33 | /** 34 | * Compares versions by checking if the version strings are equal 35 | */ 36 | public static final VersionComparator EQUAL = new VersionComparator() { 37 | @Override 38 | public boolean isNewer(String currentVersion, String checkVersion) { 39 | return !currentVersion.equals(checkVersion); 40 | } 41 | }; 42 | 43 | /** 44 | * Compares versions by their Sematic Version (Major.Minor.Patch, semver.org). Removes dots and compares the resulting Integer values 45 | */ 46 | public static final VersionComparator SEM_VER = new VersionComparator() { 47 | @Override 48 | public boolean isNewer(String currentVersion, String checkVersion) { 49 | currentVersion = currentVersion.replace(".", ""); 50 | checkVersion = checkVersion.replace(".", ""); 51 | 52 | try { 53 | int current = Integer.parseInt(currentVersion); 54 | int check = Integer.parseInt(checkVersion); 55 | 56 | return check > current; 57 | } catch (NumberFormatException e) { 58 | System.err.println("[SpigetUpdate] Invalid SemVer versions specified [" + currentVersion + "] [" + checkVersion + "]"); 59 | } 60 | return false; 61 | } 62 | }; 63 | 64 | /** 65 | * Same as {@link VersionComparator#SEM_VER}, but supports version names with '-SNAPSHOT' prefixes 66 | */ 67 | public static final VersionComparator SEM_VER_SNAPSHOT = new VersionComparator() { 68 | @Override 69 | public boolean isNewer(String currentVersion, String checkVersion) { 70 | currentVersion = currentVersion.replace("-SNAPSHOT", ""); 71 | checkVersion = checkVersion.replace("-SNAPSHOT", ""); 72 | 73 | return SEM_VER.isNewer(currentVersion, checkVersion); 74 | } 75 | }; 76 | 77 | /** 78 | * Called to check if a version is newer 79 | * 80 | * @param currentVersion Current version of the plugin 81 | * @param checkVersion Version to check 82 | * @return true if the checked version is newer 83 | */ 84 | public abstract boolean isNewer(String currentVersion, String checkVersion); 85 | 86 | } 87 | -------------------------------------------------------------------------------- /src/main/java/ua/coolboy/f3name/spiget/updater/download/DownloadCallback.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 inventivetalent. All rights reserved. 3 | * 4 | * Redistribution and use in source and binary forms, with or without modification, are 5 | * permitted provided that the following conditions are met: 6 | * 7 | * 1. Redistributions of source code must retain the above copyright notice, this list of 8 | * conditions and the following disclaimer. 9 | * 10 | * 2. Redistributions in binary form must reproduce the above copyright notice, this list 11 | * of conditions and the following disclaimer in the documentation and/or other materials 12 | * provided with the distribution. 13 | * 14 | * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ''AS IS'' AND ANY EXPRESS OR IMPLIED 15 | * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND 16 | * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR 17 | * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 18 | * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 19 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 20 | * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 21 | * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF 22 | * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 23 | * 24 | * The views and conclusions contained in the software and documentation are those of the 25 | * authors and contributors and should not be interpreted as representing official policies, 26 | * either expressed or implied, of anybody else. 27 | */ 28 | 29 | package ua.coolboy.f3name.spiget.updater.download; 30 | 31 | public interface DownloadCallback { 32 | 33 | void finished(); 34 | 35 | void error(Exception exception); 36 | 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/ua/coolboy/f3name/spiget/updater/download/UpdateDownloader.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 inventivetalent. All rights reserved. 3 | * 4 | * Redistribution and use in source and binary forms, with or without modification, are 5 | * permitted provided that the following conditions are met: 6 | * 7 | * 1. Redistributions of source code must retain the above copyright notice, this list of 8 | * conditions and the following disclaimer. 9 | * 10 | * 2. Redistributions in binary form must reproduce the above copyright notice, this list 11 | * of conditions and the following disclaimer in the documentation and/or other materials 12 | * provided with the distribution. 13 | * 14 | * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ''AS IS'' AND ANY EXPRESS OR IMPLIED 15 | * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND 16 | * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR 17 | * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 18 | * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 19 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 20 | * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 21 | * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF 22 | * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 23 | * 24 | * The views and conclusions contained in the software and documentation are those of the 25 | * authors and contributors and should not be interpreted as representing official policies, 26 | * either expressed or implied, of anybody else. 27 | */ 28 | 29 | package ua.coolboy.f3name.spiget.updater.download; 30 | 31 | import ua.coolboy.f3name.spiget.updater.ResourceInfo; 32 | 33 | import java.io.File; 34 | import java.io.FileOutputStream; 35 | import java.io.IOException; 36 | import java.net.HttpURLConnection; 37 | import java.net.URL; 38 | import java.nio.channels.Channels; 39 | import java.nio.channels.ReadableByteChannel; 40 | 41 | public class UpdateDownloader { 42 | 43 | public static final String RESOURCE_DOWNLOAD = "http://api.spiget.org/v2/resources/%s/download"; 44 | 45 | public static Runnable downloadAsync(final ResourceInfo info, final File file, final String userAgent, final DownloadCallback callback) { 46 | return () -> { //lambda 47 | try { 48 | download(info, file, userAgent); 49 | callback.finished(); 50 | } catch (Exception e) { 51 | callback.error(e); 52 | } 53 | }; 54 | } 55 | 56 | public static void download(ResourceInfo info, File file) { 57 | download(info, file, "SpigetResourceUpdater/F3Name"); //very recursive method 58 | } 59 | 60 | public static void download(ResourceInfo info, File file, String userAgent) { 61 | if (info.external) { throw new IllegalArgumentException("Cannot download external resource #" + info.id); } 62 | ReadableByteChannel channel; 63 | try { 64 | //https://stackoverflow.com/questions/921262/how-to-download-and-save-a-file-from-internet-using-java 65 | HttpURLConnection connection = (HttpURLConnection) new URL(String.format(RESOURCE_DOWNLOAD, info.id)).openConnection(); 66 | connection.setRequestProperty("User-Agent", userAgent); 67 | if (connection.getResponseCode() != 200) { 68 | throw new RuntimeException("Download returned status #" + connection.getResponseCode()); 69 | } 70 | channel = Channels.newChannel(connection.getInputStream()); 71 | } catch (IOException e) { 72 | throw new RuntimeException("Download failed", e); 73 | } 74 | try { 75 | FileOutputStream output = new FileOutputStream(file); 76 | output.getChannel().transferFrom(channel, 0, Long.MAX_VALUE); 77 | output.flush(); 78 | output.close(); 79 | } catch (IOException e) { 80 | throw new RuntimeException("Could not save file", e); 81 | } 82 | } 83 | 84 | } 85 | -------------------------------------------------------------------------------- /src/main/resources/bungee.yml: -------------------------------------------------------------------------------- 1 | name: F3Name 2 | main: ua.coolboy.f3name.bungee.F3NameBungee 3 | version: ${f3name.version} 4 | author: Cool_boy 5 | softdepend: [LuckPerms] 6 | -------------------------------------------------------------------------------- /src/main/resources/bungee_config.yml: -------------------------------------------------------------------------------- 1 | # ______ ____ _ _ # 2 | # | ____|___ \| \ | | # 3 | # | |__ __) | \| | __ _ _ __ ___ ___ # 4 | # | __| |__ <| . ` |/ _` | '_ ` _ \ / _ \ # 5 | # | | ___) | |\ | (_| | | | | | | __/ # 6 | # |_| |____/|_| \_|\__,_|_| |_| |_|\___| # 7 | # # 8 | 9 | #If you don't want to change server brand 10 | #on specific server - add server name below 11 | excluded-servers: 12 | - "excluded" 13 | groups: 14 | #Default group. DO NOT REMOVE! 15 | everyone: 16 | f3names: 17 | - "&6This is a BungeeCord version of &3F3Name" 18 | - "&6Everything the same as in the Bukkit version" 19 | - "&6Except excluded-servers list above and built-in placeholders" 20 | - "&6Your ping: &2%player_ping%&6, current server name &2%server_name%" 21 | - "&6It sends to all servers on your network" 22 | #Shuffle that list before displaying? 23 | shuffle: false 24 | #Refresh rate in ticks (1 second = 20 ticks) 25 | update-time: 200 26 | #Some example. You can edit or remove this 27 | "example": 28 | f3names: 29 | - "&6This is an example group" 30 | - "&6This text will show to players with &cexample &6group" 31 | - "&6Player groups sorted by their weight" 32 | #Shuffle and update-time are optional, their default values shown above 33 | 34 | #Prevents messages above from sending. Enable this if you have a server with the plugin 35 | #on network, but don't want to change server brand on other servers in the network 36 | only-api: false 37 | 38 | #Use colored console. Disable this if your console doesn't support colors 39 | coloredConsole: true 40 | 41 | #Makes request to spiget.org, to check if new version available 42 | check-for-updates: true 43 | -------------------------------------------------------------------------------- /src/main/resources/config.yml: -------------------------------------------------------------------------------- 1 | # ______ ____ _ _ # 2 | # | ____|___ \| \ | | # 3 | # | |__ __) | \| | __ _ _ __ ___ ___ # 4 | # | __| |__ <| . ` |/ _` | '_ ` _ \ / _ \ # 5 | # | | ___) | |\ | (_| | | | | | | __/ # 6 | # |_| |____/|_| \_|\__,_|_| |_| |_|\___| # 7 | # # 8 | 9 | groups: 10 | #Default group. DO NOT REMOVE! 11 | everyone: 12 | f3names: 13 | - "&6This is an example configuration file" 14 | - "&6Use this list to set multiple names in debug screen" 15 | - "&6It also have support of PlaceholderAPI,&r %player_displayname%" 16 | - "&6Your ping: &2%player_ping%&6, current world time &2%player_world_time_24%" 17 | #Shuffle that list before displaying? 18 | shuffle: false 19 | #Refresh rate in ticks (1 second = 20 ticks) 20 | update-time: 200 21 | #Some example. You can edit or remove this 22 | "example": 23 | f3names: 24 | - "&6This is an example group" 25 | - "&6This text will show to players with &cexample &6group" 26 | - "&6Player groups sorted by their weight" 27 | #Shuffle and update-time are optional, their default values shown above 28 | 29 | #If set to true, disables this plugin and gives bungeecord side plugin ability to send 30 | #messages through this plugin. It allows bungee to use hooked plugins (PlaceholderAPI, Vault, LuckPerms) 31 | bungeecord-as-primary: false 32 | 33 | #Use colored console. Disable this if your console doesn't support colors 34 | coloredConsole: true 35 | 36 | #Makes request to spiget.org, to check if new version available 37 | check-for-updates: true 38 | 39 | #Auto update for the plugin. Ignored if check-for-updates is set to false 40 | auto-update: true -------------------------------------------------------------------------------- /src/main/resources/plugin.yml: -------------------------------------------------------------------------------- 1 | name: F3Name 2 | main: ua.coolboy.f3name.bukkit.F3NameBukkit 3 | version: ${f3name.version} 4 | author: Cool_boy 5 | api-version: 1.13 6 | softdepend: [PlaceholderAPI, LuckPerms] 7 | commands: 8 | f3name: 9 | aliases: [fname, f3namebukkit, debugname, f3n] 10 | permission: f3name.reload 11 | --------------------------------------------------------------------------------