diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml index 6c3f2b1..e512dd5 100644 --- a/.github/workflows/maven.yml +++ b/.github/workflows/maven.yml @@ -1,28 +1,25 @@ -# This workflow will build a Java project with Maven -# For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-maven - name: Java CI with Maven on: - workflow_dispatch: push: pull_request: + workflow_dispatch: jobs: build: runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3.3.0 + - uses: actions/checkout@v4 - name: Set up JDK 11 - uses: actions/setup-java@v3.9.0 + uses: actions/setup-java@v4 with: java-version: '11' distribution: 'temurin' - name: Build with Maven run: mvn -B package --file pom.xml - name: Upload a Build Artifact - uses: actions/upload-artifact@v3.1.2 + uses: actions/upload-artifact@v4 with: - # A file, directory or wildcard pattern that describes what to upload - path: /home/runner/work/BlueMapOfflinePlayerMarkers/BlueMapOfflinePlayerMarkers/target/* + path: | + target/*.jar + !target/original-*.jar diff --git a/.gitignore b/.gitignore index 8081d9e..05a520d 100644 --- a/.gitignore +++ b/.gitignore @@ -114,3 +114,6 @@ run/ # Minecraft server for testing testserver/ + +# Player Name Caches +cachedPlayerNames.json diff --git a/pom.xml b/pom.xml index a025cac..21bef6e 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ com.technicjelle BlueMapOfflinePlayerMarkers - 2.9 + 3.0-SNAPSHOT jar BlueMapOfflinePlayerMarkers diff --git a/src/main/java/com/technicjelle/bluemapofflineplayermarkers/BlueMapOfflinePlayerMarkers.java b/src/main/java/com/technicjelle/bluemapofflineplayermarkers/BlueMapOfflinePlayerMarkers.java deleted file mode 100644 index 44d65de..0000000 --- a/src/main/java/com/technicjelle/bluemapofflineplayermarkers/BlueMapOfflinePlayerMarkers.java +++ /dev/null @@ -1,88 +0,0 @@ -package com.technicjelle.bluemapofflineplayermarkers; - -import com.technicjelle.BMUtils; -import com.technicjelle.UpdateChecker; -import de.bluecolored.bluemap.api.BlueMapAPI; -import org.bstats.bukkit.Metrics; -import org.bukkit.Bukkit; -import org.bukkit.event.EventHandler; -import org.bukkit.event.Listener; -import org.bukkit.event.player.PlayerJoinEvent; -import org.bukkit.event.player.PlayerQuitEvent; -import org.bukkit.plugin.java.JavaPlugin; - -import java.io.IOException; -import java.util.function.Consumer; -import java.util.logging.Level; - -public final class BlueMapOfflinePlayerMarkers extends JavaPlugin implements Listener { - private Config config; - private UpdateChecker updateChecker; - private MarkerHandler markerHandler; - - @Override - public void onEnable() { - new Metrics(this, 16425); - - updateChecker = new UpdateChecker("TechnicJelle", "BlueMapOfflinePlayerMarkers", getDescription().getVersion()); - updateChecker.checkAsync(); - - getServer().getPluginManager().registerEvents(this, this); - - markerHandler = new MarkerHandler(this); - - //all actual startup and shutdown logic moved to BlueMapAPI enable/disable methods, so `/bluemap reload` also reloads this plugin - BlueMapAPI.onEnable(onEnableListener); - BlueMapAPI.onDisable(onDisableListener); - } - - Consumer onEnableListener = api -> { - getLogger().info("API Ready! BlueMap Offline Player Markers plugin enabled!"); - updateChecker.logUpdateMessage(getLogger()); - - config = new Config(this); - - try { - BMUtils.copyJarResourceToBlueMap(api, getClassLoader(), "style.css", "bmopm.css", false); - BMUtils.copyJarResourceToBlueMap(api, getClassLoader(), "script.js", "bmopm.js", false); - } catch (IOException e) { - getLogger().log(Level.SEVERE, "Failed to copy resources to BlueMap webapp!", e); - } - - //create marker handler and add all offline players in a separate thread, so the server doesn't hang up while it's going - //with a delay, so any potential BlueMap SkinProviders have time to load - Bukkit.getScheduler().runTaskLaterAsynchronously(this, markerHandler::loadOfflineMarkers, 20 * 5); - }; - - Consumer onDisableListener = api -> { - getLogger().info("API disabled! BlueMap Offline Player Markers shutting down..."); - //not much to do here, actually... - }; - - @Override - public void onDisable() { - BlueMapAPI.unregisterListener(onEnableListener); - BlueMapAPI.unregisterListener(onDisableListener); - getLogger().info("BlueMap Offline Player Markers plugin disabled!"); - } - - - @EventHandler - public void onJoin(PlayerJoinEvent e) { - Bukkit.getScheduler().runTaskAsynchronously(this, () -> markerHandler.remove(e.getPlayer())); - } - - @EventHandler - public void onLeave(PlayerQuitEvent e) { - Bukkit.getScheduler().runTaskAsynchronously(this, () -> markerHandler.add(e.getPlayer())); - } - - /** - * The config instance may change when the plugin is reloaded, so this method should be used to get the current config - * - * @return the current config - */ - public Config getCurrentConfig() { - return config; - } -} diff --git a/src/main/java/com/technicjelle/bluemapofflineplayermarkers/Config.java b/src/main/java/com/technicjelle/bluemapofflineplayermarkers/Config.java deleted file mode 100644 index 4b74742..0000000 --- a/src/main/java/com/technicjelle/bluemapofflineplayermarkers/Config.java +++ /dev/null @@ -1,57 +0,0 @@ -package com.technicjelle.bluemapofflineplayermarkers; - -import com.technicjelle.MCUtils; -import org.bukkit.GameMode; -import org.bukkit.configuration.file.FileConfiguration; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; - -public class Config { - public static final String MARKER_SET_ID = "offplrs"; - - private final BlueMapOfflinePlayerMarkers plugin; - - public String markerSetName; - public boolean toggleable; - public boolean defaultHidden; - public long expireTimeInHours; - public List hiddenGameModes; - - public Config(BlueMapOfflinePlayerMarkers plugin) { - this.plugin = plugin; - - try { - MCUtils.copyPluginResourceToConfigDir(plugin, "config.yml", "config.yml", false); - } catch (IOException e) { - throw new RuntimeException(e); - } - - //Load config from disk - plugin.reloadConfig(); - - //Load config values into variables - markerSetName = configFile().getString("MarkerSetName"); - toggleable = configFile().getBoolean("Toggleable"); - defaultHidden = configFile().getBoolean("DefaultHidden"); - expireTimeInHours = configFile().getLong("ExpireTimeInHours"); - hiddenGameModes = parseGameModes(configFile().getStringList("HiddenGameModes")); - } - - private List parseGameModes(List hiddenGameModesStrings) { - ArrayList gameModes = new ArrayList<>(); - for (String gm : hiddenGameModesStrings) { - try { - gameModes.add(GameMode.valueOf(gm.toUpperCase())); - } catch (IllegalArgumentException e) { - plugin.getLogger().warning("Invalid Game Mode: " + gm); - } - } - return gameModes; - } - - private FileConfiguration configFile() { - return plugin.getConfig(); - } -} diff --git a/src/main/java/com/technicjelle/bluemapofflineplayermarkers/MarkerHandler.java b/src/main/java/com/technicjelle/bluemapofflineplayermarkers/MarkerHandler.java deleted file mode 100644 index d2a97b5..0000000 --- a/src/main/java/com/technicjelle/bluemapofflineplayermarkers/MarkerHandler.java +++ /dev/null @@ -1,191 +0,0 @@ -package com.technicjelle.bluemapofflineplayermarkers; - -import com.technicjelle.BMUtils; -import com.technicjelle.bluemapofflineplayermarkers.models.PlayerNBT; -import de.bluecolored.bluemap.api.BlueMapAPI; -import de.bluecolored.bluemap.api.BlueMapMap; -import de.bluecolored.bluemap.api.BlueMapWorld; -import de.bluecolored.bluemap.api.markers.MarkerSet; -import de.bluecolored.bluemap.api.markers.POIMarker; -import de.bluecolored.bluenbt.BlueNBT; -import de.bluecolored.bluenbt.NBTReader; -import org.bukkit.Bukkit; -import org.bukkit.GameMode; -import org.bukkit.Location; -import org.bukkit.OfflinePlayer; -import org.bukkit.entity.Player; -import org.jetbrains.annotations.NotNull; - -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.Optional; -import java.util.UUID; -import java.util.logging.Level; -import java.util.stream.Stream; -import java.util.zip.GZIPInputStream; - - -public class MarkerHandler { - private static final BlueNBT nbt = new BlueNBT(); - - private final BlueMapOfflinePlayerMarkers plugin; - - MarkerHandler(BlueMapOfflinePlayerMarkers plugin) { - this.plugin = plugin; - } - - /** - * Adds a player marker to the map. - * - * @param player The player to add the marker for. - */ - public void add(@NotNull Player player) { - add(player.getUniqueId(), player.getName(), player.getLocation(), player.getGameMode(), System.currentTimeMillis()); - } - - /** - * Adds a player marker to the map. - * - * @param player The player to add the marker for. - * @param location The location to put the marker at. - * @param gameMode The game mode of the player. - */ - public void add(@NotNull OfflinePlayer player, @NotNull Location location, @NotNull GameMode gameMode) { - String playerName = player.getName() != null ? player.getName() : player.getUniqueId().toString(); - add(player.getUniqueId(), playerName, location, gameMode, player.getLastPlayed()); - } - - /** - * Adds a player marker to the map. - * - * @param uuid The UUID of the player to add the marker for. - * @param playerName The name of the player to add the marker for. - * @param location The location to put the marker at. - * @param gameMode The game mode of the player. - * @param lastPlayed The last time the player was online. - */ - private void add(@NotNull UUID uuid, @NotNull String playerName, @NotNull Location location, @NotNull GameMode gameMode, long lastPlayed) { - Optional optionalApi = BlueMapAPI.getInstance(); - if (optionalApi.isEmpty()) { - plugin.getLogger().warning("Tried to add a marker, but BlueMap wasn't loaded!"); - return; - } - BlueMapAPI api = optionalApi.get(); - - //If this player's visibility is disabled on the map, don't add the marker. - if (!api.getWebApp().getPlayerVisibility(uuid)) return; - - //If this player's game mode is disabled on the map, don't add the marker. - if (plugin.getCurrentConfig().hiddenGameModes.contains(gameMode)) return; - - // Get BlueMapWorld for the location - BlueMapWorld blueMapWorld = api.getWorld(location.getWorld()).orElse(null); - if (blueMapWorld == null) return; - - // Create marker-template - // (add 1.8 to y to place the marker at the head-position of the player, like BlueMap does with its player-markers) - POIMarker.Builder markerBuilder = POIMarker.builder() - .label(playerName) - .detail(playerName + " (offline)
" - + "") - .styleClasses("bmopm-offline-player") - .position(location.getX(), location.getY() + 1.8, location.getZ()); - - // Create an icon and marker for each map of this world - // We need to create a separate marker per map, because the map-storage that the icon is saved in - // is different for each map - for (BlueMapMap map : blueMapWorld.getMaps()) { - markerBuilder.icon(BMUtils.getPlayerHeadIconAddress(api, uuid, map), 0, 0); // centered with CSS instead - - // get marker-set (or create new marker set if none found) - MarkerSet markerSet = map.getMarkerSets().computeIfAbsent(Config.MARKER_SET_ID, id -> MarkerSet.builder() - .label(plugin.getCurrentConfig().markerSetName) - .toggleable(plugin.getCurrentConfig().toggleable) - .defaultHidden(plugin.getCurrentConfig().defaultHidden) - .build()); - - // add marker - markerSet.put(uuid.toString(), markerBuilder.build()); - } - - plugin.getLogger().info("Marker for " + playerName + " added"); - } - - - /** - * Removes a player marker from the map. - * - * @param player The player to remove the marker for. - */ - public void remove(Player player) { - Optional optionalApi = BlueMapAPI.getInstance(); - if (optionalApi.isEmpty()) { - plugin.getLogger().warning("Tried to remove a marker, but BlueMap wasn't loaded!"); - return; - } - BlueMapAPI api = optionalApi.get(); - - // remove all markers with the players uuid - for (BlueMapMap map : api.getMaps()) { - MarkerSet set = map.getMarkerSets().get(Config.MARKER_SET_ID); - if (set != null) set.remove(player.getUniqueId().toString()); - } - - plugin.getLogger().info("Marker for " + player.getName() + " removed"); - } - - /** - * Load in markers of all offline players by going through the playerdata NBT - */ - public void loadOfflineMarkers() { - //I really don't like "getWorlds().get(0)" as a way to get the main world, but as far as I can tell there is no other way - Path playerDataFolder = Bukkit.getWorlds().get(0).getWorldFolder().toPath().resolve("playerdata"); - - //Return if playerdata is missing for some reason. - if (!Files.exists(playerDataFolder) || !Files.isDirectory(playerDataFolder)) { - plugin.getLogger().severe("Playerdata folder not found, skipping loading of offline markers"); - return; - } - - try (Stream s = Files.list(playerDataFolder)) { - s.filter(p -> p.toString().endsWith(".dat")).forEach(this::loadOfflineMarker); - } catch (IOException e) { - plugin.getLogger().log(Level.SEVERE, "Failed to stream playerdata", e); - } - } - - private void loadOfflineMarker(@NotNull Path playerDataFile) { - String fileName = playerDataFile.getFileName().toString(); - UUID uuid = UUID.fromString(fileName.replace(".dat", "")); - OfflinePlayer op = Bukkit.getOfflinePlayer(uuid); - if (op.isOnline()) return; - - long timeSinceLastPlayed = System.currentTimeMillis() - op.getLastPlayed(); - if (plugin.getCurrentConfig().expireTimeInHours > 0 && timeSinceLastPlayed > plugin.getCurrentConfig().expireTimeInHours * 60 * 60 * 1000) { - plugin.getLogger().fine("Player " + op.getName() + " (" + uuid + ") was last seen " + timeSinceLastPlayed + "ms ago, which is too long ago, skipping"); - return; - } - - try (GZIPInputStream in = new GZIPInputStream(Files.newInputStream(playerDataFile))) { - NBTReader reader = new NBTReader(in); - PlayerNBT playerNBT = nbt.read(reader, PlayerNBT.class); - Location location = playerNBT.getLocation(); - GameMode gameMode = playerNBT.getGameMode(); - - if (gameMode == null) { - plugin.getLogger().warning("Couldn't read or convert GameMode from " + fileName); - return; - } - - if (location == null) { - plugin.getLogger().warning("Couldn't read Location from " + fileName); - return; - } - - add(op, location, gameMode); - } catch (IOException e) { - plugin.getLogger().log(Level.WARNING, "Failed to read playerdata file " + fileName, e); - } - } -} diff --git a/src/main/java/com/technicjelle/bluemapofflineplayermarkers/common/Config.java b/src/main/java/com/technicjelle/bluemapofflineplayermarkers/common/Config.java new file mode 100644 index 0000000..ac5cacd --- /dev/null +++ b/src/main/java/com/technicjelle/bluemapofflineplayermarkers/common/Config.java @@ -0,0 +1,57 @@ +package com.technicjelle.bluemapofflineplayermarkers.common; + +import com.technicjelle.bluemapofflineplayermarkers.core.GameMode; +import com.technicjelle.bluemapofflineplayermarkers.core.Singletons; + +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +public interface Config { + String MARKER_SET_ID = "offline-players"; + + String getMarkerSetName(); + + boolean isToggleable(); + + boolean isDefaultHidden(); + + /** + * If you want to show only players who have joined in the last X hours, set this to a number greater than 0.

+ * If you want to show all players, set this to 0. + */ + long getExpireTimeInHours(); + + List getHiddenGameModes(); + + default boolean isGameModeHidden(GameMode gameMode) { + return getHiddenGameModes().contains(gameMode); + } + + /** + * @param playerUUID The player to check. + * @return true if the player should be hidden + */ + default boolean checkPlayerLastPlayed(UUID playerUUID) { + if (getExpireTimeInHours() <= 0) return false; // don't hide players if the expiry time is 0 or less + + Instant lastPlayed = Singletons.getServer().getPlayerLastPlayed(playerUUID); + Instant expireTime = Instant.now().minusSeconds(getExpireTimeInHours() * 60 * 60); + return lastPlayed.isBefore(expireTime); + } + + static List parseGameModes(List hiddenGameModesStrings) throws IllegalArgumentException { + ArrayList gameModes = new ArrayList<>(); + for (String hiddenGameModeString : hiddenGameModesStrings) { + try { + GameMode parsedGameMode = GameMode.getById(hiddenGameModeString); + if (parsedGameMode == null) throw new IllegalArgumentException("Invalid Game Mode: " + hiddenGameModeString); + gameModes.add(parsedGameMode); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException("Invalid Game Mode: " + hiddenGameModeString); + } + } + return gameModes; + } +} diff --git a/src/main/java/com/technicjelle/bluemapofflineplayermarkers/common/PlayerData.java b/src/main/java/com/technicjelle/bluemapofflineplayermarkers/common/PlayerData.java new file mode 100644 index 0000000..940b6c5 --- /dev/null +++ b/src/main/java/com/technicjelle/bluemapofflineplayermarkers/common/PlayerData.java @@ -0,0 +1,15 @@ +package com.technicjelle.bluemapofflineplayermarkers.common; + +import com.flowpowered.math.vector.Vector3d; +import com.technicjelle.bluemapofflineplayermarkers.core.GameMode; + +import java.util.Optional; +import java.util.UUID; + +public interface PlayerData { + GameMode getGameMode(); + + Vector3d getPosition(); + + Optional getWorldUUID(); +} diff --git a/src/main/java/com/technicjelle/bluemapofflineplayermarkers/common/Server.java b/src/main/java/com/technicjelle/bluemapofflineplayermarkers/common/Server.java new file mode 100644 index 0000000..46a761f --- /dev/null +++ b/src/main/java/com/technicjelle/bluemapofflineplayermarkers/common/Server.java @@ -0,0 +1,91 @@ +package com.technicjelle.bluemapofflineplayermarkers.common; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonObject; +import com.google.gson.reflect.TypeToken; +import com.technicjelle.bluemapofflineplayermarkers.core.Singletons; + +import java.io.*; +import java.net.URL; +import java.net.URLConnection; +import java.nio.file.Path; +import java.time.Instant; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; + +public interface Server { + Gson _gson = new GsonBuilder() + .setLenient() +// .setPrettyPrinting() //Disabled to discourage people from editing the file by hand + .enableComplexMapKeySerialization() + .create(); + + Map _cachedPlayerNames = new HashMap<>(); + String _cacheFileName = "cachedPlayerNames.json"; + + default void startUp() { + //load cached player names + Path cacheFolder = Singletons.getServer().getConfigFolder(); + File cacheFile = new File(cacheFolder.toFile(), _cacheFileName); + if (cacheFile.exists()) { + try (InputStreamReader reader = new FileReader(cacheFile)) { + Map map = _gson.fromJson(reader, new TypeToken>() {}.getType()); + if (map != null) { + _cachedPlayerNames.putAll(map); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + } + } + + default void shutDown() { + //save cached player names + Path cacheFolder = Singletons.getServer().getConfigFolder(); + File cacheFile = new File(cacheFolder.toFile(), _cacheFileName); + try (Writer writer = new FileWriter(cacheFile)) { + _gson.toJson(_cachedPlayerNames, writer); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + boolean isPlayerOnline(UUID playerUUID); + + Path getConfigFolder(); + + Path getPlayerDataFolder(); + + /** + * @param playerUUID The UUID of the player to get the last played time for. + * @return The last time the player was online in amount of milliseconds since epoch (January 1, 1970, 00:00:00 GMT). + */ + Instant getPlayerLastPlayed(UUID playerUUID); + + String getPlayerName(UUID playerUUID); + + /** + * Requests the player's name from the Mojang API. May be slow. + * + * @throws IOException If there was an error with the connection. + */ + static String nameFromMojangAPI(UUID playerUUID) throws IOException { + String name = _cachedPlayerNames.get(playerUUID); + if (name != null) return name; + + URL url = new URL("https://sessionserver.mojang.com/session/minecraft/profile/" + playerUUID); + URLConnection request = url.openConnection(); + request.connect(); + + JsonObject response = _gson.fromJson(new InputStreamReader(request.getInputStream()), JsonObject.class); + if (response == null) throw new IOException("No response from Mojang API"); + name = response.get("name").getAsString(); + _cachedPlayerNames.put(playerUUID, name); + return name; + } + + Optional guessWorldUUID(Object object); +} diff --git a/src/main/java/com/technicjelle/bluemapofflineplayermarkers/core/BMApiStatus.java b/src/main/java/com/technicjelle/bluemapofflineplayermarkers/core/BMApiStatus.java new file mode 100644 index 0000000..6d663d6 --- /dev/null +++ b/src/main/java/com/technicjelle/bluemapofflineplayermarkers/core/BMApiStatus.java @@ -0,0 +1,9 @@ +package com.technicjelle.bluemapofflineplayermarkers.core; + +import de.bluecolored.bluemap.api.BlueMapAPI; + +public class BMApiStatus { + public boolean isBlueMapAPIPresent() { + return BlueMapAPI.getInstance().isPresent(); + } +} diff --git a/src/main/java/com/technicjelle/bluemapofflineplayermarkers/core/GameMode.java b/src/main/java/com/technicjelle/bluemapofflineplayermarkers/core/GameMode.java new file mode 100644 index 0000000..e9f83f7 --- /dev/null +++ b/src/main/java/com/technicjelle/bluemapofflineplayermarkers/core/GameMode.java @@ -0,0 +1,37 @@ +package com.technicjelle.bluemapofflineplayermarkers.core; + +import java.util.HashMap; +import java.util.Map; + +public enum GameMode { + SURVIVAL(0, "survival"), + CREATIVE(1, "creative"), + ADVENTURE(2, "adventure"), + SPECTATOR(3, "spectator"); + + private static final Map BY_VALUE = new HashMap<>(); + private static final Map BY_ID = new HashMap<>(); + + static { + for (GameMode c : values()) { + BY_VALUE.put(c.value, c); + BY_ID.put(c.id, c); + } + } + + private final int value; + private final String id; + + GameMode(int value, String id) { + this.value = value; + this.id = id; + } + + public static GameMode getByValue(int value) { + return BY_VALUE.get(value); + } + + public static GameMode getById(String id) { + return BY_ID.get(id); + } +} diff --git a/src/main/java/com/technicjelle/bluemapofflineplayermarkers/core/Player.java b/src/main/java/com/technicjelle/bluemapofflineplayermarkers/core/Player.java new file mode 100644 index 0000000..350dde6 --- /dev/null +++ b/src/main/java/com/technicjelle/bluemapofflineplayermarkers/core/Player.java @@ -0,0 +1,40 @@ +package com.technicjelle.bluemapofflineplayermarkers.core; + +import com.technicjelle.bluemapofflineplayermarkers.common.PlayerData; + +import java.time.Instant; +import java.util.UUID; + +public class Player { + private final UUID playerUUID; + private final String playerName; + /** + * The last time the player was online. + * In milliseconds since epoch. + */ + private final Instant lastPlayed; + private final PlayerData playerData; + + public Player(UUID uuid, PlayerData playerData) { + this.playerUUID = uuid; + this.playerName = Singletons.getServer().getPlayerName(uuid); + this.lastPlayed = Singletons.getServer().getPlayerLastPlayed(uuid); + this.playerData = playerData; + } + + public UUID getPlayerUUID() { + return playerUUID; + } + + public String getPlayerName() { + return playerName; + } + + public Instant getLastPlayed() { + return lastPlayed; + } + + public PlayerData getPlayerData() { + return playerData; + } +} diff --git a/src/main/java/com/technicjelle/bluemapofflineplayermarkers/core/Singletons.java b/src/main/java/com/technicjelle/bluemapofflineplayermarkers/core/Singletons.java new file mode 100644 index 0000000..f5ddd37 --- /dev/null +++ b/src/main/java/com/technicjelle/bluemapofflineplayermarkers/core/Singletons.java @@ -0,0 +1,55 @@ +package com.technicjelle.bluemapofflineplayermarkers.core; + +import com.technicjelle.bluemapofflineplayermarkers.common.Config; +import com.technicjelle.bluemapofflineplayermarkers.common.Server; +import com.technicjelle.bluemapofflineplayermarkers.core.markerhandler.MarkerHandler; + +import java.util.logging.Logger; + +public class Singletons { + private static Server server; + private static Logger logger; + private static Config config; + private static MarkerHandler markerHandler; + private static BMApiStatus bmApiStatus; + + public static void init(Server server, Logger logger, Config config, MarkerHandler markerHandler, BMApiStatus bmApiStatus) { + if (Singletons.server != null || Singletons.logger != null || Singletons.config != null || Singletons.markerHandler != null || Singletons.bmApiStatus != null) + throw new RuntimeException("Singletons already initialized"); + + Singletons.server = server; + Singletons.logger = logger; + Singletons.config = config; + Singletons.markerHandler = markerHandler; + Singletons.bmApiStatus = bmApiStatus; + } + + public static void cleanup() { + server = null; + logger = null; + config = null; + markerHandler = null; + bmApiStatus = null; + System.gc(); + } + + public static Server getServer() { + return server; + } + + public static Logger getLogger() { + return logger; + } + + public static Config getConfig() { + return config; + } + + public static MarkerHandler getMarkerHandler() { + return markerHandler; + } + + public static boolean isBlueMapAPIPresent() { + return bmApiStatus.isBlueMapAPIPresent(); + } +} diff --git a/src/main/java/com/technicjelle/bluemapofflineplayermarkers/core/fileloader/FileMarkerLoader.java b/src/main/java/com/technicjelle/bluemapofflineplayermarkers/core/fileloader/FileMarkerLoader.java new file mode 100644 index 0000000..bd5a263 --- /dev/null +++ b/src/main/java/com/technicjelle/bluemapofflineplayermarkers/core/fileloader/FileMarkerLoader.java @@ -0,0 +1,88 @@ +package com.technicjelle.bluemapofflineplayermarkers.core.fileloader; + +import com.technicjelle.bluemapofflineplayermarkers.core.Player; +import com.technicjelle.bluemapofflineplayermarkers.core.Singletons; +import de.bluecolored.bluemap.api.BlueMapAPI; +import de.bluecolored.bluenbt.BlueNBT; +import de.bluecolored.bluenbt.NBTReader; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.Instant; +import java.util.UUID; +import java.util.logging.Level; +import java.util.stream.Stream; +import java.util.zip.GZIPInputStream; + +public class FileMarkerLoader { + private static final BlueNBT nbt = new BlueNBT(); + + public static void loadOfflineMarkers() { + Path playerDataFolder = Singletons.getServer().getPlayerDataFolder(); + + //Return if playerdata is missing for some reason. + if (!Files.exists(playerDataFolder) || !Files.isDirectory(playerDataFolder)) { + Singletons.getLogger().severe("Playerdata folder not found, skipping loading of offline markers from storage"); + return; + } + + BlueMapAPI api; + if (Singletons.isBlueMapAPIPresent()) { + if (BlueMapAPI.getInstance().isPresent()) + api = BlueMapAPI.getInstance().get(); + else { + Singletons.getLogger().warning("BlueMapAPI not available, skipping loading of offline markers from storage"); + return; + } + } else { + Singletons.getLogger().info("BlueMapAPI not available, probably due to running in a test environment"); + api = null; + } + + try (Stream playerDataFiles = Files.list(playerDataFolder)) { + playerDataFiles.filter(p -> p.toString().endsWith(".dat")).forEach(p -> loadOfflineMarker(p, api)); + } catch (IOException e) { + Singletons.getLogger().log(Level.SEVERE, "Failed to stream playerdata", e); + } + } + + private static void loadOfflineMarker(Path playerDataFile, BlueMapAPI api) { + final String fileName = playerDataFile.getFileName().toString(); + Singletons.getLogger().info("Loading playerdata file: " + fileName); + + final String uuidString = fileName.replace(".dat", ""); + final UUID playerUUID; + try { + playerUUID = UUID.fromString(uuidString); + } catch (IllegalArgumentException e) { + Singletons.getLogger().warning("Invalid playerdata filename: " + fileName + ", skipping"); + return; + } + + if (playerDataFile.toFile().length() == 0) { + Singletons.getLogger().warning("Playerdata file " + fileName + " is empty, skipping"); + return; + } + + if (Singletons.getServer().isPlayerOnline(playerUUID)) return; // don't add markers for online players + + if (Singletons.getConfig().checkPlayerLastPlayed(playerUUID)) { + String playerName = Singletons.getServer().getPlayerName(playerUUID); + Instant lastPlayed = Singletons.getServer().getPlayerLastPlayed(playerUUID); + Singletons.getLogger().finer("Player " + playerName + " (" + playerUUID + ") was last online at " + lastPlayed.toString() + ",\n" + + "which is more than " + Singletons.getConfig().getExpireTimeInHours() + " hours ago, so not adding marker"); + return; + } + + try (GZIPInputStream in = new GZIPInputStream(Files.newInputStream(playerDataFile))) { + NBTReader reader = new NBTReader(in); + PlayerNBTData playerNBTData = nbt.read(reader, PlayerNBTData.class); + + Player player = new Player(playerUUID, playerNBTData); + Singletons.getMarkerHandler().add(player, api); + } catch (IOException e) { + Singletons.getLogger().log(Level.SEVERE, "Failed to read playerdata file " + fileName, e); + } + } +} diff --git a/src/main/java/com/technicjelle/bluemapofflineplayermarkers/core/fileloader/PlayerNBTData.java b/src/main/java/com/technicjelle/bluemapofflineplayermarkers/core/fileloader/PlayerNBTData.java new file mode 100644 index 0000000..cc8670c --- /dev/null +++ b/src/main/java/com/technicjelle/bluemapofflineplayermarkers/core/fileloader/PlayerNBTData.java @@ -0,0 +1,50 @@ +package com.technicjelle.bluemapofflineplayermarkers.core.fileloader; + +import com.flowpowered.math.vector.Vector3d; +import com.technicjelle.bluemapofflineplayermarkers.common.PlayerData; +import com.technicjelle.bluemapofflineplayermarkers.core.GameMode; +import com.technicjelle.bluemapofflineplayermarkers.core.Singletons; +import de.bluecolored.bluenbt.NBTName; +import org.jetbrains.annotations.Nullable; + +import java.util.Optional; +import java.util.UUID; + +public class PlayerNBTData implements PlayerData { + @NBTName("playerGameType") + private int gameMode; + + @NBTName("Pos") + private double[] position; + + @NBTName("WorldUUIDLeast") + private long worldUUIDLeast; + + @NBTName("WorldUUIDMost") + private long worldUUIDMost; + + @NBTName("Dimension") + private Object dimension; + + public @Nullable GameMode getGameMode() { + return GameMode.getByValue(gameMode); + } + + public @Nullable Vector3d getPosition() { + if (position.length != 3) return null; // Position is broken + + return new Vector3d(position[0], position[1], position[2]); + } + + @Nullable + public Optional getWorldUUID() { + UUID worldUUID = new UUID(worldUUIDMost, worldUUIDLeast); + if (!worldUUID.equals(new UUID(0, 0))) { + return Optional.of(worldUUID); + } + + // If world UUID isn't valid, try to find it some other way, + // and if we can't find the world UUID, we return empty + return Singletons.getServer().guessWorldUUID(dimension); + } +} diff --git a/src/main/java/com/technicjelle/bluemapofflineplayermarkers/core/markerhandler/BlueMapMarkerHandler.java b/src/main/java/com/technicjelle/bluemapofflineplayermarkers/core/markerhandler/BlueMapMarkerHandler.java new file mode 100644 index 0000000..5850fcf --- /dev/null +++ b/src/main/java/com/technicjelle/bluemapofflineplayermarkers/core/markerhandler/BlueMapMarkerHandler.java @@ -0,0 +1,75 @@ +package com.technicjelle.bluemapofflineplayermarkers.core.markerhandler; + +import com.flowpowered.math.vector.Vector3d; +import com.technicjelle.BMUtils; +import com.technicjelle.bluemapofflineplayermarkers.common.Config; +import com.technicjelle.bluemapofflineplayermarkers.core.Player; +import com.technicjelle.bluemapofflineplayermarkers.core.Singletons; +import de.bluecolored.bluemap.api.BlueMapAPI; +import de.bluecolored.bluemap.api.BlueMapMap; +import de.bluecolored.bluemap.api.BlueMapWorld; +import de.bluecolored.bluemap.api.markers.MarkerSet; +import de.bluecolored.bluemap.api.markers.POIMarker; + +import java.util.Optional; +import java.util.UUID; + +public class BlueMapMarkerHandler implements MarkerHandler { + @Override + public void add(Player player, BlueMapAPI api) { + //If this player's visibility is disabled on the map, don't add the marker. + if (!api.getWebApp().getPlayerVisibility(player.getPlayerUUID())) return; + + //If this player's game mode is disabled on the map, don't add the marker. + if (Singletons.getConfig().isGameModeHidden(player.getPlayerData().getGameMode())) return; + + // Get BlueMapWorld for the position + Optional worldUUID = player.getPlayerData().getWorldUUID(); + if (worldUUID.isEmpty()) return; + BlueMapWorld blueMapWorld = api.getWorld(worldUUID.get()).orElse(null); + if (blueMapWorld == null) return; + Vector3d position = player.getPlayerData().getPosition(); + if (position == null) return; + + // Add 1.8 to y to place the marker at the head-position of the player, like BlueMap does with its player-markers + position = position.add(0, 1.8, 0); + + // Create marker-template + POIMarker.Builder markerBuilder = POIMarker.builder() + .label(player.getPlayerName()) + .detail(player.getPlayerName() + " (offline)
" + + "") + .styleClasses("bmopm-offline-player") + .position(position); + + // Create an icon and marker for each map of this world + // We need to create a separate marker per map, because the map-storage that the icon is saved in + // is different for each map + for (BlueMapMap map : blueMapWorld.getMaps()) { + markerBuilder.icon(BMUtils.getPlayerHeadIconAddress(api, player.getPlayerUUID(), map), 0, 0); // centered with CSS instead + + // get marker-set (or create new marker set if none found) + MarkerSet markerSet = map.getMarkerSets().computeIfAbsent(Config.MARKER_SET_ID, id -> MarkerSet.builder() + .label(Singletons.getConfig().getMarkerSetName()) + .toggleable(Singletons.getConfig().isToggleable()) + .defaultHidden(Singletons.getConfig().isDefaultHidden()) + .build()); + + // add marker + markerSet.put(player.getPlayerUUID().toString(), markerBuilder.build()); + } + + Singletons.getLogger().info("Marker for " + player.getPlayerName() + " added"); + } + + @Override + public void remove(UUID playerUUID, BlueMapAPI api) { + // remove all markers with the players uuid + for (BlueMapMap map : api.getMaps()) { + MarkerSet set = map.getMarkerSets().get(Config.MARKER_SET_ID); + if (set != null) set.remove(playerUUID.toString()); + } + + Singletons.getLogger().info("Marker for " + Singletons.getServer().getPlayerName(playerUUID) + " removed"); + } +} diff --git a/src/main/java/com/technicjelle/bluemapofflineplayermarkers/core/markerhandler/MarkerHandler.java b/src/main/java/com/technicjelle/bluemapofflineplayermarkers/core/markerhandler/MarkerHandler.java new file mode 100644 index 0000000..8e95e09 --- /dev/null +++ b/src/main/java/com/technicjelle/bluemapofflineplayermarkers/core/markerhandler/MarkerHandler.java @@ -0,0 +1,12 @@ +package com.technicjelle.bluemapofflineplayermarkers.core.markerhandler; + +import com.technicjelle.bluemapofflineplayermarkers.core.Player; +import de.bluecolored.bluemap.api.BlueMapAPI; + +import java.util.UUID; + +public interface MarkerHandler { + void add(Player player, BlueMapAPI api); + + void remove(UUID playerUUID, BlueMapAPI api); +} diff --git a/src/main/java/com/technicjelle/bluemapofflineplayermarkers/impl/paper/BlueMapOfflinePlayerMarkers.java b/src/main/java/com/technicjelle/bluemapofflineplayermarkers/impl/paper/BlueMapOfflinePlayerMarkers.java new file mode 100644 index 0000000..fcafae6 --- /dev/null +++ b/src/main/java/com/technicjelle/bluemapofflineplayermarkers/impl/paper/BlueMapOfflinePlayerMarkers.java @@ -0,0 +1,123 @@ +package com.technicjelle.bluemapofflineplayermarkers.impl.paper; + +import com.technicjelle.BMUtils; +import com.technicjelle.UpdateChecker; +import com.technicjelle.bluemapofflineplayermarkers.core.BMApiStatus; +import com.technicjelle.bluemapofflineplayermarkers.core.Player; +import com.technicjelle.bluemapofflineplayermarkers.core.Singletons; +import com.technicjelle.bluemapofflineplayermarkers.core.fileloader.FileMarkerLoader; +import com.technicjelle.bluemapofflineplayermarkers.core.markerhandler.BlueMapMarkerHandler; +import de.bluecolored.bluemap.api.BlueMapAPI; +import org.bstats.bukkit.Metrics; +import org.bukkit.Bukkit; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerJoinEvent; +import org.bukkit.event.player.PlayerQuitEvent; +import org.bukkit.plugin.java.JavaPlugin; + +import java.io.IOException; +import java.util.Optional; +import java.util.UUID; +import java.util.function.Consumer; +import java.util.logging.Level; + +public final class BlueMapOfflinePlayerMarkers extends JavaPlugin implements Listener { + private PaperConfig config; + private UpdateChecker updateChecker; + + @Override + public void onLoad() { + getLogger().info("BlueMap Offline Player Markers plugin (on)loading..."); + BlueMapAPI.onEnable(api -> { + getLogger().info("BlueMap is enabled! Copying resources to BlueMap webapp and registering them..."); + try { + BMUtils.copyJarResourceToBlueMap(api, getClassLoader(), "style.css", "bmopm.css", false); + BMUtils.copyJarResourceToBlueMap(api, getClassLoader(), "script.js", "bmopm.js", false); + } catch (IOException e) { + Singletons.getLogger().log(Level.SEVERE, "Failed to copy resources to BlueMap webapp!", e); + } + + }); + } + + @Override + public void onEnable() { + new Metrics(this, 16425); + + updateChecker = new UpdateChecker("TechnicJelle", "BlueMapOfflinePlayerMarkers", getDescription().getVersion()); + updateChecker.checkAsync(); + + getServer().getPluginManager().registerEvents(this, this); + + config = new PaperConfig(this); + + Singletons.init(new PaperServer(this), getLogger(), config, new BlueMapMarkerHandler(), new BMApiStatus()); + Singletons.getServer().startUp(); + + //all actual startup and shutdown logic moved to BlueMapAPI enable/disable methods, so `/bluemap reload` also reloads this plugin + BlueMapAPI.onEnable(onEnableListener); + BlueMapAPI.onDisable(onDisableListener); + } + + final Consumer onEnableListener = api -> { + getLogger().info("API Ready! BlueMap Offline Player Markers plugin enabled!"); + updateChecker.logUpdateMessage(Singletons.getLogger()); + + config.loadFromPlugin(this); + + //create marker handler and add all offline players in a separate thread, so the server doesn't hang up while it's going + //with a delay, so any potential BlueMap SkinProviders have time to load + Bukkit.getScheduler().runTaskLaterAsynchronously(this, FileMarkerLoader::loadOfflineMarkers, 20 * 5); + }; + + final Consumer onDisableListener = api -> { + Singletons.getLogger().info("API disabled! BlueMap Offline Player Markers shutting down..."); + //not much to do here, actually... + }; + + @Override + public void onDisable() { + BlueMapAPI.unregisterListener(onEnableListener); + BlueMapAPI.unregisterListener(onDisableListener); + Singletons.getServer().shutDown(); + Singletons.getLogger().info("BlueMap Offline Player Markers plugin disabled!"); + Singletons.cleanup(); + } + + + @EventHandler + public void onJoin(PlayerJoinEvent e) { + Bukkit.getScheduler().runTaskAsynchronously(this, () -> { + org.bukkit.entity.Player player = e.getPlayer(); + UUID playerUUID = player.getUniqueId(); + + Optional api = BlueMapAPI.getInstance(); + if (api.isEmpty()) { + Singletons.getLogger().warning("BlueMap is not loaded, not removing marker for " + player.getName()); + return; + } + + Singletons.getMarkerHandler().remove(playerUUID, api.get()); + }); + } + + @EventHandler + public void onLeave(PlayerQuitEvent e) { + Bukkit.getScheduler().runTaskAsynchronously(this, () -> { + org.bukkit.entity.Player player = e.getPlayer(); + UUID playerUUID = player.getUniqueId(); + + PlayerBukkitData playerBukkitData = new PlayerBukkitData(player); + Player playerToAdd = new Player(playerUUID, playerBukkitData); + + Optional api = BlueMapAPI.getInstance(); + if (api.isEmpty()) { + Singletons.getLogger().warning("BlueMap is not loaded, not adding marker for " + player.getName()); + return; + } + + Singletons.getMarkerHandler().add(playerToAdd, api.get()); + }); + } +} diff --git a/src/main/java/com/technicjelle/bluemapofflineplayermarkers/impl/paper/PaperConfig.java b/src/main/java/com/technicjelle/bluemapofflineplayermarkers/impl/paper/PaperConfig.java new file mode 100644 index 0000000..7c20ca5 --- /dev/null +++ b/src/main/java/com/technicjelle/bluemapofflineplayermarkers/impl/paper/PaperConfig.java @@ -0,0 +1,93 @@ +package com.technicjelle.bluemapofflineplayermarkers.impl.paper; + +import com.technicjelle.MCUtils; +import com.technicjelle.bluemapofflineplayermarkers.common.Config; +import com.technicjelle.bluemapofflineplayermarkers.core.GameMode; +import org.bukkit.plugin.java.JavaPlugin; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +public class PaperConfig implements Config { + private String markerSetName; + private boolean toggleable; + private boolean defaultHidden; + private long expireTimeInHours; + private List hiddenGameModes; + + public PaperConfig(JavaPlugin plugin) { + loadFromPlugin(plugin); + } + + public void loadFromPlugin(JavaPlugin plugin) { + try { + MCUtils.copyPluginResourceToConfigDir(plugin, "config.yml", "config.yml", false); + } catch (IOException e) { + throw new RuntimeException(e); + } + + //Load config from disk + plugin.reloadConfig(); + + //Load config values into variables + markerSetName = plugin.getConfig().getString("MarkerSetName", "Offline Players"); + toggleable = plugin.getConfig().getBoolean("Toggleable", true); + defaultHidden = plugin.getConfig().getBoolean("DefaultHidden", false); + expireTimeInHours = plugin.getConfig().getLong("ExpireTimeInHours", 0); + hiddenGameModes = Config.parseGameModes(getStringList(plugin, "HiddenGameModes", List.of("spectator"))); + } + + //Copied/Adapted from org.bukkit.configuration.MemorySection.java + @SuppressWarnings("SameParameterValue") + private List getStringList(JavaPlugin plugin, String path, List def) { + List list = plugin.getConfig().getList(path, def); + + if (list == null) { + return new ArrayList<>(0); + } + + List result = new ArrayList<>(); + + for (Object object : list) { + if ((object instanceof String) || (isPrimitiveWrapper(object))) { + result.add(String.valueOf(object)); + } + } + + return result; + } + + //Copied/Adapted from org.bukkit.configuration.MemorySection.java + private boolean isPrimitiveWrapper(Object input) { + return input instanceof Integer || input instanceof Boolean || + input instanceof Character || input instanceof Byte || + input instanceof Short || input instanceof Double || + input instanceof Long || input instanceof Float; + } + + @Override + public String getMarkerSetName() { + return markerSetName; + } + + @Override + public boolean isToggleable() { + return toggleable; + } + + @Override + public boolean isDefaultHidden() { + return defaultHidden; + } + + @Override + public long getExpireTimeInHours() { + return expireTimeInHours; + } + + @Override + public List getHiddenGameModes() { + return hiddenGameModes; + } +} diff --git a/src/main/java/com/technicjelle/bluemapofflineplayermarkers/impl/paper/PaperServer.java b/src/main/java/com/technicjelle/bluemapofflineplayermarkers/impl/paper/PaperServer.java new file mode 100644 index 0000000..c26c906 --- /dev/null +++ b/src/main/java/com/technicjelle/bluemapofflineplayermarkers/impl/paper/PaperServer.java @@ -0,0 +1,106 @@ +package com.technicjelle.bluemapofflineplayermarkers.impl.paper; + +import com.destroystokyo.paper.profile.PlayerProfile; +import com.technicjelle.bluemapofflineplayermarkers.common.Server; +import org.bukkit.Bukkit; +import org.bukkit.OfflinePlayer; +import org.bukkit.World; +import org.bukkit.plugin.java.JavaPlugin; +import org.jetbrains.annotations.Nullable; + +import java.io.IOException; +import java.nio.file.Path; +import java.time.Instant; +import java.util.Optional; +import java.util.UUID; + +public class PaperServer implements Server { + final JavaPlugin plugin; + final org.bukkit.Server server; + + public PaperServer(JavaPlugin plugin) { + this.plugin = plugin; + this.server = plugin.getServer(); + } + + @Override + public boolean isPlayerOnline(UUID playerUUID) { + OfflinePlayer op = server.getOfflinePlayer(playerUUID); + return op.isOnline(); + } + + @Override + public Path getConfigFolder() { + return plugin.getDataFolder().toPath(); + } + + @Override + public Path getPlayerDataFolder() { + //I really don't like "getWorlds().get(0)" as a way to get the main world, but as far as I can tell there is no other way + return Bukkit.getWorlds().get(0).getWorldFolder().toPath().resolve("playerdata"); + } + + @Override + public Instant getPlayerLastPlayed(UUID playerUUID) { + OfflinePlayer op = server.getOfflinePlayer(playerUUID); + long millisSinceEpoch = op.getLastPlayed(); + return Instant.ofEpochMilli(millisSinceEpoch); + } + + @Override + public String getPlayerName(UUID playerUUID) { + OfflinePlayer op = server.getOfflinePlayer(playerUUID); + @Nullable String name = op.getName(); + if (name != null) return name; + + PlayerProfile playerProfile = server.createProfile(playerUUID); + if (playerProfile.complete(false)) { + name = playerProfile.getName(); + if (name != null && !name.isBlank()) return name; + } + + try { + return Server.nameFromMojangAPI(playerUUID); + } catch (IOException e) { + //If the player is not found, return the UUID as a string + return playerUUID.toString(); + } + } + + @Override + public Optional guessWorldUUID(Object object) { + if (object instanceof String) { + String dimensionString = (String) object; + + //Try to get world by name + { + @Nullable World world = server.getWorld(dimensionString); + if (world != null) { + return Optional.of(world.getUID()); + } + } + + //Try to get world by dimension + for (World world : server.getWorlds()) { + switch (world.getEnvironment()) { + case NORMAL: + if (dimensionString.contains("overworld")) return Optional.of(world.getUID()); + case NETHER: + if (dimensionString.contains("the_nether")) return Optional.of(world.getUID()); + case THE_END: + if (dimensionString.contains("the_end")) return Optional.of(world.getUID()); + } + } + } + + if (object instanceof Integer) { + int dimensionInt = (Integer) object; + for (World world : server.getWorlds()) { + @SuppressWarnings("deprecation") int worldID = world.getEnvironment().getId(); + if (worldID == dimensionInt) return Optional.of(world.getUID()); + } + } + + return Optional.empty(); + } +} diff --git a/src/main/java/com/technicjelle/bluemapofflineplayermarkers/impl/paper/PlayerBukkitData.java b/src/main/java/com/technicjelle/bluemapofflineplayermarkers/impl/paper/PlayerBukkitData.java new file mode 100644 index 0000000..845c70a --- /dev/null +++ b/src/main/java/com/technicjelle/bluemapofflineplayermarkers/impl/paper/PlayerBukkitData.java @@ -0,0 +1,37 @@ +package com.technicjelle.bluemapofflineplayermarkers.impl.paper; + +import com.flowpowered.math.vector.Vector3d; +import com.technicjelle.bluemapofflineplayermarkers.common.PlayerData; +import com.technicjelle.bluemapofflineplayermarkers.core.GameMode; +import org.bukkit.Location; +import org.bukkit.entity.Player; + +import java.util.Optional; +import java.util.UUID; + +public class PlayerBukkitData implements PlayerData { + final Player player; + + public PlayerBukkitData(Player player) { + this.player = player; + } + + @Override + public GameMode getGameMode() { + org.bukkit.GameMode bukkitGameMode = player.getGameMode(); + @SuppressWarnings("deprecation") GameMode gameMode = GameMode.getByValue(bukkitGameMode.getValue()); + return gameMode; + + } + + @Override + public Vector3d getPosition() { + Location location = player.getLocation(); + return new Vector3d(location.getX(), location.getY(), location.getZ()); + } + + @Override + public Optional getWorldUUID() { + return Optional.of(player.getWorld().getUID()); + } +} diff --git a/src/main/java/com/technicjelle/bluemapofflineplayermarkers/models/PlayerNBT.java b/src/main/java/com/technicjelle/bluemapofflineplayermarkers/models/PlayerNBT.java deleted file mode 100644 index 2010a39..0000000 --- a/src/main/java/com/technicjelle/bluemapofflineplayermarkers/models/PlayerNBT.java +++ /dev/null @@ -1,60 +0,0 @@ -package com.technicjelle.bluemapofflineplayermarkers.models; - -import de.bluecolored.bluenbt.NBTName; -import org.bukkit.Bukkit; -import org.bukkit.GameMode; -import org.bukkit.Location; -import org.bukkit.World; -import org.jetbrains.annotations.Nullable; - -import java.util.UUID; - -@SuppressWarnings({"unused", "MismatchedReadAndWriteOfArray"}) -public class PlayerNBT { - @NBTName("playerGameType") - private int gameMode; - - @NBTName("Pos") - private double[] position; - - @NBTName("WorldUUIDLeast") - private long worldUUIDLeast; - - @NBTName("WorldUUIDMost") - private long worldUUIDMost; - - @NBTName("Dimension") - private Object dimension; - - public @Nullable GameMode getGameMode() { - //noinspection deprecation - return GameMode.getByValue(gameMode); - } - - public @Nullable Location getLocation() { - World world = getWorld(); - - //World couldn't be found, or position is broken - if (world == null || position.length != 3) return null; - - return new Location(world, position[0], position[1], position[2]); - } - - private @Nullable World getWorld() { - UUID worldUUID = new UUID(worldUUIDMost, worldUUIDLeast); - if (Bukkit.getWorld(worldUUID) != null) return Bukkit.getWorld(worldUUID); - - //If world doesn't exist, try to find it some other way... - - //By legacy dimension int - if (dimension instanceof Integer) { - int dimensionInt = (Integer) this.dimension; - for (World world : Bukkit.getWorlds()) { - //noinspection deprecation - if (world.getEnvironment().getId() == dimensionInt) return world; - } - } - - return null; - } -} diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index 0988be9..4ec3c2e 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -1,6 +1,6 @@ name: BlueMapOfflinePlayerMarkers version: ${project.version} -main: com.technicjelle.bluemapofflineplayermarkers.BlueMapOfflinePlayerMarkers +main: com.technicjelle.bluemapofflineplayermarkers.impl.paper.BlueMapOfflinePlayerMarkers api-version: 1.13 depend: - BlueMap diff --git a/src/test/java/LoadOfflineMarkersTest.java b/src/test/java/LoadOfflineMarkersTest.java new file mode 100644 index 0000000..90cde6d --- /dev/null +++ b/src/test/java/LoadOfflineMarkersTest.java @@ -0,0 +1,26 @@ +import com.technicjelle.bluemapofflineplayermarkers.core.Singletons; +import com.technicjelle.bluemapofflineplayermarkers.core.fileloader.FileMarkerLoader; +import mockery.*; +import org.junit.After; +import org.junit.Test; + +public class LoadOfflineMarkersTest { + @After + public void cleanup() { + Singletons.getServer().shutDown(); + Singletons.cleanup(); + } + + @Test + public void extract_info_from_playerdata_files() { + Singletons.init( + new MockServer("test_playerdata"), + ConsoleLogger.createLogger("extract_info_from_playerdata_files"), + new MockConfig(), + new MockMarkerHandler(), + new MockBMApiStatus() + ); + Singletons.getServer().startUp(); + FileMarkerLoader.loadOfflineMarkers(); + } +} diff --git a/src/test/java/mockery/ConsoleLogger.java b/src/test/java/mockery/ConsoleLogger.java new file mode 100644 index 0000000..81a7f97 --- /dev/null +++ b/src/test/java/mockery/ConsoleLogger.java @@ -0,0 +1,43 @@ +package mockery; + + +import java.util.logging.ConsoleHandler; +import java.util.logging.Level; +import java.util.logging.LogRecord; +import java.util.logging.Logger; +import java.util.logging.SimpleFormatter; +import java.util.logging.StreamHandler; + +public class ConsoleLogger extends StreamHandler { + static String formatLog(LogRecord lr) { + return String.format("[%1$-7s] %2$s%n", lr.getLevel().getName(), lr.getMessage()); + } + + public static Logger createLogger(String name) { + Logger logger = Logger.getLogger(name); + logger.setUseParentHandlers(false); + logger.setLevel(Level.ALL); + + ConsoleHandler warning2stderrLogger = new ConsoleHandler(); + warning2stderrLogger.setLevel(Level.WARNING); + warning2stderrLogger.setFormatter(new SimpleFormatter() { + @Override + public synchronized String format(LogRecord lr) { + return formatLog(lr); + } + }); + logger.addHandler(warning2stderrLogger); + + ConsoleLogger fine2stdoutLogger = new ConsoleLogger(); + logger.addHandler(fine2stdoutLogger); + + return logger; + } + + @Override + public void publish(LogRecord record) { + final Level level = record.getLevel(); + if (level.intValue() <= Level.INFO.intValue()) + System.out.print(formatLog(record)); + } +} diff --git a/src/test/java/mockery/MockBMApiStatus.java b/src/test/java/mockery/MockBMApiStatus.java new file mode 100644 index 0000000..89c8ffc --- /dev/null +++ b/src/test/java/mockery/MockBMApiStatus.java @@ -0,0 +1,10 @@ +package mockery; + +import com.technicjelle.bluemapofflineplayermarkers.core.BMApiStatus; + +public class MockBMApiStatus extends BMApiStatus { + @Override + public boolean isBlueMapAPIPresent() { + return false; + } +} diff --git a/src/test/java/mockery/MockConfig.java b/src/test/java/mockery/MockConfig.java new file mode 100644 index 0000000..3d48abe --- /dev/null +++ b/src/test/java/mockery/MockConfig.java @@ -0,0 +1,33 @@ +package mockery; + +import com.technicjelle.bluemapofflineplayermarkers.common.Config; +import com.technicjelle.bluemapofflineplayermarkers.core.GameMode; + +import java.util.List; + +public class MockConfig implements Config { + @Override + public String getMarkerSetName() { + return MARKER_SET_ID; + } + + @Override + public boolean isToggleable() { + return true; + } + + @Override + public boolean isDefaultHidden() { + return false; + } + + @Override + public long getExpireTimeInHours() { + return 0; + } + + @Override + public List getHiddenGameModes() { + return List.of(); + } +} diff --git a/src/test/java/mockery/MockMarkerHandler.java b/src/test/java/mockery/MockMarkerHandler.java new file mode 100644 index 0000000..58dbf62 --- /dev/null +++ b/src/test/java/mockery/MockMarkerHandler.java @@ -0,0 +1,30 @@ +package mockery; + +import com.technicjelle.bluemapofflineplayermarkers.core.Player; +import com.technicjelle.bluemapofflineplayermarkers.core.Singletons; +import com.technicjelle.bluemapofflineplayermarkers.core.markerhandler.MarkerHandler; +import de.bluecolored.bluemap.api.BlueMapAPI; + +import java.util.Optional; +import java.util.UUID; + +public class MockMarkerHandler implements MarkerHandler { + @Override + public void add(Player player, BlueMapAPI __) { + Singletons.getLogger().finer("UUID: " + player.getPlayerUUID()); + Singletons.getLogger().finer("Name: " + player.getPlayerName()); + Singletons.getLogger().finer("Last Played: " + player.getLastPlayed().toEpochMilli()); + Singletons.getLogger().finer("GameMode: " + player.getPlayerData().getGameMode()); + Singletons.getLogger().finer("Position: " + player.getPlayerData().getPosition()); + + Optional worldUUID = player.getPlayerData().getWorldUUID(); + if (worldUUID.isEmpty()) + Singletons.getLogger().warning("World UUID: null"); + else + Singletons.getLogger().finer("World UUID: " + worldUUID.get()); + } + + @Override + public void remove(UUID __, BlueMapAPI ___) { + } +} diff --git a/src/test/java/mockery/MockServer.java b/src/test/java/mockery/MockServer.java new file mode 100644 index 0000000..82acc3a --- /dev/null +++ b/src/test/java/mockery/MockServer.java @@ -0,0 +1,55 @@ +package mockery; + +import com.technicjelle.bluemapofflineplayermarkers.common.Server; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.time.Instant; +import java.util.Optional; +import java.util.UUID; + +public class MockServer implements Server { + final String playerDataFolderName; + + public MockServer(String playerDataFolderName) { + this.playerDataFolderName = playerDataFolderName; + } + + @Override + public boolean isPlayerOnline(UUID playerUUID) { + return false; + } + + @Override + public Path getConfigFolder() { + return getPlayerDataFolder(); + } + + @Override + public Path getPlayerDataFolder() { + Path path = Paths.get("").resolve("src/test/resources/" + playerDataFolderName); + assert Files.exists(path); + return path; + } + + @Override + public Instant getPlayerLastPlayed(UUID playerUUID) { + return Instant.now(); + } + + @Override + public String getPlayerName(UUID playerUUID) { + try { + return Server.nameFromMojangAPI(playerUUID); + } catch (IOException e) { + return playerUUID.toString(); + } + } + + @Override + public Optional guessWorldUUID(Object object) { + return Optional.empty(); + } +} diff --git a/src/test/resources/test_playerdata/00000000-0000-0000-0009-01fd630ed897.dat b/src/test/resources/test_playerdata/00000000-0000-0000-0009-01fd630ed897.dat new file mode 100644 index 0000000..201bba0 Binary files /dev/null and b/src/test/resources/test_playerdata/00000000-0000-0000-0009-01fd630ed897.dat differ diff --git a/src/test/resources/test_playerdata/27def3b6-dae1-4c37-9f64-5f26e44aca38.dat b/src/test/resources/test_playerdata/27def3b6-dae1-4c37-9f64-5f26e44aca38.dat new file mode 100644 index 0000000..ed195d3 Binary files /dev/null and b/src/test/resources/test_playerdata/27def3b6-dae1-4c37-9f64-5f26e44aca38.dat differ diff --git a/src/test/resources/test_playerdata/3e6b6179-b774-450e-bd16-e6f24ec7185c.dat b/src/test/resources/test_playerdata/3e6b6179-b774-450e-bd16-e6f24ec7185c.dat new file mode 100644 index 0000000..12ac0eb Binary files /dev/null and b/src/test/resources/test_playerdata/3e6b6179-b774-450e-bd16-e6f24ec7185c.dat differ