From 04c66bed539dea97d39623dca7f82810b9593995 Mon Sep 17 00:00:00 2001 From: TechnicJelle <22576047+TechnicJelle@users.noreply.github.com> Date: Tue, 3 Jan 2023 01:51:24 +0100 Subject: [PATCH] Updated to new BlueMapAPI and simplified A TON --- README.md | 19 +- pom.xml | 12 +- .../technicjelle/bluemapfloodgate/Config.java | 92 ----- .../technicjelle/bluemapfloodgate/Main.java | 342 ++++-------------- src/main/resources/config.yml | 8 - 5 files changed, 85 insertions(+), 388 deletions(-) delete mode 100644 src/main/java/com/technicjelle/bluemapfloodgate/Config.java delete mode 100644 src/main/resources/config.yml diff --git a/README.md b/README.md index 2f65211..9ff63ee 100644 --- a/README.md +++ b/README.md @@ -7,24 +7,7 @@ by default, floodgate players don't have the correct playerhead image on BlueMap | Without | With | |----------------------------------------------------------|----------------------------------------------------| -| ![without the plugin](.github/readme_assets/without.png) | ![with the plugin](.github/readme_assets/with.png) | - -You can safely reload the config by reloading BlueMap itself with `/bluemap reload`. - -### Config -- `configVersion_DO_NOT_TOUCH` - - Do not touch this one! It's needed internally to keep track of the config versions. -- `cacheHours: 72` - - How long to keep the playerheads cached for. The lower this number, the faster skin updates will appear, but the more network usage there will be. -- `verboseLogging: true` - - Set to `true` if you want more messages telling you what the plugin is up to. -- `customAPI: ''` - - **This option is hidden by default, but you can add it to the config manually.** - - If you want to use a custom skin API, you can set it here. Keep empty or removed if you want to use my own custom skin grabber.\ - Please tell me if you do use a custom skin API, because I genuinely want to know why anyone would want to do that. - - Make sure the API returns a full skin image, not just the head. - - Available placeholders: `{UUID}`, `{NAME}` (case sensitive)\ - If there was no placeholder found in the URL, the UUID will be appended to the end of the URL. +| ![without the plugin](.github/readme_assets/without.png) | ![with the plugin](.github/readme_assets/with.png) | ## [Click here to download!](../../releases/latest) diff --git a/pom.xml b/pom.xml index efc4722..0bce197 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ com.technicjelle BlueMapFloodgate - 1.6.0 + 2.0.0 jar BlueMapFloodgate @@ -76,9 +76,13 @@ https://jitpack.io - opencollab-snapshot + opencollab-releases https://repo.opencollab.dev/maven-snapshots/ + + + + @@ -91,13 +95,13 @@ com.github.BlueMap-Minecraft BlueMapAPI - v2.1.0 + v2.4.0 provided org.geysermc.floodgate api - 2.0-SNAPSHOT + 2.2.0-SNAPSHOT provided diff --git a/src/main/java/com/technicjelle/bluemapfloodgate/Config.java b/src/main/java/com/technicjelle/bluemapfloodgate/Config.java deleted file mode 100644 index 110cff3..0000000 --- a/src/main/java/com/technicjelle/bluemapfloodgate/Config.java +++ /dev/null @@ -1,92 +0,0 @@ -package com.technicjelle.bluemapfloodgate; - -import org.bstats.charts.SimplePie; -import org.bukkit.configuration.file.FileConfiguration; -import org.jetbrains.annotations.NotNull; - -import java.io.File; -import java.io.IOException; -import java.nio.file.Files; -import java.util.Objects; - -import static com.technicjelle.bluemapfloodgate.Main.logger; - -public class Config { - - private final Main plugin; - - private @NotNull FileConfiguration configFile() { - return plugin.getConfig(); - } - - private final String CONFIG_VERSION_KEY = "configVersion_DO_NOT_TOUCH"; - private final int CONFIG_VERSION_NEWEST = 4; //Also change in config.yml - private final String VERBOSE_LOGGING_KEY = "verboseLogging"; - private final boolean VERBOSE_LOGGING_DEFAULT = false; - private final String CACHE_HOURS_KEY = "cacheHours"; - private final int CACHE_HOURS_DEFAULT = 72; - private final String CUSTOM_API_KEY = "customAPI"; - private final String CUSTOM_API_DEFAULT = ""; - - private final boolean verboseLogging; - private final long cacheHours; - private final String customAPI; - - public Config(@NotNull Main plugin) { - this.plugin = plugin; - - //Setup config directory - if(plugin.getDataFolder().mkdirs()) logger.info("Created plugin directory"); - - //Setup config file - File configFile = new File(plugin.getDataFolder(), "config.yml"); - if (!configFile.exists()) { - try { - logger.info("Creating config file"); - Files.copy(Objects.requireNonNull(plugin.getResource("config.yml")), configFile.toPath()); - } catch (IOException e) { - e.printStackTrace(); - } - } - - //Load config from disk - plugin.reloadConfig(); - - //Load config values into variables - int configVersionCurrent = configFile().getInt(CONFIG_VERSION_KEY, 0); //is 0 if the config file doesn't exist - - boolean outOfDate = configVersionCurrent != 0 && configVersionCurrent != CONFIG_VERSION_NEWEST - //use config.contains to check for all old config settings - || configFile().contains("verboseUpdateMessages") - || configFile().contains("useTydiumCraftSkinAPI"); - - if (outOfDate){ - logger.severe("Config is out of date, please delete the config.yml file and reload BlueMap to regenerate a new one."); - logger.warning("If you have any custom settings, you will need to re-add them to the new config file."); - logger.warning("Using built-in defaults for now."); - - cacheHours = CACHE_HOURS_DEFAULT; - verboseLogging = VERBOSE_LOGGING_DEFAULT; - customAPI = CUSTOM_API_DEFAULT; - } else { - //Config file is up-to-date - cacheHours = configFile().getLong(CACHE_HOURS_KEY, CACHE_HOURS_DEFAULT); - verboseLogging = configFile().getBoolean(VERBOSE_LOGGING_KEY, VERBOSE_LOGGING_DEFAULT); - customAPI = configFile().getString(CUSTOM_API_KEY, CUSTOM_API_DEFAULT); - } - - plugin.metrics.addCustomChart(new SimplePie("custom_api", () -> customAPI.isBlank() ? "Direct" : "Custom")); - } - - public boolean verboseLogging() { - return verboseLogging; - } - - public long cacheHours() { - return cacheHours; - } - - public String customAPI() { - return customAPI; - } -} diff --git a/src/main/java/com/technicjelle/bluemapfloodgate/Main.java b/src/main/java/com/technicjelle/bluemapfloodgate/Main.java index a065d93..bc6d9e0 100644 --- a/src/main/java/com/technicjelle/bluemapfloodgate/Main.java +++ b/src/main/java/com/technicjelle/bluemapfloodgate/Main.java @@ -4,12 +4,8 @@ import com.google.gson.JsonObject; import com.google.gson.JsonParser; import de.bluecolored.bluemap.api.BlueMapAPI; +import de.bluecolored.bluemap.api.plugin.SkinProvider; import org.bstats.bukkit.Metrics; -import org.bukkit.Bukkit; -import org.bukkit.entity.Player; -import org.bukkit.event.EventHandler; -import org.bukkit.event.Listener; -import org.bukkit.event.player.PlayerJoinEvent; import org.bukkit.plugin.java.JavaPlugin; import org.geysermc.floodgate.api.FloodgateApi; import org.geysermc.floodgate.api.player.FloodgatePlayer; @@ -18,333 +14,147 @@ import javax.imageio.ImageIO; import java.awt.image.BufferedImage; -import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.net.MalformedURLException; import java.net.URL; import java.net.URLConnection; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.nio.file.StandardCopyOption; -import java.util.Calendar; -import java.util.HashSet; -import java.util.Objects; +import java.util.Optional; import java.util.UUID; import java.util.function.Consumer; -import java.util.logging.Logger; -public final class Main extends JavaPlugin implements Listener { - - private static class CachedPlayer { - UUID uuid; - String xuid; - String name; - - CachedPlayer(UUID _uuid, String _xuid, String _name) { - uuid = _uuid; - xuid = _xuid; - name = _name; - } - - //Two functions to assist the Set in knowing if a CachedPlayer is already in the set - @Override - public boolean equals(Object obj) { - if (obj == this) - return true; - if (!(obj instanceof CachedPlayer)) - return false; - CachedPlayer in = (CachedPlayer) obj; - boolean resultUUID = uuid.equals(in.uuid); - boolean resultXUID = Objects.equals(xuid, in.xuid); -// verboseLog(uuid + ":" + in.uuid + " & " + xuid + ":" + in.xuid + "==>" + resultUUID + resultXUID); - return resultUUID && resultXUID; - } - - @Override - public int hashCode() { -// verboseLog(uuid + xuid + "==>" + (uuid.toString() + xuid).hashCode()); - return (uuid.toString() + xuid).hashCode(); - } +public final class Main extends JavaPlugin { + private final boolean VERBOSE_LOGGING = false; + private void verboseLog(String message) { + if (VERBOSE_LOGGING) getLogger().info(message); } - public Metrics metrics; - - public static Logger logger; - public static Config config; - - private FloodgateApi floodgateApi; - private File blueMapPlayerheadsDirectory; - private File ownPlayerheadsDirectory; - - @Nullable private HashSet playersToProcess; //null when BlueMap is online, to keep track of players that join before BlueMap is online - @Override public void onEnable() { - metrics = new Metrics(this, 16426); - - logger = getLogger(); - - floodgateApi = FloodgateApi.getInstance(); - playersToProcess = new HashSet<>(); + new Metrics(this, 16426); - getServer().getPluginManager().registerEvents(this, this); - - //all actual startup and shutdown logic moved to BlueMapAPI enable/disable methods, so `/bluemap reload` also reloads this plugin BlueMapAPI.onEnable(blueMapOnEnableListener); - BlueMapAPI.onDisable(blueMapOnDisableListener); - } - Consumer blueMapOnEnableListener = blueMapAPI -> { - //Config - config = new Config(this); - - - //Directory - ownPlayerheadsDirectory = new File(getDataFolder() + "/playerheads"); - if (ownPlayerheadsDirectory.mkdir()) logger.info(ownPlayerheadsDirectory.toString() + " directory made"); - - //TODO: replace with new BlueMapAPI methods etc - blueMapPlayerheadsDirectory = new File(blueMapAPI.getWebApp().getWebRoot() + "/assets/playerheads/"); + getLogger().info("BlueMap Floodgate compatibility plugin enabled!"); + } - //Process all players that joined before BlueMap was online or while it was reloading - Bukkit.getScheduler().runTaskAsynchronously(this, () -> { -// verboseLog(">async"); - if (playersToProcess != null) { -// verboseLog(">not null"); - for (CachedPlayer cachedPlayer : playersToProcess) { - floodgateJoin(cachedPlayer); + private final Consumer blueMapOnEnableListener = blueMapAPI -> { + SkinProvider floodgateSkinProvider = new SkinProvider() { + private final FloodgateApi floodgateApi = FloodgateApi.getInstance(); + private final SkinProvider defaultSkinProvider = blueMapAPI.getPlugin().getSkinProvider(); + + @Override + public Optional load(UUID playerUUID) throws IOException { + if (floodgateApi.isFloodgatePlayer(playerUUID)) { + FloodgatePlayer floodgatePlayer = floodgateApi.getPlayer(playerUUID); + String xuid = floodgatePlayer.getXuid(); + @Nullable String textureID = textureIDFromXUID(xuid); + if (textureID == null) { + verboseLog("TextureID for " + playerUUID + " is null"); + return Optional.empty(); + } + @Nullable BufferedImage skin = skinFromTextureID(textureID); + if (skin == null) { + verboseLog("Skin for " + playerUUID + " is null"); + return Optional.empty(); + } + verboseLog("Skin for " + playerUUID + " successfully gotten!"); + return Optional.of(skin); + } else { + return defaultSkinProvider.load(playerUUID); } - playersToProcess = null; - } else { - playersToProcessNULL(); } - }); + }; - logger.info("BlueMap Floodgate compatibility plugin enabled!"); - }; + blueMapAPI.getPlugin().setSkinProvider(floodgateSkinProvider); - private void playersToProcessNULL() { - logger.severe("playersToProcess was null! Please report this on GitHub! https://github.com/TechnicJelle/BlueMapFloodgate/issues"); - for (Player p : Bukkit.getOnlinePlayers()) { - if (floodgateApi.isFloodgatePlayer(p.getUniqueId())) { - FloodgatePlayer floodgatePlayer = floodgateApi.getPlayer(p.getUniqueId()); - String xuid = floodgatePlayer.getXuid(); - CachedPlayer cachedPlayer = new CachedPlayer(p.getUniqueId(), xuid, floodgatePlayer.getUsername()); - floodgateJoin(cachedPlayer); - } - } - } - - Consumer blueMapOnDisableListener = blueMapAPI -> { - playersToProcess = new HashSet<>(); - logger.info("BlueMap API Shutting down!"); }; @Override public void onDisable() { BlueMapAPI.unregisterListener(blueMapOnEnableListener); - BlueMapAPI.unregisterListener(blueMapOnDisableListener); - - playersToProcess = null; - logger.info("BlueMap Floodgate compatibility plugin disabled!"); - } - - private void verboseLog(String message) { - if (config.verboseLogging()) logger.info(message); + getLogger().info("BlueMap Floodgate compatibility plugin disabled!"); } - @EventHandler - public void onJoin(PlayerJoinEvent e) { - Bukkit.getScheduler().runTaskAsynchronously(this, () -> { - Player p = e.getPlayer(); - if (floodgateApi.isFloodgatePlayer(p.getUniqueId())) { - FloodgatePlayer floodgatePlayer = floodgateApi.getPlayer(p.getUniqueId()); - String xuid = floodgatePlayer.getXuid(); - CachedPlayer cachedPlayer = new CachedPlayer(p.getUniqueId(), xuid, floodgatePlayer.getUsername()); + // ================================================================================================================ + // ===============================================Util Methods===================================================== + // ================================================================================================================ - BlueMapAPI.getInstance().ifPresentOrElse(api -> { - //BlueMap IS currently loaded - floodgateJoin(cachedPlayer); - }, () -> { - //BlueMap is currently NOT loaded - if (playersToProcess != null) { - if (playersToProcess.add(cachedPlayer)) { - verboseLog("Added " + p.getUniqueId() + " to processing queue"); - } else { - verboseLog("Player " + p.getUniqueId() + " was already in the processing queue"); - } - } else { - playersToProcessNULL(); - } - }); - } - }); - } - - private void floodgateJoin(@NotNull CachedPlayer cachedPlayer) { - File ownHeadFile = new File(ownPlayerheadsDirectory + "/" + cachedPlayer.uuid + ".png"); - if (ownHeadFile.exists()) { - long lastModified = ownHeadFile.lastModified(); //long value representing the time the file was last modified, measured in milliseconds since the epoch (00:00:00 GMT, January 1, 1970) - Calendar currentDate = Calendar.getInstance(); - long dateNow = currentDate.getTimeInMillis(); - if (dateNow > lastModified + 1000L * 60L * 60L * config.cacheHours()) { - verboseLog("Cache for " + cachedPlayer.uuid + " outdated"); - downloadHeadToCache(cachedPlayer, ownHeadFile); - - if (ownHeadFile.setLastModified(dateNow)) { - verboseLog(" Cache updated"); - } else { - logger.severe(" Cache wasn't updated. This should never happen! Please report this on GitHub! https://github.com/TechnicJelle/BlueMapFloodgate/issues"); - } - - } else { - verboseLog("Head for " + cachedPlayer.uuid + " already cached"); - } - } else { - downloadHeadToCache(cachedPlayer, ownHeadFile); - } - - - //TODO: replace with new BlueMapAPI methods etc - Path destination = Paths.get(blueMapPlayerheadsDirectory.toString(), ownHeadFile.getName()); - Bukkit.getScheduler().runTaskLater(this, () -> { - verboseLog("Overwriting BlueMap's head with floodgate's head"); - try { - Files.copy(ownHeadFile.toPath(), destination, StandardCopyOption.REPLACE_EXISTING); - verboseLog("BlueMap file overwritten!"); - } catch (IOException e) { - logger.warning("Failed to copy the head image file from own directory to BlueMap's directory"); - e.printStackTrace(); - } - }, 30); - } - - private void downloadHeadToCache(CachedPlayer cachedPlayer, File f) { - String placeholderUUID = "{UUID}"; - String placeholderName = "{NAME}"; - - BufferedImage skin; - - if (config.customAPI().isBlank()) { - String textureID = textureIDFromXUID(cachedPlayer.xuid); - skin = skinFromTextureID(textureID); - } else { - String link = config.customAPI().contains(placeholderUUID) - ? config.customAPI().replace(placeholderUUID, cachedPlayer.uuid.toString()) - : config.customAPI() + cachedPlayer.uuid; - link = link.replace(placeholderName, cachedPlayer.name); - - verboseLog("Getting " + cachedPlayer.name + "'s skin from custom Skin API: " + link); - skin = imageFromURL(link); - } - - BufferedImage head; - - if (skin == null) { - logger.warning("Skin was null, falling back to Steve head!" - + (config.verboseLogging() ? "" : " Turn on verbose logging in the config to find out why, next time")); - head = getSteveHead(); - } else { - //if skin is already a head, just use that - if (skin.getWidth() == 8 && skin.getHeight() == 8) { - verboseLog("Skin was already a head"); - head = skin; - } else { - verboseLog("Skin was not a head, cropping..."); - head = headFromSkin(skin); - } - } - - if (head == null) { - logger.warning("Head was null!"); - } else { - try { - ImageIO.write(head, "png", f); - verboseLog(f + " saved"); - } catch (IOException e) { - logger.warning("Failed to write the head image file to own directory"); - e.printStackTrace(); - } - } - } - - //TODO: Make return nullable, check usages and make sure they can handle null - //TODO: Inverts all these nested if-statements, replace with early returns - private String textureIDFromXUID(@NotNull String xuid) { + /** + * @param xuid XUID of the floodgate player + * @return the texture ID of the player, or null if it could not be found + */ + private @Nullable String textureIDFromXUID(@NotNull String xuid) { try { URL url = new URL("https://api.geysermc.org/v2/skin/" + xuid); + verboseLog("Getting textureID from " + url); try { URLConnection request = url.openConnection(); request.connect(); JsonObject joRoot = JsonParser.parseReader(new InputStreamReader((InputStream) request.getContent())).getAsJsonObject(); - if (joRoot != null) { - JsonElement jeTextureID = joRoot.get("texture_id"); - if (jeTextureID != null) { - String textureID = jeTextureID.getAsString(); - if (textureID != null) { - return textureID; - } else { - verboseLog("textureID is null!"); - } - } else { - verboseLog("jeTextureID is null!"); - } - } else { + if (joRoot == null) { verboseLog("joRoot is null!"); + return null; } + + JsonElement jeTextureID = joRoot.get("texture_id"); + if (jeTextureID == null) { + verboseLog("jeTextureID is null!"); + return null; + } + + String textureID = jeTextureID.getAsString(); + if (textureID == null) { + verboseLog("textureID is null!"); + return null; + } + + return textureID; } catch (IOException e) { - logger.warning("Failed to get the textureID"); + getLogger().warning("Failed to get the textureID for " + xuid + " from " + url); e.printStackTrace(); + return null; } } catch (MalformedURLException e) { e.printStackTrace(); + return null; } - return null; } - private BufferedImage skinFromTextureID(String textureID) { + /** + * @param textureID texture ID of the floodgate player + * @return the skin of the player, or null if it could not be found + */ + private @Nullable BufferedImage skinFromTextureID(@NotNull String textureID) { verboseLog("Getting skin from textureID: " + textureID); return imageFromURL("https://textures.minecraft.net/texture/" + textureID); } - private @Nullable BufferedImage imageFromURL(String url) { + /** + * @param url URL of the image + * @return the image, or null if it could not be found + */ + private @Nullable BufferedImage imageFromURL(@NotNull String url) { BufferedImage result; try { URL imageUrl = new URL(url); + verboseLog("Getting image from URL: " + url); try { InputStream in = imageUrl.openStream(); result = ImageIO.read(in); in.close(); } catch (IOException e) { - logger.warning("Failed to get the image from " + url); + getLogger().warning("Failed to get the image from " + url); e.printStackTrace(); return null; } } catch (MalformedURLException e) { - logger.warning("URL: " + url); e.printStackTrace(); return null; } return result; } - - //TODO: replace with new BlueMapAPI methods etc - private BufferedImage headFromSkin(@NotNull BufferedImage skin) { - return skin.getSubimage(8, 8, 8, 8); - } - - private @Nullable BufferedImage getSteveHead() { - try { - return ImageIO.read(new File(blueMapPlayerheadsDirectory.getParent() + "/steve.png")); - } catch (IOException e) { - logger.warning("Failed to load BlueMap's fallback steve.png!"); - e.printStackTrace(); - return null; - } - } } diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml deleted file mode 100644 index adc7445..0000000 --- a/src/main/resources/config.yml +++ /dev/null @@ -1,8 +0,0 @@ -# Do not touch this one! It's needed internally to keep track of the config versions. -configVersion_DO_NOT_TOUCH: 4 - -# How long to keep the playerheads cached for. The lower this number, the faster skin updates will appear, but the more network usage there will be. -cacheHours: 72 - -# Set to true if you want more messages telling you what the plugin is up to. -verboseLogging: false